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]
This commit is contained in:
@@ -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 @@
|
|||||||
"fieldConfig": {
|
"fieldConfig": {
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"color": {
|
"color": {
|
||||||
"mode": "palette-saturated"
|
"mode": "palette-classic"
|
||||||
},
|
},
|
||||||
"custom": {},
|
"custom": {},
|
||||||
"decimals": 1,
|
"decimals": 1,
|
||||||
@@ -149,7 +149,7 @@
|
|||||||
"fieldConfig": {
|
"fieldConfig": {
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"color": {
|
"color": {
|
||||||
"mode": "palette-saturated"
|
"mode": "palette-classic"
|
||||||
},
|
},
|
||||||
"custom": {},
|
"custom": {},
|
||||||
"mappings": [],
|
"mappings": [],
|
||||||
@@ -210,7 +210,7 @@
|
|||||||
"fieldConfig": {
|
"fieldConfig": {
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"color": {
|
"color": {
|
||||||
"mode": "palette-saturated"
|
"mode": "palette-classic"
|
||||||
},
|
},
|
||||||
"custom": {},
|
"custom": {},
|
||||||
"mappings": [],
|
"mappings": [],
|
||||||
@@ -271,7 +271,7 @@
|
|||||||
"fieldConfig": {
|
"fieldConfig": {
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"color": {
|
"color": {
|
||||||
"mode": "palette-saturated"
|
"mode": "palette-classic"
|
||||||
},
|
},
|
||||||
"custom": {},
|
"custom": {},
|
||||||
"mappings": [],
|
"mappings": [],
|
||||||
@@ -332,7 +332,7 @@
|
|||||||
"fieldConfig": {
|
"fieldConfig": {
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"color": {
|
"color": {
|
||||||
"mode": "palette-saturated"
|
"mode": "palette-classic"
|
||||||
},
|
},
|
||||||
"custom": {},
|
"custom": {},
|
||||||
"mappings": [],
|
"mappings": [],
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
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';
|
|||||||
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 {
|
|||||||
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
|
|||||||
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
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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
|
|||||||
>
|
>
|
||||||
<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) {
|
|||||||
.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`
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
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>;
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user