diff --git a/docs/sources/panels-visualizations/visualizations/pie-chart/index.md b/docs/sources/panels-visualizations/visualizations/pie-chart/index.md index 679f4e61f3b..0c46b734d1d 100644 --- a/docs/sources/panels-visualizations/visualizations/pie-chart/index.md +++ b/docs/sources/panels-visualizations/visualizations/pie-chart/index.md @@ -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. diff --git a/packages/grafana-schema/src/raw/composable/piechart/panelcfg/x/PieChartPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/piechart/panelcfg/x/PieChartPanelCfg_types.gen.ts index c5f420a0b11..913fa45c9ac 100644 --- a/packages/grafana-schema/src/raw/composable/piechart/panelcfg/x/PieChartPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/piechart/panelcfg/x/PieChartPanelCfg_types.gen.ts @@ -54,6 +54,7 @@ export interface Options extends common.OptionsWithTooltip, common.SingleStatBas displayLabels: Array; legend: PieChartLegendOptions; pieType: PieChartType; + sort: common.SortOrder; } export const defaultOptions: Partial = { diff --git a/public/app/plugins/panel/piechart/PieChart.tsx b/public/app/plugins/panel/piechart/PieChart.tsx index 3abbe438cfb..0619a92e2c5 100644 --- a/public/app/plugins/panel/piechart/PieChart.tsx +++ b/public/app/plugins/panel/piechart/PieChart.tsx @@ -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 = ({ 0} outerRadius={layout.outerRadius} innerRadius={layout.innerRadius} cornerRadius={3} diff --git a/public/app/plugins/panel/piechart/PieChartPanel.test.tsx b/public/app/plugins/panel/piechart/PieChartPanel.test.tsx index 059877f28bb..7358062953c 100644 --- a/public/app/plugins/panel/piechart/PieChartPanel.test.tsx +++ b/public/app/plugins/panel/piechart/PieChartPanel.test.tsx @@ -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, diff --git a/public/app/plugins/panel/piechart/PieChartPanel.tsx b/public/app/plugins/panel/piechart/PieChartPanel.tsx index ae7bc3624b7..0deabf1da10 100644 --- a/public/app/plugins/panel/piechart/PieChartPanel.tsx +++ b/public/app/plugins/panel/piechart/PieChartPanel.tsx @@ -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((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); } diff --git a/public/app/plugins/panel/piechart/module.tsx b/public/app/plugins/panel/piechart/module.tsx index 111c481f533..33726bf3a0f 100644 --- a/public/app/plugins/panel/piechart/module.tsx +++ b/public/app/plugins/panel/piechart/module.tsx @@ -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(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, diff --git a/public/app/plugins/panel/piechart/panelcfg.cue b/public/app/plugins/panel/piechart/panelcfg.cue index 5a75d388dd4..1ac2f4a1639 100644 --- a/public/app/plugins/panel/piechart/panelcfg.cue +++ b/public/app/plugins/panel/piechart/panelcfg.cue @@ -46,6 +46,7 @@ composableKinds: PanelCfg: { common.OptionsWithTooltip common.SingleStatBaseOptions pieType: PieChartType + sort: common.SortOrder displayLabels: [...PieChartLabels] legend: PieChartLegendOptions } @cuetsy(kind="interface") diff --git a/public/app/plugins/panel/piechart/panelcfg.gen.ts b/public/app/plugins/panel/piechart/panelcfg.gen.ts index 494d6678173..9afffa589b3 100644 --- a/public/app/plugins/panel/piechart/panelcfg.gen.ts +++ b/public/app/plugins/panel/piechart/panelcfg.gen.ts @@ -52,6 +52,7 @@ export interface Options extends common.OptionsWithTooltip, common.SingleStatBas displayLabels: Array; legend: PieChartLegendOptions; pieType: PieChartType; + sort: common.SortOrder; } export const defaultOptions: Partial = {