Skip to main content
cancel
Showing results for 
Search instead for 
Did you mean: 

Earn the coveted Fabric Analytics Engineer certification. 100% off your exam for a limited time only!

Reply
ShutTap
Helper I
Helper I

Create stacked column chart

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

32 REPLIES 32
v-viig
Community Champion
Community Champion

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

pbicvsupport@microsoft.com

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

v-viig
Community Champion
Community Champion

You should parse current color by using this code.

 

Ignat Vilesov,

Software Engineer

 

Microsoft Power BI Custom Visuals

pbicvsupport@microsoft.com

It working over Categories, but i need it over Series. How do this?

v-viig
Community Champion
Community Champion

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

pbicvsupport@microsoft.com

I don't understand how colors may be kept in the category, because series have more items than categories. Can you write example?
v-viig
Community Champion
Community Champion

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

pbicvsupport@microsoft.com

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 }
				}
			}
		}
	}
}
v-viig
Community Champion
Community Champion

Please take a look at the updated code and let me know if it meets your requirements.image.png

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Ignat Vilesov,

Software Engineer

 

Microsoft Power BI Custom Visuals

pbicvsupport@microsoft.com

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 displaysort.jpg

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)

v-viig
Community Champion
Community Champion

Power BI Custom Visuals documentation is available here.

 

Ignat Vilesov,

Software Engineer

 

Microsoft Power BI Custom Visuals

pbicvsupport@microsoft.com

Helpful resources

Announcements
April AMA free

Microsoft Fabric AMA Livestream

Join us Tuesday, April 09, 9:00 – 10:00 AM PST for a live, expert-led Q&A session on all things Microsoft Fabric!

March Fabric Community Update

Fabric Community Update - March 2024

Find out what's new and trending in the Fabric Community.