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:
co-authored by
Isabel Matwawana
Paul Marbach
Lukas Wieg
parent
17444fdc0d
commit
cc651e2e6e
@@ -136,6 +136,15 @@ Select the pie chart display style. Choose from **Pie** or **Donut**.
|
||||
|
||||

|
||||
|
||||
#### 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.
|
||||
|
||||
+1
@@ -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> = {
|
||||
|
||||
Reference in New Issue
Block a user