Register now to learn Fabric in free live sessions led by the best Microsoft experts. From Apr 16 to May 9, in English and Spanish.
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
"legend" i mean legend as labels and legend as stack
Hello @ShutTap,
You might take a look at powerbi-visuals-utils-chartutils that support legend.
You might also take a look at Tornado Chart that uses this module.
Ignat Vilesov,
Software Engineer
Microsoft Power BI Custom Visuals
Thanks, very helpful
One more question: which analogue of getCategoricalObjectValue but for Values (in "legend"), it's needed to add default color to groups of legend, that repeated from category to category
getCategoricalObjectValue<Fill>(category, k, 'colorSelector', 'fill', defaultColor)
but fo legends value.
it's named "series", ok...
You might get a default color for each category by using colorPalette.getColor method.
Please take a look at this example to find out more.
Ignat Vilesov,
Software Engineer
Microsoft Power BI Custom Visuals
Thanks, great 🙂
One more question.
I take color from colorPallete, display colors of legends (series) in Editor, but i don't know how associate it. To edit colors from Editor
You should follow these steps below to assign colors for fromatting panel:
Please let me know if you have extra questions.
Ignat Vilesov,
Software Engineer
Microsoft Power BI Custom Visuals
Thank you very much! 🙂
When visualisation updates i clear old data with .exit().remove(); but it not working. What i doing wrong?
Also i try to package project to PowerBI Desktop, it exports without errors, but in PBI Desctop clear white plane without any data. What it can be?
P.S. There is no official documentation?
Can you post a piece of code that clears old data?
Does your visual work in Power BI web? Are there any exceptions?
Ignat Vilesov,
Software Engineer
Microsoft Power BI Custom Visuals
In PowerBI web is working correctly, exception - no clearing old data.
For example:
let barsSeries = this.barContainer.selectAll('dev').data(viewModel.dataPoints); barsSeries.enter().append('rect').classed('bar', true).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 });
and clear it
barsSeries.exit().remove();
This code looks well for me but I have a couple of questions:
Ignat Vilesov,
Software Engineer
Microsoft Power BI Custom Visuals
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; index: PrimitiveValue; conventions: string; color: string; selectionId: powerbi.visuals.ISelectionId; }; interface VisualSettings { enableAxis: { show: boolean; }; generalView: { opacity: number; }; legend: { show: boolean; }; /* colorSelector: { defaultColor: string[]; };*/ } function visualTransform(options: VisualUpdateOptions, host: IVisualHost): VisualViewModel { let dataViews = options.dataViews; let defaultSettings: VisualSettings = { enableAxis: { show: false }, generalView: { opacity: 100 }, legend: { show: true }/*, 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), }/*, colorSelector: { defaultColor: [] }*/ }; dataValue.sort(function(a,b) {return d3.ascending(a.source.groupName+'', b.source.groupName+'');}); for (let k = 0; k < dataValue.length; k++) { //VisualSettings.colorSelector.defaultColor[k] = colorPalette.getColor(dataValue[k].source.groupName + '').value; VisualDataLegends.push({ valueName: dataValue[k].source.groupName + '', index: k, conventions: 'dataValue[k].source.groupName', color: colorPalette.getColor(dataValue[k].source.groupName+'').value, selectionId: host.createSelectionIdBuilder().withMeasure(dataValue[k].source.groupName+'').createSelectionId() }); } dataMax=0; dataValue.sort(function(a,b) {return d3.descending(a.source.groupName+'', b.source.groupName+'');}); 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]; VisualDataPoints.push({ category: category.values[i] + '', valueName: dataValue[k].source.groupName + '', value: dataValue[k].values[i], sumHeight: sumHeight, conventions: 'dataValue[k].source.groupName', color: colorPalette.getColor(dataValue[k].source.groupName+'').value, selectionId: host.createSelectionIdBuilder().withMeasure(dataValue[k].source.groupName+'').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 title: d3.Selection<SVGElement>; private legend: d3.Selection<SVGElement>; static Config = { xScalePadding: 0.2, solidOpacity: 1, transparentOpacity: 0.5, margins: { top: 30, right: 160, 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.title = 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 -= margins.right; } 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; let legend = this.legend.selectAll('dev').data(viewModel.dataLegends); //bars (with series) let barsSeries = this.barContainer.selectAll('dev').data(viewModel.dataPoints); barsSeries.enter().append('rect').classed('bar', true).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 }); //console.log(viewModel.dataPoints); 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(); } }); //Legends if (settings.legend.show) { legend.enter().append('circle').classed('legendPoint', true).attr({ r: 4, cx: width+margins.right-8, cy: d => <number>d.index * 14+margins.top-4, fill: d => d.color }); legend.enter().append('text').classed('legend', true).attr({ x: width+margins.right-20, y: d => <number>d.index * 14+margins.top, 'font-size': 12, fill: d => '#777', 'text-anchor': 'end' }).text(d => d.valueName); 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; this.title.append('text').attr({ x: "50%", y: 16, fill: '#000', "text-anchor": "middle" }).text('Гистограмма с накоплением'); 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(); } }); } barsSeries.exit().remove(); legend.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 }, 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 }]; } } }
There's an issue in selector:
this.barContainer.selectAll('dev').data(viewModel.dataPoints);
You must select all elements by ".bar" selector instead of "dev" ("dev" selector assumes that there're DOM element called <DEV> but there aren't).
Ignat Vilesov,
Software Engineer
Microsoft Power BI Custom Visuals
Thanks,
replaced
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; index: PrimitiveValue; conventions: string; color: string; selectionId: powerbi.visuals.ISelectionId; }; interface VisualSettings { enableAxis: { show: boolean; }; generalView: { opacity: number; }; legend: { show: boolean; }; /* colorSelector: { defaultColor: string[]; };*/ } function visualTransform(options: VisualUpdateOptions, host: IVisualHost): VisualViewModel { let dataViews = options.dataViews; let defaultSettings: VisualSettings = { enableAxis: { show: false }, generalView: { opacity: 100 }, legend: { show: true }/*, 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), }/*, colorSelector: { defaultColor: [] }*/ }; dataValue.sort(function(a,b) {return d3.ascending(a.source.groupName+'', b.source.groupName+'');}); for (let k = 0; k < dataValue.length; k++) { //VisualSettings.colorSelector.defaultColor[k] = colorPalette.getColor(dataValue[k].source.groupName + '').value; VisualDataLegends.push({ valueName: dataValue[k].source.groupName + '', index: k, conventions: 'dataValue[k].source.groupName', color: colorPalette.getColor(dataValue[k].source.groupName+'').value, selectionId: host.createSelectionIdBuilder().withMeasure(dataValue[k].source.groupName+'').createSelectionId() }); } dataMax=0; dataValue.sort(function(a,b) {return d3.descending(a.source.groupName+'', b.source.groupName+'');}); 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]; VisualDataPoints.push({ category: category.values[i] + '', valueName: dataValue[k].source.groupName + '', value: dataValue[k].values[i], sumHeight: sumHeight, conventions: 'dataValue[k].source.groupName', color: colorPalette.getColor(dataValue[k].source.groupName+'').value, selectionId: host.createSelectionIdBuilder().withMeasure(dataValue[k].source.groupName+'').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 title: d3.Selection<SVGElement>; private legend: d3.Selection<SVGElement>; static Config = { xScalePadding: 0.2, solidOpacity: 1, transparentOpacity: 0.5, margins: { top: 30, right: 160, 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.title = 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 -= margins.right; } 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; let legend = this.legend.selectAll('.legend').data(viewModel.dataLegends); //bars (with series) let barsSeries = this.barContainer.selectAll('.bar').data(viewModel.dataPoints); barsSeries.enter().append('rect').classed('bar', true).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 }); //console.log(viewModel.dataPoints); 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(); } }); //Legends if (settings.legend.show) { legend.enter().append('circle').classed('legend', true).attr({ r: 4, cx: width+margins.right-8, cy: d => <number>d.index * 14+margins.top-4, fill: d => d.color }); legend.enter().append('text').classed('legend', true).attr({ x: width+margins.right-20, y: d => <number>d.index * 14+margins.top, 'font-size': 12, fill: d => '#777', 'text-anchor': 'end' }).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(); } }); } 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; this.title.append('text').attr({ x: "50%", y: 16, fill: '#000', "text-anchor": "middle" }).text('Гистограмма с накоплением'); barsSeries.exit().remove(); legend.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 }, 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 }]; } } }
May be i don't understand something, but, for example, this.legend = svg.append("g").classed("legend", true); - created "layer", then let legend = this.legend.selectAll('.legend').data(viewModel.dataLegends); - put some prepared elements with class "legend", fill with data, then put some elements based at previous prepared elements legend.enter().append('circle').classed('legend', true), legend.enter().append('text').classed('legend', true) ?
So, we have some elements with data and visualization. Next, i have legend.on('click', function(bar) that doing something at clicking at "legends". Why it working with last added legend.enter().append('text') only , but not working at both (legend.enter().append('circle') and legend.enter().append('text')). What i understand wrong?
I'm not sure that your D3 flow is correct. It'b be good to use this flow:
let legend = svg.selectAll('.legend').data(viewModel.dataLegends); legend .enter() .append("g") .classed("legend", true);
let circle = legend.selectAll("circle").data([{put your data here}]);
circle
.enter()
.append('circle');
circle
.exit()
.remove();
let text = legend.selectAll("text").data([{put your data here}]);
text
.enter()
.append('text');
text
.exit()
.remove();
legend .exit() .remove();
Ignat Vilesov,
Software Engineer
Microsoft Power BI Custom Visuals
trying to use it, is no difference...
Can you share the updated source code to investigate this issue deeper?
Ignat Vilesov,
Software Engineer
Microsoft Power BI Custom Visuals
I try to make working only "title" (textLabel), but it still not working... i don't undersand why)
export class Visual implements IVisual { private textLabel: d3.Selection<SVGElement>; ...
constructor(options: VisualConstructorOptions) { this.textLabel = svg.append("g").classed("textLabel", true);
...
public update(options: VisualUpdateOptions, title: string) {
...
// Title let fontSizeValue: number = Math.min(width, height) / 5; let fontSizeLabel: number = fontSizeValue / 4; let textLabel = this.textLabel; textLabel.append('text').attr({ x: width/2, y: 16+'px', fill: '#000', "text-anchor": "middle" }).text('Some text');
...
UPD
I take example of sampleBarChart from Git, insert my code of textLabel - and situation repeats but only with textLabel. Bars are ok.
module powerbi.extensibility.visual { /** * Interface for BarCharts viewmodel. * * @interface * @property {BarChartDataPoint[]} dataPoints - Set of data points the visual will render. * @property {number} dataMax - Maximum data value in the set of data points. */ interface BarChartViewModel { dataPoints: BarChartDataPoint[]; dataMax: number; settings: BarChartSettings; }; /** * Interface for BarChart data points. * * @interface * @property {number} value - Data value for point. * @property {string} category - Corresponding category of data value. * @property {string} color - Color corresponding to data point. * @property {ISelectionId} selectionId - Id assigned to data point for cross filtering * and visual interaction. */ interface BarChartDataPoint { value: PrimitiveValue; category: string; color: string; selectionId: powerbi.visuals.ISelectionId; }; /** * Interface for BarChart settings. * * @interface * @property {{show:boolean}} enableAxis - Object property that allows axis to be enabled. */ interface BarChartSettings { enableAxis: { show: boolean; }; generalView: { opacity: number; }; } /** * Function that converts queried data into a view model that will be used by the visual. * * @function * @param {VisualUpdateOptions} options - Contains references to the size of the container * and the dataView which contains all the data * the visual had queried. * @param {IVisualHost} host - Contains references to the host which contains services */ function visualTransform(options: VisualUpdateOptions, host: IVisualHost): BarChartViewModel { let dataViews = options.dataViews; let defaultSettings: BarChartSettings = { enableAxis: { show: false, }, generalView: { opacity: 100 } }; let viewModel: BarChartViewModel = { dataPoints: [], dataMax: 0, settings: <BarChartSettings>{} }; 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[0]; let barChartDataPoints: BarChartDataPoint[] = []; let dataMax: number; let colorPalette: IColorPalette = host.colorPalette; let objects = dataViews[0].metadata.objects; let barChartSettings: BarChartSettings = { enableAxis: { show: getValue<boolean>(objects, 'enableAxis', 'show', defaultSettings.enableAxis.show), }, generalView: { opacity: getValue<number>(objects, 'generalView', 'opacity', defaultSettings.generalView.opacity), } }; for (let i = 0, len = Math.max(category.values.length, dataValue.values.length); i < len; i++) { let defaultColor: Fill = { solid: { color: colorPalette.getColor(category.values[i] + '').value } }; barChartDataPoints.push({ category: category.values[i] + '', value: dataValue.values[i], color: getCategoricalObjectValue<Fill>(category, i, 'colorSelector', 'fill', defaultColor).solid.color, selectionId: host.createSelectionIdBuilder() .withCategory(category, i) .createSelectionId() }); } dataMax = <number>dataValue.maxLocal; return { dataPoints: barChartDataPoints, dataMax: dataMax, settings: barChartSettings, }; } export class BarChart implements IVisual { private svg: d3.Selection<SVGElement>; private host: IVisualHost; private selectionManager: ISelectionManager; private barChartContainer: d3.Selection<SVGElement>; private barContainer: d3.Selection<SVGElement>; private xAxis: d3.Selection<SVGElement>; private barDataPoints: BarChartDataPoint[]; private barChartSettings: BarChartSettings; private tooltipServiceWrapper: ITooltipServiceWrapper; private locale: string; private textLabel: d3.Selection<SVGElement>; static Config = { xScalePadding: 0.1, solidOpacity: 1, transparentOpacity: 0.5, margins: { top: 0, right: 0, bottom: 25, left: 30, }, xAxisFontMultiplier: 0.04, }; /** * Creates instance of BarChart. This method is only called once. * * @constructor * @param {VisualConstructorOptions} options - Contains references to the element that will * contain the visual and a reference to the host * which contains services. */ 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('barChart', 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); } /** * Updates the state of the visual. Every sequential databinding and resize will call update. * * @function * @param {VisualUpdateOptions} options - Contains references to the size of the container * and the dataView which contains all the data * the visual had queried. */ public update(options: VisualUpdateOptions) { let viewModel: BarChartViewModel = visualTransform(options, this.host); let settings = this.barChartSettings = viewModel.settings; this.barDataPoints = viewModel.dataPoints; let width = options.viewport.width; let height = options.viewport.height; this.svg.attr({ width: width, height: height }); if (settings.enableAxis.show) { let margins = BarChart.Config.margins; height -= margins.bottom; } this.xAxis.style({ 'font-size': d3.min([height, width]) * BarChart.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], BarChart.Config.xScalePadding, 0.2); let xAxis = d3.svg.axis() .scale(xScale) .orient('bottom'); this.xAxis.attr('transform', 'translate(0, ' + height + ')') .call(xAxis); let bars = this.barContainer.selectAll('.bar').data(viewModel.dataPoints); bars.enter() .append('rect') .classed('bar', true); bars.attr({ width: xScale.rangeBand(), height: d => height - yScale(<number>d.value), y: d => yScale(<number>d.value), x: d => xScale(d.category), fill: d => d.color, 'fill-opacity': viewModel.settings.generalView.opacity / 100 }); // Title let fontSizeValue: number = Math.min(width, height) / 5; let fontSizeLabel: number = fontSizeValue / 4; let textLabel = this.textLabel; textLabel.append('text').attr({ x: width/2, y: 16+'px', fill: '#000', "text-anchor": "middle" }).text('Some text'); this.tooltipServiceWrapper.addTooltip(this.barContainer.selectAll('.bar'), (tooltipEvent: TooltipEventArgs<number>) => this.getTooltipData(tooltipEvent.data), (tooltipEvent: TooltipEventArgs<number>) => null); let selectionManager = this.selectionManager; let allowInteractions = this.host.allowInteractions; // This must be an anonymous function instead of a lambda because // d3 uses 'this' as the reference to the element that was clicked. bars.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[]) => { bars.attr({ 'fill-opacity': ids.length > 0 ? BarChart.Config.transparentOpacity : BarChart.Config.solidOpacity }); d3.select(this).attr({ 'fill-opacity': BarChart.Config.solidOpacity }); }); (<Event>d3.event).stopPropagation(); } }); bars.exit() .remove(); } /** * Enumerates through the objects defined in the capabilities and adds the properties to the format pane * * @function * @param {EnumerateVisualObjectInstancesOptions} options - Map of defined objects */ public enumerateObjectInstances(options: EnumerateVisualObjectInstancesOptions): VisualObjectInstanceEnumeration { let objectName = options.objectName; let objectEnumeration: VisualObjectInstance[] = []; switch (objectName) { case 'enableAxis': objectEnumeration.push({ objectName: objectName, properties: { show: this.barChartSettings.enableAxis.show, }, selector: null }); break; case 'colorSelector': for (let barDataPoint of this.barDataPoints) { objectEnumeration.push({ objectName: objectName, displayName: barDataPoint.category, properties: { fill: { solid: { color: barDataPoint.color } } }, selector: barDataPoint.selectionId.getSelector() }); } break; case 'generalView': objectEnumeration.push({ objectName: objectName, properties: { opacity: this.barChartSettings.generalView.opacity, }, validValues: { opacity: { numberRange: { min: 10, max: 100 } } }, selector: null }); break; }; return objectEnumeration; } /** * Destroy runs when the visual is removed. Any cleanup that the visual needs to * do should be done here. * * @function */ public destroy(): void { // Perform any cleanup tasks here } private getTooltipData(value: any): VisualTooltipDataItem[] { let language = getLocalizedString(this.locale, "LanguageKey"); return [{ displayName: value.category, value: value.value.toString(), color: value.color, header: language && "displayed language " + language }]; } } }
Your code creates a new element for each update call.
This issue can be fixed by using D3's data-binding. It will look something like this in your case:
const textLabelSelection = this.textLabel .selectAll("text") .data(["Some text"]); textLabelSelection .enter() .append("text"); textLabelSelection .attr({ x: width / 2, y: 16 + 'px', }) .style({ fill: '#000', "text-anchor": "middle", }) .text((textValue) => textValue); textLabelSelection .exit() .remove();
Ignat Vilesov,
Software Engineer
Microsoft Power BI Custom Visuals
All done, thanks.
Colors are not associate with the editor (in Editor takes from bars, but can't edits from Editor)
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; index: PrimitiveValue; conventions: string; color: string; selectionId: powerbi.visuals.ISelectionId; }; interface VisualSettings { enableAxis: { show: boolean; }; generalView: { opacity: number; }; legend: { show: boolean; }; /* colorSelector: { defaultColor: string[]; };*/ } function visualTransform(options: VisualUpdateOptions, host: IVisualHost): VisualViewModel { let dataViews = options.dataViews; let defaultSettings: VisualSettings = { enableAxis: { show: false }, generalView: { opacity: 100 }, legend: { show: true }/*, 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), }/*, colorSelector: { defaultColor: [] }*/ }; dataValue.sort(function(a,b) {return d3.ascending(a.source.groupName+'', b.source.groupName+'');}); for (let k = 0; k < dataValue.length; k++) { //VisualSettings.colorSelector.defaultColor[k] = colorPalette.getColor(dataValue[k].source.groupName + '').value; VisualDataLegends.push({ valueName: dataValue[k].source.groupName + '', index: k, conventions: 'dataValue[k].source.groupName', color: colorPalette.getColor(dataValue[k].source.groupName+'').value, selectionId: host.createSelectionIdBuilder().withMeasure(dataValue[k].source.groupName+'').createSelectionId() }); } dataMax=0; dataValue.sort(function(a,b) {return d3.descending(a.source.groupName+'', b.source.groupName+'');}); 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]; VisualDataPoints.push({ category: category.values[i] + '', valueName: dataValue[k].source.groupName + '', value: dataValue[k].values[i], sumHeight: sumHeight, conventions: 'dataValue[k].source.groupName', color: colorPalette.getColor(dataValue[k].source.groupName+'').value, selectionId: host.createSelectionIdBuilder().withMeasure(dataValue[k].source.groupName+'').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: 160, 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 -= margins.right; } 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 : [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); legendLine.append('circle').attr({ r: 4, cx: width+margins.right-8, cy: d => <number>d.index * 14+margins.top-4, fill: d => d.color }); legendLine.append('text').attr({ x: width+margins.right-20, y: d => <number>d.index * 14+margins.top, 'font-size': 12, fill: d => '#777', 'text-anchor': 'end' }).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 }, 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 }]; } } }
And i think i wrong using sorting. How i can to get index of element in "append"? like d => d.value, but i => i.index or something like this
UPD .attr('y', function(d,i) {return i;})
Covering the world! 9:00-10:30 AM Sydney, 4:00-5:30 PM CET (Paris/Berlin), 7:00-8:30 PM Mexico City
Check out the April 2024 Power BI update to learn about new features.
User | Count |
---|---|
15 | |
2 | |
1 | |
1 | |
1 |