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

Don'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.

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
Las Vegas 2025

Join us at the Microsoft Fabric Community Conference

March 31 - April 2, 2025, in Las Vegas, Nevada. Use code MSCUST for a $150 discount! Prices go up Feb. 11th.

Jan25PBI_Carousel

Power BI Monthly Update - January 2025

Check out the January 2025 Power BI update to learn about new features in Reporting, Modeling, and Data Connectivity.

Jan NL Carousel

Fabric Community Update - January 2025

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

Top Solution Authors
Top Kudoed Authors