PieChart: Improve piechart legend and options (#31446)

* Add percent of total to piechart legend

* Remove defaults

* Add label selector

* Fix multiselect option ui

* Add percent of total to piechart legend

* Add label selector

* add multiselect options ui

* change how pie chart labels are displayed

* Fixed right aligned values in legend

* added titles to display values so they show in table mode

* Move legend display value options to below other options

* Add addMultiSelect method to ui builder

* Use addMultiSelect on builder

* Use multiselect for the legend columns and update the panel test dashboard

* Remove explicit typing on addMultiselect and remove non existing properties from piechart story

* Add release tag

Co-authored-by: Torkel Ödegaard <torkel@grafana.org>
This commit is contained in:
Oscar Kilhed
2021-03-01 11:18:24 +01:00
committed by GitHub
parent c360ac7278
commit 10def28989
11 changed files with 250 additions and 92 deletions

View File

@ -15,7 +15,7 @@
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"id": 231,
"id": 479,
"links": [],
"panels": [
{
@ -49,23 +49,34 @@
"h": 14,
"w": 8,
"x": 0,
"y": 3
"y": 0
},
"id": 2,
"options": {
"displayLabels": [
"name"
],
"labelOptions": {
"showName": true,
"showPercent": false,
"showValue": false
},
"legend": {
"displayColumns": [],
"displayMode": "list",
"placement": "right"
},
"pieType": "donut",
"reduceOptions": {
"calcs": ["mean"],
"calcs": [
"mean"
],
"fields": "",
"values": false
},
"showLegend": true,
"strokeWidth": 1
"strokeWidth": 1,
"text": {}
},
"pluginVersion": "7.3.0-pre",
"targets": [
@ -112,23 +123,37 @@
"h": 14,
"w": 8,
"x": 8,
"y": 3
"y": 0
},
"id": 11,
"options": {
"displayLabels": [
"name",
"percent"
],
"labelOptions": {
"showName": true,
"showPercent": true,
"showValue": false
},
"legend": {
"displayColumns": [
"percent"
],
"displayMode": "list",
"placement": "right"
},
"pieType": "pie",
"reduceOptions": {
"calcs": ["mean"],
"calcs": [
"mean"
],
"fields": "",
"values": false
},
"showLegend": true,
"strokeWidth": 1
"strokeWidth": 1,
"text": {}
},
"pluginVersion": "7.3.0-pre",
"targets": [
@ -173,23 +198,34 @@
"h": 14,
"w": 8,
"x": 16,
"y": 3
"y": 0
},
"id": 8,
"options": {
"displayLabels": [
"name"
],
"labelOptions": {
"showName": true,
"showPercent": false,
"showValue": false
},
"legend": {
"displayColumns": [],
"displayMode": "hidden",
"placement": "right"
},
"pieType": "pie",
"reduceOptions": {
"calcs": ["mean"],
"calcs": [
"mean"
],
"fields": "",
"values": false
},
"showLegend": false,
"strokeWidth": 1
"strokeWidth": 1,
"text": {}
},
"pluginVersion": "7.3.0-pre",
"targets": [
@ -202,7 +238,7 @@
],
"timeFrom": null,
"timeShift": null,
"title": "Name",
"title": "Name & No legend",
"type": "piechart"
},
{
@ -234,23 +270,36 @@
"h": 9,
"w": 5,
"x": 0,
"y": 17
"y": 14
},
"id": 3,
"options": {
"displayLabels": [
"percent"
],
"labelOptions": {
"showName": false,
"showPercent": true,
"showValue": false
},
"legend": {
"displayColumns": [
"percent"
],
"displayMode": "table",
"placement": "bottom"
},
"pieType": "pie",
"reduceOptions": {
"calcs": ["mean"],
"calcs": [
"mean"
],
"fields": "",
"values": false
},
"showLegend": false,
"strokeWidth": 1
"strokeWidth": 1,
"text": {}
},
"pluginVersion": "7.3.0-pre",
"targets": [
@ -295,23 +344,36 @@
"h": 9,
"w": 5,
"x": 5,
"y": 17
"y": 14
},
"id": 9,
"options": {
"displayLabels": [
"value"
],
"labelOptions": {
"showName": false,
"showPercent": false,
"showValue": true
},
"legend": {
"displayColumns": [
"value"
],
"displayMode": "table",
"placement": "bottom"
},
"pieType": "pie",
"reduceOptions": {
"calcs": ["mean"],
"calcs": [
"mean"
],
"fields": "",
"values": false
},
"showLegend": false,
"strokeWidth": 1
"strokeWidth": 1,
"text": {}
},
"pluginVersion": "7.3.0-pre",
"targets": [
@ -356,23 +418,34 @@
"h": 9,
"w": 6,
"x": 10,
"y": 17
"y": 14
},
"id": 6,
"options": {
"displayLabels": [
"name"
],
"labelOptions": {
"showName": true,
"showPercent": false,
"showValue": false
},
"pieType": "donut",
"legend": {
"displayColumns": [],
"displayMode": "list",
"placement": "bottom"
},
"pieType": "pie",
"reduceOptions": {
"calcs": ["mean"],
"calcs": [
"mean"
],
"fields": "",
"values": false
},
"showLegend": false,
"strokeWidth": 1
"strokeWidth": 1,
"text": {}
},
"pluginVersion": "7.3.0-pre",
"targets": [
@ -417,23 +490,32 @@
"h": 9,
"w": 8,
"x": 16,
"y": 17
"y": 14
},
"id": 10,
"options": {
"displayLabels": [],
"labelOptions": {
"showName": true,
"showPercent": false,
"showValue": false
},
"legend": {
"displayColumns": ["percent", "value"],
"displayMode": "table",
"placement": "bottom"
},
"pieType": "pie",
"reduceOptions": {
"calcs": ["mean"],
"calcs": [
"mean"
],
"fields": "",
"values": false
},
"showLegend": false,
"strokeWidth": 1
"strokeWidth": 1,
"text": {}
},
"pluginVersion": "7.3.0-pre",
"targets": [
@ -450,9 +532,12 @@
"type": "piechart"
}
],
"schemaVersion": 26,
"schemaVersion": 27,
"style": "dark",
"tags": ["gdev", "panel-tests"],
"tags": [
"gdev",
"panel-tests"
],
"templating": {
"list": []
},
@ -464,5 +549,5 @@
"timezone": "",
"title": "Panel Tests - Pie chart",
"uid": "lVE-2YFMz",
"version": 1
"version": 9
}

View File

@ -9,6 +9,10 @@ interface State<T> {
type Props<T> = FieldConfigEditorProps<T[], SelectFieldConfigSettings<T>>;
/**
* MultiSelect for options UI
* @alpha
*/
export class MultiSelectValueEditor<T> extends React.PureComponent<Props<T>, State<T>> {
state: State<T> = {
isLoading: true,

View File

@ -28,23 +28,13 @@ const getKnobs = () => {
};
export const basic = () => {
const { datapoints, pieType, width, height, showLabelName, showLabelPercent, showLabelValue } = getKnobs();
const labelOptions = { showName: showLabelName, showPercent: showLabelPercent, showValue: showLabelValue };
const { datapoints, pieType, width, height } = getKnobs();
return <PieChart width={width} height={height} values={datapoints} pieType={pieType} labelOptions={labelOptions} />;
return <PieChart width={width} height={height} values={datapoints} pieType={pieType} />;
};
export const donut = () => {
const { datapoints, width, height, showLabelName, showLabelPercent, showLabelValue } = getKnobs();
const labelOptions = { showName: showLabelName, showPercent: showLabelPercent, showValue: showLabelValue };
const { datapoints, width, height } = getKnobs();
return (
<PieChart
width={width}
height={height}
values={datapoints}
pieType={PieChartType.Donut}
labelOptions={labelOptions}
/>
);
return <PieChart width={width} height={height} values={datapoints} pieType={PieChartType.Donut} />;
};

View File

@ -13,16 +13,27 @@ import { VizLegend, VizLegendItem } from '..';
import { VizLayout } from '../VizLayout/VizLayout';
import { LegendDisplayMode, VizLegendOptions } from '../VizLegend/types';
export enum PieChartLabels {
Name = 'name',
Value = 'value',
Percent = 'percent',
}
export enum LegendColumns {
Value = 'value',
Percent = 'percent',
}
interface SvgProps {
height: number;
width: number;
values: DisplayValue[];
pieType: PieChartType;
labelOptions?: PieChartLabelOptions;
displayLabels?: PieChartLabels[];
useGradients?: boolean;
}
export interface Props extends SvgProps {
legendOptions?: VizLegendOptions;
legendOptions?: PieChartLegendOptions;
}
export enum PieChartType {
@ -30,29 +41,49 @@ export enum PieChartType {
Donut = 'donut',
}
export interface PieChartLabelOptions {
showName?: boolean;
showValue?: boolean;
showPercent?: boolean;
export interface PieChartLegendOptions extends VizLegendOptions {
displayColumns: LegendColumns[];
}
const defaultLegendOptions: VizLegendOptions = {
const defaultLegendOptions: PieChartLegendOptions = {
displayMode: LegendDisplayMode.List,
placement: 'right',
calcs: [],
displayColumns: [LegendColumns.Percent],
};
export const PieChart: FC<Props> = ({ values, legendOptions = defaultLegendOptions, width, height, ...restProps }) => {
const getLegend = (values: DisplayValue[], legendOptions: VizLegendOptions) => {
const getLegend = (values: DisplayValue[], legendOptions: PieChartLegendOptions) => {
if (legendOptions.displayMode === LegendDisplayMode.Hidden) {
return undefined;
}
const total = values.reduce((acc, item) => item.numeric + acc, 0);
const legendItems = values.map<VizLegendItem>((value) => {
return {
label: value.title ?? '',
color: value.color ?? FALLBACK_COLOR,
yAxis: 1,
getDisplayValues: () => {
let displayValues = [];
if (legendOptions.displayColumns.includes(LegendColumns.Value)) {
displayValues.push({ numeric: value.numeric, text: formattedValueToString(value), title: 'Value' });
}
if (legendOptions.displayColumns.includes(LegendColumns.Percent)) {
const fractionOfTotal = value.numeric / total;
const percentOfTotal = fractionOfTotal * 100;
displayValues.push({
numeric: fractionOfTotal,
percent: percentOfTotal,
text: percentOfTotal.toFixed(0) + '%',
title: 'Percent',
});
}
return displayValues;
},
};
});
@ -76,7 +107,7 @@ export const PieChartSvg: FC<SvgProps> = ({
width,
height,
useGradients = true,
labelOptions = { showName: true },
displayLabels = [],
}) => {
const theme = useTheme();
const componentInstanceId = useComponentInstanceId('PieChart');
@ -106,7 +137,7 @@ export const PieChartSvg: FC<SvgProps> = ({
});
};
const showLabel = labelOptions.showName || labelOptions.showPercent || labelOptions.showValue;
const showLabel = displayLabels.length > 0;
const total = values.reduce((acc, item) => item.numeric + acc, 0);
const layout = getPieLayout(width, height, pieType);
@ -159,7 +190,7 @@ export const PieChartSvg: FC<SvgProps> = ({
arc={arc}
outerRadius={layout.outerRadius}
innerRadius={layout.innerRadius}
labelOptions={labelOptions}
displayLabels={displayLabels}
total={total}
/>
)}
@ -183,9 +214,9 @@ const PieLabel: FC<{
arc: PieArcDatum<DisplayValue>;
outerRadius: number;
innerRadius: number;
labelOptions: PieChartLabelOptions;
displayLabels: PieChartLabels[];
total: number;
}> = ({ arc, outerRadius, innerRadius, labelOptions, total }) => {
}> = ({ arc, outerRadius, innerRadius, displayLabels, total }) => {
const labelRadius = innerRadius === 0 ? outerRadius / 6 : innerRadius;
const [labelX, labelY] = getLabelPos(arc, outerRadius, labelRadius);
const hasSpaceForLabel = arc.endAngle - arc.startAngle >= 0.3;
@ -194,7 +225,7 @@ const PieLabel: FC<{
return null;
}
let labelFontSize = labelOptions.showName
let labelFontSize = displayLabels.includes(PieChartLabels.Name)
? Math.min(Math.max((outerRadius / 150) * 14, 12), 30)
: Math.min(Math.max((outerRadius / 100) * 14, 12), 36);
@ -209,17 +240,17 @@ const PieLabel: FC<{
textAnchor="middle"
pointerEvents="none"
>
{labelOptions.showName && (
{displayLabels.includes(PieChartLabels.Name) && (
<tspan x={labelX} dy="1.2em">
{arc.data.title}
</tspan>
)}
{labelOptions.showValue && (
{displayLabels.includes(PieChartLabels.Value) && (
<tspan x={labelX} dy="1.2em">
{formattedValueToString(arc.data)}
</tspan>
)}
{labelOptions.showPercent && (
{displayLabels.includes(PieChartLabels.Percent) && (
<tspan x={labelX} dy="1.2em">
{((arc.data.numeric / total) * 100).toFixed(0) + '%'}
</tspan>

View File

@ -39,7 +39,7 @@ export const VizLegendList: React.FunctionComponent<Props> = ({
return (
<div className={cx(styles.rightWrapper, className)}>
<List items={items} renderItem={renderItem} getItemKey={getItemKey} className={className} />
<List items={items} renderItem={renderItem} getItemKey={getItemKey} />
</div>
);
}

View File

@ -62,6 +62,7 @@ const getStyles = (theme: GrafanaTheme) => ({
display: flex;
white-space: nowrap;
align-items: center;
flex-grow: 1;
`,
value: css`
text-align: right;

View File

@ -3,29 +3,39 @@ import { InlineList } from '../List/InlineList';
import { css } from 'emotion';
import { DisplayValue, formattedValueToString } from '@grafana/data';
import capitalize from 'lodash/capitalize';
const VizLegendItemStat: React.FunctionComponent<{ stat: DisplayValue }> = ({ stat }) => {
const styles = css`
margin-left: 8px;
`;
return (
<div className={styles}>
{stat.title && `${capitalize(stat.title)}:`} {formattedValueToString(stat)}
</div>
);
};
VizLegendItemStat.displayName = 'VizLegendItemStat';
import { useStyles } from '../../themes/ThemeContext';
/**
* @internal
*/
export const VizLegendStatsList: React.FunctionComponent<{ stats: DisplayValue[] }> = ({ stats }) => {
const styles = useStyles(getStyles);
if (stats.length === 0) {
return null;
}
return <InlineList items={stats} renderItem={(stat) => <VizLegendItemStat stat={stat} />} />;
return (
<InlineList
className={styles.list}
items={stats}
renderItem={(stat) => (
<div className={styles.item}>
{stat.title && `${capitalize(stat.title)}:`} {formattedValueToString(stat)}
</div>
)}
/>
);
};
const getStyles = () => ({
list: css`
flex-grow: 1;
text-align: right;
`,
item: css`
margin-left: 8px;
`,
});
VizLegendStatsList.displayName = 'VizLegendStatsList';

View File

@ -17,7 +17,7 @@ export { LoadingPlaceholder, LoadingPlaceholderProps } from './LoadingPlaceholde
export { ColorPicker, SeriesColorPicker } from './ColorPicker/ColorPicker';
export { SeriesColorPickerPopover, SeriesColorPickerPopoverWithTheme } from './ColorPicker/SeriesColorPickerPopover';
export { EmptySearchResult } from './EmptySearchResult/EmptySearchResult';
export { PieChart, PieChartType, PieChartLabelOptions } from './PieChart/PieChart';
export { PieChart, PieChartType, PieChartLabels, PieChartLegendOptions } from './PieChart/PieChart';
export { UnitPicker } from './UnitPicker/UnitPicker';
export { StatsPicker } from './StatsPicker/StatsPicker';
export { RefreshPicker, defaultIntervals } from './RefreshPicker/RefreshPicker';

View File

@ -25,7 +25,8 @@ export class PieChartPanel extends PureComponent<Props> {
height={height}
values={values}
pieType={options.pieType}
labelOptions={options.labelOptions}
displayLabels={options.displayLabels}
legendOptions={options.legend}
/>
);
}

View File

@ -2,7 +2,8 @@ import { FieldColorModeId, FieldConfigProperty, PanelPlugin } from '@grafana/dat
import { PieChartPanel } from './PieChartPanel';
import { PieChartOptions } from './types';
import { addStandardDataReduceOptions } from '../stat/types';
import { PieChartType } from '@grafana/ui';
import { LegendDisplayMode, PieChartType } from '@grafana/ui';
import { LegendColumns, PieChartLabels } from '@grafana/ui/src/components/PieChart/PieChart';
export const plugin = new PanelPlugin<PieChartOptions>(PieChartPanel)
.useFieldConfig({
@ -35,19 +36,53 @@ export const plugin = new PanelPlugin<PieChartOptions>(PieChartPanel)
},
defaultValue: PieChartType.Pie,
})
.addBooleanSwitch({
name: 'Show name',
path: 'labelOptions.showName',
defaultValue: true,
.addMultiSelect({
name: 'Labels',
path: 'displayLabels',
description: 'Select the labels to be displayed in the pie chart',
settings: {
options: [
{ value: PieChartLabels.Percent, label: 'Percent' },
{ value: PieChartLabels.Name, label: 'Name' },
{ value: PieChartLabels.Value, label: 'Value' },
],
},
})
.addBooleanSwitch({
name: 'Show value',
path: 'labelOptions.showValue',
defaultValue: false,
.addRadio({
path: 'legend.displayMode',
name: 'Legend mode',
description: '',
defaultValue: LegendDisplayMode.List,
settings: {
options: [
{ value: LegendDisplayMode.List, label: 'List' },
{ value: LegendDisplayMode.Table, label: 'Table' },
{ value: LegendDisplayMode.Hidden, label: 'Hidden' },
],
},
})
.addBooleanSwitch({
name: 'Show percent',
path: 'labelOptions.showPercent',
defaultValue: false,
.addRadio({
path: 'legend.placement',
name: 'Legend placement',
description: '',
defaultValue: 'right',
settings: {
options: [
{ value: 'bottom', label: 'Bottom' },
{ value: 'right', label: 'Right' },
],
},
showIf: (c) => c.legend.displayMode !== LegendDisplayMode.Hidden,
})
.addMultiSelect({
name: 'Legend values',
path: 'legend.displayColumns',
settings: {
options: [
{ value: LegendColumns.Percent, label: 'Percent' },
{ value: LegendColumns.Value, label: 'Value' },
],
},
showIf: (c) => c.legend.displayMode !== LegendDisplayMode.Hidden,
});
});

View File

@ -1,6 +1,7 @@
import { PieChartType, SingleStatBaseOptions, PieChartLabelOptions } from '@grafana/ui';
import { PieChartType, SingleStatBaseOptions, PieChartLabels, PieChartLegendOptions } from '@grafana/ui';
export interface PieChartOptions extends SingleStatBaseOptions {
pieType: PieChartType;
labelOptions: PieChartLabelOptions;
displayLabels: PieChartLabels[];
legend: PieChartLegendOptions;
}