Join us for an expert-led overview of the tools and concepts you'll need to pass exam PL-300. The first session starts on June 11th. See you there!
Get registeredPower BI is turning 10! Let’s celebrate together with dataviz contests, interactive sessions, and giveaways. 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);
// }
}
}