Check your eligibility for this 50% exam voucher offer and join us for free live learning sessions to get prepared for Exam DP-700.
Get StartedDon't miss out! 2025 Microsoft Fabric Community Conference, March 31 - April 2, Las Vegas, Nevada. Use code MSCUST for a $150 discount. Prices go up February 11th. Register now.
Hi,
I've created a custom 3D Pie Chart using react + typescript.
I need to define property to
i) change color of each slice based on adoptive data
ii) font size & style of dataLabels
in format visual panel under Vizualizations pane.
Please suggest a approach to take for same.
I'm attaching my source code snippets as well.
PieChart.tsx File:
import * as React from "react";
import * as Highcharts from "highcharts";
import HighchartsReact from "highcharts-react-official";
import HC_more from "highcharts/highcharts-3d";
import powerbi from "powerbi-visuals-api";
import DataView = powerbi.DataView;
import IVisual = powerbi.extensibility.visual.IVisual;
import VisualConstructorOptions = powerbi.extensibility.visual.VisualConstructorOptions;
import VisualUpdateOptions = powerbi.extensibility.visual.VisualUpdateOptions;
// Initialize the 3D module
HC_more(Highcharts);
interface Props {
dataView: DataView | undefined;
chartData?: { name: string; y: number }[]; // Make chartData optional
}
interface State {
chartOptions: Highcharts.Options;
}
export class CustomPieChart extends React.Component<Props, State> implements IVisual {
private target: HTMLElement;
private updateCount: number;
constructor(props: Props) {
super(props);
this.updateCount = 0;
const defaultData = [
{ name: "No Data", y: 100 }
];
this.state = {
chartOptions: this.getChartOptions(props.chartData || defaultData)
};
}
public update(options: VisualUpdateOptions) {
if (options.dataViews && options.dataViews[0]) {
const dataView = options.dataViews[0];
const extractedData = this.extractDataFromDataView(dataView);
const userDefinedColors = extractedData.map((_, index) => {
const dataPointObject = dataView.metadata.objects?.dataPoint;
const fillProperty = dataPointObject && dataPointObject[`fill_$[index]`];
return typeof fillProperty === 'object' && 'solid' in fillProperty ? fillProperty.solid.color : undefined;
// return dataPointObject && dataPointObject[`fill_${index}`]?.solid?.color;
});
const dynamicColors = extractedData.map((_, index) =>
userDefinedColors[index] || this.state.chartOptions.colors?.[index]
);
const chartOptions = this.getChartOptions(extractedData);
chartOptions.colors = dynamicColors; // Apply updated colors
this.setState({ chartOptions: this.getChartOptions(extractedData) });
}
}
componentDidMount() {
if (this.props.dataView) {
const extractedData = this.extractDataFromDataView(this.props.dataView);
this.setState({ chartOptions: this.getChartOptions(extractedData) });
}
}
componentDidUpdate(prevProps: Props) {
if (prevProps.chartData !== this.props.chartData) {
this.setState({ chartOptions: this.getChartOptions(this.props.chartData) });
}
}
extractDataFromDataView(dataView: DataView): any[] {
if (!dataView?.categorical?.categories?.[0]?.values || !dataView?.categorical?.values?.[0]?.values) {
return [{ name: "No Data", y: 100 }];
}
try {
const categorical = dataView.categorical;
const categories = categorical.categories[0].values;
const values = categorical.values[0].values;
return categories.map((category, index) => ({
name: String(category),
y: Number(values[index]) || 0
}));
} catch (error) {
console.error("Error extracting data:", error);
return [{ name: "Error", y: 100 }];
}
}
getChartOptions(data: any[]): Highcharts.Options {
return {
colors: ['#AEDFF7', '#73C6F3', '#3498DB', '#2E86C1', '#1B4F72', '#154360'],
// colors: ['#00a6e9', '#9bdafc'],
chart: {
type: 'pie',
options3d: {
enabled: true,
alpha: 50,
beta: 0
},
height: 400,
animation: true,
backgroundColor: 'transparent'
},
title: {
text: '3D Pie Chart',
align: 'left',
style: {
fontSize: '14px',
color: 'red'
}
},
accessibility: {
point: {
valueSuffix: '%'
}
},
tooltip: {
pointFormat: '{series.name}: <b>{point.percentage:.1f}%</b>'
},
plotOptions: {
pie: {
allowPointSelect: true,
cursor: 'pointer',
depth: 55,
dataLabels: {
enabled: true,
useHTML: true,
formatter: function () {
const point = this.point;
const color = point.color || 'black'; // Default to black if no color is defined
return `<span style="color:${color}; font-size:11px;">${point.name}: ${point.percentage.toFixed(1)}%</span>`;
},
style: {
fontSize: '11px',
textOutline: 'none' // Prevent text from having an outline
}
},
showInLegend: true
}
},
legend: {
enabled: true,
itemStyle: {
fontSize: '11px'
}
},
series: [{
type: 'pie',
name: 'Share',
data: data
}] as any,
credits: {
enabled: false
}
};
}
render() {
return (
<div style={{
minHeight: "400px",
width: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center"
}}>
<HighchartsReact
highcharts={Highcharts}
options={this.state.chartOptions}
containerProps={{ style: { height: "100%", width: "100%" } }}
/>
</div>
);
}
}
export default CustomPieChart;
Visual.ts File:
import * as React from "react";
import * as ReactDOM from "react-dom";
import DataView = powerbi.DataView;
import VisualConstructorOptions = powerbi.extensibility.visual.VisualConstructorOptions;
import VisualUpdateOptions = powerbi.extensibility.visual.VisualUpdateOptions;
import IVisual = powerbi.extensibility.visual.IVisual;
import ITooltipService = powerbi.extensibility.ITooltipService;
import IVisualHost = powerbi.extensibility.visual.IVisualHost;
import { VisualFormattingSettingsModel } from "./settings";
import customPieChart from "./PieChart";
import powerbi from "powerbi-visuals-api";
import "./../style/visual.less";
export class Visual implements IVisual {
private chartData: Array<{ name: string; y: number }> = [{ name: "No Data", y: 100 }];
private target: HTMLElement;
private reactRoot: React.ReactElement;
private dataView: DataView | undefined;
private host: IVisualHost;
private visualSettings: VisualFormattingSettingsModel;
private tooltipService: ITooltipService;
constructor(options: VisualConstructorOptions) {
this.target = options.element;
this.host = options.host;
this.chartData = []
this.tooltipService = this.host.tooltipService;
// Initialize with empty chartData
this.reactRoot = React.createElement(customPieChart, {
dataView: undefined,
chartData: [{ name: "No Data", y: 100 }]
});
ReactDOM.render(this.reactRoot, this.target);
}
public update(options: VisualUpdateOptions) {
// Update the dataView with the new data
this.dataView = options.dataViews?.[0];
let extractedData = [{ name: "No Data", y: 100 }]; // Default data
if (this.dataView && this.dataView.categorical) {
const categorical = this.dataView.categorical;
const categories = categorical.categories?.[0]?.values;
const values = categorical.values?.[0]?.values;
if (categories && values) {
extractedData = categories.map((category, index) => ({
name: String(category),
y: Number(values[index]) || 0
}));
}
}
// Re-render the component with the updated or default data
this.reactRoot = React.createElement(customPieChart, {
dataView: this.dataView,
chartData: extractedData
});
ReactDOM.render(this.reactRoot, this.target);
}
public enumerateObjectInstances(options: powerbi.EnumerateVisualObjectInstancesOptions): powerbi.VisualObjectInstanceEnumeration {
const enumeration: powerbi.VisualObjectInstance[] = [];
if (options.objectName === "dataPoint") {
this.chartData?.forEach((dataPoint: any, index: string | number) => {
enumeration.push({
objectName: options.objectName,
displayName: dataPoint.name,
selector: { id: dataPoint.name },
properties: {
fill: {
solid: {
color: this.chartData?.[index]?.color
}
}
},
});
});
}
return enumeration;
}
}
Capabilities.json File:
{
"dataRoles": [
{
"displayName": "Category",
"name": "category",
"kind": "Grouping"
},
{
"displayName": "Values",
"name": "values",
"kind": "Measure"
}
],
"dataViewMappings": [
{
"categorical": {
"categories": {
"for": { "in": "category" },
"dataReductionAlgorithm": { "top": {} }
},
"values": {
"for": { "in": "values" }
}
}
}
],
"objects": {
"general": {
"displayName": "General",
"properties": {
"show": {
"displayName": "Show",
"type": { "bool": true }
}
}
},
"visuals": {
"displayName": "Visuals",
"properties": {
"fill": {
"displayName": "Fill",
"type": { "fill": { "solid": { "color": true } } }
}
}
},
"dataPoint":{
"displayName": "Data Point Colors",
"properties": {
"fill": {
"type": {"fill": {"solid": {"color": true } } }
}
}
},
"colorSettings": {
"displayName": "Colors",
"properties": {
"fill": {
"displayName": "Fill Color",
"type": { "fill": { "solid": { "color": true } } }
}
}
},
"sizeSettings": {
"displayName": "Size",
"properties": {
"size": {
"displayName": "Size",
"type": { "numeric": true }
}
}
}
},
"supportsHighlight": true,
"supportsMultiVisualSelection": true,
"tooltips": {
"roles": ["category", "values"]
},
"privileges": []
}
Any Suggestions community ?
I think that you also need to configure settings
.ts for it to show under your visualizations pane. So the objects in capabilities also need to be correctly set-up in settings.ts
Hi, I've configured my settings.ts file as well. I'm able to get properties panel but the color change change is not reflected in UI.
UI Looks like this:
here is my code for settings.ts file
"use strict";
import { formattingSettings } from "powerbi-visuals-utils-formattingmodel";
class LabelsCardSetting extends formattingSettings.SimpleCard {
name: string = "labels"; // same as capabilities object name
displayName: string = "Labels";
// selector is not needed and has been removed
public fontFamily: formattingSettings.FontPicker = new formattingSettings.FontPicker({
name: "fontFamily", // same as capabilities property name
value: "Arial, sans-serif"
});
public fontSize: formattingSettings.NumUpDown = new formattingSettings.NumUpDown({
name: "fontSize", // same as capabilities property name
value: 11
});
public bold: formattingSettings.ToggleSwitch = new formattingSettings.ToggleSwitch({
name: "bold", // same as capabilities property name
value: false
});
public italic: formattingSettings.ToggleSwitch = new formattingSettings.ToggleSwitch({
name: "italic", // same as capabilities property name
value: false
});
public underline: formattingSettings.ToggleSwitch = new formattingSettings.ToggleSwitch({
name: "underline", // same as capabilities property name
value: false
});
public font: formattingSettings.FontControl = new formattingSettings.FontControl({
name: "font", // must be unique within the same object
displayName: "Font",
fontFamily: this.fontFamily,
fontSize: this.fontSize,
bold: this.bold, //optional
italic: this.italic, //optional
underline: this.underline //optional
});
public slices: formattingSettings.Slice[] = [this.font];
}
// Slice color settings
class SlicesColorCardSetting extends formattingSettings.SimpleCard {
name: string = "sliceColors";
displayName: string = "Slice Colors";
public dynamicSliceColors: formattingSettings.ColorPicker[] = [];
private defaultColors: string[] = ['#AEDFF7', '#73C6F3', '#3498DB', '#2E86C1', '#1B4F72', '#154360'];
// Dynamically generate ColorPicker settings based on the number of slices
public updateSliceColors(sliceNames: string[], currentColors: Record<string, string> = {}) {
// Create a lookup for existing colors
const existingColors: Record<string, formattingSettings.ColorPicker> = {};
for (const colorPicker of this.dynamicSliceColors) {
existingColors[colorPicker.name] = {
name: colorPicker.name,
value: colorPicker.value,
};
}
// Map slice names to dynamic slice colors
const updatedSliceColors: formattingSettings.ColorPicker[] = [];
for (let i = 0; i < sliceNames.length; i++) {
const sliceName = sliceNames[i];
const colorName = `sliceColor${i + 1}`;
const defaultColor = this.defaultColors[i % this.defaultColors.length];
updatedSliceColors.push(
new formattingSettings.ColorPicker({
name: colorName,
displayName: `${sliceName} Color`,
value: typeof currentColors[colorName] === 'string' ? { value: currentColors[colorName] } : currentColors[colorName] || existingColors[colorName]?.value || defaultColor,
})
);
}
this.dynamicSliceColors = updatedSliceColors;
this.slices = this.dynamicSliceColors;
console.log("Updated Slice Colors:", this.dynamicSliceColors);
console.log("Retained Colors:", existingColors);
console.log("Dynamic Slice Colors After Update:", this.dynamicSliceColors);
}
public slices: formattingSettings.Slice[] = [];
}
export class VisualFormattingSettingsModel extends formattingSettings.Model {
updateSliceColors(sliceNames: string[]) {
throw new Error("Method not implemented.");
}
public labels: LabelsCardSetting = new LabelsCardSetting();
public sliceColors: SlicesColorCardSetting = new SlicesColorCardSetting();
public cards: formattingSettings.SimpleCard[] = [this.labels, this.sliceColors];
static parse: any;
// Bind font properties to DataLabels objects
public bindToDataLabels(dataLabels: any) {
dataLabels.fontFamily = this.labels.fontFamily.value;
dataLabels.fontSize = this.labels.fontSize.value;
dataLabels.bold = this.labels.bold.value;
dataLabels.italic = this.labels.italic.value;
dataLabels.underline = this.labels.underline.value;
console.log(dataLabels);
}
// }
public bindToSliceColors(sliceNames: string[], sliceColors: any[]) {
const currentColors = sliceNames.reduce((acc, sliceName, index) => {
acc[`sliceColor${index + 1}`] = sliceColors[index];
return acc;
}, {} as Record<string, string>);
this.sliceColors.updateSliceColors(sliceNames, currentColors);
this.sliceColors.dynamicSliceColors.forEach((colorPicker, index) => {
sliceColors[index] = colorPicker.value.value; // Assuming colorPicker.value is an object with a 'value' property
});
// public getSelectedSliceColors(): any[] {
// return this.sliceColors.dynamicSliceColors.map(colorPicker => colorPicker.value);
// }
}
}
March 31 - April 2, 2025, in Las Vegas, Nevada. Use code MSCUST for a $150 discount! Prices go up Feb. 11th.
Check out the January 2025 Power BI update to learn about new features in Reporting, Modeling, and Data Connectivity.