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:
co-authored by
Ryan McKinley
parent
24b878ca4f
commit
2ded2aef71
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user