Hello, I have been writing my own bar chart visual lately with categorical data mapping and am now trying to implement selecting a column, and then change the appearance of the other non-selected columns ...
The selection manager and selections are set up correctly, as clicking on one column does highlight other visuals on the page. Though the visual itself does not update ... which I guess makes sense when looking at the visual system integration.
But I am now a little confused as how to implement my desired behavior. which is very much a thing in other standard Power BI visuals. It's odd because when I read the highlighting guide, it seems to work with the assumption that the visual is going to be re-rendered upon selection.
/*
* Power BI Visual CLI
*
* Copyright (c) Microsoft Corporation
* All rights reserved.
* MIT License
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the ""Software""), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
'use strict';
import './../style/visual.less';
import powerbi from 'powerbi-visuals-api';
import VisualConstructorOptions = powerbi.extensibility.visual.VisualConstructorOptions;
import VisualUpdateOptions = powerbi.extensibility.visual.VisualUpdateOptions;
import IVisual = powerbi.extensibility.visual.IVisual;
import IVisualHost = powerbi.extensibility.visual.IVisualHost;
import ISelectionManager = powerbi.extensibility.ISelectionManager;
import ISelectionId = powerbi.visuals.ISelectionId;
import DataView = powerbi.DataView;
import { VisualSettings } from './settings';
import { FormattingSettingsService } from 'powerbi-visuals-utils-formattingmodel';
import { MainLabelSettings, OffsetLabelSettings } from './settings';
// Import D3
import * as d3 from 'd3';
type Selection<T extends d3.BaseType> = d3.Selection<T, any, any, any>;
/**
* Interface for the visual's view model, which holds the data points.
*/
interface BarChartViewModel {
dataPoints: BarChartDataPoint[];
legendData: LegendEntry[];
dataMax: number;
dataMin: number;
hasHighlights: boolean;
}
/**
* Interface for the visual's data points.
*/
interface BarChartDataPoint {
category: Date;
mainValue: number;
offsets: number[];
totalValue: number; // Sum of mainValue and all offsets
selection: ISelectionId;
isHighlighted: boolean;
}
interface LegendEntry {
displayName: string;
color: string;
}
/**
* Main Visual class for the Power BI custom visual.
*/
export class Visual implements IVisual {
private target: HTMLElement;
private host: IVisualHost;
private selectionManager: ISelectionManager;
private visualSettings: VisualSettings;
private formattingSettingsService: FormattingSettingsService;
private svg: Selection<SVGSVGElement>;
private chartContainer: Selection<SVGGElement>;
private legendContainer: Selection<SVGGElement>;
private isValueBased: boolean;
private offsetColors: string[];
private lastOptions: any;
private dateFormater: (date: Date) => string;
// Storing visual settings in a dedicated object
private settings = {
vars: {
ticks: 5,
},
padding: {
inGroup: 0.2,
outGroup: 0.2,
offsetMainRatio: 0.3, // Ratio of bandwidth to be used as padding between main and offset bars
},
ratios: {
mainBar: 0.3, // Ratio of barGroup bandwith allocated for the main bar
},
margin: { top: 30, right: 30, bottom: 30, left: 0 },
};
constructor(options: VisualConstructorOptions) {
this.host = options.host;
this.selectionManager = this.host.createSelectionManager();
this.formattingSettingsService = new FormattingSettingsService();
this.target = options.element;
// Initialize the SVG and chart container
this.svg = d3
.select(this.target)
.append('svg')
.classed('bar-chart', true)
.style('font-family', 'Reddit Sans');
this.chartContainer = this.svg.append('g').classed('chart-container', true);
this.legendContainer = this.svg.append('g').classed('legend-container', true);
// ------ Utility Formatter for Category Labels ------
this.dateFormater = d3.timeFormat('%d.%m.%Y');
// ---------------------------------------------------
}
/* PBIVIZ Method that gets the current values from the settings panel. */
public getFormattingModel(): powerbi.visuals.FormattingModel {
return this.formattingSettingsService.buildFormattingModel(this.visualSettings);
}
/**
* The main update function, called when data or settings change.
* @param options - The update options from Power BI.
*/
public update(options: VisualUpdateOptions) {
this.lastOptions = options;
console.log(`[DEBUG] Visual Update `);
this.visualSettings = this.formattingSettingsService.populateFormattingSettingsModel(
VisualSettings,
options.dataViews[0],
);
const barColors = this.visualSettings.barColors;
this.isValueBased = !barColors.coloringLogic.value;
this.offsetColors = [
barColors.offsetColor1.value.value,
barColors.offsetColor2.value.value,
barColors.offsetColor3.value.value,
barColors.offsetColor4.value.value,
barColors.offsetColor5.value.value,
];
barColors.negativeOffset.visible = this.isValueBased;
barColors.positiveOffset.visible = this.isValueBased;
barColors.offsetColor1.visible = !this.isValueBased;
barColors.offsetColor2.visible = !this.isValueBased;
barColors.offsetColor3.visible = !this.isValueBased;
barColors.offsetColor4.visible = !this.isValueBased;
barColors.offsetColor5.visible = !this.isValueBased;
this.settings.margin.left = this.visualSettings.axis.displayAxis.value ? 60 : 30;
const viewModel = this.transformData(options);
if (!viewModel || viewModel.dataPoints.length === 0) {
this.clearVisual();
return;
}
console.log(`[DEBUG] Visual Update `);
console.log(`[DEBUG] dataView Object: `, options.dataViews[0]);
this.drawVisual(viewModel, options.viewport, viewModel.hasHighlights);
}
/**
* Clears the visual content.
*/
private clearVisual() {
this.chartContainer.selectAll('*').remove();
this.legendContainer.selectAll('*').remove();
}
/**
* Draws the entire visual.
* @param viewModel - The view model containing the data.
* @param viewport - The viewport dimensions.
*/
private drawVisual(
viewModel: BarChartViewModel,
viewport: powerbi.IViewport,
hasHighlights: boolean,
) {
// Clear previous elements before drawing
this.clearVisual();
const { width, height } = viewport;
const { margin } = this.settings;
this.svg.attr('width', width).attr('height', height);
const chartWidth = width - margin.left - margin.right;
const chartHeight = height - margin.top - margin.bottom;
this.chartContainer.attr('transform', `translate(0, ${margin.top})`);
// Create scales
const xScale = d3
.scaleBand()
.domain(viewModel.dataPoints.map((d) => this.dateFormater(d.category)))
.range([0, chartWidth])
.padding(this.settings.padding.outGroup);
const yScale = d3
.scaleLinear()
.domain([Math.min(0, viewModel.dataMin), Math.max(0, viewModel.dataMax)])
.nice()
.range([chartHeight, 0]);
// Draw Axis
if (this.visualSettings.axis.displayAxis.value) {
// Declare Axis Generators
const xAxis = d3.axisBottom(xScale);
const yAxis = d3.axisLeft(yScale).ticks(this.settings.vars.ticks);
// Create "g" SVG Elements in the chart container
const xAxisGroup = this.chartContainer
.append('g')
.classed('x-axis', true)
.attr('transform', `translate(${margin.left}, ${chartHeight})`);
const yAxisGroup = this.chartContainer
.append('g')
.classed('y-axis', true)
.attr('transform', `translate(${margin.left}, ${0})`);
// Render Axis to the SVG
xAxisGroup.call(xAxis);
yAxisGroup.call(yAxis);
// Style Axis Elements
xAxisGroup
.selectAll('.tick')
.attr('font-family', this.visualSettings.mainLabel.font.fontFamily.value);
yAxisGroup
.selectAll('.tick')
.attr('font-family', this.visualSettings.mainLabel.font.fontFamily.value);
}
// Draw bar GROUPS
const barsContainer = this.chartContainer.append('g').classed('bars-container', true);
const barGroups = barsContainer
.selectAll<SVGGElement, BarChartDataPoint>('.bar-group')
.data(viewModel.dataPoints, (d) => this.dateFormater(d.category))
.join('g')
.classed('bar-group', true)
.attr('transform', (d) => `translate(${xScale(this.dateFormater(d.category))}, 0)`);
// Calculate Layout Variables
const { padding, ratios } = this.settings;
const totalBandwidth = xScale.bandwidth();
const hasOffsets = barGroups.data().some((d) => d.offsets.length > 0);
const mainBarWidth = hasOffsets ? totalBandwidth * ratios.mainBar : totalBandwidth;
const offsetMainPadding = hasOffsets ? totalBandwidth * padding.offsetMainRatio : 0;
const offsetBarsGroupWidth = hasOffsets
? totalBandwidth * (1 - ratios.mainBar) - offsetMainPadding
: 0;
const mainBarX = offsetBarsGroupWidth > 0 ? offsetBarsGroupWidth + offsetMainPadding : 0;
// Adjustments
barsContainer.attr(
'transform',
`translate(${margin.left - (offsetBarsGroupWidth + offsetMainPadding) / 2},0)`,
);
const layout = {
padding,
ratios,
totalBandwidth,
hasOffsets,
mainBarWidth,
offsetMainPadding,
offsetBarsGroupWidth,
mainBarX,
};
this.drawBars(barGroups, xScale, yScale, layout, hasHighlights);
this.drawLabels(barGroups, xScale, yScale, chartHeight, layout, hasHighlights);
if (this.visualSettings.legend.displayLegend.value) {
this.drawLegend(viewModel.legendData, width);
}
}
/**
* Draws the bars for each data point group.
* @param barGroups - The D3 selection of bar group elements.
* @param xScale - The x-axis scale.
* @param yScale - The y-axis scale.
*/
private drawBars(
barGroups: Selection<SVGGElement>,
xScale: d3.ScaleBand<string>,
yScale: d3.ScaleLinear<number, number>,
layout,
hasHighlights: boolean,
) {
// --- Main Bars ---
barGroups
.selectAll('.main-bar')
.data((d: BarChartDataPoint) => [d]) // Bind the group's data to a single-element array
.join('rect')
.classed('main-bar', true)
.attr('x', layout.mainBarX)
.attr('y', (d) => (d.mainValue >= 0 ? yScale(d.mainValue) : yScale(0)))
.attr('width', layout.mainBarWidth)
.attr('height', (d) => Math.abs(yScale(0) - yScale(d.mainValue)))
.attr('fill', this.visualSettings.barColors.mainBar.value.value)
.on('click', (event, d) => {
let res: any;
this.selectionManager.select(d.selection).then((pRes) => {
res = pRes;
});
console.log(
this.selectionManager.getSelectionIds().map((sel: ISelectionId) => {
return sel.equals(d.selection);
}),
);
// Selection doesnt update the visual ...
//
// supportsHighlight flag doesnt seem to do anything as dataView doesnt return a highlights array.
// https://learn.microsoft.com/en-us/power-bi/developer/visuals/visuals-interactions
//
// https://learn.microsoft.com/en-us/power-bi/developer/visuals/power-bi-visuals-concept
//
// First step doesnt happen here
// https://learn.microsoft.com/en-us/power-bi/developer/visuals/highlight?tabs=Highlight
console.log(
`[DEBUG] bar ${this.dateFormater(d.category)} was clicked. highlighted State is: ${d.isHighlighted}.`,
);
})
.style('opacity', (d) => {
return this.selectionManager.hasSelection() && !d.isHighlighted ? 0.25 : 1.0;
});
// Early return for if there are no offset bars defined in the visual
if (!layout.hasOffsets) {
barGroups.selectAll('.offset-bar').remove();
return;
}
barGroups.each((d, i, nodes) => {
if (i == 0) return; // Don't create offsets for the first bar
const group = d3.select(nodes[i]);
const prevMainValue = barGroups.data()[i - 1]?.mainValue ?? 0;
const offsetScale = d3
.scaleBand()
.domain(d3.range(d.offsets.length).map(String))
.range([0, layout.offsetBarsGroupWidth])
.padding(layout.padding.inGroup);
// --- Offset bars ---
group
.selectAll('.offset-bar')
.data(d.offsets)
.join('rect')
.classed('offset-bar', true)
.attr('x', (_, j) => offsetScale(String(j)))
.attr('y', (offset: number) =>
offset >= 0 ? yScale(prevMainValue + offset) : yScale(prevMainValue),
)
.attr('width', offsetScale.bandwidth())
.attr('height', (offset) =>
Math.abs(yScale(prevMainValue) - yScale(prevMainValue + offset)),
)
.attr('fill', (offset, j) => this.getOffsetColor(offset as number, j, this.isValueBased));
});
}
/**
* Draws the labels for each bar group.
* @param barGroups - The D3 selection of bar group elements.
* @param xScale - The x-axis scale.
* @param yScale - The y-axis scale.
* @param chartHeight - The height of the chart area.
* @param dateFormater - The date formatting function.
*/
private drawLabels(
barGroups: Selection<SVGGElement>,
xScale: d3.ScaleBand<string>,
yScale: d3.ScaleLinear<number, number>,
chartHeight: number,
layout,
hasHighlights: boolean,
) {
barGroups.each((d, i, nodes) => {
const group = d3.select(nodes[i]);
const prev_group = barGroups.data()[i - 1];
// Main value label
const mainLabel = group
.append('text')
.classed('main-label', true)
.attr('x', layout.mainBarX + layout.mainBarWidth / 2)
.attr('y', d.mainValue >= 0 ? yScale(d.mainValue) - 10 : yScale(d.mainValue) + 20)
.attr('text-anchor', 'middle')
.text(d.mainValue);
this.applyTextFormatting(mainLabel, this.visualSettings.mainLabel);
// Background for main label
const labelBBox = (mainLabel.node() as SVGTextElement).getBBox();
group
.insert('rect', 'text.main-label')
.classed('main-label-bg', true)
.attr('x', labelBBox.x - 4)
.attr('y', labelBBox.y - 2)
.attr('width', labelBBox.width + 8)
.attr('height', labelBBox.height + 4)
.attr('fill', this.visualSettings.mainLabel.backgroundColor.value.value)
.attr('rx', 3);
// Category label
if (!this.visualSettings.axis.displayAxis.value) {
group
.append('text')
.classed('cat-label', true)
.attr('x', layout.mainBarX + layout.mainBarWidth / 2)
.attr('y', chartHeight + 15)
.attr('text-anchor', 'middle')
.attr('font-size', '13px')
.attr('fill', this.visualSettings.mainLabel.fontColor.value.value)
.text(this.dateFormater(d.category));
}
if (!layout.hasOffsets) return;
const offsetScale = d3
.scaleBand()
.domain(d.offsets.map((_, j) => `offset_${j}`))
.range([0, layout.offsetBarsGroupWidth])
.padding(layout.padding.inGroup);
// Offset labels
d.offsets.forEach((offset, j) => {
if (i === 0) return;
const barValue = (prev_group?.mainValue ?? 0) + offset;
const label = group
.append('text')
.classed('offset-label', true)
.attr('x', offsetScale(`offset_${j}`) + offsetScale.bandwidth() / 2)
.attr('y', offset >= 0 ? yScale(barValue) - 5 : yScale(barValue) + 15)
.attr('text-anchor', 'middle')
.text(offset);
this.applyTextFormatting(label, this.visualSettings.offsetLabel);
});
});
}
/**
* Draws the legend at the top of the visual.
* @param legendData - Data for the legend entries.
* @param width - The total width of the visual viewport.
*/
private drawLegend(legendData: LegendEntry[], width: number) {
// Define legend item properties
const itemHeight = 20;
const symbolSize = 10;
const symbolTextPadding = 5;
const horizontalPadding = 15;
// Create a group for each legend item using a data join
const legendItems = this.legendContainer
.selectAll('.legend-item')
.data(legendData)
.join('g')
.classed('legend-item', true);
// Add the colored rectangle symbol for each legend item
legendItems
.append('rect')
.attr('width', symbolSize)
.attr('height', symbolSize)
.attr('y', (itemHeight - symbolSize) / 2) // Center vertically
.attr('fill', (d) => d.color);
// Add the text label for each legend item
legendItems
.append('text')
.attr('x', symbolSize + symbolTextPadding)
.attr('y', itemHeight / 2)
.attr('dy', '0.35em') // Vertical alignment trick
.text((d) => d.displayName)
.style('font-size', '12px')
.style('fill', '#666666');
// Position legend items horizontally
const itemWidths: number[] = [];
legendItems.each(function () {
// 'this' refers to the <g> element of the legend item
itemWidths.push((this as SVGGElement).getBBox().width);
});
let currentX = 0;
legendItems.attr('transform', (d, i) => {
const x = currentX;
currentX += itemWidths[i] + horizontalPadding;
return `translate(${x}, 0)`;
});
// Center the entire legend container horizontally
const totalLegendWidth = this.legendContainer.node().getBBox().width;
let legendPosition: number;
if (this.visualSettings.legend.alignLegend.value === 'left') {
legendPosition = 0;
} else if (this.visualSettings.legend.alignLegend.value === 'center') {
legendPosition = (width - totalLegendWidth) / 2;
} else if (this.visualSettings.legend.alignLegend.value === 'right') {
legendPosition = width - totalLegendWidth;
}
this.legendContainer.attr(
'transform',
`translate(${legendPosition}, 0)`, // 10px from the top
);
}
/**
* Transforms the data from Power BI's DataView into a view model.
* @param options - The update options from Power BI.
* @returns A BarChartViewModel object.
*/
private transformData(options: VisualUpdateOptions): BarChartViewModel {
const dataView = options.dataViews[0];
if (!dataView?.categorical?.categories?.[0] || !dataView?.categorical?.values) {
return null;
}
const categorical = dataView.categorical;
const categories = categorical.categories[0].values;
const values = categorical.values;
// ---- This should have a 'highlights' property, but it doesnt ...
console.log(categorical);
const mainMeasure = values.find((v) => v.source.roles['y_axis']);
const offsetMeasures = values.filter((v) => v.source.roles['offs_y_axis']);
if (!mainMeasure) {
return null;
}
const highlights = mainMeasure.highlights;
let allValues: number[] = [];
const dataPoints: BarChartDataPoint[] = categories.map((category, index) => {
const mainValue = (mainMeasure.values[index] as number) || 0;
// For the first data point, treat offsets as 0, as it's the baseline for the waterfall.
const offsets =
index === 0
? offsetMeasures.map(() => 0)
: offsetMeasures.map((offset) => (offset.values[index] as number) || 0);
const totalValue = mainValue + d3.sum(offsets);
allValues.push(mainValue);
offsets.forEach((offset) => allValues.push(mainValue + offset));
const categorySelectionId = this.host
.createSelectionIdBuilder()
// We only have one category column
.withCategory(categorical.categories[0], index)
.createSelectionId();
return {
category: category as Date,
mainValue,
offsets,
totalValue,
selection: categorySelectionId,
isHighlighted: highlights ? highlights[index] !== null : false,
};
});
const legendData: LegendEntry[] = [];
legendData.push({
displayName: mainMeasure.source.displayName,
color: this.visualSettings.barColors.mainBar.value.value,
});
if (this.isValueBased) {
// Should these even be in the legend?
legendData.push({
displayName: 'Positive',
color: this.visualSettings.barColors.positiveOffset.value.value,
});
legendData.push({
displayName: 'Negative',
color: this.visualSettings.barColors.negativeOffset.value.value,
});
} else {
offsetMeasures.forEach((om, idx) => {
legendData.push({
displayName: om.source.displayName,
color: this.offsetColors[idx],
});
});
}
return {
dataPoints,
dataMax: d3.max(allValues),
dataMin: d3.min(allValues),
legendData,
hasHighlights: dataPoints.some((d) => d.isHighlighted),
};
}
/* Utility Method for getting the Color of an Offset Bar */
private getOffsetColor(value: number, idx: number, isValueBased: boolean) {
let offsetColor;
if (isValueBased) {
offsetColor =
value > 0
? this.visualSettings.barColors.positiveOffset.value.value
: this.visualSettings.barColors.negativeOffset.value.value;
} else {
offsetColor = this.offsetColors[idx];
}
return offsetColor;
}
/* Utility Method for applying text format settings. */
private applyTextFormatting(
textSelection: d3.Selection<SVGTextElement, any, any, any>,
settings: MainLabelSettings | OffsetLabelSettings,
) {
textSelection
.attr('font-size', settings.font.fontSize.value)
.attr('font-family', settings.font.fontFamily.value)
.attr('fill', settings.fontColor.value.value)
.attr('font-weight', settings.font.bold.value ? 'bold' : 'normal')
.attr('fill', settings.fontColor.value.value)
.attr('font-style', settings.font.italic.value ? 'italic' : 'normal')
.attr('text-decoration', settings.font.underline.value ? 'underline' : 'none');
}
}
{
"dataRoles": [
{
"displayName": "X-Axis",
"name": "x_axis",
"kind": "Grouping"
},
{
"displayName": "Y-Axis",
"name": "y_axis",
"kind": "Measure"
},
{
"displayName": "Offset Y-Axis",
"name": "offs_y_axis",
"kind": "Measure"
}
],
"objects": {
"legend": {
"displayName": "Legend",
"properties": {
"displayLegend": {
"displayName": "Display Legend",
"type": {
"bool": true
}
},
"alignLegend": {
"displayName": "Legend Alignment",
"type": {
"formatting": {
"alignment": true
}
}
}
}
},
"axis": {
"displayName": "Axis",
"properties": {
"displayAxis": {
"displayName": "Display Axis",
"type": {
"bool": true
}
}
}
},
"barColors": {
"displayName": "Bar Colors",
"properties": {
"coloringLogic": {
"displayName": "Customize Bar Colors",
"type": {
"bool": true
}
},
"mainBar": {
"displayName": "Main Bar Color",
"type": {
"fill": {
"solid": {
"color": true
}
}
}
},
"positiveOffset": {
"displayName": "Positive Offset Color",
"type": {
"fill": {
"solid": {
"color": true
}
}
}
},
"negativeOffset": {
"displayName": "Negative Offset Color",
"type": {
"fill": {
"solid": {
"color": true
}
}
}
},
"offsetColor1": {
"displayName": "Offset Color 1",
"type": {
"fill": {
"solid": {
"color": true
}
}
}
},
"offsetColor2": {
"displayName": "Offset Color 2",
"type": {
"fill": {
"solid": {
"color": true
}
}
}
},
"offsetColor3": {
"displayName": "Offset Color 3",
"type": {
"fill": {
"solid": {
"color": true
}
}
}
},
"offsetColor4": {
"displayName": "Offset Color 4",
"type": {
"fill": {
"solid": {
"color": true
}
}
}
},
"offsetColor5": {
"displayName": "Offset Color 5",
"type": {
"fill": {
"solid": {
"color": true
}
}
}
}
}
},
"mainLabel": {
"displayName": "Main Labels",
"properties": {
"fontColor": {
"displayName": "Font Color",
"type": {
"fill": {
"solid": {
"color": true
}
}
}
},
"backgroundColor": {
"displayName": "Background Color",
"type": {
"fill": {
"solid": {
"color": true
}
}
}
},
"fontFamily": {
"displayName": "Font Family",
"type": {
"formatting": {
"fontFamily": true
}
}
},
"fontSize": {
"displayName": "Font Size",
"type": {
"numeric": true
}
},
"bold": {
"displayName": "Bold",
"type": {
"bool": true
}
},
"italic": {
"displayName": "Italic",
"type": {
"bool": true
}
},
"underline": {
"displayName": "Underline",
"type": {
"bool": true
}
}
}
},
"offsetLabel": {
"displayName": "Offset Labels",
"properties": {
"fontColor": {
"displayName": "Font Color",
"type": {
"fill": {
"solid": {
"color": true
}
}
}
},
"backgroundColor": {
"displayName": "Background Color",
"type": {
"fill": {
"solid": {
"color": true
}
}
}
},
"fontFamily": {
"displayName": "Font Family",
"type": {
"formatting": {
"fontFamily": true
}
}
},
"fontSize": {
"displayName": "Font Size",
"type": {
"numeric": true
}
},
"bold": {
"displayName": "Bold",
"type": {
"bool": true
}
},
"italic": {
"displayName": "Italic",
"type": {
"bool": true
}
},
"underline": {
"displayName": "Underline",
"type": {
"bool": true
}
}
}
}
},
"dataViewMappings": [
{
"conditions": [
{
"x_axis": { "max": 1 },
"y_axis": { "max": 1 },
"offs_y_axis": { "max": 5 }
}
],
"categorical": {
"categories": {
"for": {
"in": "x_axis"
}
},
"values": {
"select": [
{ "bind": { "to": "y_axis" } },
{ "bind": { "to": "offs_y_axis" } }
]
}
}
}
],
"supportsHighlight": true,
"supportsMultiVisualSelection": true,
"privileges": []
}
Hello, I have been writing my own bar chart visual lately with categorical data mapping and am now trying to implement selecting a column, and then change the appearance of the other non-selected columns ...
Given the guide on the microsoft documentation, I made all the steps to ensure that highlighting and selection is set up correctly.
The selection manager and selections are set up correctly, as clicking on one column does highlight other visuals on the page. Though the visual itself does not update ... which I guess makes sense when looking at the visual system integration.
But I am now a little confused as how to implement my desired behavior. which is very much a thing in other standard Power BI visuals. It's odd because when I read the highlighting guide, it seems to work with the assumption that the visual is going to be re-rendered upon selection.
Am I misreading or misinterpretating something here?
As a reference, here is my source code:
And my capabilites.json:
{ "dataRoles": [ { "displayName": "X-Axis", "name": "x_axis", "kind": "Grouping" }, { "displayName": "Y-Axis", "name": "y_axis", "kind": "Measure" }, { "displayName": "Offset Y-Axis", "name": "offs_y_axis", "kind": "Measure" } ], "objects": { "legend": { "displayName": "Legend", "properties": { "displayLegend": { "displayName": "Display Legend", "type": { "bool": true } }, "alignLegend": { "displayName": "Legend Alignment", "type": { "formatting": { "alignment": true } } } } }, "axis": { "displayName": "Axis", "properties": { "displayAxis": { "displayName": "Display Axis", "type": { "bool": true } } } }, "barColors": { "displayName": "Bar Colors", "properties": { "coloringLogic": { "displayName": "Customize Bar Colors", "type": { "bool": true } }, "mainBar": { "displayName": "Main Bar Color", "type": { "fill": { "solid": { "color": true } } } }, "positiveOffset": { "displayName": "Positive Offset Color", "type": { "fill": { "solid": { "color": true } } } }, "negativeOffset": { "displayName": "Negative Offset Color", "type": { "fill": { "solid": { "color": true } } } }, "offsetColor1": { "displayName": "Offset Color 1", "type": { "fill": { "solid": { "color": true } } } }, "offsetColor2": { "displayName": "Offset Color 2", "type": { "fill": { "solid": { "color": true } } } }, "offsetColor3": { "displayName": "Offset Color 3", "type": { "fill": { "solid": { "color": true } } } }, "offsetColor4": { "displayName": "Offset Color 4", "type": { "fill": { "solid": { "color": true } } } }, "offsetColor5": { "displayName": "Offset Color 5", "type": { "fill": { "solid": { "color": true } } } } } }, "mainLabel": { "displayName": "Main Labels", "properties": { "fontColor": { "displayName": "Font Color", "type": { "fill": { "solid": { "color": true } } } }, "backgroundColor": { "displayName": "Background Color", "type": { "fill": { "solid": { "color": true } } } }, "fontFamily": { "displayName": "Font Family", "type": { "formatting": { "fontFamily": true } } }, "fontSize": { "displayName": "Font Size", "type": { "numeric": true } }, "bold": { "displayName": "Bold", "type": { "bool": true } }, "italic": { "displayName": "Italic", "type": { "bool": true } }, "underline": { "displayName": "Underline", "type": { "bool": true } } } }, "offsetLabel": { "displayName": "Offset Labels", "properties": { "fontColor": { "displayName": "Font Color", "type": { "fill": { "solid": { "color": true } } } }, "backgroundColor": { "displayName": "Background Color", "type": { "fill": { "solid": { "color": true } } } }, "fontFamily": { "displayName": "Font Family", "type": { "formatting": { "fontFamily": true } } }, "fontSize": { "displayName": "Font Size", "type": { "numeric": true } }, "bold": { "displayName": "Bold", "type": { "bool": true } }, "italic": { "displayName": "Italic", "type": { "bool": true } }, "underline": { "displayName": "Underline", "type": { "bool": true } } } } }, "dataViewMappings": [ { "conditions": [ { "x_axis": { "max": 1 }, "y_axis": { "max": 1 }, "offs_y_axis": { "max": 5 } } ], "categorical": { "categories": { "for": { "in": "x_axis" } }, "values": { "select": [ { "bind": { "to": "y_axis" } }, { "bind": { "to": "offs_y_axis" } } ] } } } ], "supportsHighlight": true, "supportsMultiVisualSelection": true, "privileges": [] }