mirror of
https://github.com/grafana/grafana.git
synced 2025-01-13 09:32:12 -06:00
Add datalinks support to PieChart v2 (#31642)
* Remove default value for multiSelect * Add data links support for PieChart v2 * Temporarily fix story * Refactor PieSlice, deduplicate colors * Add field config to context menu of pie chart * Add custom key to legend This is a bit hacky. When there are multiple DisplayValues with the same title the react keys collide and LegendListItems are not cleared correctly. * Forgot to add the interface * Fix missing tooltip in edit mode
This commit is contained in:
parent
477a54ae54
commit
e7757b0175
@ -186,7 +186,6 @@ export class PanelOptionsEditorBuilder<TOptions> extends OptionsUIRegistryBuilde
|
||||
) {
|
||||
return this.addCustomEditor({
|
||||
...config,
|
||||
defaultValue: config.defaultValue ?? [],
|
||||
id: config.path,
|
||||
editor: standardEditorsRegistry.get('multi-select').editor as any,
|
||||
});
|
||||
|
@ -12,7 +12,7 @@ interface DataLinksContextMenuProps {
|
||||
}
|
||||
|
||||
export interface DataLinksContextMenuApi {
|
||||
openMenu?: React.MouseEventHandler<HTMLElement>;
|
||||
openMenu?: React.MouseEventHandler<HTMLOrSVGElement>;
|
||||
targetClassName?: string;
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { object, select, number, boolean } from '@storybook/addon-knobs';
|
||||
import { PieChart, PieChartType } from '@grafana/ui';
|
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
import { FieldConfig } from '@grafana/data';
|
||||
|
||||
export default {
|
||||
title: 'Visualizations/PieChart',
|
||||
@ -9,14 +10,25 @@ export default {
|
||||
component: PieChart,
|
||||
};
|
||||
|
||||
const fieldConfig: FieldConfig = {
|
||||
displayName: '',
|
||||
min: 0,
|
||||
max: 10,
|
||||
decimals: 10,
|
||||
thresholds: {} as any,
|
||||
noValue: 'no value',
|
||||
unit: 'km/s',
|
||||
links: {} as any,
|
||||
};
|
||||
|
||||
const getKnobs = () => {
|
||||
return {
|
||||
datapoints: object('datapoints', [
|
||||
{ numeric: 100, text: '100', title: 'USA' },
|
||||
{ numeric: 200, text: '200', title: 'Canada' },
|
||||
{ numeric: 20, text: '20', title: 'Sweden' },
|
||||
{ numeric: 50, text: '50', title: 'Spain' },
|
||||
{ numeric: 70, text: '70', title: 'Germeny' },
|
||||
{ field: fieldConfig, hasLinks: false, name: 'USA', display: { numeric: 100, text: '100', title: 'USA' } },
|
||||
{ field: fieldConfig, hasLinks: false, name: 'Canada', display: { numeric: 200, text: '200', title: 'Canada' } },
|
||||
{ field: fieldConfig, hasLinks: false, name: 'Sweden', display: { numeric: 20, text: '20', title: 'Sweden' } },
|
||||
{ field: fieldConfig, hasLinks: false, name: 'Spain', display: { numeric: 50, text: '50', title: 'Spain' } },
|
||||
{ field: fieldConfig, hasLinks: false, name: 'Germany', display: { numeric: 70, text: '70', title: 'Germeny' } },
|
||||
]),
|
||||
width: number('Width', 500),
|
||||
height: number('Height', 500),
|
||||
@ -30,11 +42,11 @@ const getKnobs = () => {
|
||||
export const basic = () => {
|
||||
const { datapoints, pieType, width, height } = getKnobs();
|
||||
|
||||
return <PieChart width={width} height={height} values={datapoints} pieType={pieType} />;
|
||||
return <PieChart width={width} height={height} fieldDisplayValues={datapoints} pieType={pieType} />;
|
||||
};
|
||||
|
||||
export const donut = () => {
|
||||
const { datapoints, width, height } = getKnobs();
|
||||
|
||||
return <PieChart width={width} height={height} values={datapoints} pieType={PieChartType.Donut} />;
|
||||
return <PieChart width={width} height={height} fieldDisplayValues={datapoints} pieType={PieChartType.Donut} />;
|
||||
};
|
||||
|
@ -1,8 +1,8 @@
|
||||
import React, { FC } from 'react';
|
||||
import { DisplayValue, FALLBACK_COLOR, formattedValueToString, GrafanaTheme } from '@grafana/data';
|
||||
import React, { FC, ReactNode } from 'react';
|
||||
import { DisplayValue, FALLBACK_COLOR, FieldDisplay, formattedValueToString, GrafanaTheme } from '@grafana/data';
|
||||
import { useStyles, useTheme } from '../../themes/ThemeContext';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import Pie, { PieArcDatum } from '@visx/shape/lib/shapes/Pie';
|
||||
import Pie, { PieArcDatum, ProvidedProps } from '@visx/shape/lib/shapes/Pie';
|
||||
import { Group } from '@visx/group';
|
||||
import { RadialGradient } from '@visx/gradient';
|
||||
import { localPoint } from '@visx/event';
|
||||
@ -12,6 +12,8 @@ import { css } from 'emotion';
|
||||
import { VizLegend, VizLegendItem } from '..';
|
||||
import { VizLayout } from '../VizLayout/VizLayout';
|
||||
import { LegendDisplayMode, VizLegendOptions } from '../VizLegend/types';
|
||||
import { DataLinksContextMenu } from '../DataLinks/DataLinksContextMenu';
|
||||
import { UseTooltipParams } from '@visx/tooltip/lib/hooks/useTooltip';
|
||||
|
||||
export enum PieChartLabels {
|
||||
Name = 'name',
|
||||
@ -27,7 +29,7 @@ export enum PieChartLegendValues {
|
||||
interface SvgProps {
|
||||
height: number;
|
||||
width: number;
|
||||
values: DisplayValue[];
|
||||
fieldDisplayValues: FieldDisplay[];
|
||||
pieType: PieChartType;
|
||||
displayLabels?: PieChartLabels[];
|
||||
useGradients?: boolean;
|
||||
@ -54,24 +56,26 @@ const defaultLegendOptions: PieChartLegendOptions = {
|
||||
};
|
||||
|
||||
export const PieChart: FC<Props> = ({
|
||||
values,
|
||||
fieldDisplayValues,
|
||||
legendOptions = defaultLegendOptions,
|
||||
onSeriesColorChange,
|
||||
width,
|
||||
height,
|
||||
...restProps
|
||||
}) => {
|
||||
const getLegend = (values: DisplayValue[], legendOptions: PieChartLegendOptions) => {
|
||||
const getLegend = (fields: FieldDisplay[], legendOptions: PieChartLegendOptions) => {
|
||||
if (legendOptions.displayMode === LegendDisplayMode.Hidden) {
|
||||
return undefined;
|
||||
}
|
||||
const values = fields.map((v) => v.display);
|
||||
const total = values.reduce((acc, item) => item.numeric + acc, 0);
|
||||
|
||||
const legendItems = values.map<VizLegendItem>((value) => {
|
||||
const legendItems = values.map<VizLegendItem>((value, idx) => {
|
||||
return {
|
||||
label: value.title ?? '',
|
||||
color: value.color ?? FALLBACK_COLOR,
|
||||
yAxis: 1,
|
||||
getItemKey: () => (value.title ?? '') + idx,
|
||||
getDisplayValues: () => {
|
||||
const valuesToShow = legendOptions.values ?? [];
|
||||
let displayValues = [];
|
||||
@ -108,16 +112,18 @@ export const PieChart: FC<Props> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<VizLayout width={width} height={height} legend={getLegend(values, legendOptions)}>
|
||||
<VizLayout width={width} height={height} legend={getLegend(fieldDisplayValues, legendOptions)}>
|
||||
{(vizWidth: number, vizHeight: number) => {
|
||||
return <PieChartSvg width={vizWidth} height={vizHeight} values={values} {...restProps} />;
|
||||
return (
|
||||
<PieChartSvg width={vizWidth} height={vizHeight} fieldDisplayValues={fieldDisplayValues} {...restProps} />
|
||||
);
|
||||
}}
|
||||
</VizLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export const PieChartSvg: FC<SvgProps> = ({
|
||||
values,
|
||||
fieldDisplayValues,
|
||||
pieType,
|
||||
width,
|
||||
height,
|
||||
@ -127,44 +133,37 @@ export const PieChartSvg: FC<SvgProps> = ({
|
||||
const theme = useTheme();
|
||||
const componentInstanceId = useComponentInstanceId('PieChart');
|
||||
const styles = useStyles(getStyles);
|
||||
const { tooltipData, tooltipLeft, tooltipTop, tooltipOpen, showTooltip, hideTooltip } = useTooltip<DisplayValue>();
|
||||
const tooltip = useTooltip<DisplayValue>();
|
||||
const { containerRef, TooltipInPortal } = useTooltipInPortal({
|
||||
detectBounds: true,
|
||||
scroll: true,
|
||||
});
|
||||
|
||||
if (values.length < 0) {
|
||||
if (fieldDisplayValues.length < 0) {
|
||||
return <div>No data</div>;
|
||||
}
|
||||
|
||||
const getValue = (d: DisplayValue) => d.numeric;
|
||||
const getValue = (d: FieldDisplay) => d.display.numeric;
|
||||
const getGradientId = (color: string) => `${componentInstanceId}-${color}`;
|
||||
const getGradientColor = (color: string) => {
|
||||
return `url(#${getGradientId(color)})`;
|
||||
};
|
||||
|
||||
const onMouseMoveOverArc = (event: any, datum: any) => {
|
||||
const coords = localPoint(event.target.ownerSVGElement, event);
|
||||
showTooltip({
|
||||
tooltipLeft: coords!.x,
|
||||
tooltipTop: coords!.y,
|
||||
tooltipData: datum,
|
||||
});
|
||||
};
|
||||
|
||||
const showLabel = displayLabels.length > 0;
|
||||
const total = values.reduce((acc, item) => item.numeric + acc, 0);
|
||||
const total = fieldDisplayValues.reduce((acc, item) => item.display.numeric + acc, 0);
|
||||
const layout = getPieLayout(width, height, pieType);
|
||||
const colors = [
|
||||
...new Set(fieldDisplayValues.map((fieldDisplayValue) => fieldDisplayValue.display.color ?? FALLBACK_COLOR)),
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<svg width={layout.size} height={layout.size} ref={containerRef}>
|
||||
<Group top={layout.position} left={layout.position}>
|
||||
{values.map((value) => {
|
||||
const color = value.color ?? FALLBACK_COLOR;
|
||||
{colors.map((color) => {
|
||||
return (
|
||||
<RadialGradient
|
||||
key={value.color}
|
||||
key={color}
|
||||
id={getGradientId(color)}
|
||||
from={getGradientColorFrom(color, theme)}
|
||||
to={getGradientColorTo(color, theme)}
|
||||
@ -178,7 +177,7 @@ export const PieChartSvg: FC<SvgProps> = ({
|
||||
);
|
||||
})}
|
||||
<Pie
|
||||
data={values}
|
||||
data={fieldDisplayValues}
|
||||
pieValue={getValue}
|
||||
outerRadius={layout.outerRadius}
|
||||
innerRadius={layout.innerRadius}
|
||||
@ -187,47 +186,95 @@ export const PieChartSvg: FC<SvgProps> = ({
|
||||
>
|
||||
{(pie) => {
|
||||
return pie.arcs.map((arc) => {
|
||||
return (
|
||||
<g
|
||||
key={arc.data.title}
|
||||
className={styles.svgArg}
|
||||
onMouseMove={(event) => onMouseMoveOverArc(event, arc.data)}
|
||||
onMouseOut={hideTooltip}
|
||||
>
|
||||
<path
|
||||
d={pie.path({ ...arc })!}
|
||||
fill={useGradients ? getGradientColor(arc.data.color ?? FALLBACK_COLOR) : arc.data.color}
|
||||
stroke={theme.colors.panelBg}
|
||||
strokeWidth={1}
|
||||
/>
|
||||
{showLabel && (
|
||||
<PieLabel
|
||||
arc={arc}
|
||||
outerRadius={layout.outerRadius}
|
||||
innerRadius={layout.innerRadius}
|
||||
displayLabels={displayLabels}
|
||||
total={total}
|
||||
color={theme.colors.text}
|
||||
/>
|
||||
)}
|
||||
</g>
|
||||
);
|
||||
const color = arc.data.display.color ?? FALLBACK_COLOR;
|
||||
const label = showLabel ? (
|
||||
<PieLabel
|
||||
arc={arc}
|
||||
outerRadius={layout.outerRadius}
|
||||
innerRadius={layout.innerRadius}
|
||||
displayLabels={displayLabels}
|
||||
total={total}
|
||||
color={theme.colors.text}
|
||||
/>
|
||||
) : undefined;
|
||||
if (arc.data.hasLinks && arc.data.getLinks) {
|
||||
return (
|
||||
<DataLinksContextMenu config={arc.data.field} key={arc.index} links={arc.data.getLinks}>
|
||||
{(api) => (
|
||||
<PieSlice
|
||||
tooltip={tooltip}
|
||||
arc={arc}
|
||||
pie={pie}
|
||||
fill={getGradientColor(color)}
|
||||
openMenu={api.openMenu}
|
||||
>
|
||||
{label}
|
||||
</PieSlice>
|
||||
)}
|
||||
</DataLinksContextMenu>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<PieSlice key={arc.index} tooltip={tooltip} arc={arc} pie={pie} fill={getGradientColor(color)}>
|
||||
{label}
|
||||
</PieSlice>
|
||||
);
|
||||
}
|
||||
});
|
||||
}}
|
||||
</Pie>
|
||||
</Group>
|
||||
</svg>
|
||||
{tooltipOpen && (
|
||||
<TooltipInPortal key={Math.random()} top={tooltipTop} left={tooltipLeft}>
|
||||
{tooltipData!.title} {formattedValueToString(tooltipData!)}
|
||||
{tooltip.tooltipOpen && (
|
||||
<TooltipInPortal
|
||||
key={Math.random()}
|
||||
top={tooltip.tooltipTop}
|
||||
className={styles.tooltipPortal}
|
||||
left={tooltip.tooltipLeft}
|
||||
>
|
||||
{tooltip.tooltipData!.title} {formattedValueToString(tooltip.tooltipData!)}
|
||||
</TooltipInPortal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PieSlice: FC<{
|
||||
children: ReactNode;
|
||||
arc: PieArcDatum<FieldDisplay>;
|
||||
pie: ProvidedProps<FieldDisplay>;
|
||||
fill: string;
|
||||
tooltip: UseTooltipParams<DisplayValue>;
|
||||
openMenu?: (event: React.MouseEvent<SVGElement>) => void;
|
||||
}> = ({ arc, children, pie, openMenu, fill, tooltip }) => {
|
||||
const theme = useTheme();
|
||||
const styles = useStyles(getStyles);
|
||||
|
||||
const onMouseMoveOverArc = (event: any, datum: any) => {
|
||||
const coords = localPoint(event.target.ownerSVGElement, event);
|
||||
tooltip.showTooltip({
|
||||
tooltipLeft: coords!.x,
|
||||
tooltipTop: coords!.y,
|
||||
tooltipData: datum,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<g
|
||||
key={arc.data.display.title}
|
||||
className={styles.svgArg}
|
||||
onMouseMove={(event) => onMouseMoveOverArc(event, arc.data.display)}
|
||||
onMouseOut={tooltip.hideTooltip}
|
||||
onClick={openMenu}
|
||||
>
|
||||
<path d={pie.path({ ...arc })!} fill={fill} stroke={theme.colors.panelBg} strokeWidth={1} />
|
||||
{children}
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
const PieLabel: FC<{
|
||||
arc: PieArcDatum<DisplayValue>;
|
||||
arc: PieArcDatum<FieldDisplay>;
|
||||
outerRadius: number;
|
||||
innerRadius: number;
|
||||
displayLabels: PieChartLabels[];
|
||||
@ -259,17 +306,17 @@ const PieLabel: FC<{
|
||||
>
|
||||
{displayLabels.includes(PieChartLabels.Name) && (
|
||||
<tspan x={labelX} dy="1.2em">
|
||||
{arc.data.title}
|
||||
{arc.data.display.title}
|
||||
</tspan>
|
||||
)}
|
||||
{displayLabels.includes(PieChartLabels.Value) && (
|
||||
<tspan x={labelX} dy="1.2em">
|
||||
{formattedValueToString(arc.data)}
|
||||
{formattedValueToString(arc.data.display)}
|
||||
</tspan>
|
||||
)}
|
||||
{displayLabels.includes(PieChartLabels.Percent) && (
|
||||
<tspan x={labelX} dy="1.2em">
|
||||
{((arc.data.numeric / total) * 100).toFixed(0) + '%'}
|
||||
{((arc.data.display.numeric / total) * 100).toFixed(0) + '%'}
|
||||
</tspan>
|
||||
)}
|
||||
</text>
|
||||
@ -277,7 +324,7 @@ const PieLabel: FC<{
|
||||
);
|
||||
};
|
||||
|
||||
function getLabelPos(arc: PieArcDatum<DisplayValue>, outerRadius: number, innerRadius: number) {
|
||||
function getLabelPos(arc: PieArcDatum<FieldDisplay>, outerRadius: number, innerRadius: number) {
|
||||
const r = (outerRadius + innerRadius) / 2;
|
||||
const a = (+arc.startAngle + +arc.endAngle) / 2 - Math.PI / 2;
|
||||
return [Math.cos(a) * r, Math.sin(a) * r];
|
||||
@ -337,5 +384,8 @@ const getStyles = (theme: GrafanaTheme) => {
|
||||
transform: scale3d(1.03, 1.03, 1);
|
||||
}
|
||||
`,
|
||||
tooltipPortal: css`
|
||||
z-index: 1050;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
@ -29,7 +29,7 @@ export const VizLegendList: React.FunctionComponent<Props> = ({
|
||||
);
|
||||
}
|
||||
|
||||
const getItemKey = (item: VizLegendItem) => `${item.label}`;
|
||||
const getItemKey = (item: VizLegendItem) => `${item.getItemKey ? item.getItemKey() : item.label}`;
|
||||
|
||||
switch (placement) {
|
||||
case 'right': {
|
||||
|
@ -20,6 +20,7 @@ export interface LegendProps extends VizLegendBaseProps, VizLegendTableProps {
|
||||
}
|
||||
|
||||
export interface VizLegendItem {
|
||||
getItemKey?: () => string;
|
||||
label: string;
|
||||
color: string;
|
||||
yAxis: number;
|
||||
|
@ -23,20 +23,20 @@ export const PieChartPanel: React.FC<Props> = ({
|
||||
[fieldConfig, onFieldConfigChange]
|
||||
);
|
||||
|
||||
const values = getFieldDisplayValues({
|
||||
const fieldDisplayValues = getFieldDisplayValues({
|
||||
fieldConfig,
|
||||
reduceOptions: options.reduceOptions,
|
||||
data: data.series,
|
||||
theme: useTheme(),
|
||||
replaceVariables: replaceVariables,
|
||||
timeZone,
|
||||
}).map((v) => v.display);
|
||||
});
|
||||
|
||||
return (
|
||||
<PieChart
|
||||
width={width}
|
||||
height={height}
|
||||
values={values}
|
||||
fieldDisplayValues={fieldDisplayValues}
|
||||
onSeriesColorChange={onSeriesColorChange}
|
||||
pieType={options.pieType}
|
||||
displayLabels={options.displayLabels}
|
||||
|
Loading…
Reference in New Issue
Block a user