Find everything you need to get certified on Fabric—skills challenges, live sessions, exam prep, role guidance, and more. Get started
Hi everyone,
I am trying to build a custom visual that show 2 donut chart (inner and outer donuts), data and visual as below:
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':
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": []
}