Earn a 50% discount on the DP-600 certification exam by completing the Fabric 30 Days to Learn It challenge.
Hi. How to add "Legend" and use it in visualisation? Please help, with some examples of declaring and using, or with link on learning documentation
I'm not sure that undrestand the color issue well. Could you please clarify color issue a bit deeper?
I think it'd be better to include index into data-model (VisualDataPoint).
Ignat Vilesov,
Software Engineer
Microsoft Power BI Custom Visuals
Question about indexes resolved by the (function(d,i) {return i;} and .sort()
About colors: using colorPalette.getColor(dataValue[k].source.groupName + '').value for assign colors to bars and legends labels. Then in enumerateObjectInstances
case 'colorSelector': for (let labelDataLegend of this.labelDataLegends) { objectEnumeration.push({ objectName: objectName, displayName: labelDataLegend.valueName, properties: { fill: { solid: { color: labelDataLegend.color } } }, selector: labelDataLegend.selectionId.getSelector() }); } break;
assigns colors to Editor. It assigns correctly. But when i try to change colors by Editor it don't work, colors are not change anywhere
You should parse current color by using this code.
Ignat Vilesov,
Software Engineer
Microsoft Power BI Custom Visuals
It working over Categories, but i need it over Series. How do this?
The given example should actually work with measure as well due to fact that color can be kept only in the category.
Ignat Vilesov,
Software Engineer
Microsoft Power BI Custom Visuals
If you use categorical data mapping in the capabilities.json that means that amount of categories and amount of measures are always the same.
In other words, color can be kept in the category using the code above.
Ignat Vilesov,
Software Engineer
Microsoft Power BI Custom Visuals
Don't work...
module powerbi.extensibility.visual { "use strict"; interface VisualViewModel { dataPoints: VisualDataPoint[]; dataLegends: VisualDataLegend[]; dataMax: number; settings: VisualSettings; }; interface VisualDataPoint { category: string; valueName: string; value: PrimitiveValue; sumHeight: PrimitiveValue; conventions: string; color: string; selectionId: powerbi.visuals.ISelectionId; }; interface VisualDataLegend { valueName: string; conventions: string; color: string; selectionId: powerbi.visuals.ISelectionId; }; interface VisualSettings { enableAxis: { show: boolean; }; generalView: { opacity: number; }; legend: { show: boolean; fontSize: number; }; colorSelector: { defaultColor: Fill[]; }; } function visualTransform(options: VisualUpdateOptions, host: IVisualHost): VisualViewModel { let dataViews = options.dataViews; let defaultSettings: VisualSettings = { enableAxis: { show: false }, generalView: { opacity: 100 }, legend: { show: true, fontSize: 12 }, colorSelector: { defaultColor: [] } }; let viewModel: VisualViewModel = { dataPoints: [], dataLegends: [], dataMax: 0, settings: <VisualSettings>{} }; if (!dataViews || !dataViews[0] || !dataViews[0].categorical || !dataViews[0].categorical.categories || !dataViews[0].categorical.categories[0].source || !dataViews[0].categorical.values) return viewModel; let categorical = dataViews[0].categorical; let category = categorical.categories[0]; let dataValue = categorical.values; let VisualDataPoints: VisualDataPoint[] = []; let VisualDataLegends: VisualDataLegend[] = []; let dataMax: number; let colorPalette: IColorPalette = host.colorPalette; let objects = dataViews[0].metadata.objects; let VisualSettings: VisualSettings = { enableAxis: { show: getValue<boolean>(objects, 'enableAxis', 'show', defaultSettings.enableAxis.show), }, generalView: { opacity: getValue<number>(objects, 'generalView', 'opacity', defaultSettings.generalView.opacity), }, legend: { show: getValue<boolean>(objects, 'legend', 'show', defaultSettings.legend.show), fontSize: getValue<number>(objects, 'legend', 'fontSize', defaultSettings.legend.fontSize) }, colorSelector: { defaultColor: getValue<Fill[]>(objects, 'colorSelector', 'defaultColor', defaultSettings.colorSelector.defaultColor) } }; dataValue.sort(function(a,b) {return d3.descending(a.source.groupName+'', b.source.groupName+'');}); //legends data and all colors for (let k = 0; k < dataValue.length; k++) { let defaultColor: Fill = { solid: { color: colorPalette.getColor(category.values[k] + '').value } }; VisualDataLegends.push({ valueName: dataValue[k].source.groupName + '', conventions: 'dataValue['+k+'].source.groupName', color: getCategoricalObjectValue<Fill>(category, k, 'colorSelector', 'fill', defaultColor).solid.color, selectionId: host.createSelectionIdBuilder().withMeasure(dataValue[k].source.groupName?dataValue[k].source.groupName+'':'empty').createSelectionId() }); } //bars data dataMax=0; for (let i = 0; i < category.values.length; i++) { let sumHeight=0; for (let k = 0; k < dataValue.length; k++) { sumHeight += <number>dataValue[k].values[i]; let defaultColor: Fill = { solid: { color: colorPalette.getColor(category.values[k] + '').value } }; VisualDataPoints.push({ category: category.values[i] + '', valueName: dataValue[k].source.groupName + '', value: dataValue[k].values[i], sumHeight: sumHeight, conventions: 'dataValue['+k+'].source.groupName', color: getCategoricalObjectValue<Fill>(category, k, 'colorSelector', 'fill', defaultColor).solid.color, selectionId: host.createSelectionIdBuilder().withMeasure(dataValue[k].source.groupName?dataValue[k].source.groupName+'':'empty').createSelectionId() }); } dataMax = sumHeight > dataMax ? sumHeight : dataMax; } return { dataPoints: VisualDataPoints, dataLegends: VisualDataLegends, dataMax: dataMax, settings: VisualSettings, }; } export class Visual implements IVisual { private svg: d3.Selection<SVGElement>; private host: IVisualHost; private selectionManager: ISelectionManager; private VisualContainer: d3.Selection<SVGElement>; private barContainer: d3.Selection<SVGElement>; private xAxis: d3.Selection<SVGElement>; private labelDataLegends: VisualDataLegend[]; private VisualSettings: VisualSettings; private tooltipServiceWrapper: ITooltipServiceWrapper; private locale: string; private textLabel: d3.Selection<SVGElement>; private legend: d3.Selection<SVGElement>; static Config = { xScalePadding: 0.2, solidOpacity: 1, transparentOpacity: 0.5, margins: { top: 30, right: 0, bottom: 30, left: 0, }, xAxisFontMultiplier: 0.04, }; constructor(options: VisualConstructorOptions) { this.host = options.host; this.selectionManager = options.host.createSelectionManager(); this.tooltipServiceWrapper = createTooltipServiceWrapper(this.host.tooltipService, options.element); let svg = this.svg = d3.select(options.element).append('svg').classed('Visual', true); this.locale = options.host.locale; this.barContainer = svg.append('g').classed('barContainer', true); this.xAxis = svg.append('g').classed('xAxis', true); this.textLabel = svg.append("g").classed("textLabel", true); this.legend = svg.append("g").classed("legend", true); } public update(options: VisualUpdateOptions, title: string) { let viewModel: VisualViewModel = visualTransform(options, this.host); let settings = this.VisualSettings = viewModel.settings; this.labelDataLegends = viewModel.dataLegends; let width = options.viewport.width; let height = options.viewport.height; let margins = Visual.Config.margins; this.svg.attr({ width: width, height: height }); if (settings.enableAxis.show) { height -= margins.bottom; } if (settings.legend.show) { width -= settings.legend.fontSize/12*160; } height -= margins.top; width -= margins.left; this.xAxis.style({ 'font-size': d3.min([height, width]) * Visual.Config.xAxisFontMultiplier, }); let yScale = d3.scale.linear() .domain([0, viewModel.dataMax]) .range([height, 0]); let xScale = d3.scale.ordinal() .domain(viewModel.dataPoints.map(d => d.category)) .rangeRoundBands([0, width], Visual.Config.xScalePadding, 0.2); let xAxis = d3.svg.axis() .scale(xScale) .orient('bottom'); this.xAxis.attr('transform', 'translate('+margins.left+', ' + (height+margins.top) + ')') .call(xAxis); // This must be an anonymous function instead of a lambda because // d3 uses 'this' as the reference to the element that was clicked. let selectionManager = this.selectionManager; let allowInteractions = this.host.allowInteractions; //bars (with series) let barsSeries = this.barContainer.selectAll('rect').data(viewModel.dataPoints); let legend = this.legend.selectAll('.legendLine').data(settings.legend.show ? viewModel.dataLegends.sort(function(a,b) {return d3.ascending(a.valueName+'', b.valueName+'');}) : [null]); barsSeries.enter().append('rect'); barsSeries.attr({ width: xScale.rangeBand(), height: d => height - yScale(<number>d.value), x: d => xScale(d.category)+margins.left, y: d => yScale(<number>d.sumHeight)+margins.top, fill: d => d.color, 'fill-opacity': viewModel.settings.generalView.opacity / 100 }); barsSeries.on('click', function(d) { // Allow selection only if the visual is rendered in a view that supports interactivity (e.g. Report) if (allowInteractions) { selectionManager.select(d.selectionId).then((ids: ISelectionId[]) => { barsSeries.attr({ 'fill-opacity': ids.length > 0 ? Visual.Config.transparentOpacity : Visual.Config.solidOpacity }); d3.select(this).attr({ 'fill-opacity': Visual.Config.solidOpacity }); if (settings.legend.show) { legend.attr({ 'fill-opacity': Visual.Config.solidOpacity }); } }); (<Event>d3.event).stopPropagation(); } }); barsSeries.exit().remove(); //Legends let legendLine = legend.enter().append('g').classed('legendLine', true); let LCircle = legendLine.append('circle'); LCircle.attr({ r: settings.legend.fontSize*0.4, cx: width+settings.legend.fontSize/12*160-settings.legend.fontSize*0.4, fill: d => d.color }); LCircle.attr('cy', function(d,i) {return (i-0.3)*settings.legend.fontSize+margins.top;}); let LText = legendLine.append('text'); LText.attr({ x: width+settings.legend.fontSize/12*160-settings.legend.fontSize, 'font-size': settings.legend.fontSize, fill: d => '#777', 'text-anchor': 'end' }); LText.attr('y', function(d,i) {return i*settings.legend.fontSize+margins.top;}).text(d => d.valueName); legend.on('click', function(bar) { // Allow selection only if the visual is rendered in a view that supports interactivity (e.g. Report) if (allowInteractions) { selectionManager.select(bar.selectionId).then((ids: ISelectionId[]) => { barsSeries.attr({ 'fill-opacity': ids.length > 0 ? Visual.Config.transparentOpacity : Visual.Config.solidOpacity }); legend.attr({ //hide all legend 'fill-opacity': ids.length > 0 ? Visual.Config.transparentOpacity : Visual.Config.solidOpacity }); barsSeries.attr({ //show active bars 'fill-opacity': d => ids[0]['key'] == d.selectionId['key'] ? Visual.Config.solidOpacity : Visual.Config.transparentOpacity }); d3.select(this).attr({ //show active legend 'fill-opacity': Visual.Config.solidOpacity }); }); (<Event>d3.event).stopPropagation(); } }); legend.exit().remove(); this.tooltipServiceWrapper.addTooltip(this.barContainer.selectAll('.bar'), (tooltipEvent: TooltipEventArgs<number>) => this.getTooltipData(tooltipEvent.data), (tooltipEvent: TooltipEventArgs<number>) => null); // Title let fontSizeValue: number = Math.min(width, height) / 5; let fontSizeLabel: number = fontSizeValue / 4; let textLabel = this.textLabel.selectAll('text').data(['textLabel']); textLabel.enter().append('text'); textLabel.attr({ x: width/2, y: 16, fill: '#000', "text-anchor": "middle" }).text('Гистограмма с накоплением'); textLabel.exit().remove(); } public enumerateObjectInstances(options: EnumerateVisualObjectInstancesOptions): VisualObjectInstanceEnumeration { let objectName = options.objectName; let objectEnumeration: VisualObjectInstance[] = []; switch (objectName) { case 'enableAxis': objectEnumeration.push({ objectName: objectName, properties: { show: this.VisualSettings.enableAxis.show, }, selector: null }); break; case 'colorSelector': for (let labelDataLegend of this.labelDataLegends) { objectEnumeration.push({ objectName: objectName, displayName: labelDataLegend.valueName, properties: { fill: { solid: { color: labelDataLegend.color } } }, selector: labelDataLegend.selectionId.getSelector() }); } break; case 'generalView': objectEnumeration.push({ objectName: objectName, properties: { opacity: this.VisualSettings.generalView.opacity, }, validValues: { opacity: { numberRange: { min: 10, max: 100 } } }, selector: null }); break; case 'legend': objectEnumeration.push({ objectName: objectName, displayName: 'show', properties: { show: this.VisualSettings.legend.show, fontSize: this.VisualSettings.legend.fontSize }, validValues: { fontSize: { numberRange: { min: 12, max: 48 } } }, selector: null }); break; }; return objectEnumeration; } public destroy(): void { // Perform any cleanup tasks here } private getTooltipData(value: any): VisualTooltipDataItem[] { return [{ displayName: value.valueName, value: value.value.toString(), color: value.color }]; } } }
My capabilities.json
{ "dataRoles": [ { "displayName": "Ось", "displayNameKey": "Visual_Group", "name": "Category", "kind": "Grouping" }, { "displayName": "Условные обозначения", "displayNameKey": "Visual_Legend", "name": "Series", "kind": "Grouping" }, { "displayName": "Значения", "displayNameKey": "Visual_Values", "name": "Values", "kind": "Measure" } ], "dataViewMappings": [ { "conditions": [ { "Category": { "max": 1 }, "Values": { "min": 0, "max": 1 }, "Series": { "min": 0, "max": 1 } } ], "categorical": { "categories": { "for": { "in": "category" } }, "values": { "group": { "by": "Series", "select": [ { "for": { "in": "Values" } } ], "dataReductionAlgorithm": { "top": {} } } } } } ], "objects": { "enableAxis": { "displayName": "Enable Axis", "properties": { "show": { "displayName": "Enable Axis", "type": { "bool": true } } } }, "colorSelector": { "displayName": "Data Colors", "displayNameKey": "Visual_Data_Colors", "properties": { "fill": { "displayName": "Color", "displayNameKey": "Visual_Color", "type": { "fill": { "solid": { "color": true } } } } } }, "generalView": { "displayName": "General View", "properties": { "opacity": { "displayName": "Bars Opacity", "type": { "integer": true } } } }, "legend": { "displayName": "Legend", "displayNameKey": "Visual_Legend", "properties": { "show": { "displayName": "show", "displayNameKey": "Visual_Show", "type": { "bool": true } }, "fontSize": { "displayName": "Text Size", "displayNameKey": "Visual_TextSize", "type": { "integer": true } } } } } }
Please take a look at the updated code and let me know if it meets your requirements.
Ignat Vilesov,
Software Engineer
Microsoft Power BI Custom Visuals
yes, all done with small issues with clearing legend (legendLine), corrected it.
But because of "grouped()", how i understand, sorting broken. So i can't correct sort data in bars DESC and legends ASC (because legends "grouped()" and their indexes ) and sort(function(a,b) {return d3.ascending(a.source.groupName+'', b.source.groupName+'');} not working. Or may be i dont correct use "sort". Please, how i can sort bars and legends from up to down (and in bars and in legends) on display
UPD: All done, thank you very much!
Do you know where is some documentation like https://docs.unity3d.com/Manual/index.html or http://php.net (i mean full and detailed)
Power BI Custom Visuals documentation is available here.
Ignat Vilesov,
Software Engineer
Microsoft Power BI Custom Visuals
User | Count |
---|---|
16 | |
5 | |
2 | |
1 | |
1 |
User | Count |
---|---|
22 | |
3 | |
3 | |
2 | |
1 |