PieChart: move the pie chart component into the panel (#33800)

* move pie chart

* move pie chart

* Pass displayLabels to piechart

Co-authored-by: Oscar Kilhed <oscar.kilhed@grafana.com>
This commit is contained in:
Ryan McKinley
2021-05-10 02:50:25 -07:00
committed by GitHub
parent 1d15686bdf
commit 95464e1bfb
10 changed files with 213 additions and 627 deletions

View File

@@ -1,317 +0,0 @@
import React from 'react';
import { Meta, Story } from '@storybook/react';
import { PieChart, PieChartProps, PieChartType, TooltipDisplayMode } from '@grafana/ui';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import {
FieldColorModeId,
FieldConfigSource,
FieldType,
InterpolateFunction,
ReduceDataOptions,
ThresholdsMode,
toDataFrame,
} from '@grafana/data';
import { LegendDisplayMode, LegendPlacement } from '../VizLegend/models.gen';
import { PieChartLabels, PieChartLegendValues } from './types';
const fieldConfig: FieldConfigSource = {
defaults: {
thresholds: {
mode: ThresholdsMode.Percentage,
steps: [{ color: 'green', value: 0 }],
},
color: {
mode: FieldColorModeId.PaletteClassic,
},
},
overrides: [],
};
const reduceOptions: ReduceDataOptions = { calcs: [] };
const replaceVariables: InterpolateFunction = (v) => v;
const data = [
{
fields: [
{
name: 'time',
type: FieldType.time,
config: {
color: {
mode: 'palette-classic',
},
},
values: [1620110299324, 1620110329324],
state: {
displayName: null,
seriesIndex: 0,
},
},
{
name: 'Cellar',
type: FieldType.number,
config: {
color: {
mode: 'palette-classic',
},
},
values: [27.008127592438484, 27.287423219155304],
state: {
displayName: 'Cellar',
seriesIndex: 1,
},
},
],
},
{
fields: [
{
name: 'time',
type: FieldType.time,
config: {
color: {
mode: 'palette-classic',
},
},
values: [1620110299324, 1620110329324],
state: {
displayName: null,
seriesIndex: 1,
},
},
{
name: 'Living room',
type: FieldType.number,
config: {
color: {
mode: 'palette-classic',
},
},
values: [65.97347908625065, 65.6588311671438],
state: {
displayName: 'Living room',
seriesIndex: 2,
},
},
],
},
{
fields: [
{
name: 'time',
type: FieldType.time,
config: {
color: {
mode: 'palette-classic',
},
},
values: [1620110299324, 1620110329324],
state: {
displayName: null,
seriesIndex: 2,
},
},
{
name: 'Porch',
type: FieldType.number,
config: {
color: {
mode: 'palette-classic',
},
},
values: [42.01219662436388, 42.48471312850685],
state: {
displayName: 'Porch',
seriesIndex: 3,
},
},
],
},
{
fields: [
{
name: 'time',
type: FieldType.time,
config: {
color: {
mode: 'palette-classic',
},
},
values: [1620110299324, 1620110329324],
state: {
displayName: null,
seriesIndex: 3,
},
},
{
name: 'Bedroom',
type: FieldType.number,
config: {
color: {
mode: 'palette-classic',
},
},
values: [34.28143812581964, 34.37741979130198],
state: {
displayName: 'Bedroom',
seriesIndex: 4,
},
},
],
},
{
fields: [
{
name: 'time',
type: FieldType.time,
config: {
color: {
mode: 'palette-classic',
},
mappings: [],
},
values: [1620110299324, 1620110329324],
state: {
displayName: null,
seriesIndex: 4,
},
},
{
name: 'Guest room',
type: FieldType.number,
config: {
color: {
mode: 'palette-classic',
},
},
values: [57.855438763786104, 57.521663794462654],
state: {
displayName: 'Guest room',
seriesIndex: 5,
},
},
],
},
];
export default {
title: 'Visualizations/PieChart',
decorators: [withCenteredStory],
component: PieChart,
args: {
width: 500,
height: 500,
fieldConfig,
data,
},
argTypes: {
tooltipMode: {
control: {
type: 'select',
options: Object.values(TooltipDisplayMode),
},
},
legendDisplayMode: {
control: {
type: 'select',
options: Object.values(LegendDisplayMode),
},
},
legendPlacement: {
control: {
type: 'select',
options: ['bottom', 'right'],
},
},
legendValues: {
control: {
type: 'check',
options: Object.values(PieChartLegendValues),
},
},
displayLabels: {
control: {
type: 'check',
options: Object.values(PieChartLabels),
},
},
height: {
control: { type: 'range', min: 300, max: 1000, step: 50 },
},
width: {
control: { type: 'range', min: 300, max: 1000, step: 50 },
},
},
parameters: {
controls: {
exclude: [
'reduceOptions',
'replaceVariables',
'tooltipOptions',
'onSeriesColorChange',
'legendOptions',
'timeZone',
],
},
},
} as Meta;
interface PieChartStoryProps extends PieChartProps {
tooltipMode: TooltipDisplayMode;
legendDisplayMode: LegendDisplayMode;
legendPlacement: LegendPlacement;
legendValues: PieChartLegendValues[];
}
const Template: Story<PieChartStoryProps> = ({
tooltipMode,
legendDisplayMode,
legendPlacement,
legendValues,
data,
...args
}) => {
const tooltipOpts = {
mode: tooltipMode,
};
const legendOptions = {
displayMode: legendDisplayMode,
placement: legendPlacement,
values: legendValues,
calcs: [],
};
const dataPoints = data.map((d) => toDataFrame(d));
return (
<PieChart
{...args}
data={dataPoints}
tooltipOptions={tooltipOpts}
legendOptions={legendOptions}
replaceVariables={replaceVariables}
reduceOptions={reduceOptions}
/>
);
};
export const basic = Template.bind({});
basic.args = {
pieType: PieChartType.Pie,
tooltipMode: TooltipDisplayMode.Single,
legendDisplayMode: LegendDisplayMode.List,
legendPlacement: 'bottom',
legendValues: [PieChartLegendValues.Value],
displayLabels: [PieChartLabels.Name],
};
export const donut = Template.bind({});
donut.args = {
pieType: PieChartType.Donut,
tooltipMode: TooltipDisplayMode.Single,
legendDisplayMode: LegendDisplayMode.List,
legendPlacement: 'bottom',
legendValues: [PieChartLegendValues.Value],
displayLabels: [PieChartLabels.Name],
};

View File

@@ -1,67 +0,0 @@
import { DataFrame, FieldConfigSource, FieldDisplay, InterpolateFunction, ReduceDataOptions } from '@grafana/data';
import { VizTooltipOptions } from '../VizTooltip';
import { VizLegendOptions } from '..';
/**
* @beta
*/
export interface PieChartSvgProps {
height: number;
width: number;
fieldDisplayValues: FieldDisplay[];
pieType: PieChartType;
highlightedTitle?: string;
displayLabels?: PieChartLabels[];
useGradients?: boolean;
tooltipOptions: VizTooltipOptions;
}
/**
* @beta
*/
export interface PieChartProps {
height: number;
width: number;
pieType: PieChartType;
displayLabels?: PieChartLabels[];
useGradients?: boolean;
legendOptions?: PieChartLegendOptions;
tooltipOptions: VizTooltipOptions;
reduceOptions: ReduceDataOptions;
fieldConfig: FieldConfigSource<any>;
replaceVariables: InterpolateFunction;
data: DataFrame[];
timeZone?: string;
}
/**
* @beta
*/
export enum PieChartType {
Pie = 'pie',
Donut = 'donut',
}
/**
* @beta
*/
export enum PieChartLegendValues {
Value = 'value',
Percent = 'percent',
}
/**
* @beta
*/
export enum PieChartLabels {
Name = 'name',
Value = 'value',
Percent = 'percent',
}
/**
* @beta
*/
export interface PieChartLegendOptions extends VizLegendOptions {
values: PieChartLegendValues[];
}

View File

@@ -18,14 +18,6 @@ export { ColorPicker, SeriesColorPicker } from './ColorPicker/ColorPicker';
export { ColorValueEditor, ColorValueEditorProps } from './OptionsUI/color';
export { SeriesColorPickerPopover, SeriesColorPickerPopoverWithTheme } from './ColorPicker/SeriesColorPickerPopover';
export { EmptySearchResult } from './EmptySearchResult/EmptySearchResult';
export { PieChart } from './PieChart/PieChart';
export {
PieChartType,
PieChartProps,
PieChartLabels,
PieChartLegendOptions,
PieChartLegendValues,
} from './PieChart/types';
export { UnitPicker } from './UnitPicker/UnitPicker';
export { StatsPicker } from './StatsPicker/StatsPicker';
export { RefreshPicker, defaultIntervals } from './RefreshPicker/RefreshPicker';
@@ -105,7 +97,7 @@ export {
usePanelContext,
} from './PanelChrome';
export { VizLayout, VizLayoutComponentType, VizLayoutLegendProps, VizLayoutProps } from './VizLayout/VizLayout';
export { VizLegendItem } from './VizLegend/types';
export { VizLegendItem, SeriesVisibilityChangeBehavior } from './VizLegend/types';
export { LegendPlacement, LegendDisplayMode, VizLegendOptions } from './VizLegend/models.gen';
export { VizLegend } from './VizLegend/VizLegend';

View File

@@ -1,176 +1,43 @@
import React, { FC, useEffect, useState } from 'react';
import React, { FC } from 'react';
import { FieldDisplay, FALLBACK_COLOR, formattedValueToString, GrafanaTheme2 } from '@grafana/data';
import {
DataHoverClearEvent,
DataHoverEvent,
FALLBACK_COLOR,
FieldDisplay,
formattedValueToString,
getFieldDisplayValues,
GrafanaTheme2,
} from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { useStyles2, useTheme2 } from '../../themes/ThemeContext';
import tinycolor from 'tinycolor2';
VizTooltipOptions,
useTheme2,
useStyles2,
SeriesTableRowProps,
DataLinksContextMenu,
SeriesTable,
} from '@grafana/ui';
import { PieChartType, PieChartLabels } from './types';
import { useTooltip, useTooltipInPortal } from '@visx/tooltip';
import Pie, { PieArcDatum, ProvidedProps } from '@visx/shape/lib/shapes/Pie';
import { Group } from '@visx/group';
import { UseTooltipParams } from '@visx/tooltip/lib/hooks/useTooltip';
import { RadialGradient } from '@visx/gradient';
import { localPoint } from '@visx/event';
import { useTooltip, useTooltipInPortal } from '@visx/tooltip';
import { useComponentInstanceId } from '../../utils/useComponetInstanceId';
import { Group } from '@visx/group';
import tinycolor from 'tinycolor2';
import { css } from '@emotion/css';
import { VizLegend, VizLegendItem } from '..';
import { VizLayout } from '../VizLayout/VizLayout';
import { LegendDisplayMode } from '../VizLegend/models.gen';
import { DataLinksContextMenu } from '../DataLinks/DataLinksContextMenu';
import { UseTooltipParams } from '@visx/tooltip/lib/hooks/useTooltip';
import {
PieChartLabels,
PieChartLegendOptions,
PieChartLegendValues,
PieChartProps,
PieChartSvgProps,
PieChartType,
} from './types';
import { getTooltipContainerStyles } from '../../themes/mixins';
import { SeriesTable, SeriesTableRowProps, VizTooltipOptions } from '../VizTooltip';
import { usePanelContext } from '../PanelChrome';
import { Subscription } from 'rxjs';
import { SeriesVisibilityChangeBehavior } from '../VizLegend/types';
const defaultLegendOptions: PieChartLegendOptions = {
displayMode: LegendDisplayMode.List,
placement: 'right',
calcs: [],
values: [PieChartLegendValues.Percent],
};
import { useComponentInstanceId } from '@grafana/ui/src/utils/useComponetInstanceId';
import { getTooltipContainerStyles } from '@grafana/ui/src/themes/mixins';
import { selectors } from '@grafana/e2e-selectors';
/**
* @beta
*/
export function PieChart(props: PieChartProps) {
const {
data,
timeZone,
reduceOptions,
fieldConfig,
replaceVariables,
tooltipOptions,
width,
height,
...restProps
} = props;
const theme = useTheme2();
const highlightedTitle = useSliceHighlightState();
const fieldDisplayValues = getFieldDisplayValues({
fieldConfig,
reduceOptions,
data,
theme: theme,
replaceVariables,
timeZone,
});
return (
<VizLayout width={width} height={height} legend={getLegend(props, fieldDisplayValues)}>
{(vizWidth: number, vizHeight: number) => {
return (
<PieChartSvg
width={vizWidth}
height={vizHeight}
highlightedTitle={highlightedTitle}
fieldDisplayValues={fieldDisplayValues}
tooltipOptions={tooltipOptions}
{...restProps}
/>
);
}}
</VizLayout>
);
interface PieChartProps {
height: number;
width: number;
fieldDisplayValues: FieldDisplay[];
pieType: PieChartType;
highlightedTitle?: string;
displayLabels?: PieChartLabels[];
useGradients?: boolean; // not used?
tooltipOptions: VizTooltipOptions;
}
function getLegend(props: PieChartProps, displayValues: FieldDisplay[]) {
const { legendOptions = defaultLegendOptions } = props;
if (legendOptions.displayMode === LegendDisplayMode.Hidden) {
return undefined;
}
const total = displayValues
.filter((item) => {
return !item.field.custom.hideFrom.viz;
})
.reduce((acc, item) => item.display.numeric + acc, 0);
const legendItems = displayValues.map<VizLegendItem>((value, idx) => {
const hidden = value.field.custom.hideFrom.viz;
const display = value.display;
return {
label: display.title ?? '',
color: display.color ?? FALLBACK_COLOR,
yAxis: 1,
disabled: hidden,
getItemKey: () => (display.title ?? '') + idx,
getDisplayValues: () => {
const valuesToShow = legendOptions.values ?? [];
let displayValues = [];
if (valuesToShow.includes(PieChartLegendValues.Value)) {
displayValues.push({ numeric: display.numeric, text: formattedValueToString(display), title: 'Value' });
}
if (valuesToShow.includes(PieChartLegendValues.Percent)) {
const fractionOfTotal = hidden ? 0 : display.numeric / total;
const percentOfTotal = fractionOfTotal * 100;
displayValues.push({
numeric: fractionOfTotal,
percent: percentOfTotal,
text: hidden ? '-' : percentOfTotal.toFixed(0) + '%',
title: valuesToShow.length > 1 ? 'Percent' : undefined,
});
}
return displayValues;
},
};
});
return (
<VizLegend
items={legendItems}
seriesVisibilityChangeBehavior={SeriesVisibilityChangeBehavior.Hide}
placement={legendOptions.placement}
displayMode={legendOptions.displayMode}
/>
);
}
function useSliceHighlightState() {
const [highlightedTitle, setHighlightedTitle] = useState<string>();
const { eventBus } = usePanelContext();
useEffect(() => {
const setHighlightedSlice = (event: DataHoverEvent) => {
setHighlightedTitle(event.payload.dataId);
};
const resetHighlightedSlice = (event: DataHoverClearEvent) => {
setHighlightedTitle(undefined);
};
const subs = new Subscription()
.add(eventBus.getStream(DataHoverEvent).subscribe({ next: setHighlightedSlice }))
.add(eventBus.getStream(DataHoverClearEvent).subscribe({ next: resetHighlightedSlice }));
return () => {
subs.unsubscribe();
};
}, [setHighlightedTitle, eventBus]);
return highlightedTitle;
}
export const PieChartSvg: FC<PieChartSvgProps> = ({
export const PieChart: FC<PieChartProps> = ({
fieldDisplayValues,
pieType,
width,

View File

@@ -1,35 +0,0 @@
import React, { PureComponent } from 'react';
import { LegacyForms, InlineFormLabel, PieChartType } from '@grafana/ui';
import { PanelEditorProps } from '@grafana/data';
import { PieChartOptions } from './types';
const { Select } = LegacyForms;
const labelWidth = 8;
const pieChartOptions = [
{ value: PieChartType.Pie, label: 'Pie' },
{ value: PieChartType.Donut, label: 'Donut' },
];
export class PieChartOptionsBox extends PureComponent<PanelEditorProps<PieChartOptions>> {
onPieTypeChange = (pieType: any) => this.props.onOptionsChange({ ...this.props.options, pieType: pieType.value });
render() {
const { options } = this.props;
const { pieType } = options;
return (
<>
<div className="gf-form">
<InlineFormLabel width={labelWidth}>Type</InlineFormLabel>
<Select
width={12}
options={pieChartOptions}
onChange={this.onPieTypeChange}
value={pieChartOptions.find((option) => option.value === pieType)}
/>
</div>
</>
);
}
}

View File

@@ -1,32 +1,150 @@
import React from 'react';
import { PieChart } from '@grafana/ui';
import { PieChartOptions } from './types';
import { PanelProps } from '@grafana/data';
import React, { useEffect, useState } from 'react';
import {
DataHoverClearEvent,
DataHoverEvent,
FALLBACK_COLOR,
FieldDisplay,
formattedValueToString,
getFieldDisplayValues,
PanelProps,
} from '@grafana/data';
import { PieChart } from './PieChart';
import { PieChartLegendOptions, PieChartLegendValues, PieChartOptions } from './types';
import { Subscription } from 'rxjs';
import {
LegendDisplayMode,
usePanelContext,
useTheme2,
VizLayout,
VizLegend,
VizLegendItem,
SeriesVisibilityChangeBehavior,
} from '@grafana/ui';
const defaultLegendOptions: PieChartLegendOptions = {
displayMode: LegendDisplayMode.List,
placement: 'right',
calcs: [],
values: [PieChartLegendValues.Percent],
};
interface Props extends PanelProps<PieChartOptions> {}
export const PieChartPanel: React.FC<Props> = ({
width,
height,
options,
data,
replaceVariables,
fieldConfig,
timeZone,
}) => {
/**
* @beta
*/
export function PieChartPanel(props: Props) {
const { data, timeZone, fieldConfig, replaceVariables, width, height, options } = props;
const theme = useTheme2();
const highlightedTitle = useSliceHighlightState();
const fieldDisplayValues = getFieldDisplayValues({
fieldConfig,
reduceOptions: options.reduceOptions,
data: data.series,
theme: theme,
replaceVariables,
timeZone,
});
return (
<PieChart
width={width}
height={height}
timeZone={timeZone}
fieldConfig={fieldConfig}
reduceOptions={options.reduceOptions}
replaceVariables={replaceVariables}
data={data.series}
pieType={options.pieType}
displayLabels={options.displayLabels}
legendOptions={options.legend}
tooltipOptions={options.tooltip}
<VizLayout width={width} height={height} legend={getLegend(props, fieldDisplayValues)}>
{(vizWidth: number, vizHeight: number) => {
return (
<PieChart
width={vizWidth}
height={vizHeight}
highlightedTitle={highlightedTitle}
fieldDisplayValues={fieldDisplayValues}
tooltipOptions={options.tooltip}
pieType={options.pieType}
displayLabels={options.displayLabels}
/>
);
}}
</VizLayout>
);
}
function getLegend(props: Props, displayValues: FieldDisplay[]) {
const legendOptions = props.options.legend ?? defaultLegendOptions;
if (legendOptions.displayMode === LegendDisplayMode.Hidden) {
return undefined;
}
const total = displayValues
.filter((item) => {
return !item.field.custom.hideFrom.viz;
})
.reduce((acc, item) => item.display.numeric + acc, 0);
const legendItems = displayValues.map<VizLegendItem>((value, idx) => {
const hidden = value.field.custom.hideFrom.viz;
const display = value.display;
return {
label: display.title ?? '',
color: display.color ?? FALLBACK_COLOR,
yAxis: 1,
disabled: hidden,
getItemKey: () => (display.title ?? '') + idx,
getDisplayValues: () => {
const valuesToShow = legendOptions.values ?? [];
let displayValues = [];
if (valuesToShow.includes(PieChartLegendValues.Value)) {
displayValues.push({ numeric: display.numeric, text: formattedValueToString(display), title: 'Value' });
}
if (valuesToShow.includes(PieChartLegendValues.Percent)) {
const fractionOfTotal = hidden ? 0 : display.numeric / total;
const percentOfTotal = fractionOfTotal * 100;
displayValues.push({
numeric: fractionOfTotal,
percent: percentOfTotal,
text: hidden ? '-' : percentOfTotal.toFixed(0) + '%',
title: valuesToShow.length > 1 ? 'Percent' : undefined,
});
}
return displayValues;
},
};
});
return (
<VizLegend
items={legendItems}
seriesVisibilityChangeBehavior={SeriesVisibilityChangeBehavior.Hide}
placement={legendOptions.placement}
displayMode={legendOptions.displayMode}
/>
);
};
}
function useSliceHighlightState() {
const [highlightedTitle, setHighlightedTitle] = useState<string>();
const { eventBus } = usePanelContext();
useEffect(() => {
const setHighlightedSlice = (event: DataHoverEvent) => {
setHighlightedTitle(event.payload.dataId);
};
const resetHighlightedSlice = (event: DataHoverClearEvent) => {
setHighlightedTitle(undefined);
};
const subs = new Subscription()
.add(eventBus.getStream(DataHoverEvent).subscribe({ next: setHighlightedSlice }))
.add(eventBus.getStream(DataHoverClearEvent).subscribe({ next: resetHighlightedSlice }));
return () => {
subs.unsubscribe();
};
}, [setHighlightedTitle, eventBus]);
return highlightedTitle;
}

View File

@@ -1,6 +1,7 @@
import { FieldColorModeId, FieldConfigProperty, FieldMatcherID, PanelModel } from '@grafana/data';
import { LegendDisplayMode, PieChartLabels } from '@grafana/ui';
import { LegendDisplayMode } from '@grafana/ui';
import { PieChartPanelChangedHandler } from './migrations';
import { PieChartLabels } from './types';
describe('PieChart -> PieChartV2 migrations', () => {
it('only migrates old piechart', () => {

View File

@@ -1,6 +1,6 @@
import { FieldColorModeId, FieldConfigProperty, FieldMatcherID, PanelModel } from '@grafana/data';
import { LegendDisplayMode, PieChartLabels, PieChartLegendValues, PieChartType } from '@grafana/ui';
import { PieChartOptions } from './types';
import { LegendDisplayMode } from '@grafana/ui';
import { PieChartOptions, PieChartLabels, PieChartLegendValues, PieChartType } from './types';
export const PieChartPanelChangedHandler = (
panel: PanelModel<Partial<PieChartOptions>> | any,

View File

@@ -1,7 +1,7 @@
import { FieldColorModeId, FieldConfigProperty, PanelPlugin, ReducerID, standardEditorsRegistry } from '@grafana/data';
import { PieChartPanel } from './PieChartPanel';
import { PieChartOptions } from './types';
import { LegendDisplayMode, PieChartType, PieChartLabels, PieChartLegendValues } from '@grafana/ui';
import { PieChartOptions, PieChartType, PieChartLabels, PieChartLegendValues } from './types';
import { LegendDisplayMode } from '@grafana/ui';
import { PieChartPanelChangedHandler } from './migrations';
import { addHideFrom } from '../timeseries/config';

View File

@@ -1,10 +1,37 @@
import {
PieChartType,
SingleStatBaseOptions,
PieChartLabels,
PieChartLegendOptions,
VizTooltipOptions,
} from '@grafana/ui';
import { SingleStatBaseOptions, VizLegendOptions, VizTooltipOptions } from '@grafana/ui';
/**
* @beta
*/
export enum PieChartType {
Pie = 'pie',
Donut = 'donut',
}
/**
* @beta
*/
export enum PieChartLegendValues {
Value = 'value',
Percent = 'percent',
}
/**
* @beta
*/
export enum PieChartLabels {
Name = 'name',
Value = 'value',
Percent = 'percent',
}
/**
* @beta
*/
export interface PieChartLegendOptions extends VizLegendOptions {
values: PieChartLegendValues[];
}
export interface PieChartOptions extends SingleStatBaseOptions {
pieType: PieChartType;
displayLabels: PieChartLabels[];