Commit aeb4bcb3 by Oscar Kilhed Committed by GitHub

Grafana/UI: Add basic legend to the PieChart (#31278)

* Add basic legend to the PieChart

* Remove console log

* Remove PieChartWithLegend and refactor PieChart a bit

* Use FALLBACK_COLOR

* Refactor PieChart

* Change back to [function]
parent fe74c51d
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
"fieldConfig": { "fieldConfig": {
"defaults": { "defaults": {
"color": { "color": {
"mode": "palette-saturated" "mode": "palette-classic"
}, },
"custom": {}, "custom": {},
"decimals": 1, "decimals": 1,
...@@ -86,7 +86,7 @@ ...@@ -86,7 +86,7 @@
"fieldConfig": { "fieldConfig": {
"defaults": { "defaults": {
"color": { "color": {
"mode": "palette-saturated" "mode": "palette-classic"
}, },
"custom": {}, "custom": {},
"decimals": 1, "decimals": 1,
...@@ -149,7 +149,7 @@ ...@@ -149,7 +149,7 @@
"fieldConfig": { "fieldConfig": {
"defaults": { "defaults": {
"color": { "color": {
"mode": "palette-saturated" "mode": "palette-classic"
}, },
"custom": {}, "custom": {},
"mappings": [], "mappings": [],
...@@ -210,7 +210,7 @@ ...@@ -210,7 +210,7 @@
"fieldConfig": { "fieldConfig": {
"defaults": { "defaults": {
"color": { "color": {
"mode": "palette-saturated" "mode": "palette-classic"
}, },
"custom": {}, "custom": {},
"mappings": [], "mappings": [],
...@@ -271,7 +271,7 @@ ...@@ -271,7 +271,7 @@
"fieldConfig": { "fieldConfig": {
"defaults": { "defaults": {
"color": { "color": {
"mode": "palette-saturated" "mode": "palette-classic"
}, },
"custom": {}, "custom": {},
"mappings": [], "mappings": [],
...@@ -332,7 +332,7 @@ ...@@ -332,7 +332,7 @@
"fieldConfig": { "fieldConfig": {
"defaults": { "defaults": {
"color": { "color": {
"mode": "palette-saturated" "mode": "palette-classic"
}, },
"custom": {}, "custom": {},
"mappings": [], "mappings": [],
......
import React, { FC } from 'react'; import React, { FC } from 'react';
import { DisplayValue, formattedValueToString, GrafanaTheme } from '@grafana/data'; import { DisplayValue, FALLBACK_COLOR, formattedValueToString, GrafanaTheme } from '@grafana/data';
import { useStyles, useTheme } from '../../themes/ThemeContext'; import { useStyles, useTheme } from '../../themes/ThemeContext';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import Pie, { PieArcDatum } from '@visx/shape/lib/shapes/Pie'; import Pie, { PieArcDatum } from '@visx/shape/lib/shapes/Pie';
...@@ -9,13 +9,20 @@ import { localPoint } from '@visx/event'; ...@@ -9,13 +9,20 @@ import { localPoint } from '@visx/event';
import { useTooltip, useTooltipInPortal } from '@visx/tooltip'; import { useTooltip, useTooltipInPortal } from '@visx/tooltip';
import { useComponentInstanceId } from '../../utils/useComponetInstanceId'; import { useComponentInstanceId } from '../../utils/useComponetInstanceId';
import { css } from 'emotion'; import { css } from 'emotion';
import { VizLegend, VizLegendItem } from '..';
import { VizLayout } from '../VizLayout/VizLayout';
import { LegendDisplayMode, VizLegendOptions } from '../VizLegend/types';
export interface Props { interface SvgProps {
height: number; height: number;
width: number; width: number;
values: DisplayValue[]; values: DisplayValue[];
pieType: PieChartType; pieType: PieChartType;
labelOptions?: PieChartLabelOptions; labelOptions?: PieChartLabelOptions;
useGradients?: boolean;
}
export interface Props extends SvgProps {
legendOptions?: VizLegendOptions;
} }
export enum PieChartType { export enum PieChartType {
...@@ -29,7 +36,48 @@ export interface PieChartLabelOptions { ...@@ -29,7 +36,48 @@ export interface PieChartLabelOptions {
showPercent?: boolean; showPercent?: boolean;
} }
export const PieChart: FC<Props> = ({ values, pieType, width, height, labelOptions = { showName: true } }) => { const defaultLegendOptions: VizLegendOptions = {
displayMode: LegendDisplayMode.List,
placement: 'right',
calcs: [],
};
export const PieChart: FC<Props> = ({ values, legendOptions = defaultLegendOptions, width, height, ...restProps }) => {
const getLegend = (values: DisplayValue[], legendOptions: VizLegendOptions) => {
if (legendOptions.displayMode === LegendDisplayMode.Hidden) {
return undefined;
}
const legendItems = values.map<VizLegendItem>((value) => {
return {
label: value.title ?? '',
color: value.color ?? FALLBACK_COLOR,
yAxis: 1,
};
});
return (
<VizLegend items={legendItems} placement={legendOptions.placement} displayMode={legendOptions.displayMode} />
);
};
return (
<VizLayout width={width} height={height} legend={getLegend(values, legendOptions)}>
{(vizWidth: number, vizHeight: number) => {
return <PieChartSvg width={vizWidth} height={vizHeight} values={values} {...restProps} />;
}}
</VizLayout>
);
};
export const PieChartSvg: FC<SvgProps> = ({
values,
pieType,
width,
height,
useGradients = true,
labelOptions = { showName: true },
}) => {
const theme = useTheme(); const theme = useTheme();
const componentInstanceId = useComponentInstanceId('PieChart'); const componentInstanceId = useComponentInstanceId('PieChart');
const styles = useStyles(getStyles); const styles = useStyles(getStyles);
...@@ -43,20 +91,11 @@ export const PieChart: FC<Props> = ({ values, pieType, width, height, labelOptio ...@@ -43,20 +91,11 @@ export const PieChart: FC<Props> = ({ values, pieType, width, height, labelOptio
return <div>No data</div>; return <div>No data</div>;
} }
const margin = 16;
const size = Math.min(width, height);
const outerRadius = (size - margin * 2) / 2;
const donutThickness = pieType === PieChartType.Pie ? outerRadius : Math.max(outerRadius / 3, 20);
const innerRadius = outerRadius - donutThickness;
const centerOffset = (size - margin * 2) / 2;
const total = values.reduce((acc, item) => item.numeric + acc, 0);
// for non donut pie charts shift gradient out a bit
const gradientFromOffset = 1 - (outerRadius - innerRadius) / outerRadius;
const showLabel = labelOptions.showName || labelOptions.showPercent || labelOptions.showValue;
const getValue = (d: DisplayValue) => d.numeric; const getValue = (d: DisplayValue) => d.numeric;
const getGradientId = (idx: number) => `${componentInstanceId}-${idx}`; const getGradientId = (color: string) => `${componentInstanceId}-${color}`;
const getColor = (arc: PieArcDatum<DisplayValue>) => `url(#${getGradientId(arc.index)})`; const getGradientColor = (color: string) => {
return `url(#${getGradientId(color)})`;
};
const onMouseMoveOverArc = (event: any, datum: any) => { const onMouseMoveOverArc = (event: any, datum: any) => {
const coords = localPoint(event.target.ownerSVGElement, event); const coords = localPoint(event.target.ownerSVGElement, event);
...@@ -67,32 +106,36 @@ export const PieChart: FC<Props> = ({ values, pieType, width, height, labelOptio ...@@ -67,32 +106,36 @@ export const PieChart: FC<Props> = ({ values, pieType, width, height, labelOptio
}); });
}; };
const showLabel = labelOptions.showName || labelOptions.showPercent || labelOptions.showValue;
const total = values.reduce((acc, item) => item.numeric + acc, 0);
const layout = getPieLayout(width, height, pieType);
return ( return (
<div className={styles.container}> <div className={styles.container}>
<svg width={size} height={size} ref={containerRef}> <svg width={layout.size} height={layout.size} ref={containerRef}>
<Group top={centerOffset + margin} left={centerOffset + margin}> <Group top={layout.position} left={layout.position}>
{values.map((value, idx) => { {values.map((value) => {
const color = value.color ?? 'gray'; const color = value.color ?? FALLBACK_COLOR;
return ( return (
<RadialGradient <RadialGradient
key={idx} key={value.color}
id={getGradientId(idx)} id={getGradientId(color)}
from={getGradientColorFrom(color, theme)} from={getGradientColorFrom(color, theme)}
to={getGradientColorTo(color, theme)} to={getGradientColorTo(color, theme)}
fromOffset={gradientFromOffset} fromOffset={layout.gradientFromOffset}
toOffset="1" toOffset="1"
gradientUnits="userSpaceOnUse" gradientUnits="userSpaceOnUse"
cx={0} cx={0}
cy={0} cy={0}
radius={outerRadius} radius={layout.outerRadius}
/> />
); );
})} })}
<Pie <Pie
data={values} data={values}
pieValue={getValue} pieValue={getValue}
outerRadius={outerRadius} outerRadius={layout.outerRadius}
innerRadius={innerRadius} innerRadius={layout.innerRadius}
cornerRadius={3} cornerRadius={3}
padAngle={0.005} padAngle={0.005}
> >
...@@ -107,15 +150,15 @@ export const PieChart: FC<Props> = ({ values, pieType, width, height, labelOptio ...@@ -107,15 +150,15 @@ export const PieChart: FC<Props> = ({ values, pieType, width, height, labelOptio
> >
<path <path
d={pie.path({ ...arc })!} d={pie.path({ ...arc })!}
fill={getColor(arc)} fill={useGradients ? getGradientColor(arc.data.color ?? FALLBACK_COLOR) : arc.data.color}
stroke={theme.colors.panelBg} stroke={theme.colors.panelBg}
strokeWidth={1} strokeWidth={1}
/> />
{showLabel && ( {showLabel && (
<PieLabel <PieLabel
arc={arc} arc={arc}
outerRadius={outerRadius} outerRadius={layout.outerRadius}
innerRadius={innerRadius} innerRadius={layout.innerRadius}
labelOptions={labelOptions} labelOptions={labelOptions}
total={total} total={total}
/> />
...@@ -206,6 +249,31 @@ function getGradientColorTo(color: string, theme: GrafanaTheme) { ...@@ -206,6 +249,31 @@ function getGradientColorTo(color: string, theme: GrafanaTheme) {
.toRgbString(); .toRgbString();
} }
interface PieLayout {
position: number;
size: number;
outerRadius: number;
innerRadius: number;
gradientFromOffset: number;
}
function getPieLayout(height: number, width: number, pieType: PieChartType, margin = 16): PieLayout {
const size = Math.min(width, height);
const outerRadius = (size - margin * 2) / 2;
const donutThickness = pieType === PieChartType.Pie ? outerRadius : Math.max(outerRadius / 3, 20);
const innerRadius = outerRadius - donutThickness;
const centerOffset = (size - margin * 2) / 2;
// for non donut pie charts shift gradient out a bit
const gradientFromOffset = 1 - (outerRadius - innerRadius) / outerRadius;
return {
position: centerOffset + margin,
size: size,
outerRadius: outerRadius,
innerRadius: innerRadius,
gradientFromOffset: gradientFromOffset,
};
}
const getStyles = (theme: GrafanaTheme) => { const getStyles = (theme: GrafanaTheme) => {
return { return {
container: css` container: css`
......
import React, { FC } from 'react';
import { Props as PieChartProps } from './PieChart';
export interface Props extends PieChartProps {}
export const PieChartWithLegend: FC<Props> = ({ width, height, ...restProps }) => {
return <div>Need VizLayout in grafana/ui</div>;
};
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment