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

Power BI is turning 10! Let’s celebrate together with dataviz contests, interactive sessions, and giveaways. Register now.

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
Join our Fabric User Panel

Join our Fabric User Panel

This is your chance to engage directly with the engineering team behind Fabric and Power BI. Share your experiences and shape the future.

June 2025 Power BI Update Carousel

Power BI Monthly Update - June 2025

Check out the June 2025 Power BI update to learn about new features.

June 2025 community update carousel

Fabric Community Update - June 2025

Find out what's new and trending in the Fabric community.

Top Solution Authors
Top Kudoed Authors