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
stawpeed
Frequent Visitor

Problem with the order of donut's slice

Hi everyone,

I am trying to build a custom visual that show 2 donut chart (inner and outer donuts), data and visual as below:

stawpeed_0-1703059327208.png

I was expected that slice's order of outer donut to be sorted base on Categories and Subcategories. But it seems to be sorted base on Values.

In Show data as table view, it already sorted by 'Sum of Value':

stawpeed_1-1703059533660.png

How could I get this problem fixed?

 

Below is my visual.ts and capabilities.json

"use strict";

import * as d3 from "d3";
import { group } from "d3-array";
import powerbi from "powerbi-visuals-api";
import VisualConstructorOptions = powerbi.extensibility.visual.VisualConstructorOptions;
import VisualUpdateOptions = powerbi.extensibility.visual.VisualUpdateOptions;
import IVisual = powerbi.extensibility.visual.IVisual;
import DataView = powerbi.DataView;
import IVisualHost = powerbi.extensibility.IVisualHost;
type Selection<T extends d3.BaseType> = d3.Selection<T, any, any, any>;

export class Visual implements IVisual {
    private host: IVisualHost;
    private svg: Selection<SVGElement>;
    private container: Selection<SVGElement>;
    private firstDonutContainer: Selection<SVGElement>;
    private secondDonutContainer: Selection<SVGElement>;

    constructor(options: VisualConstructorOptions) {
        this.svg = d3.select(options.element)
            .append('svg')
            .classed('doubleDonutChart', true); // Change class to doubleDonutChart
        this.container = this.svg.append("g")
            .classed('container', true);

        // Create the first (inner) donut chart
        this.firstDonutContainer = this.container.append("g")
            .classed('firstDonutContainer', true);

        // Create the second (outer) donut chart
        this.secondDonutContainer = this.container.append("g")
            .classed('secondDonutContainer', true);
    }

    public update(options: VisualUpdateOptions) {
        // Assign the size of viewport
        let width: number = options.viewport.width;
        let height: number = options.viewport.height;
        this.svg.attr("width", width);
        this.svg.attr("height", height);

        // Assuming data contains a category field and a measure field
        let dataView: DataView = options.dataViews[0];
        let categoryValues = dataView.categorical.categories[0].values;
        let measureValues = dataView.categorical.values[0].values.map(val => Number(val));

// Grouping data by categories and calculating the sum
let groupedData = Array.from(d3.group(measureValues, (d, i) => categoryValues[i]), ([key, group]) => ({ category: key, value: d3.sum(group) }));

// Sort the data based on the original order of categories
groupedData.sort((a, b) => d3.descending(a.category, b.category));

// Create a color scale for categories
const categoryColorScale = d3.scaleOrdinal()
    .domain(categoryValues as string[])  // Assuming category values are strings
    .range(["#b4bbbf", "#717171", "#B4BDFF", "#83A2FF"]);  // Adjust the color range as needed

// Creating the first (inner) donut chart using d3 pie function with innerRadius
let pie = d3.pie<any>().value(d => d.value);
let pieData = pie(groupedData).sort((a, b) => d3.ascending(a.data.key, b.data.key));


        let radius: number = Math.min(width, height) / 3;
        let innerRadius: number = radius / 1.4; // Set inner radius for the first donut

        let firstArc: d3.Arc<any, d3.DefaultArcObject> = d3.arc()
            .innerRadius(innerRadius) // Set inner radius for the first donut
            .outerRadius(radius);

        // Update the first (inner) donut chart
        let firstDonutArcs = this.firstDonutContainer.selectAll("path")
            .data(pieData);

        firstDonutArcs.enter().append("path")
            .merge(firstDonutArcs as d3.Selection<SVGPathElement, any, SVGElement, any>)
            .attr("d", function (d: d3.DefaultArcObject | any) {
                return firstArc(d) as string;
            })
            .attr("fill", (d, i) => String(categoryColorScale(d.data.category)))
            .attr('stroke', 'white')
            .style('stroke-width', '0.5px')
            .style('opacity', 1);

        firstDonutArcs.exit().remove();

        // Center the first (inner) donut chart in the middle of the SVG
        this.firstDonutContainer.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");

        // Assuming there is a second category and measure in the data
        let subcategoryValues = dataView.categorical.categories[1].values;
        let secondMeasureValues = dataView.categorical.values[0].values.map(val => Number(val));

// Grouping data by subcategories and calculating the sum
let groupedSecondData = Array.from(d3.group(secondMeasureValues, (d, i) => subcategoryValues[i]), ([key, group]) => ({ category: key, value: d3.sum(group) }));

// Sort the data based on the original order of subcategories
groupedSecondData.sort((a, b) => d3.descending(a.category, b.category));

// Create a color scale for subcategories
const subcategoryColorScale = d3.scaleOrdinal()
    .domain(subcategoryValues as string[])  // Assuming subcategory values are strings
    .range(["#7D1D57", "#AD719A", "#8DA2AC", "#496E87", "#6d8b9e", "#F1E6EE", "#717171", "#E6E6E6"]);  // Adjust the color range as needed

// Creating the second (outer) donut chart using d3 pie function with innerRadius and outerRadius
let secondPie = d3.pie<any>().value(d => d.value);
let secondPieData = secondPie(groupedSecondData).sort((a, b) => d3.descending(a.data.key, b.data.key));


        let outerRadius: number = radius * 1.3; // Set outer radius for the second donut

        let secondArc: d3.Arc<any, d3.DefaultArcObject> = d3.arc()
            .innerRadius(radius) // Inner radius is the outer radius of the first donut
            .outerRadius(outerRadius);

        // Update the second (outer) donut chart
        let secondDonutArcs = this.secondDonutContainer.selectAll("path")
            .data(secondPieData);

        secondDonutArcs.enter().append("path")
            .merge(secondDonutArcs as d3.Selection<SVGPathElement, any, SVGElement, any>)
            .attr("d", function (d: d3.DefaultArcObject | any) {
                return secondArc(d) as string;
            })
            .attr("fill", (d, i) => String(subcategoryColorScale(d.data.category)))
            .attr('stroke', 'white')
            .style('stroke-width', '0.5px')
            .style('opacity', 1);

        secondDonutArcs.exit().remove();

        // Center the second (outer) donut chart in the middle of the SVG
        this.secondDonutContainer.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
    }
}
{
    "dataRoles": [
        {
            "displayName": "Category Data",
            "name": "category",
            "kind": "Grouping"
        },
        {
            "displayName": "Measure Data",
            "name": "measure",
            "kind": "Measure"
        }
    ],
  
    "dataViewMappings": [
        {
            "categorical": {
                "categories": {
                    "for": {
                        "in": "category"
                    },
                    "dataReductionAlgorithm": {
                        "top": {}
                    }
                },
                "values": {
                    "select": [
                        {
                            "for": {
                                "in": "measure"
                            }
                        }
                    ]
                }
            }
        }
    ],    
    "sorting": {
        "implicit": {
            "clauses": [
                {
                    "role": "category",
                    "direction": 1
                },
                {
                    "role": "measure",
                    "direction": 2
                }
            ]
        }
    },
    "privileges": []
}
0 REPLIES 0

Helpful resources

Announcements
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.

May 2025 Monthly Update

Fabric Community Update - May 2025

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

Top Solution Authors
Top Kudoed Authors