Skip to content

[Question] How to update visual after selection #553

@GitVex

Description

@GitVex

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:

/*
 * 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');
	}
}

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": []
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions