Legend/GraphNG: Refactoring legend types and options (#29067)

* Legend/GraphNG: Refactoring legend types and options

* Rename label

* Minor update

* Fixed legend placement crash issue

* remove unused

* Minor tweaks and fixes
This commit is contained in:
Torkel Ödegaard
2020-11-13 17:08:55 +01:00
committed by GitHub
parent ec864f6461
commit 28ce2f12ed
19 changed files with 83 additions and 69 deletions

View File

@ -44,7 +44,7 @@ export const GraphLegendListItem: React.FunctionComponent<GraphLegendItemProps>
onLabelClick(item, event); onLabelClick(item, event);
} }
}} }}
className={cx(styles.label, !item.isVisible && styles.labelDisabled)} className={cx(styles.label, item.disabled && styles.labelDisabled)}
> >
{item.label} {item.label}
</div> </div>

View File

@ -123,8 +123,7 @@ export const graphWithLegend = () => {
} }
return s; return s;
}), }),
displayMode: renderLegendAsTable ? LegendDisplayMode.Table : LegendDisplayMode.List, legendDisplayMode: renderLegendAsTable ? LegendDisplayMode.Table : LegendDisplayMode.List,
isLegendVisible: true,
onToggleSort: () => {}, onToggleSort: () => {},
timeRange: { timeRange: {
from: dateTime(1546372800000), from: dateTime(1546372800000),

View File

@ -15,8 +15,7 @@ export type SeriesColorChangeHandler = SeriesOptionChangeHandler<string>;
export type SeriesAxisToggleHandler = SeriesOptionChangeHandler<number>; export type SeriesAxisToggleHandler = SeriesOptionChangeHandler<number>;
export interface GraphWithLegendProps extends GraphProps, LegendRenderOptions { export interface GraphWithLegendProps extends GraphProps, LegendRenderOptions {
isLegendVisible: boolean; legendDisplayMode: LegendDisplayMode;
displayMode: LegendDisplayMode;
sortLegendBy?: string; sortLegendBy?: string;
sortLegendDesc?: boolean; sortLegendDesc?: boolean;
onSeriesColorChange?: SeriesColorChangeHandler; onSeriesColorChange?: SeriesColorChangeHandler;
@ -59,8 +58,7 @@ export const GraphWithLegend: React.FunctionComponent<GraphWithLegendProps> = (p
showPoints, showPoints,
sortLegendBy, sortLegendBy,
sortLegendDesc, sortLegendDesc,
isLegendVisible, legendDisplayMode,
displayMode,
placement, placement,
onSeriesAxisToggle, onSeriesAxisToggle,
onSeriesColorChange, onSeriesColorChange,
@ -84,7 +82,7 @@ export const GraphWithLegend: React.FunctionComponent<GraphWithLegendProps> = (p
{ {
label: s.label, label: s.label,
color: s.color || '', color: s.color || '',
isVisible: s.isVisible, disabled: !s.isVisible,
yAxis: s.yAxis.index, yAxis: s.yAxis.index,
displayValues: s.info || [], displayValues: s.info || [],
}, },
@ -103,7 +101,6 @@ export const GraphWithLegend: React.FunctionComponent<GraphWithLegendProps> = (p
showBars={showBars} showBars={showBars}
width={width} width={width}
height={height} height={height}
key={isLegendVisible ? 'legend-visible' : 'legend-invisible'}
isStacked={isStacked} isStacked={isStacked}
lineWidth={lineWidth} lineWidth={lineWidth}
onHorizontalRegionSelected={onHorizontalRegionSelected} onHorizontalRegionSelected={onHorizontalRegionSelected}
@ -112,12 +109,12 @@ export const GraphWithLegend: React.FunctionComponent<GraphWithLegendProps> = (p
</Graph> </Graph>
</div> </div>
{isLegendVisible && ( {legendDisplayMode !== LegendDisplayMode.Hidden && (
<div className={legendContainer}> <div className={legendContainer}>
<CustomScrollbar hideHorizontalTrack> <CustomScrollbar hideHorizontalTrack>
<GraphLegend <GraphLegend
items={legendItems} items={legendItems}
displayMode={displayMode} displayMode={legendDisplayMode}
placement={placement} placement={placement}
sortBy={sortLegendBy} sortBy={sortLegendBy}
sortDesc={sortLegendDesc} sortDesc={sortLegendDesc}

View File

@ -6,6 +6,7 @@ import { dateTime } from '@grafana/data';
import { LegendDisplayMode } from '../Legend/Legend'; import { LegendDisplayMode } from '../Legend/Legend';
import { prepDataForStorybook } from '../../utils/storybook/data'; import { prepDataForStorybook } from '../../utils/storybook/data';
import { useTheme } from '../../themes'; import { useTheme } from '../../themes';
import { text, select } from '@storybook/addon-knobs';
export default { export default {
title: 'Visualizations/GraphNG', title: 'Visualizations/GraphNG',
@ -16,7 +17,23 @@ export default {
}, },
}; };
const getKnobs = () => {
return {
unit: text('Unit', 'short'),
legendPlacement: select(
'Legend placement',
{
bottom: 'bottom',
right: 'right',
},
'bottom'
),
};
};
export const Lines: React.FC = () => { export const Lines: React.FC = () => {
const { unit, legendPlacement } = getKnobs();
const theme = useTheme(); const theme = useTheme();
const seriesA = toDataFrame({ const seriesA = toDataFrame({
target: 'SeriesA', target: 'SeriesA',
@ -29,7 +46,7 @@ export const Lines: React.FC = () => {
seriesA.fields[1].config.custom = { line: { show: true } }; seriesA.fields[1].config.custom = { line: { show: true } };
seriesA.fields[1].config.color = { mode: FieldColorModeId.PaletteClassic }; seriesA.fields[1].config.color = { mode: FieldColorModeId.PaletteClassic };
seriesA.fields[1].config.unit = 'degree'; seriesA.fields[1].config.unit = unit;
const data = prepDataForStorybook([seriesA], theme); const data = prepDataForStorybook([seriesA], theme);
@ -46,7 +63,7 @@ export const Lines: React.FC = () => {
to: dateTime(1546380000000), to: dateTime(1546380000000),
}, },
}} }}
legend={{ isVisible: true, displayMode: LegendDisplayMode.List, placement: 'bottom' }} legend={{ displayMode: LegendDisplayMode.List, placement: legendPlacement }}
timeZone="browser" timeZone="browser"
></GraphNG> ></GraphNG>
); );

View File

@ -35,7 +35,6 @@ const mockData = () => {
}; };
const defaultLegendOptions: LegendOptions = { const defaultLegendOptions: LegendOptions = {
isVisible: false,
displayMode: LegendDisplayMode.List, displayMode: LegendDisplayMode.List,
placement: 'bottom', placement: 'bottom',
}; };

View File

@ -15,7 +15,7 @@ import { UPlotChart } from '../uPlot/Plot';
import { AxisSide, GraphCustomFieldConfig, PlotProps } from '../uPlot/types'; import { AxisSide, GraphCustomFieldConfig, PlotProps } from '../uPlot/types';
import { useTheme } from '../../themes'; import { useTheme } from '../../themes';
import { VizLayout } from '../VizLayout/VizLayout'; import { VizLayout } from '../VizLayout/VizLayout';
import { LegendItem, LegendOptions } from '../Legend/Legend'; import { LegendDisplayMode, LegendItem, LegendOptions } from '../Legend/Legend';
import { GraphLegend } from '../Graph/GraphLegend'; import { GraphLegend } from '../Graph/GraphLegend';
const defaultFormatter = (v: any) => (v == null ? '-' : v.toFixed(1)); const defaultFormatter = (v: any) => (v == null ? '-' : v.toFixed(1));
@ -63,6 +63,7 @@ export const GraphNG: React.FC<GraphNGProps> = ({
let seriesIdx = 0; let seriesIdx = 0;
const legendItems: LegendItem[] = []; const legendItems: LegendItem[] = [];
const uniqueScales: Record<string, boolean> = {}; const uniqueScales: Record<string, boolean> = {};
const hasLegend = legend && legend.displayMode !== LegendDisplayMode.Hidden;
for (let i = 0; i < alignedData.fields.length; i++) { for (let i = 0; i < alignedData.fields.length; i++) {
const seriesGeometry = []; const seriesGeometry = [];
@ -132,11 +133,10 @@ export const GraphNG: React.FC<GraphNGProps> = ({
geometries.push(seriesGeometry); geometries.push(seriesGeometry);
} }
if (legend?.isVisible) { if (hasLegend) {
legendItems.push({ legendItems.push({
color: seriesColor, color: seriesColor,
label: getFieldDisplayName(field, alignedData), label: getFieldDisplayName(field, alignedData),
isVisible: true,
yAxis: customConfig?.axis?.side === 1 ? 3 : 1, yAxis: customConfig?.axis?.side === 1 ? 3 : 1,
}); });
} }
@ -146,10 +146,10 @@ export const GraphNG: React.FC<GraphNGProps> = ({
let legendElement: React.ReactElement | undefined; let legendElement: React.ReactElement | undefined;
if (legend?.isVisible && legendItems.length > 0) { if (hasLegend && legendItems.length > 0) {
legendElement = ( legendElement = (
<VizLayout.Legend position={legend.placement} maxHeight="35%" maxWidth="60%"> <VizLayout.Legend position={legend!.placement} maxHeight="35%" maxWidth="60%">
<GraphLegend placement={legend.placement} items={legendItems} displayMode={legend.displayMode} /> <GraphLegend placement={legend!.placement} items={legendItems} displayMode={legend!.displayMode} />
</VizLayout.Legend> </VizLayout.Legend>
); );
} }

View File

@ -19,8 +19,8 @@ const getStoriesKnobs = (table = false) => {
const rawRenderer = (item: LegendItem) => ( const rawRenderer = (item: LegendItem) => (
<> <>
Label: <strong>{item.label}</strong>, Color: <strong>{item.color}</strong>, isVisible:{' '} Label: <strong>{item.label}</strong>, Color: <strong>{item.color}</strong>, disabled:{' '}
<strong>{item.isVisible ? 'yes' : 'no'}</strong> <strong>{item.disabled ? 'yes' : 'no'}</strong>
</> </>
); );

View File

@ -11,7 +11,6 @@ export const generateLegendItems = (numberOfSeries: number, statsToDisplay?: Dis
return { return {
label: `${alphabet[i].toUpperCase()}-series`, label: `${alphabet[i].toUpperCase()}-series`,
color: tinycolor.fromRatio({ h: i / alphabet.length, s: 1, v: 1 }).toHexString(), color: tinycolor.fromRatio({ h: i / alphabet.length, s: 1, v: 1 }).toHexString(),
isVisible: true,
yAxis: 1, yAxis: 1,
displayValues: statsToDisplay || [], displayValues: statsToDisplay || [],
}; };
@ -21,9 +20,9 @@ export const generateLegendItems = (numberOfSeries: number, statsToDisplay?: Dis
export enum LegendDisplayMode { export enum LegendDisplayMode {
List = 'list', List = 'list',
Table = 'table', Table = 'table',
Hidden = 'hidden',
} }
export interface LegendBasicOptions { export interface LegendBasicOptions {
isVisible: boolean;
displayMode: LegendDisplayMode; displayMode: LegendDisplayMode;
} }
@ -40,8 +39,8 @@ export interface LegendOptions extends LegendBasicOptions, LegendRenderOptions {
export interface LegendItem { export interface LegendItem {
label: string; label: string;
color: string; color: string;
isVisible: boolean;
yAxis: number; yAxis: number;
disabled?: boolean;
displayValues?: DisplayValue[]; displayValues?: DisplayValue[];
} }

View File

@ -34,7 +34,7 @@ export const BottomLegend = () => {
); );
return ( return (
<VizLayout width={400} height={500} legend={legend}> <VizLayout width={600} height={500} legend={legend}>
{(vizWidth: number, vizHeight: number) => { {(vizWidth: number, vizHeight: number) => {
return <div style={{ width: vizWidth, height: vizHeight, background: 'red' }} />; return <div style={{ width: vizWidth, height: vizHeight, background: 'red' }} />;
}} }}
@ -57,7 +57,7 @@ export const RightLegend = () => {
); );
return ( return (
<VizLayout width={400} height={500} legend={legend}> <VizLayout width={810} height={400} legend={legend}>
{(vizWidth: number, vizHeight: number) => { {(vizWidth: number, vizHeight: number) => {
return <div style={{ width: vizWidth, height: vizHeight, background: 'red' }} />; return <div style={{ width: vizWidth, height: vizHeight, background: 'red' }} />;
}} }}

View File

@ -38,7 +38,7 @@ export const VizLayout: VizLayoutComponentType = ({ width, height, legend, child
let size: VizSize | null = null; let size: VizSize | null = null;
const vizStyle: CSSProperties = { const vizStyle: CSSProperties = {
flexGrow: 1, flexGrow: 2,
}; };
const legendStyle: CSSProperties = {}; const legendStyle: CSSProperties = {};
@ -62,6 +62,16 @@ export const VizLayout: VizLayoutComponentType = ({ width, height, legend, child
break; break;
} }
// This happens when position is switched from bottom to right
// Then we preserve old with for one render cycle until lenged is measured in it's new position
if (size?.width === 0) {
size.width = width;
}
if (size?.height === 0) {
size.height = height;
}
return ( return (
<div style={containerStyle}> <div style={containerStyle}>
<div style={vizStyle}>{size && children(size.width, size.height)}</div> <div style={vizStyle}>{size && children(size.width, size.height)}</div>

View File

@ -119,7 +119,7 @@ function calculateAxisSize(self: uPlot, values: string[], axisIdx: number) {
} }
} }
return measureText(maxLength, 12).width; return measureText(maxLength, 12).width - 8;
} }
/** Format time axis ticks */ /** Format time axis ticks */

View File

@ -17,7 +17,7 @@ describe('usePlotConfig', () => {
"alpha": 1, "alpha": 1,
}, },
"gutters": Object { "gutters": Object {
"x": 16, "x": 8,
"y": 8, "y": 8,
}, },
"height": 0, "height": 0,
@ -59,7 +59,7 @@ describe('usePlotConfig', () => {
"alpha": 1, "alpha": 1,
}, },
"gutters": Object { "gutters": Object {
"x": 16, "x": 8,
"y": 8, "y": 8,
}, },
"height": 0, "height": 0,
@ -108,7 +108,7 @@ describe('usePlotConfig', () => {
"alpha": 1, "alpha": 1,
}, },
"gutters": Object { "gutters": Object {
"x": 16, "x": 8,
"y": 8, "y": 8,
}, },
"height": 0, "height": 0,
@ -155,7 +155,7 @@ describe('usePlotConfig', () => {
"alpha": 1, "alpha": 1,
}, },
"gutters": Object { "gutters": Object {
"x": 16, "x": 8,
"y": 8, "y": 8,
}, },
"height": 0, "height": 0,
@ -203,7 +203,7 @@ describe('usePlotConfig', () => {
"alpha": 1, "alpha": 1,
}, },
"gutters": Object { "gutters": Object {
"x": 16, "x": 8,
"y": 8, "y": 8,
}, },
"height": 0, "height": 0,
@ -253,7 +253,7 @@ describe('usePlotConfig', () => {
"alpha": 1, "alpha": 1,
}, },
"gutters": Object { "gutters": Object {
"x": 16, "x": 8,
"y": 8, "y": 8,
}, },
"height": 0, "height": 0,
@ -297,7 +297,7 @@ describe('usePlotConfig', () => {
"alpha": 1, "alpha": 1,
}, },
"gutters": Object { "gutters": Object {
"x": 16, "x": 8,
"y": 8, "y": 8,
}, },
"height": 0, "height": 0,
@ -341,7 +341,7 @@ describe('usePlotConfig', () => {
"alpha": 1, "alpha": 1,
}, },
"gutters": Object { "gutters": Object {
"x": 16, "x": 8,
"y": 8, "y": 8,
}, },
"height": 0, "height": 0,
@ -391,7 +391,7 @@ describe('usePlotConfig', () => {
"alpha": 1, "alpha": 1,
}, },
"gutters": Object { "gutters": Object {
"x": 16, "x": 8,
"y": 8, "y": 8,
}, },
"height": 0, "height": 0,
@ -439,7 +439,7 @@ describe('usePlotConfig', () => {
"alpha": 1, "alpha": 1,
}, },
"gutters": Object { "gutters": Object {
"x": 16, "x": 8,
"y": 8, "y": 8,
}, },
"height": 0, "height": 0,
@ -484,7 +484,7 @@ describe('usePlotConfig', () => {
"alpha": 1, "alpha": 1,
}, },
"gutters": Object { "gutters": Object {
"x": 16, "x": 8,
"y": 8, "y": 8,
}, },
"height": 0, "height": 0,
@ -538,7 +538,7 @@ describe('usePlotConfig', () => {
"alpha": 1, "alpha": 1,
}, },
"gutters": Object { "gutters": Object {
"x": 16, "x": 8,
"y": 8, "y": 8,
}, },
"height": 0, "height": 0,

View File

@ -95,7 +95,7 @@ export const DEFAULT_PLOT_CONFIG = {
show: false, show: false,
}, },
gutters: { gutters: {
x: 16, x: 8,
y: 8, y: 8,
}, },
series: [], series: [],

View File

@ -113,9 +113,8 @@ class UnThemedExploreGraphPanel extends PureComponent<Props, State> {
return ( return (
<GraphWithLegend <GraphWithLegend
ariaLabel={ariaLabel} ariaLabel={ariaLabel}
displayMode={LegendDisplayMode.List} legendDisplayMode={LegendDisplayMode.List}
height={height} height={height}
isLegendVisible={true}
placement={'bottom'} placement={'bottom'}
width={width} width={width}
timeRange={timeRange} timeRange={timeRange}

View File

@ -92,7 +92,7 @@ export const decorateWithGraphResult = (data: ExplorePanelData): ExplorePanelDat
data.request?.timezone ?? 'browser', data.request?.timezone ?? 'browser',
{}, {},
{ showBars: false, showLines: true, showPoints: false }, { showBars: false, showLines: true, showPoints: false },
{ displayMode: LegendDisplayMode.List, isVisible: true, placement: 'bottom' } { displayMode: LegendDisplayMode.List, placement: 'bottom' }
); );
return { ...data, graphResult }; return { ...data, graphResult };

View File

@ -53,8 +53,7 @@ export const GraphPanel: React.FunctionComponent<GraphPanelProps> = ({
timeZone={timeZone} timeZone={timeZone}
width={width} width={width}
height={height} height={height}
displayMode={legendOptions.displayMode} legendDisplayMode={legendOptions.displayMode}
isLegendVisible={legendOptions.isVisible}
placement={legendOptions.placement} placement={legendOptions.placement}
sortLegendBy={legendOptions.sortBy} sortLegendBy={legendOptions.sortBy}
sortLegendDesc={legendOptions.sortDesc} sortLegendDesc={legendOptions.sortDesc}

View File

@ -28,7 +28,6 @@ export const defaults: Options = {
showPoints: false, showPoints: false,
}, },
legend: { legend: {
isVisible: true,
displayMode: LegendDisplayMode.List, displayMode: LegendDisplayMode.List,
placement: 'bottom', placement: 'bottom',
}, },

View File

@ -22,7 +22,7 @@ export const GraphPanel: React.FC<GraphPanelProps> = ({
timeRange={timeRange} timeRange={timeRange}
timeZone={timeZone} timeZone={timeZone}
width={width} width={width}
height={height - 8} height={height}
legend={options.legend} legend={options.legend}
> >
<TooltipPlugin mode={options.tooltipOptions.mode as any} timeZone={timeZone} /> <TooltipPlugin mode={options.tooltipOptions.mode as any} timeZone={timeZone} />

View File

@ -1,10 +1,9 @@
import { FieldColorModeId, FieldConfigProperty, PanelPlugin } from '@grafana/data'; import { FieldColorModeId, FieldConfigProperty, PanelPlugin } from '@grafana/data';
import { AxisSide, GraphCustomFieldConfig } from '@grafana/ui'; import { AxisSide, GraphCustomFieldConfig, LegendDisplayMode } from '@grafana/ui';
import { GraphPanel } from './GraphPanel'; import { GraphPanel } from './GraphPanel';
import { Options } from './types'; import { Options } from './types';
export const plugin = new PanelPlugin<Options, GraphCustomFieldConfig>(GraphPanel) export const plugin = new PanelPlugin<Options, GraphCustomFieldConfig>(GraphPanel)
.setNoPadding()
.useFieldConfig({ .useFieldConfig({
standardOptions: { standardOptions: {
[FieldConfigProperty.Color]: { [FieldConfigProperty.Color]: {
@ -133,29 +132,26 @@ export const plugin = new PanelPlugin<Options, GraphCustomFieldConfig>(GraphPane
defaultValue: 'single', defaultValue: 'single',
settings: { settings: {
options: [ options: [
{ value: 'single', label: 'Single series' }, { value: 'single', label: 'Single' },
{ value: 'multi', label: 'All series' }, { value: 'multi', label: 'All' },
{ value: 'none', label: 'No tooltip' }, { value: 'none', label: 'Hidden' },
], ],
}, },
}) })
.addBooleanSwitch({ .addRadio({
category: ['Legend'], path: 'legend.displayMode',
path: 'legend.isVisible', name: 'Legend mode',
name: 'Show legend',
description: '', description: '',
defaultValue: true, defaultValue: LegendDisplayMode.List,
}) settings: {
.addBooleanSwitch({ options: [
category: ['Legend'], { value: LegendDisplayMode.List, label: 'List' },
path: 'legend.asTable', { value: LegendDisplayMode.Table, label: 'Table' },
name: 'Display legend as table', { value: LegendDisplayMode.Hidden, label: 'Hidden' },
description: '', ],
defaultValue: false, },
showIf: c => c.legend.isVisible,
}) })
.addRadio({ .addRadio({
category: ['Legend'],
path: 'legend.placement', path: 'legend.placement',
name: 'Legend placement', name: 'Legend placement',
description: '', description: '',
@ -166,6 +162,6 @@ export const plugin = new PanelPlugin<Options, GraphCustomFieldConfig>(GraphPane
{ value: 'right', label: 'Right' }, { value: 'right', label: 'Right' },
], ],
}, },
showIf: c => c.legend.isVisible, showIf: c => c.legend.displayMode !== LegendDisplayMode.Hidden,
}); });
}); });