Forum Discussion

Sharma_Devesh's avatar
Sharma_Devesh
Regular Visitor
1 year ago

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

    • AnViL999's avatar
      AnViL999
      Frequent Visitor

      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

      • Sharma_Devesh's avatar
        Sharma_Devesh
        Regular Visitor

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