Skip to main content
cancel
Showing results for 
Search instead for 
Did you mean: 

The Power BI Data Visualization World Championships is back! Get ahead of the game and start preparing now! Learn more

Reply
Sharma_Devesh
Regular Visitor

Need help for defining properties in format visual pane.

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

 

3 REPLIES 3
Sharma_Devesh
Regular Visitor

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: 
Screenshot 2025-01-02 160115.png


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

 

Helpful resources

Announcements
Power BI DataViz World Championships

Power BI Dataviz World Championships

The Power BI Data Visualization World Championships is back! Get ahead of the game and start preparing now!

December 2025 Power BI Update Carousel

Power BI Monthly Update - December 2025

Check out the December 2025 Power BI Holiday Recap!

FabCon Atlanta 2026 carousel

FabCon Atlanta 2026

Join us at FabCon Atlanta, March 16-20, for the ultimate Fabric, Power BI, AI and SQL community-led event. Save $200 with code FABCOMM.

Top Kudoed Authors