BarChart: enable series toggling via legend (#33955)

* Adds support for Tooltip in BarChartPanel

* Revert some formatting

* Remove BarChart story

* BarChart: move visualization to core

* BarChart: enable series toggling via legend

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
Dominik Prokop
2021-05-11 21:40:04 +02:00
committed by GitHub
co-authored by Ryan McKinley
parent 24b878ca4f
commit 2ded2aef71
9 changed files with 53 additions and 135 deletions
@@ -41,7 +41,7 @@ export interface GraphNGProps extends Themeable2 {
prepConfig: (alignedFrame: DataFrame, getTimeRange: () => TimeRange) => UPlotConfigBuilder; prepConfig: (alignedFrame: DataFrame, getTimeRange: () => TimeRange) => UPlotConfigBuilder;
propsToDiff?: string[]; propsToDiff?: string[];
preparePlotFrame?: (frames: DataFrame[], dimFields: XYFieldMatchers) => DataFrame; preparePlotFrame?: (frames: DataFrame[], dimFields: XYFieldMatchers) => DataFrame;
renderLegend: (config: UPlotConfigBuilder) => React.ReactElement; renderLegend: (config: UPlotConfigBuilder) => React.ReactElement | null;
} }
function sameProps(prevProps: any, nextProps: any, propsToDiff: string[] = []) { function sameProps(prevProps: any, nextProps: any, propsToDiff: string[] = []) {
@@ -23,22 +23,13 @@ export class UnthemedTimeSeries extends React.Component<TimeSeriesProps> {
}; };
renderLegend = (config: UPlotConfigBuilder) => { renderLegend = (config: UPlotConfigBuilder) => {
const { legend, onLegendClick, frames } = this.props; const { legend, frames } = this.props;
if (!config || (legend && legend.displayMode === LegendDisplayMode.Hidden)) { if (!config || (legend && legend.displayMode === LegendDisplayMode.Hidden)) {
return; return null;
} }
return ( return <PlotLegend data={frames} config={config} maxHeight="35%" maxWidth="60%" {...legend} />;
<PlotLegend
data={frames}
config={config}
onLegendClick={onLegendClick}
maxHeight="35%"
maxWidth="60%"
{...legend}
/>
);
}; };
render() { render() {
@@ -9,7 +9,7 @@ import { LegendPlacement } from '..';
export interface VizLayoutProps { export interface VizLayoutProps {
width: number; width: number;
height: number; height: number;
legend?: React.ReactElement<VizLayoutLegendProps>; legend?: React.ReactElement<VizLayoutLegendProps> | null;
children: (width: number, height: number) => React.ReactNode; children: (width: number, height: number) => React.ReactNode;
} }
@@ -1,4 +1,4 @@
import React, { useCallback } from 'react'; import React from 'react';
import { DataFrame, DisplayValue, fieldReducers, getFieldDisplayName, reduceField } from '@grafana/data'; import { DataFrame, DisplayValue, fieldReducers, getFieldDisplayName, reduceField } from '@grafana/data';
import { UPlotConfigBuilder } from './config/UPlotConfigBuilder'; import { UPlotConfigBuilder } from './config/UPlotConfigBuilder';
import { VizLegendItem } from '../VizLegend/types'; import { VizLegendItem } from '../VizLegend/types';
@@ -6,42 +6,22 @@ import { VizLegendOptions } from '../VizLegend/models.gen';
import { AxisPlacement } from './config'; import { AxisPlacement } from './config';
import { VizLayout, VizLayoutLegendProps } from '../VizLayout/VizLayout'; import { VizLayout, VizLayoutLegendProps } from '../VizLayout/VizLayout';
import { VizLegend } from '../VizLegend/VizLegend'; import { VizLegend } from '../VizLegend/VizLegend';
import { GraphNGLegendEvent } from '..';
import { mapMouseEventToMode } from '../VizLegend/utils';
const defaultFormatter = (v: any) => (v == null ? '-' : v.toFixed(1)); const defaultFormatter = (v: any) => (v == null ? '-' : v.toFixed(1));
interface PlotLegendProps extends VizLegendOptions, Omit<VizLayoutLegendProps, 'children'> { interface PlotLegendProps extends VizLegendOptions, Omit<VizLayoutLegendProps, 'children'> {
data: DataFrame[]; data: DataFrame[];
config: UPlotConfigBuilder; config: UPlotConfigBuilder;
onLegendClick?: (event: GraphNGLegendEvent) => void;
} }
export const PlotLegend: React.FC<PlotLegendProps> = ({ export const PlotLegend: React.FC<PlotLegendProps> = ({
data, data,
config, config,
onLegendClick,
placement, placement,
calcs, calcs,
displayMode, displayMode,
...vizLayoutLegendProps ...vizLayoutLegendProps
}) => { }) => {
const onLegendLabelClick = useCallback(
(legend: VizLegendItem, event: React.MouseEvent) => {
const { fieldIndex } = legend;
if (!onLegendClick || !fieldIndex) {
return;
}
onLegendClick({
fieldIndex,
mode: mapMouseEventToMode(event),
});
},
[onLegendClick]
);
const legendItems = config const legendItems = config
.getSeries() .getSeries()
.map<VizLegendItem | undefined>((s) => { .map<VizLegendItem | undefined>((s) => {
@@ -92,12 +72,7 @@ export const PlotLegend: React.FC<PlotLegendProps> = ({
return ( return (
<VizLayout.Legend placement={placement} {...vizLayoutLegendProps}> <VizLayout.Legend placement={placement} {...vizLayoutLegendProps}>
<VizLegend <VizLegend placement={placement} items={legendItems} displayMode={displayMode} />
onLabelClick={onLegendLabelClick}
placement={placement}
items={legendItems}
displayMode={displayMode}
/>
</VizLayout.Legend> </VizLayout.Legend>
); );
}; };
+36 -40
View File
@@ -1,6 +1,14 @@
import React from 'react'; import React from 'react';
import { DataFrame, TimeRange } from '@grafana/data'; import { DataFrame, TimeRange } from '@grafana/data';
import { GraphNG, GraphNGProps, LegendDisplayMode, PlotLegend, UPlotConfigBuilder, withTheme2 } from '@grafana/ui'; import {
GraphNG,
GraphNGProps,
LegendDisplayMode,
PlotLegend,
UPlotConfigBuilder,
usePanelContext,
useTheme2,
} from '@grafana/ui';
import { BarChartOptions } from './types'; import { BarChartOptions } from './types';
import { preparePlotConfigBuilder, preparePlotFrame } from './utils'; import { preparePlotConfigBuilder, preparePlotFrame } from './utils';
@@ -9,14 +17,24 @@ import { preparePlotConfigBuilder, preparePlotFrame } from './utils';
*/ */
export interface BarChartProps export interface BarChartProps
extends BarChartOptions, extends BarChartOptions,
Omit<GraphNGProps, 'prepConfig' | 'propsToDiff' | 'renderLegend'> {} Omit<GraphNGProps, 'prepConfig' | 'propsToDiff' | 'renderLegend' | 'theme'> {}
const propsToDiff: string[] = ['orientation', 'barWidth', 'groupWidth', 'showValue']; const propsToDiff: string[] = ['orientation', 'barWidth', 'groupWidth', 'showValue'];
class UnthemedBarChart extends React.Component<BarChartProps> { export const BarChart: React.FC<BarChartProps> = (props) => {
prepConfig = (alignedFrame: DataFrame, getTimeRange: () => TimeRange) => { const theme = useTheme2();
const { eventBus } = this.context; const { eventBus } = usePanelContext();
const { theme, timeZone, orientation, barWidth, showValue, groupWidth, stacking, legend, tooltip } = this.props;
const renderLegend = (config: UPlotConfigBuilder) => {
if (!config || props.legend.displayMode === LegendDisplayMode.Hidden) {
return null;
}
return <PlotLegend data={props.frames} config={config} maxHeight="35%" maxWidth="60%" {...props.legend} />;
};
const prepConfig = (alignedFrame: DataFrame, getTimeRange: () => TimeRange) => {
const { timeZone, orientation, barWidth, showValue, groupWidth, stacking, legend, tooltip } = props;
return preparePlotConfigBuilder({ return preparePlotConfigBuilder({
frame: alignedFrame, frame: alignedFrame,
getTimeRange, getTimeRange,
@@ -33,38 +51,16 @@ class UnthemedBarChart extends React.Component<BarChartProps> {
}); });
}; };
renderLegend = (config: UPlotConfigBuilder) => { return (
const { legend, onLegendClick, frames } = this.props; <GraphNG
{...props}
if (!config || legend.displayMode === LegendDisplayMode.Hidden) { theme={theme}
return; frames={props.frames}
} prepConfig={prepConfig}
propsToDiff={propsToDiff}
return ( preparePlotFrame={preparePlotFrame}
<PlotLegend renderLegend={renderLegend}
data={frames} />
config={config} );
onLegendClick={onLegendClick} };
maxHeight="35%"
maxWidth="60%"
{...legend}
/>
);
};
render() {
return (
<GraphNG
{...this.props}
frames={this.props.frames}
prepConfig={this.prepConfig}
propsToDiff={propsToDiff}
preparePlotFrame={preparePlotFrame}
renderLegend={this.renderLegend as any}
/>
);
}
}
export const BarChart = withTheme2(UnthemedBarChart);
BarChart.displayName = 'BarChart'; BarChart.displayName = 'BarChart';
@@ -1,7 +1,6 @@
import React, { useCallback, useMemo } from 'react'; import React, { useMemo } from 'react';
import { FieldType, PanelProps, TimeRange, VizOrientation } from '@grafana/data'; import { FieldType, PanelProps, TimeRange, VizOrientation } from '@grafana/data';
import { GraphNGLegendEvent, TooltipPlugin } from '@grafana/ui'; import { TooltipPlugin } from '@grafana/ui';
import { hideSeriesConfigFactory } from '../timeseries/overrides/hideSeriesConfigFactory';
import { BarChartOptions } from './types'; import { BarChartOptions } from './types';
import { BarChart } from './BarChart'; import { BarChart } from './BarChart';
@@ -10,15 +9,7 @@ interface Props extends PanelProps<BarChartOptions> {}
/** /**
* @alpha * @alpha
*/ */
export const BarChartPanel: React.FunctionComponent<Props> = ({ export const BarChartPanel: React.FunctionComponent<Props> = ({ data, options, width, height, timeZone }) => {
data,
options,
width,
height,
fieldConfig,
timeZone,
onFieldConfigChange,
}) => {
const orientation = useMemo(() => { const orientation = useMemo(() => {
if (!options.orientation || options.orientation === VizOrientation.Auto) { if (!options.orientation || options.orientation === VizOrientation.Auto) {
return width < height ? VizOrientation.Horizontal : VizOrientation.Vertical; return width < height ? VizOrientation.Horizontal : VizOrientation.Vertical;
@@ -27,13 +18,6 @@ export const BarChartPanel: React.FunctionComponent<Props> = ({
return options.orientation; return options.orientation;
}, [width, height, options.orientation]); }, [width, height, options.orientation]);
const onLegendClick = useCallback(
(event: GraphNGLegendEvent) => {
onFieldConfigChange(hideSeriesConfigFactory(event, fieldConfig, data.series));
},
[fieldConfig, onFieldConfigChange, data.series]
);
if (!data || !data.series?.length) { if (!data || !data.series?.length) {
return ( return (
<div className="panel-empty"> <div className="panel-empty">
@@ -66,7 +50,6 @@ export const BarChartPanel: React.FunctionComponent<Props> = ({
structureRev={data.structureRev} structureRev={data.structureRev}
width={width} width={width}
height={height} height={height}
onLegendClick={onLegendClick}
{...options} {...options}
orientation={orientation} orientation={orientation}
> >
+1 -1
View File
@@ -18,8 +18,8 @@ import {
ScaleDistribution, ScaleDistribution,
ScaleOrientation, ScaleOrientation,
UPlotConfigBuilder, UPlotConfigBuilder,
UPlotConfigPrepFn,
} from '@grafana/ui'; } from '@grafana/ui';
import { UPlotConfigPrepFn } from '@grafana/ui/src/components/uPlot/config/UPlotConfigBuilder';
/** @alpha */ /** @alpha */
function getBarCharScaleOrientation(orientation: VizOrientation) { function getBarCharScaleOrientation(orientation: VizOrientation) {
@@ -1,12 +1,5 @@
import React from 'react'; import React from 'react';
import { import { PanelContext, PanelContextRoot, GraphNG, GraphNGProps, BarValueVisibility } from '@grafana/ui';
PanelContext,
PanelContextRoot,
UPlotConfigBuilder,
GraphNG,
GraphNGProps,
BarValueVisibility,
} from '@grafana/ui';
import { DataFrame, FieldType, TimeRange } from '@grafana/data'; import { DataFrame, FieldType, TimeRange } from '@grafana/data';
import { preparePlotConfigBuilder } from './utils'; import { preparePlotConfigBuilder } from './utils';
import { TimelineMode } from './types'; import { TimelineMode } from './types';
@@ -39,9 +32,7 @@ export class TimelineChart extends React.Component<TimelineProps> {
}); });
}; };
renderLegend = (config: UPlotConfigBuilder) => { renderLegend = () => null;
return;
};
render() { render() {
return ( return (
@@ -53,7 +44,7 @@ export class TimelineChart extends React.Component<TimelineProps> {
}} }}
prepConfig={this.prepConfig} prepConfig={this.prepConfig}
propsToDiff={propsToDiff} propsToDiff={propsToDiff}
renderLegend={this.renderLegend as any} renderLegend={this.renderLegend}
/> />
); );
} }
@@ -1,7 +1,6 @@
import React, { useCallback } from 'react'; import React from 'react';
import { PanelProps } from '@grafana/data'; import { PanelProps } from '@grafana/data';
import { GraphNGLegendEvent, useTheme2 } from '@grafana/ui'; import { useTheme2 } from '@grafana/ui';
import { hideSeriesConfigFactory } from '../timeseries/overrides/hideSeriesConfigFactory';
import { TimelineOptions } from './types'; import { TimelineOptions } from './types';
import { TimelineChart } from './TimelineChart'; import { TimelineChart } from './TimelineChart';
@@ -10,25 +9,9 @@ interface TimelinePanelProps extends PanelProps<TimelineOptions> {}
/** /**
* @alpha * @alpha
*/ */
export const TimelinePanel: React.FC<TimelinePanelProps> = ({ export const TimelinePanel: React.FC<TimelinePanelProps> = ({ data, timeRange, timeZone, options, width, height }) => {
data,
timeRange,
timeZone,
options,
width,
height,
fieldConfig,
onFieldConfigChange,
}) => {
const theme = useTheme2(); const theme = useTheme2();
const onLegendClick = useCallback(
(event: GraphNGLegendEvent) => {
onFieldConfigChange(hideSeriesConfigFactory(event, fieldConfig, data.series));
},
[fieldConfig, onFieldConfigChange, data.series]
);
if (!data || !data.series?.length) { if (!data || !data.series?.length) {
return ( return (
<div className="panel-empty"> <div className="panel-empty">
@@ -46,7 +29,6 @@ export const TimelinePanel: React.FC<TimelinePanelProps> = ({
timeZone={timeZone} timeZone={timeZone}
width={width} width={width}
height={height} height={height}
onLegendClick={onLegendClick}
{...options} {...options}
/> />
); );