Forum Discussion
Create stacked column chart
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
- ShutTap8 years agoHelper I
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 }]; } } } - v-viig8 years agoCommunity Champion
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
- ShutTap8 years agoHelper I
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;})
- v-viig8 years agoCommunity 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
- ShutTap8 years agoHelper I
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-viig8 years agoCommunity Champion
You should parse current color by using this code.
Ignat Vilesov,
Software Engineer
Microsoft Power BI Custom Visuals
- ShutTap8 years agoHelper I
It working over Categories, but i need it over Series. How do this?
- v-viig8 years agoCommunity 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
- ShutTap8 years agoHelper II don't understand how colors may be kept in the category, because series have more items than categories. Can you write example?
- v-viig8 years agoCommunity 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
- ShutTap8 years agoHelper I
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-viig8 years agoCommunity Champion
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
- ShutTap8 years agoHelper I
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 - ShutTap8 years agoHelper I
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)