PieChart: Add panel options for ascending/descending sort, and no sorting (#109564)

* Add PieSortOption.

* Add sorting options to pie chart panel options.

* Fix that sorting is not part of the panel options.

* Implement custom sorting in piechart and piechart legend.

* Update piechart documentation

* Fix default and clockwise/counterclockwise confusion.

* refactor(piechart): use asc/desc terminology instead of clockwise

* fix(piechart): simplify comparator sort, avoid a/b swap mutation

* fix(pichart): remove redundant sorting from Pie component invocation

* refactor(piechart): hoist comparator for sorting display values

* docs(piechart): include recommendations from @imatwawanaimatwawana

* test(piechart): add test coverage for new sorting comparator

* chore(piechart): regenerate Cue schema files

* docs(piechart): apply suggestions from @imatwawana code review

Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com>

* fix(piechart): fix typo, match punctuation in option description

* chore(piechart): update schema to use existing `common.SortOrder` instead of custom

Co-authored-by: Paul Marbach <paul.marbach@grafana.com>

---------

Co-authored-by: Lukas Wieg <lukaswieg@googlemail.com>
Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com>
Co-authored-by: Paul Marbach <paul.marbach@grafana.com>
This commit is contained in:
Jesse David Peterson
2025-08-14 16:46:53 -04:00
committed by GitHub
co-authored by Isabel Matwawana Paul Marbach Lukas Wieg
parent 17444fdc0d
commit cc651e2e6e
8 changed files with 104 additions and 14 deletions
@@ -136,6 +136,15 @@ Select the pie chart display style. Choose from **Pie** or **Donut**.
![Pie chart types](/media/docs/grafana/panels-visualizations/screenshot-pie-chart-types.png)
#### Slice sorting
By default, the pie chart is sorted so that the slices decrease in size clockwise around the circle.
You can configure the sorting of the slices, and by extension the legend, with the following options:
- **Descending** - The slices decrease in size, clockwise (default).
- **Ascending** - The slices increase in size, clockwise.
- **None** - No sorting is applied. The original order of the data is maintained.
#### Labels
Select labels to display on the pie chart. You can select more than one.
@@ -54,6 +54,7 @@ export interface Options extends common.OptionsWithTooltip, common.SingleStatBas
displayLabels: Array<PieChartLabels>;
legend: PieChartLegendOptions;
pieType: PieChartType;
sort: common.SortOrder;
}
export const defaultOptions: Partial<Options> = {
@@ -18,7 +18,7 @@ import {
DataHoverEvent,
} from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { VizTooltipOptions } from '@grafana/schema';
import { SortOrder, VizTooltipOptions } from '@grafana/schema';
import {
useTheme2,
useStyles2,
@@ -40,6 +40,7 @@ interface PieChartProps {
width: number;
fieldDisplayValues: FieldDisplay[];
pieType: PieChartType;
sort: SortOrder;
highlightedTitle?: string;
displayLabels?: PieChartLabels[];
useGradients?: boolean; // not used?
@@ -49,6 +50,7 @@ interface PieChartProps {
export const PieChart = ({
fieldDisplayValues,
pieType,
sort,
width,
height,
highlightedTitle,
@@ -105,6 +107,7 @@ export const PieChart = ({
<Pie
data={filteredFieldDisplayValues}
pieValue={getValue}
pieSortValues={() => 0}
outerRadius={layout.outerRadius}
innerRadius={layout.innerRadius}
cornerRadius={3}
@@ -13,7 +13,7 @@ import {
} from '@grafana/data';
import { LegendDisplayMode, SortOrder, TooltipDisplayMode } from '@grafana/schema';
import { PieChartPanel } from './PieChartPanel';
import { PieChartPanel, comparePieChartItemsByValue } from './PieChartPanel';
import { Options, PieChartType, PieChartLegendValues } from './panelcfg.gen';
jest.mock('react-use', () => ({
@@ -165,6 +165,52 @@ describe('PieChartPanel', () => {
});
});
describe('comparePieChartItemsByValue', () => {
const makeFieldDisplay = (n: number) =>
({ display: { numeric: n } }) as unknown as import('@grafana/data').FieldDisplay;
it.each([
{
name: 'always: NaN a sorts after 1',
sort: SortOrder.Descending,
a: makeFieldDisplay(NaN),
b: makeFieldDisplay(1),
expected: 1,
},
{
name: 'always: NaN b sorts before 1',
sort: SortOrder.Descending,
a: makeFieldDisplay(1),
b: makeFieldDisplay(NaN),
expected: -1,
},
{
name: 'descending: larger a sorts before smaller b (negative)',
sort: SortOrder.Descending,
a: makeFieldDisplay(10),
b: makeFieldDisplay(5),
expected: -5,
},
{
name: 'ascending: smaller a sorts before larger b (negative)',
sort: SortOrder.Ascending,
a: makeFieldDisplay(5),
b: makeFieldDisplay(10),
expected: -5,
},
{
name: 'none: comparator returns 0 regardless of values',
sort: SortOrder.None,
a: makeFieldDisplay(5),
b: makeFieldDisplay(10),
expected: 0,
},
])('$name', ({ sort, a, b, expected }) => {
const cmp = comparePieChartItemsByValue(sort);
expect(cmp(a, b)).toBe(expected);
});
});
const setup = (propsOverrides?: {}) => {
const fieldConfig: FieldConfigSource = {
defaults: {},
@@ -173,6 +219,7 @@ const setup = (propsOverrides?: {}) => {
const options: Options = {
pieType: PieChartType.Pie,
sort: SortOrder.Descending,
displayLabels: [],
legend: {
displayMode: LegendDisplayMode.List,
@@ -11,7 +11,7 @@ import {
PanelProps,
} from '@grafana/data';
import { PanelDataErrorView } from '@grafana/runtime';
import { HideSeriesConfig, LegendDisplayMode } from '@grafana/schema';
import { HideSeriesConfig, SortOrder, LegendDisplayMode } from '@grafana/schema';
import {
SeriesVisibilityChangeBehavior,
usePanelContext,
@@ -67,6 +67,7 @@ export function PieChartPanel(props: Props) {
fieldDisplayValues={fieldDisplayValues}
tooltipOptions={options.tooltip}
pieType={options.pieType}
sort={options.sort}
displayLabels={options.displayLabels}
/>
);
@@ -81,19 +82,12 @@ function getLegend(props: Props, displayValues: FieldDisplay[]) {
if (legendOptions.showLegend === false) {
return undefined;
}
const sortedDisplayValues = displayValues.sort(comparePieChartItemsByValue(props.options.sort));
const total = displayValues.filter(filterDisplayItems).reduce(sumDisplayItemsReducer, 0);
const legendItems: VizLegendItem[] = displayValues
// Since the pie chart is always sorted, let's sort the legend as well.
.sort((a, b) => {
if (isNaN(a.display.numeric)) {
return 1;
} else if (isNaN(b.display.numeric)) {
return -1;
} else {
return b.display.numeric - a.display.numeric;
}
})
const legendItems: VizLegendItem[] = sortedDisplayValues
.map<VizLegendItem | undefined>((value: FieldDisplay, idx: number) => {
const hideFrom: HideSeriesConfig = value.field.custom?.hideFrom ?? {};
@@ -151,6 +145,26 @@ function getLegend(props: Props, displayValues: FieldDisplay[]) {
);
}
export function comparePieChartItemsByValue(sort: SortOrder): (a: FieldDisplay, b: FieldDisplay) => number {
return function (a: FieldDisplay, b: FieldDisplay) {
if (isNaN(a.display.numeric)) {
return 1;
}
if (isNaN(b.display.numeric)) {
return -1;
}
if (sort === SortOrder.Descending) {
return b.display.numeric - a.display.numeric;
}
if (sort === SortOrder.Ascending) {
return a.display.numeric - b.display.numeric;
}
return 0;
};
}
function hasFrames(fieldDisplayValues: FieldDisplay[]) {
return fieldDisplayValues.some((fd) => fd.view?.dataFrame.length);
}
@@ -1,5 +1,6 @@
import { FieldColorModeId, FieldConfigProperty, PanelPlugin } from '@grafana/data';
import { t } from '@grafana/i18n';
import { SortOrder } from '@grafana/schema/dist/esm/index';
import { commonOptionsBuilder } from '@grafana/ui';
import { optsWithHideZeros } from '@grafana/ui/internal';
@@ -48,6 +49,19 @@ export const plugin = new PanelPlugin<Options, FieldConfig>(PieChartPanel)
},
defaultValue: PieChartType.Pie,
})
.addSelect({
name: 'Slice sorting',
description: 'Select how to sort the pie slices',
path: 'sort',
settings: {
options: [
{ value: SortOrder.Descending, label: 'Descending' },
{ value: SortOrder.Ascending, label: 'Ascending' },
{ value: SortOrder.None, label: 'None' },
],
},
defaultValue: SortOrder.Descending,
})
.addMultiSelect({
name: t('piechart.name-labels', 'Labels'),
category,
@@ -46,6 +46,7 @@ composableKinds: PanelCfg: {
common.OptionsWithTooltip
common.SingleStatBaseOptions
pieType: PieChartType
sort: common.SortOrder
displayLabels: [...PieChartLabels]
legend: PieChartLegendOptions
} @cuetsy(kind="interface")
@@ -52,6 +52,7 @@ export interface Options extends common.OptionsWithTooltip, common.SingleStatBas
displayLabels: Array<PieChartLabels>;
legend: PieChartLegendOptions;
pieType: PieChartType;
sort: common.SortOrder;
}
export const defaultOptions: Partial<Options> = {