mirror of
https://github.com/grafana/grafana.git
synced 2025-09-18 20:12:51 +08:00
Legends: Refactoring and rewrites of legend components to simplify components & reuse (#30165)
* Legends: Refactoring and rewrites of legend components to simplify components & reuse * Removed onSeriesAxisToggle * More removal of onSeriesAxisToggle and storybook improvements * Added story with legend values * Move table legend styles from inline to defined in stylesFactory * Update styles * Change to circle * Updated style to fat line * Rename to VizLegend * More renamed and fixes / polish * Removed imports * Minor change * Updates * Updates
This commit is contained in:
@ -13,7 +13,7 @@ import {
|
|||||||
import { useTheme } from '../../themes';
|
import { useTheme } from '../../themes';
|
||||||
import { HorizontalGroup } from '../Layout/Layout';
|
import { HorizontalGroup } from '../Layout/Layout';
|
||||||
import { FormattedValueDisplay } from '../FormattedValueDisplay/FormattedValueDisplay';
|
import { FormattedValueDisplay } from '../FormattedValueDisplay/FormattedValueDisplay';
|
||||||
import { SeriesIcon } from '../Legend/SeriesIcon';
|
import { SeriesIcon } from '../VizLegend/SeriesIcon';
|
||||||
import { css } from 'emotion';
|
import { css } from 'emotion';
|
||||||
|
|
||||||
export type ContextDimensions<T extends Dimensions = any> = { [key in keyof T]: [number, number | undefined] | null };
|
export type ContextDimensions<T extends Dimensions = any> = { [key in keyof T]: [number, number | undefined] | null };
|
||||||
|
@ -1,106 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { GraphLegend } from './GraphLegend';
|
|
||||||
import { action } from '@storybook/addon-actions';
|
|
||||||
import { select, number } from '@storybook/addon-knobs';
|
|
||||||
import { withHorizontallyCenteredStory } from '../../utils/storybook/withCenteredStory';
|
|
||||||
import { generateLegendItems } from '../Legend/Legend';
|
|
||||||
import { LegendPlacement, LegendDisplayMode } from '../Legend/Legend';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
title: 'Visualizations/Graph/GraphLegend',
|
|
||||||
component: GraphLegend,
|
|
||||||
decorators: [withHorizontallyCenteredStory],
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStoriesKnobs = (isList = false) => {
|
|
||||||
const statsToDisplay = select<any>(
|
|
||||||
'Stats to display',
|
|
||||||
{
|
|
||||||
none: [],
|
|
||||||
'single (min)': [{ text: '10ms', title: 'min', numeric: 10 }],
|
|
||||||
'multiple (min, max)': [
|
|
||||||
{ text: '10ms', title: 'min', numeric: 10 },
|
|
||||||
{ text: '100ms', title: 'max', numeric: 100 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const numberOfSeries = number('Number of series', 3);
|
|
||||||
|
|
||||||
const containerWidth = select(
|
|
||||||
'Container width',
|
|
||||||
{
|
|
||||||
Small: '200px',
|
|
||||||
Medium: '500px',
|
|
||||||
'Full width': '100%',
|
|
||||||
},
|
|
||||||
'100%'
|
|
||||||
);
|
|
||||||
|
|
||||||
const legendPlacement = select<LegendPlacement>(
|
|
||||||
'Legend placement',
|
|
||||||
{
|
|
||||||
bottom: 'bottom',
|
|
||||||
right: 'right',
|
|
||||||
},
|
|
||||||
'bottom'
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
statsToDisplay,
|
|
||||||
numberOfSeries,
|
|
||||||
containerWidth,
|
|
||||||
legendPlacement,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const list = () => {
|
|
||||||
const { statsToDisplay, numberOfSeries, containerWidth, legendPlacement } = getStoriesKnobs(true);
|
|
||||||
return (
|
|
||||||
<div style={{ width: containerWidth }}>
|
|
||||||
<GraphLegend
|
|
||||||
displayMode={LegendDisplayMode.List}
|
|
||||||
items={generateLegendItems(numberOfSeries, statsToDisplay)}
|
|
||||||
onLabelClick={(item, event) => {
|
|
||||||
action('Series label clicked')(item, event);
|
|
||||||
}}
|
|
||||||
onSeriesColorChange={(label, color) => {
|
|
||||||
action('Series color changed')(label, color);
|
|
||||||
}}
|
|
||||||
onSeriesAxisToggle={(label, useRightYAxis) => {
|
|
||||||
action('Series axis toggle')(label, useRightYAxis);
|
|
||||||
}}
|
|
||||||
onToggleSort={sortBy => {
|
|
||||||
action('Toggle legend sort')(sortBy);
|
|
||||||
}}
|
|
||||||
placement={legendPlacement}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const table = () => {
|
|
||||||
const { statsToDisplay, numberOfSeries, containerWidth, legendPlacement } = getStoriesKnobs();
|
|
||||||
return (
|
|
||||||
<div style={{ width: containerWidth }}>
|
|
||||||
<GraphLegend
|
|
||||||
displayMode={LegendDisplayMode.Table}
|
|
||||||
items={generateLegendItems(numberOfSeries, statsToDisplay)}
|
|
||||||
onLabelClick={item => {
|
|
||||||
action('Series label clicked')(item);
|
|
||||||
}}
|
|
||||||
onSeriesColorChange={(label, color) => {
|
|
||||||
action('Series color changed')(label, color);
|
|
||||||
}}
|
|
||||||
onSeriesAxisToggle={(label, useRightYAxis) => {
|
|
||||||
action('Series axis toggle')(label, useRightYAxis);
|
|
||||||
}}
|
|
||||||
onToggleSort={sortBy => {
|
|
||||||
action('Toggle legend sort')(sortBy);
|
|
||||||
}}
|
|
||||||
placement={legendPlacement}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,116 +0,0 @@
|
|||||||
import React, { useContext } from 'react';
|
|
||||||
import { LegendProps, LegendItem, LegendDisplayMode } from '../Legend/Legend';
|
|
||||||
import { GraphLegendListItem, GraphLegendTableRow } from './GraphLegendItem';
|
|
||||||
import { SeriesColorChangeHandler, SeriesAxisToggleHandler } from './GraphWithLegend';
|
|
||||||
import { LegendTable } from '../Legend/LegendTable';
|
|
||||||
import { LegendList } from '../Legend/LegendList';
|
|
||||||
import union from 'lodash/union';
|
|
||||||
import sortBy from 'lodash/sortBy';
|
|
||||||
import { ThemeContext } from '../../themes/ThemeContext';
|
|
||||||
import { css } from 'emotion';
|
|
||||||
|
|
||||||
export interface GraphLegendProps extends LegendProps {
|
|
||||||
displayMode: LegendDisplayMode;
|
|
||||||
sortBy?: string;
|
|
||||||
sortDesc?: boolean;
|
|
||||||
onSeriesColorChange?: SeriesColorChangeHandler;
|
|
||||||
onSeriesAxisToggle?: SeriesAxisToggleHandler;
|
|
||||||
onToggleSort?: (sortBy: string) => void;
|
|
||||||
onLabelClick?: (item: LegendItem, event: React.MouseEvent<HTMLElement>) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const GraphLegend: React.FunctionComponent<GraphLegendProps> = ({
|
|
||||||
items,
|
|
||||||
displayMode,
|
|
||||||
sortBy: sortKey,
|
|
||||||
sortDesc,
|
|
||||||
onToggleSort,
|
|
||||||
onSeriesAxisToggle,
|
|
||||||
placement,
|
|
||||||
className,
|
|
||||||
...graphLegendItemProps
|
|
||||||
}) => {
|
|
||||||
const theme = useContext(ThemeContext);
|
|
||||||
|
|
||||||
if (displayMode === LegendDisplayMode.Table) {
|
|
||||||
const columns = items
|
|
||||||
.map(item => {
|
|
||||||
if (item.displayValues) {
|
|
||||||
return item.displayValues.map(i => i.title);
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
})
|
|
||||||
.reduce(
|
|
||||||
(acc, current) => {
|
|
||||||
return union(
|
|
||||||
acc,
|
|
||||||
current.filter(item => !!item)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
['']
|
|
||||||
) as string[];
|
|
||||||
|
|
||||||
const sortedItems = sortKey
|
|
||||||
? sortBy(items, item => {
|
|
||||||
if (item.displayValues) {
|
|
||||||
const stat = item.displayValues.filter(stat => stat.title === sortKey)[0];
|
|
||||||
return stat && stat.numeric;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
})
|
|
||||||
: items;
|
|
||||||
|
|
||||||
const legendTableEvenRowBackground = theme.isDark ? theme.palette.dark6 : theme.palette.gray5;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<LegendTable
|
|
||||||
className={css`
|
|
||||||
font-size: ${theme.typography.size.sm};
|
|
||||||
th {
|
|
||||||
padding: ${theme.spacing.xxs} ${theme.spacing.sm};
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
items={sortDesc ? sortedItems.reverse() : sortedItems}
|
|
||||||
columns={columns}
|
|
||||||
placement={placement}
|
|
||||||
sortBy={sortKey}
|
|
||||||
sortDesc={sortDesc}
|
|
||||||
itemRenderer={(item, index) => (
|
|
||||||
<GraphLegendTableRow
|
|
||||||
key={`${item.label}-${index}`}
|
|
||||||
item={item}
|
|
||||||
onToggleAxis={() => {
|
|
||||||
if (onSeriesAxisToggle) {
|
|
||||||
onSeriesAxisToggle(item.label, item.yAxis === 1 ? 2 : 1);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className={css`
|
|
||||||
background: ${index % 2 === 0 ? legendTableEvenRowBackground : 'none'};
|
|
||||||
`}
|
|
||||||
{...graphLegendItemProps}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
onToggleSort={onToggleSort}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<LegendList
|
|
||||||
items={items}
|
|
||||||
placement={placement}
|
|
||||||
itemRenderer={item => (
|
|
||||||
<GraphLegendListItem
|
|
||||||
item={item}
|
|
||||||
onToggleAxis={() => {
|
|
||||||
if (onSeriesAxisToggle) {
|
|
||||||
onSeriesAxisToggle(item.label, item.yAxis === 1 ? 2 : 1);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
{...graphLegendItemProps}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
GraphLegend.displayName = 'GraphLegend';
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import { stylesFactory } from '../../../themes/stylesFactory';
|
import { stylesFactory } from '../../../themes/stylesFactory';
|
||||||
import { GrafanaTheme, GraphSeriesValue } from '@grafana/data';
|
import { GrafanaTheme, GraphSeriesValue } from '@grafana/data';
|
||||||
import { css, cx } from 'emotion';
|
import { css, cx } from 'emotion';
|
||||||
import { SeriesIcon } from '../../Legend/SeriesIcon';
|
import { SeriesIcon } from '../../VizLegend/SeriesIcon';
|
||||||
import { useTheme } from '../../../themes';
|
import { useTheme } from '../../../themes';
|
||||||
|
|
||||||
export interface SeriesTableRowProps {
|
export interface SeriesTableRowProps {
|
||||||
|
@ -3,8 +3,7 @@ import React from 'react';
|
|||||||
import { select, text } from '@storybook/addon-knobs';
|
import { select, text } from '@storybook/addon-knobs';
|
||||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||||
import { GraphWithLegend, GraphWithLegendProps } from './GraphWithLegend';
|
import { GraphWithLegend, GraphWithLegendProps } from './GraphWithLegend';
|
||||||
|
import { LegendPlacement, LegendDisplayMode } from '../VizLegend/types';
|
||||||
import { LegendPlacement, LegendDisplayMode } from '../Legend/Legend';
|
|
||||||
import { GraphSeriesXY, FieldType, ArrayVector, dateTime, FieldColorModeId } from '@grafana/data';
|
import { GraphSeriesXY, FieldType, ArrayVector, dateTime, FieldColorModeId } from '@grafana/data';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -5,21 +5,19 @@ import { css } from 'emotion';
|
|||||||
import { GraphSeriesValue } from '@grafana/data';
|
import { GraphSeriesValue } from '@grafana/data';
|
||||||
|
|
||||||
import { Graph, GraphProps } from './Graph';
|
import { Graph, GraphProps } from './Graph';
|
||||||
import { LegendRenderOptions, LegendItem, LegendDisplayMode } from '../Legend/Legend';
|
import { VizLegendItem, LegendDisplayMode, SeriesColorChangeHandler, LegendPlacement } from '../VizLegend/types';
|
||||||
import { GraphLegend } from './GraphLegend';
|
import { VizLegend } from '../VizLegend/VizLegend';
|
||||||
import { CustomScrollbar } from '../CustomScrollbar/CustomScrollbar';
|
import { CustomScrollbar } from '../CustomScrollbar/CustomScrollbar';
|
||||||
import { stylesFactory } from '../../themes';
|
import { stylesFactory } from '../../themes';
|
||||||
|
|
||||||
export type SeriesOptionChangeHandler<TOption> = (label: string, option: TOption) => void;
|
export interface GraphWithLegendProps extends GraphProps {
|
||||||
export type SeriesColorChangeHandler = SeriesOptionChangeHandler<string>;
|
|
||||||
export type SeriesAxisToggleHandler = SeriesOptionChangeHandler<number>;
|
|
||||||
|
|
||||||
export interface GraphWithLegendProps extends GraphProps, LegendRenderOptions {
|
|
||||||
legendDisplayMode: LegendDisplayMode;
|
legendDisplayMode: LegendDisplayMode;
|
||||||
|
placement: LegendPlacement;
|
||||||
|
hideEmpty?: boolean;
|
||||||
|
hideZero?: boolean;
|
||||||
sortLegendBy?: string;
|
sortLegendBy?: string;
|
||||||
sortLegendDesc?: boolean;
|
sortLegendDesc?: boolean;
|
||||||
onSeriesColorChange?: SeriesColorChangeHandler;
|
onSeriesColorChange?: SeriesColorChangeHandler;
|
||||||
onSeriesAxisToggle?: SeriesAxisToggleHandler;
|
|
||||||
onSeriesToggle?: (label: string, event: React.MouseEvent<HTMLElement>) => void;
|
onSeriesToggle?: (label: string, event: React.MouseEvent<HTMLElement>) => void;
|
||||||
onToggleSort: (sortBy: string) => void;
|
onToggleSort: (sortBy: string) => void;
|
||||||
}
|
}
|
||||||
@ -60,7 +58,6 @@ export const GraphWithLegend: React.FunctionComponent<GraphWithLegendProps> = (p
|
|||||||
sortLegendDesc,
|
sortLegendDesc,
|
||||||
legendDisplayMode,
|
legendDisplayMode,
|
||||||
placement,
|
placement,
|
||||||
onSeriesAxisToggle,
|
|
||||||
onSeriesColorChange,
|
onSeriesColorChange,
|
||||||
onSeriesToggle,
|
onSeriesToggle,
|
||||||
onToggleSort,
|
onToggleSort,
|
||||||
@ -75,7 +72,7 @@ export const GraphWithLegend: React.FunctionComponent<GraphWithLegendProps> = (p
|
|||||||
} = props;
|
} = props;
|
||||||
const { graphContainer, wrapper, legendContainer } = getGraphWithLegendStyles(props);
|
const { graphContainer, wrapper, legendContainer } = getGraphWithLegendStyles(props);
|
||||||
|
|
||||||
const legendItems = series.reduce<LegendItem[]>((acc, s) => {
|
const legendItems = series.reduce<VizLegendItem[]>((acc, s) => {
|
||||||
return shouldHideLegendItem(s.data, hideEmpty, hideZero)
|
return shouldHideLegendItem(s.data, hideEmpty, hideZero)
|
||||||
? acc
|
? acc
|
||||||
: acc.concat([
|
: acc.concat([
|
||||||
@ -112,7 +109,7 @@ export const GraphWithLegend: React.FunctionComponent<GraphWithLegendProps> = (p
|
|||||||
{legendDisplayMode !== LegendDisplayMode.Hidden && (
|
{legendDisplayMode !== LegendDisplayMode.Hidden && (
|
||||||
<div className={legendContainer}>
|
<div className={legendContainer}>
|
||||||
<CustomScrollbar hideHorizontalTrack>
|
<CustomScrollbar hideHorizontalTrack>
|
||||||
<GraphLegend
|
<VizLegend
|
||||||
items={legendItems}
|
items={legendItems}
|
||||||
displayMode={legendDisplayMode}
|
displayMode={legendDisplayMode}
|
||||||
placement={placement}
|
placement={placement}
|
||||||
@ -124,7 +121,6 @@ export const GraphWithLegend: React.FunctionComponent<GraphWithLegendProps> = (p
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onSeriesColorChange={onSeriesColorChange}
|
onSeriesColorChange={onSeriesColorChange}
|
||||||
onSeriesAxisToggle={onSeriesAxisToggle}
|
|
||||||
onToggleSort={onToggleSort}
|
onToggleSort={onToggleSort}
|
||||||
/>
|
/>
|
||||||
</CustomScrollbar>
|
</CustomScrollbar>
|
||||||
|
@ -3,7 +3,7 @@ import React from 'react';
|
|||||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||||
import { GraphNG } from './GraphNG';
|
import { GraphNG } from './GraphNG';
|
||||||
import { dateTime } from '@grafana/data';
|
import { dateTime } from '@grafana/data';
|
||||||
import { LegendDisplayMode } from '../Legend/Legend';
|
import { LegendDisplayMode } from '../VizLegend/types';
|
||||||
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';
|
import { text, select } from '@storybook/addon-knobs';
|
||||||
|
@ -16,8 +16,8 @@ import { PlotProps } from '../uPlot/types';
|
|||||||
import { AxisPlacement, DrawStyle, GraphFieldConfig, PointVisibility } from '../uPlot/config';
|
import { AxisPlacement, DrawStyle, GraphFieldConfig, PointVisibility } from '../uPlot/config';
|
||||||
import { useTheme } from '../../themes';
|
import { useTheme } from '../../themes';
|
||||||
import { VizLayout } from '../VizLayout/VizLayout';
|
import { VizLayout } from '../VizLayout/VizLayout';
|
||||||
import { LegendDisplayMode, LegendItem, LegendOptions } from '../Legend/Legend';
|
import { LegendDisplayMode, VizLegendItem, VizLegendOptions } from '../VizLegend/types';
|
||||||
import { GraphLegend } from '../Graph/GraphLegend';
|
import { VizLegend } from '../VizLegend/VizLegend';
|
||||||
import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder';
|
import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder';
|
||||||
import { useRevision } from '../uPlot/hooks';
|
import { useRevision } from '../uPlot/hooks';
|
||||||
import { GraphNGLegendEvent, GraphNGLegendEventMode } from './types';
|
import { GraphNGLegendEvent, GraphNGLegendEventMode } from './types';
|
||||||
@ -30,7 +30,7 @@ export interface XYFieldMatchers {
|
|||||||
}
|
}
|
||||||
export interface GraphNGProps extends Omit<PlotProps, 'data' | 'config'> {
|
export interface GraphNGProps extends Omit<PlotProps, 'data' | 'config'> {
|
||||||
data: DataFrame[];
|
data: DataFrame[];
|
||||||
legend?: LegendOptions;
|
legend?: VizLegendOptions;
|
||||||
fields?: XYFieldMatchers; // default will assume timeseries data
|
fields?: XYFieldMatchers; // default will assume timeseries data
|
||||||
onLegendClick?: (event: GraphNGLegendEvent) => void;
|
onLegendClick?: (event: GraphNGLegendEvent) => void;
|
||||||
}
|
}
|
||||||
@ -55,7 +55,7 @@ export const GraphNG: React.FC<GraphNGProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const alignedFrameWithGapTest = useMemo(() => alignDataFrames(data, fields), [data, fields]);
|
const alignedFrameWithGapTest = useMemo(() => alignDataFrames(data, fields), [data, fields]);
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const legendItemsRef = useRef<LegendItem[]>([]);
|
const legendItemsRef = useRef<VizLegendItem[]>([]);
|
||||||
const hasLegend = useRef(legend && legend.displayMode !== LegendDisplayMode.Hidden);
|
const hasLegend = useRef(legend && legend.displayMode !== LegendDisplayMode.Hidden);
|
||||||
const alignedFrame = alignedFrameWithGapTest?.frame;
|
const alignedFrame = alignedFrameWithGapTest?.frame;
|
||||||
const getDataFrameFieldIndex = alignedFrameWithGapTest?.getDataFrameFieldIndex;
|
const getDataFrameFieldIndex = alignedFrameWithGapTest?.getDataFrameFieldIndex;
|
||||||
@ -68,7 +68,7 @@ export const GraphNG: React.FC<GraphNGProps> = ({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onLabelClick = useCallback(
|
const onLabelClick = useCallback(
|
||||||
(legend: LegendItem, event: React.MouseEvent) => {
|
(legend: VizLegendItem, event: React.MouseEvent) => {
|
||||||
const { fieldIndex } = legend;
|
const { fieldIndex } = legend;
|
||||||
|
|
||||||
if (!onLegendClick || !fieldIndex) {
|
if (!onLegendClick || !fieldIndex) {
|
||||||
@ -128,7 +128,7 @@ export const GraphNG: React.FC<GraphNGProps> = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const legendItems: LegendItem[] = [];
|
const legendItems: VizLegendItem[] = [];
|
||||||
|
|
||||||
for (let i = 0; i < alignedFrame.fields.length; i++) {
|
for (let i = 0; i < alignedFrame.fields.length; i++) {
|
||||||
const field = alignedFrame.fields[i];
|
const field = alignedFrame.fields[i];
|
||||||
@ -217,7 +217,7 @@ export const GraphNG: React.FC<GraphNGProps> = ({
|
|||||||
if (hasLegend && legendItemsRef.current.length > 0) {
|
if (hasLegend && legendItemsRef.current.length > 0) {
|
||||||
legendElement = (
|
legendElement = (
|
||||||
<VizLayout.Legend position={legend!.placement} maxHeight="35%" maxWidth="60%">
|
<VizLayout.Legend position={legend!.placement} maxHeight="35%" maxWidth="60%">
|
||||||
<GraphLegend
|
<VizLegend
|
||||||
onLabelClick={onLabelClick}
|
onLabelClick={onLabelClick}
|
||||||
placement={legend!.placement}
|
placement={legend!.placement}
|
||||||
items={legendItemsRef.current}
|
items={legendItemsRef.current}
|
||||||
|
@ -3,7 +3,7 @@ import { DataFrameFieldIndex } from '@grafana/data';
|
|||||||
/**
|
/**
|
||||||
* Mode to describe if a legend is isolated/selected or being appended to an existing
|
* Mode to describe if a legend is isolated/selected or being appended to an existing
|
||||||
* series selection.
|
* series selection.
|
||||||
* @public
|
* @alpha
|
||||||
*/
|
*/
|
||||||
export enum GraphNGLegendEventMode {
|
export enum GraphNGLegendEventMode {
|
||||||
ToggleSelection = 'select',
|
ToggleSelection = 'select',
|
||||||
@ -12,7 +12,7 @@ export enum GraphNGLegendEventMode {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Event being triggered when the user interact with the Graph legend.
|
* Event being triggered when the user interact with the Graph legend.
|
||||||
* @public
|
* @alpha
|
||||||
*/
|
*/
|
||||||
export interface GraphNGLegendEvent {
|
export interface GraphNGLegendEvent {
|
||||||
fieldIndex: DataFrameFieldIndex;
|
fieldIndex: DataFrameFieldIndex;
|
||||||
|
@ -1,131 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { generateLegendItems } from './Legend';
|
|
||||||
import { LegendList, LegendPlacement, LegendItem, LegendTable } from '@grafana/ui';
|
|
||||||
import { number, select, text } from '@storybook/addon-knobs';
|
|
||||||
import { action } from '@storybook/addon-actions';
|
|
||||||
import { GraphLegendListItem, GraphLegendTableRow, GraphLegendItemProps } from '../Graph/GraphLegendItem';
|
|
||||||
|
|
||||||
const getStoriesKnobs = (table = false) => {
|
|
||||||
const numberOfSeries = number('Number of series', 3);
|
|
||||||
const containerWidth = select(
|
|
||||||
'Container width',
|
|
||||||
{
|
|
||||||
Small: '200px',
|
|
||||||
Medium: '500px',
|
|
||||||
'Full width': '100%',
|
|
||||||
},
|
|
||||||
'100%'
|
|
||||||
);
|
|
||||||
|
|
||||||
const rawRenderer = (item: LegendItem) => (
|
|
||||||
<>
|
|
||||||
Label: <strong>{item.label}</strong>, Color: <strong>{item.color}</strong>, disabled:{' '}
|
|
||||||
<strong>{item.disabled ? 'yes' : 'no'}</strong>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
// eslint-disable-next-line react/display-name
|
|
||||||
const customRenderer = (component: React.ComponentType<GraphLegendItemProps>) => (item: LegendItem) =>
|
|
||||||
React.createElement(component, {
|
|
||||||
item,
|
|
||||||
onLabelClick: action('GraphLegendItem label clicked'),
|
|
||||||
onSeriesColorChange: action('Series color changed'),
|
|
||||||
onToggleAxis: action('Y-axis toggle'),
|
|
||||||
});
|
|
||||||
|
|
||||||
const typeSpecificRenderer = table
|
|
||||||
? {
|
|
||||||
'Custom renderer(GraphLegendTablerow)': 'custom-table',
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
'Custom renderer(GraphLegendListItem)': 'custom-list',
|
|
||||||
};
|
|
||||||
const legendItemRenderer = select(
|
|
||||||
'Item rendered',
|
|
||||||
{
|
|
||||||
'Raw renderer': 'raw',
|
|
||||||
...typeSpecificRenderer,
|
|
||||||
},
|
|
||||||
'raw'
|
|
||||||
);
|
|
||||||
|
|
||||||
const rightAxisSeries = text('Right y-axis series, i.e. A,C', '');
|
|
||||||
|
|
||||||
const legendPlacement = select<LegendPlacement>(
|
|
||||||
'Legend placement',
|
|
||||||
{
|
|
||||||
bottom: 'bottom',
|
|
||||||
right: 'right',
|
|
||||||
},
|
|
||||||
'bottom'
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
numberOfSeries,
|
|
||||||
containerWidth,
|
|
||||||
itemRenderer:
|
|
||||||
legendItemRenderer === 'raw'
|
|
||||||
? rawRenderer
|
|
||||||
: customRenderer(legendItemRenderer === 'custom-list' ? GraphLegendListItem : GraphLegendTableRow),
|
|
||||||
rightAxisSeries,
|
|
||||||
legendPlacement,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
title: 'Visualizations/Legend',
|
|
||||||
component: LegendList,
|
|
||||||
subcomponents: { LegendTable },
|
|
||||||
};
|
|
||||||
|
|
||||||
export const list = () => {
|
|
||||||
const { numberOfSeries, itemRenderer, containerWidth, rightAxisSeries, legendPlacement } = getStoriesKnobs();
|
|
||||||
let items = generateLegendItems(numberOfSeries);
|
|
||||||
|
|
||||||
items = items.map(i => {
|
|
||||||
if (
|
|
||||||
rightAxisSeries
|
|
||||||
.split(',')
|
|
||||||
.map(s => s.trim())
|
|
||||||
.indexOf(i.label.split('-')[0]) > -1
|
|
||||||
) {
|
|
||||||
i.yAxis = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
return i;
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<div style={{ width: containerWidth }}>
|
|
||||||
<LegendList itemRenderer={itemRenderer} items={items} placement={legendPlacement} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const table = () => {
|
|
||||||
const { numberOfSeries, itemRenderer, containerWidth, rightAxisSeries, legendPlacement } = getStoriesKnobs(true);
|
|
||||||
let items = generateLegendItems(numberOfSeries);
|
|
||||||
|
|
||||||
items = items.map(i => {
|
|
||||||
if (
|
|
||||||
rightAxisSeries
|
|
||||||
.split(',')
|
|
||||||
.map(s => s.trim())
|
|
||||||
.indexOf(i.label.split('-')[0]) > -1
|
|
||||||
) {
|
|
||||||
i.yAxis = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...i,
|
|
||||||
info: [
|
|
||||||
{ title: 'min', text: '14.42', numeric: 14.427101844163694 },
|
|
||||||
{ title: 'max', text: '18.42', numeric: 18.427101844163694 },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<div style={{ width: containerWidth }}>
|
|
||||||
<LegendTable itemRenderer={itemRenderer} items={items} columns={['', 'min', 'max']} placement={legendPlacement} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,58 +0,0 @@
|
|||||||
import { DataFrameFieldIndex, DisplayValue } from '@grafana/data';
|
|
||||||
|
|
||||||
import { LegendList } from './LegendList';
|
|
||||||
import { LegendTable } from './LegendTable';
|
|
||||||
import tinycolor from 'tinycolor2';
|
|
||||||
|
|
||||||
export const generateLegendItems = (numberOfSeries: number, statsToDisplay?: DisplayValue[]): LegendItem[] => {
|
|
||||||
const alphabet = 'abcdefghijklmnopqrstuvwxyz'.split('');
|
|
||||||
|
|
||||||
return [...new Array(numberOfSeries)].map((item, i) => {
|
|
||||||
return {
|
|
||||||
label: `${alphabet[i].toUpperCase()}-series`,
|
|
||||||
color: tinycolor.fromRatio({ h: i / alphabet.length, s: 1, v: 1 }).toHexString(),
|
|
||||||
yAxis: 1,
|
|
||||||
displayValues: statsToDisplay || [],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export enum LegendDisplayMode {
|
|
||||||
List = 'list',
|
|
||||||
Table = 'table',
|
|
||||||
Hidden = 'hidden',
|
|
||||||
}
|
|
||||||
export interface LegendBasicOptions {
|
|
||||||
displayMode: LegendDisplayMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LegendRenderOptions {
|
|
||||||
placement: LegendPlacement;
|
|
||||||
hideEmpty?: boolean;
|
|
||||||
hideZero?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type LegendPlacement = 'bottom' | 'right';
|
|
||||||
|
|
||||||
export interface LegendOptions extends LegendBasicOptions, LegendRenderOptions {}
|
|
||||||
|
|
||||||
export interface LegendItem {
|
|
||||||
label: string;
|
|
||||||
color: string;
|
|
||||||
yAxis: number;
|
|
||||||
disabled?: boolean;
|
|
||||||
displayValues?: DisplayValue[];
|
|
||||||
fieldIndex?: DataFrameFieldIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LegendComponentProps {
|
|
||||||
className?: string;
|
|
||||||
items: LegendItem[];
|
|
||||||
placement: LegendPlacement;
|
|
||||||
// Function to render given item
|
|
||||||
itemRenderer?: (item: LegendItem, index: number) => JSX.Element;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LegendProps extends LegendComponentProps {}
|
|
||||||
|
|
||||||
export { LegendList, LegendTable };
|
|
@ -1,61 +0,0 @@
|
|||||||
import React, { useContext } from 'react';
|
|
||||||
import { LegendComponentProps, LegendItem } from './Legend';
|
|
||||||
import { InlineList } from '../List/InlineList';
|
|
||||||
import { List } from '../List/List';
|
|
||||||
import { css, cx } from 'emotion';
|
|
||||||
import { ThemeContext } from '../../themes/ThemeContext';
|
|
||||||
import { stylesFactory } from '../../themes';
|
|
||||||
import { GrafanaTheme } from '@grafana/data';
|
|
||||||
|
|
||||||
const getStyles = stylesFactory((theme: GrafanaTheme) => ({
|
|
||||||
item: css`
|
|
||||||
padding-left: 10px;
|
|
||||||
display: flex;
|
|
||||||
font-size: ${theme.typography.size.sm};
|
|
||||||
white-space: nowrap;
|
|
||||||
`,
|
|
||||||
wrapper: css`
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: space-between;
|
|
||||||
width: 100%;
|
|
||||||
`,
|
|
||||||
section: css`
|
|
||||||
display: flex;
|
|
||||||
`,
|
|
||||||
sectionRight: css`
|
|
||||||
justify-content: flex-end;
|
|
||||||
flex-grow: 1;
|
|
||||||
`,
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const LegendList: React.FunctionComponent<LegendComponentProps> = ({
|
|
||||||
items,
|
|
||||||
itemRenderer,
|
|
||||||
placement,
|
|
||||||
className,
|
|
||||||
}) => {
|
|
||||||
const theme = useContext(ThemeContext);
|
|
||||||
const styles = getStyles(theme);
|
|
||||||
|
|
||||||
const renderItem = (item: LegendItem, index: number) => {
|
|
||||||
return <span className={styles.item}>{itemRenderer ? itemRenderer(item, index) : item.label}</span>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getItemKey = (item: LegendItem) => `${item.label}`;
|
|
||||||
|
|
||||||
return placement === 'bottom' ? (
|
|
||||||
<div className={cx(styles.wrapper, className)}>
|
|
||||||
<div className={styles.section}>
|
|
||||||
<InlineList items={items.filter(item => item.yAxis === 1)} renderItem={renderItem} getItemKey={getItemKey} />
|
|
||||||
</div>
|
|
||||||
<div className={cx(styles.section, styles.sectionRight)}>
|
|
||||||
<InlineList items={items.filter(item => item.yAxis !== 1)} renderItem={renderItem} getItemKey={getItemKey} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<List items={items} renderItem={renderItem} getItemKey={getItemKey} className={className} />
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
LegendList.displayName = 'LegendList';
|
|
@ -1,61 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { css, cx } from 'emotion';
|
|
||||||
import { SeriesColorPicker } from '../ColorPicker/ColorPicker';
|
|
||||||
import { SeriesIcon, SeriesIconProps } from './SeriesIcon';
|
|
||||||
|
|
||||||
interface LegendSeriesIconProps {
|
|
||||||
disabled: boolean;
|
|
||||||
color: string;
|
|
||||||
yAxis: number;
|
|
||||||
onColorChange: (color: string) => void;
|
|
||||||
onToggleAxis?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LegendSeriesIcon: React.FunctionComponent<LegendSeriesIconProps> = ({
|
|
||||||
disabled,
|
|
||||||
yAxis,
|
|
||||||
color,
|
|
||||||
onColorChange,
|
|
||||||
onToggleAxis,
|
|
||||||
}) => {
|
|
||||||
let iconProps: SeriesIconProps = {
|
|
||||||
color,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!disabled) {
|
|
||||||
iconProps = {
|
|
||||||
...iconProps,
|
|
||||||
className: 'pointer',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return disabled ? (
|
|
||||||
<span
|
|
||||||
className={cx(
|
|
||||||
'graph-legend-icon',
|
|
||||||
disabled &&
|
|
||||||
css`
|
|
||||||
cursor: default;
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<SeriesIcon {...iconProps} />
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<SeriesColorPicker
|
|
||||||
yaxis={yAxis}
|
|
||||||
color={color}
|
|
||||||
onChange={onColorChange}
|
|
||||||
onToggleAxis={onToggleAxis}
|
|
||||||
enableNamedColors
|
|
||||||
>
|
|
||||||
{({ ref, showColorPicker, hideColorPicker }) => (
|
|
||||||
<span ref={ref} onClick={showColorPicker} onMouseLeave={hideColorPicker} className="graph-legend-icon">
|
|
||||||
<SeriesIcon {...iconProps} />
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</SeriesColorPicker>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
LegendSeriesIcon.displayName = 'LegendSeriesIcon';
|
|
@ -1,28 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { InlineList } from '../List/InlineList';
|
|
||||||
import { css } from 'emotion';
|
|
||||||
import { DisplayValue, formattedValueToString } from '@grafana/data';
|
|
||||||
import capitalize from 'lodash/capitalize';
|
|
||||||
|
|
||||||
const LegendItemStat: React.FunctionComponent<{ stat: DisplayValue }> = ({ stat }) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={css`
|
|
||||||
margin-left: 6px;
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
{stat.title && `${capitalize(stat.title)}:`} {formattedValueToString(stat)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
LegendItemStat.displayName = 'LegendItemStat';
|
|
||||||
|
|
||||||
export const LegendStatsList: React.FunctionComponent<{ stats: DisplayValue[] }> = ({ stats }) => {
|
|
||||||
if (stats.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return <InlineList items={stats} renderItem={stat => <LegendItemStat stat={stat} />} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
LegendStatsList.displayName = 'LegendStatsList';
|
|
@ -1,82 +0,0 @@
|
|||||||
import React, { useContext } from 'react';
|
|
||||||
import { css, cx } from 'emotion';
|
|
||||||
import { LegendComponentProps } from './Legend';
|
|
||||||
import { Icon } from '../Icon/Icon';
|
|
||||||
import { ThemeContext } from '../../themes/ThemeContext';
|
|
||||||
|
|
||||||
export interface LegendTableProps extends LegendComponentProps {
|
|
||||||
columns: string[];
|
|
||||||
sortBy?: string;
|
|
||||||
sortDesc?: boolean;
|
|
||||||
onToggleSort?: (sortBy: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LegendTable: React.FunctionComponent<LegendTableProps> = ({
|
|
||||||
items,
|
|
||||||
columns,
|
|
||||||
sortBy,
|
|
||||||
sortDesc,
|
|
||||||
itemRenderer,
|
|
||||||
className,
|
|
||||||
onToggleSort,
|
|
||||||
}) => {
|
|
||||||
const theme = useContext(ThemeContext);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<table
|
|
||||||
className={cx(
|
|
||||||
css`
|
|
||||||
width: 100%;
|
|
||||||
td {
|
|
||||||
padding: 2px 10px;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
{columns.map(columnHeader => {
|
|
||||||
return (
|
|
||||||
<th
|
|
||||||
key={columnHeader}
|
|
||||||
className={css`
|
|
||||||
color: ${theme.colors.textBlue};
|
|
||||||
font-weight: bold;
|
|
||||||
text-align: right;
|
|
||||||
cursor: pointer;
|
|
||||||
`}
|
|
||||||
onClick={() => {
|
|
||||||
if (onToggleSort) {
|
|
||||||
onToggleSort(columnHeader);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{columnHeader}
|
|
||||||
{sortBy === columnHeader && (
|
|
||||||
<Icon
|
|
||||||
className={css`
|
|
||||||
margin-left: ${theme.spacing.sm};
|
|
||||||
`}
|
|
||||||
name={sortDesc ? 'angle-down' : 'angle-up'}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</th>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{items.map((item, index) => {
|
|
||||||
return itemRenderer ? (
|
|
||||||
itemRenderer(item, index)
|
|
||||||
) : (
|
|
||||||
<tr key={`${item.label}-${index}`}>
|
|
||||||
<td>{item.label}</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,13 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Icon } from '../Icon/Icon';
|
|
||||||
|
|
||||||
export interface SeriesIconProps {
|
|
||||||
color: string;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SeriesIcon: React.FunctionComponent<SeriesIconProps> = ({ color, className }) => {
|
|
||||||
return <Icon name="minus" className={className} style={{ color }} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
SeriesIcon.displayName = 'SeriesIcon';
|
|
20
packages/grafana-ui/src/components/VizLegend/SeriesIcon.tsx
Normal file
20
packages/grafana-ui/src/components/VizLegend/SeriesIcon.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import React, { CSSProperties } from 'react';
|
||||||
|
|
||||||
|
export interface Props extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SeriesIcon = React.forwardRef<HTMLDivElement, Props>(({ color, className, ...restProps }, ref) => {
|
||||||
|
const styles: CSSProperties = {
|
||||||
|
backgroundColor: color,
|
||||||
|
width: '14px',
|
||||||
|
height: '4px',
|
||||||
|
borderRadius: '1px',
|
||||||
|
display: 'inline-block',
|
||||||
|
marginRight: '8px',
|
||||||
|
};
|
||||||
|
|
||||||
|
return <div ref={ref} className={className} style={styles} {...restProps} />;
|
||||||
|
});
|
||||||
|
|
||||||
|
SeriesIcon.displayName = 'SeriesIcon';
|
184
packages/grafana-ui/src/components/VizLegend/VizLegend.story.tsx
Normal file
184
packages/grafana-ui/src/components/VizLegend/VizLegend.story.tsx
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
import React, { FC, useState } from 'react';
|
||||||
|
import { useTheme, VizLegend } from '@grafana/ui';
|
||||||
|
import { number, select } from '@storybook/addon-knobs';
|
||||||
|
import {} from './VizLegendListItem';
|
||||||
|
import { DisplayValue, getColorForTheme, GrafanaTheme } from '@grafana/data';
|
||||||
|
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||||
|
import { LegendDisplayMode, VizLegendItem, LegendPlacement } from './types';
|
||||||
|
|
||||||
|
const getStoriesKnobs = (table = false) => {
|
||||||
|
const seriesCount = number('Number of series', 5);
|
||||||
|
const containerWidth = select(
|
||||||
|
'Container width',
|
||||||
|
{
|
||||||
|
Small: '200px',
|
||||||
|
Medium: '500px',
|
||||||
|
'Full width': '100%',
|
||||||
|
},
|
||||||
|
'100%'
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
seriesCount,
|
||||||
|
containerWidth,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Visualizations/VizLegend',
|
||||||
|
component: VizLegend,
|
||||||
|
decorators: [withCenteredStory],
|
||||||
|
};
|
||||||
|
|
||||||
|
interface LegendStoryDemoProps {
|
||||||
|
name: string;
|
||||||
|
displayMode: LegendDisplayMode;
|
||||||
|
placement: LegendPlacement;
|
||||||
|
seriesCount: number;
|
||||||
|
stats?: DisplayValue[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const LegendStoryDemo: FC<LegendStoryDemoProps> = ({ displayMode, seriesCount, name, placement, stats }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const [items, setItems] = useState<VizLegendItem[]>(generateLegendItems(seriesCount, theme, stats));
|
||||||
|
|
||||||
|
const onSeriesColorChange = (label: string, color: string) => {
|
||||||
|
setItems(
|
||||||
|
items.map(item => {
|
||||||
|
if (item.label === label) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
color: color,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onLabelClick = (clickItem: VizLegendItem) => {
|
||||||
|
setItems(
|
||||||
|
items.map(item => {
|
||||||
|
if (item !== clickItem) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
disabled: true,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
disabled: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p style={{ marginBottom: '32px' }}>
|
||||||
|
<h3 style={{ marginBottom: '32px' }}>{name}</h3>
|
||||||
|
<VizLegend
|
||||||
|
displayMode={displayMode}
|
||||||
|
items={items}
|
||||||
|
placement={placement}
|
||||||
|
onSeriesColorChange={onSeriesColorChange}
|
||||||
|
onLabelClick={onLabelClick}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithNoValues = () => {
|
||||||
|
const { seriesCount, containerWidth } = getStoriesKnobs();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ width: containerWidth }}>
|
||||||
|
<LegendStoryDemo
|
||||||
|
name="List mode, placement bottom"
|
||||||
|
displayMode={LegendDisplayMode.List}
|
||||||
|
seriesCount={seriesCount}
|
||||||
|
placement="bottom"
|
||||||
|
/>
|
||||||
|
<LegendStoryDemo
|
||||||
|
name="List mode, placement right"
|
||||||
|
displayMode={LegendDisplayMode.List}
|
||||||
|
seriesCount={seriesCount}
|
||||||
|
placement="right"
|
||||||
|
/>
|
||||||
|
<LegendStoryDemo
|
||||||
|
name="Table mode"
|
||||||
|
displayMode={LegendDisplayMode.Table}
|
||||||
|
seriesCount={seriesCount}
|
||||||
|
placement="bottom"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithValues = () => {
|
||||||
|
const { seriesCount, containerWidth } = getStoriesKnobs();
|
||||||
|
const stats: DisplayValue[] = [
|
||||||
|
{
|
||||||
|
title: 'Min',
|
||||||
|
text: '5.00',
|
||||||
|
numeric: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Max',
|
||||||
|
text: '10.00',
|
||||||
|
numeric: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Last',
|
||||||
|
text: '2.00',
|
||||||
|
numeric: 2,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ width: containerWidth }}>
|
||||||
|
<LegendStoryDemo
|
||||||
|
name="List mode, placement bottom"
|
||||||
|
displayMode={LegendDisplayMode.List}
|
||||||
|
seriesCount={seriesCount}
|
||||||
|
placement="bottom"
|
||||||
|
stats={stats}
|
||||||
|
/>
|
||||||
|
<LegendStoryDemo
|
||||||
|
name="List mode, placement right"
|
||||||
|
displayMode={LegendDisplayMode.List}
|
||||||
|
seriesCount={seriesCount}
|
||||||
|
placement="right"
|
||||||
|
stats={stats}
|
||||||
|
/>
|
||||||
|
<LegendStoryDemo
|
||||||
|
name="Table mode"
|
||||||
|
displayMode={LegendDisplayMode.Table}
|
||||||
|
seriesCount={seriesCount}
|
||||||
|
placement="bottom"
|
||||||
|
stats={stats}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function generateLegendItems(
|
||||||
|
numberOfSeries: number,
|
||||||
|
theme: GrafanaTheme,
|
||||||
|
statsToDisplay?: DisplayValue[]
|
||||||
|
): VizLegendItem[] {
|
||||||
|
const alphabet = 'abcdefghijklmnopqrstuvwxyz'.split('');
|
||||||
|
const colors = ['green', 'blue', 'red', 'purple', 'orange', 'dark-green', 'yellow', 'light-blue'].map(c =>
|
||||||
|
getColorForTheme(c, theme)
|
||||||
|
);
|
||||||
|
|
||||||
|
return [...new Array(numberOfSeries)].map((item, i) => {
|
||||||
|
return {
|
||||||
|
label: `${alphabet[i].toUpperCase()}-series`,
|
||||||
|
color: colors[i],
|
||||||
|
yAxis: 1,
|
||||||
|
displayValues: statsToDisplay || [],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
46
packages/grafana-ui/src/components/VizLegend/VizLegend.tsx
Normal file
46
packages/grafana-ui/src/components/VizLegend/VizLegend.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { LegendProps, LegendDisplayMode } from './types';
|
||||||
|
import { VizLegendTable } from './VizLegendTable';
|
||||||
|
import { VizLegendList } from './VizLegendList';
|
||||||
|
|
||||||
|
export const VizLegend: React.FunctionComponent<LegendProps> = ({
|
||||||
|
items,
|
||||||
|
displayMode,
|
||||||
|
sortBy: sortKey,
|
||||||
|
sortDesc,
|
||||||
|
onToggleSort,
|
||||||
|
onLabelClick,
|
||||||
|
onSeriesColorChange,
|
||||||
|
placement,
|
||||||
|
className,
|
||||||
|
}) => {
|
||||||
|
switch (displayMode) {
|
||||||
|
case LegendDisplayMode.Table:
|
||||||
|
return (
|
||||||
|
<VizLegendTable
|
||||||
|
className={className}
|
||||||
|
items={items}
|
||||||
|
placement={placement}
|
||||||
|
sortBy={sortKey}
|
||||||
|
sortDesc={sortDesc}
|
||||||
|
onLabelClick={onLabelClick}
|
||||||
|
onToggleSort={onToggleSort}
|
||||||
|
onSeriesColorChange={onSeriesColorChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case LegendDisplayMode.List:
|
||||||
|
return (
|
||||||
|
<VizLegendList
|
||||||
|
className={className}
|
||||||
|
items={items}
|
||||||
|
placement={placement}
|
||||||
|
onLabelClick={onLabelClick}
|
||||||
|
onSeriesColorChange={onSeriesColorChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
VizLegend.displayName = 'Legend';
|
@ -0,0 +1,92 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { VizLegendBaseProps, VizLegendItem } from './types';
|
||||||
|
import { InlineList } from '../List/InlineList';
|
||||||
|
import { List } from '../List/List';
|
||||||
|
import { css, cx } from 'emotion';
|
||||||
|
import { useStyles } from '../../themes';
|
||||||
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
|
import { VizLegendListItem } from './VizLegendListItem';
|
||||||
|
|
||||||
|
export interface Props extends VizLegendBaseProps {}
|
||||||
|
|
||||||
|
export const VizLegendList: React.FunctionComponent<Props> = ({
|
||||||
|
items,
|
||||||
|
itemRenderer,
|
||||||
|
onSeriesColorChange,
|
||||||
|
onLabelClick,
|
||||||
|
placement,
|
||||||
|
className,
|
||||||
|
}) => {
|
||||||
|
const styles = useStyles(getStyles);
|
||||||
|
|
||||||
|
if (!itemRenderer) {
|
||||||
|
/* eslint-disable-next-line react/display-name */
|
||||||
|
itemRenderer = item => (
|
||||||
|
<VizLegendListItem item={item} onLabelClick={onLabelClick} onSeriesColorChange={onSeriesColorChange} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderItem = (item: VizLegendItem, index: number) => {
|
||||||
|
return <span className={styles.item}>{itemRenderer!(item, index)}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getItemKey = (item: VizLegendItem) => `${item.label}`;
|
||||||
|
|
||||||
|
switch (placement) {
|
||||||
|
case 'right':
|
||||||
|
return (
|
||||||
|
<div className={cx(styles.rightWrapper, className)}>
|
||||||
|
<List items={items} renderItem={renderItem} getItemKey={getItemKey} className={className} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case 'bottom':
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<div className={cx(styles.bottomWrapper, className)}>
|
||||||
|
<div className={styles.section}>
|
||||||
|
<InlineList
|
||||||
|
items={items.filter(item => item.yAxis === 1)}
|
||||||
|
renderItem={renderItem}
|
||||||
|
getItemKey={getItemKey}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={cx(styles.section, styles.sectionRight)}>
|
||||||
|
<InlineList
|
||||||
|
items={items.filter(item => item.yAxis !== 1)}
|
||||||
|
renderItem={renderItem}
|
||||||
|
getItemKey={getItemKey}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
VizLegendList.displayName = 'VizLegendList';
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme) => ({
|
||||||
|
item: css`
|
||||||
|
padding-right: 10px;
|
||||||
|
display: flex;
|
||||||
|
font-size: ${theme.typography.size.sm};
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-bottom: ${theme.spacing.xs};
|
||||||
|
`,
|
||||||
|
rightWrapper: css`
|
||||||
|
margin-left: ${theme.spacing.sm};
|
||||||
|
`,
|
||||||
|
bottomWrapper: css`
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
margin-left: ${theme.spacing.md};
|
||||||
|
`,
|
||||||
|
section: css`
|
||||||
|
display: flex;
|
||||||
|
`,
|
||||||
|
sectionRight: css`
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-grow: 1;
|
||||||
|
`,
|
||||||
|
});
|
@ -0,0 +1,71 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { css, cx } from 'emotion';
|
||||||
|
import { VizLegendSeriesIcon } from './VizLegendSeriesIcon';
|
||||||
|
import { VizLegendItem } from './types';
|
||||||
|
import { SeriesColorChangeHandler } from './types';
|
||||||
|
import { VizLegendStatsList } from './VizLegendStatsList';
|
||||||
|
import { useStyles } from '../../themes';
|
||||||
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
item: VizLegendItem;
|
||||||
|
className?: string;
|
||||||
|
onLabelClick?: (item: VizLegendItem, event: React.MouseEvent<HTMLDivElement>) => void;
|
||||||
|
onSeriesColorChange?: SeriesColorChangeHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VizLegendListItem: React.FunctionComponent<Props> = ({ item, onSeriesColorChange, onLabelClick }) => {
|
||||||
|
const styles = useStyles(getStyles);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.itemWrapper}>
|
||||||
|
<VizLegendSeriesIcon
|
||||||
|
disabled={!onSeriesColorChange}
|
||||||
|
color={item.color}
|
||||||
|
onColorChange={color => {
|
||||||
|
if (onSeriesColorChange) {
|
||||||
|
onSeriesColorChange(item.label, color);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
yAxis={item.yAxis}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
onClick={event => {
|
||||||
|
if (onLabelClick) {
|
||||||
|
onLabelClick(item, event);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={cx(styles.label, item.disabled && styles.labelDisabled)}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{item.displayValues && <VizLegendStatsList stats={item.displayValues} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
VizLegendListItem.displayName = 'VizLegendListItem';
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme) => ({
|
||||||
|
label: css`
|
||||||
|
label: LegendLabel;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
`,
|
||||||
|
labelDisabled: css`
|
||||||
|
label: LegendLabelDisabled;
|
||||||
|
color: ${theme.colors.linkDisabled};
|
||||||
|
`,
|
||||||
|
itemWrapper: css`
|
||||||
|
display: flex;
|
||||||
|
white-space: nowrap;
|
||||||
|
align-items: center;
|
||||||
|
`,
|
||||||
|
value: css`
|
||||||
|
text-align: right;
|
||||||
|
`,
|
||||||
|
yAxisLabel: css`
|
||||||
|
color: ${theme.palette.gray2};
|
||||||
|
`,
|
||||||
|
});
|
@ -0,0 +1,43 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { SeriesColorPicker } from '../ColorPicker/ColorPicker';
|
||||||
|
import { SeriesIcon } from './SeriesIcon';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
disabled: boolean;
|
||||||
|
color: string;
|
||||||
|
yAxis: number;
|
||||||
|
onColorChange: (color: string) => void;
|
||||||
|
onToggleAxis?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VizLegendSeriesIcon: React.FunctionComponent<Props> = ({
|
||||||
|
disabled,
|
||||||
|
yAxis,
|
||||||
|
color,
|
||||||
|
onColorChange,
|
||||||
|
onToggleAxis,
|
||||||
|
}) => {
|
||||||
|
return disabled ? (
|
||||||
|
<SeriesIcon color={color} />
|
||||||
|
) : (
|
||||||
|
<SeriesColorPicker
|
||||||
|
yaxis={yAxis}
|
||||||
|
color={color}
|
||||||
|
onChange={onColorChange}
|
||||||
|
onToggleAxis={onToggleAxis}
|
||||||
|
enableNamedColors
|
||||||
|
>
|
||||||
|
{({ ref, showColorPicker, hideColorPicker }) => (
|
||||||
|
<SeriesIcon
|
||||||
|
color={color}
|
||||||
|
className="pointer"
|
||||||
|
ref={ref}
|
||||||
|
onClick={showColorPicker}
|
||||||
|
onMouseLeave={hideColorPicker}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</SeriesColorPicker>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
VizLegendSeriesIcon.displayName = 'VizLegendSeriesIcon';
|
@ -0,0 +1,28 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { InlineList } from '../List/InlineList';
|
||||||
|
import { css } from 'emotion';
|
||||||
|
import { DisplayValue, formattedValueToString } from '@grafana/data';
|
||||||
|
import capitalize from 'lodash/capitalize';
|
||||||
|
|
||||||
|
const VizLegendItemStat: React.FunctionComponent<{ stat: DisplayValue }> = ({ stat }) => {
|
||||||
|
const styles = css`
|
||||||
|
margin-left: 8px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles}>
|
||||||
|
{stat.title && `${capitalize(stat.title)}:`} {formattedValueToString(stat)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
VizLegendItemStat.displayName = 'VizLegendItemStat';
|
||||||
|
|
||||||
|
export const VizLegendStatsList: React.FunctionComponent<{ stats: DisplayValue[] }> = ({ stats }) => {
|
||||||
|
if (stats.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return <InlineList items={stats} renderItem={stat => <VizLegendItemStat stat={stat} />} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
VizLegendStatsList.displayName = 'VizLegendStatsList';
|
107
packages/grafana-ui/src/components/VizLegend/VizLegendTable.tsx
Normal file
107
packages/grafana-ui/src/components/VizLegend/VizLegendTable.tsx
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import React, { FC } from 'react';
|
||||||
|
import { css, cx } from 'emotion';
|
||||||
|
import { VizLegendTableProps } from './types';
|
||||||
|
import { Icon } from '../Icon/Icon';
|
||||||
|
import { useStyles } from '../../themes/ThemeContext';
|
||||||
|
import union from 'lodash/union';
|
||||||
|
import sortBy from 'lodash/sortBy';
|
||||||
|
import { LegendTableItem } from './VizLegendTableItem';
|
||||||
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
|
|
||||||
|
export const VizLegendTable: FC<VizLegendTableProps> = ({
|
||||||
|
items,
|
||||||
|
sortBy: sortKey,
|
||||||
|
sortDesc,
|
||||||
|
itemRenderer,
|
||||||
|
className,
|
||||||
|
onToggleSort,
|
||||||
|
onLabelClick,
|
||||||
|
onSeriesColorChange,
|
||||||
|
}) => {
|
||||||
|
const styles = useStyles(getStyles);
|
||||||
|
|
||||||
|
const columns = items
|
||||||
|
.map(item => {
|
||||||
|
if (item.displayValues) {
|
||||||
|
return item.displayValues.map(i => i.title);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
})
|
||||||
|
.reduce(
|
||||||
|
(acc, current) => {
|
||||||
|
return union(
|
||||||
|
acc,
|
||||||
|
current.filter(item => !!item)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
['']
|
||||||
|
) as string[];
|
||||||
|
|
||||||
|
const sortedItems = sortKey
|
||||||
|
? sortBy(items, item => {
|
||||||
|
if (item.displayValues) {
|
||||||
|
const stat = item.displayValues.filter(stat => stat.title === sortKey)[0];
|
||||||
|
return stat && stat.numeric;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
})
|
||||||
|
: items;
|
||||||
|
|
||||||
|
if (!itemRenderer) {
|
||||||
|
/* eslint-disable-next-line react/display-name */
|
||||||
|
itemRenderer = (item, index) => (
|
||||||
|
<LegendTableItem
|
||||||
|
key={`${item.label}-${index}`}
|
||||||
|
item={item}
|
||||||
|
onSeriesColorChange={onSeriesColorChange}
|
||||||
|
onLabelClick={onLabelClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<table className={cx(styles.table, className)}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{columns.map(columnHeader => {
|
||||||
|
return (
|
||||||
|
<th
|
||||||
|
key={columnHeader}
|
||||||
|
className={styles.header}
|
||||||
|
onClick={() => {
|
||||||
|
if (onToggleSort) {
|
||||||
|
onToggleSort(columnHeader);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{columnHeader}
|
||||||
|
{sortKey === columnHeader && (
|
||||||
|
<Icon className={styles.sortIcon} name={sortDesc ? 'angle-down' : 'angle-up'} />
|
||||||
|
)}
|
||||||
|
</th>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>{sortedItems.map(itemRenderer!)}</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme) => ({
|
||||||
|
table: css`
|
||||||
|
width: 100%;
|
||||||
|
margin-left: ${theme.spacing.sm};
|
||||||
|
`,
|
||||||
|
header: css`
|
||||||
|
color: ${theme.colors.textBlue};
|
||||||
|
font-weight: ${theme.typography.weight.semibold};
|
||||||
|
border-bottom: 1px solid ${theme.colors.border1};
|
||||||
|
padding: ${theme.spacing.xxs} ${theme.spacing.sm};
|
||||||
|
text-align: right;
|
||||||
|
cursor: pointer;
|
||||||
|
`,
|
||||||
|
sortIcon: css`
|
||||||
|
margin-left: ${theme.spacing.sm};
|
||||||
|
`,
|
||||||
|
});
|
@ -1,33 +1,33 @@
|
|||||||
import React, { useContext } from 'react';
|
import React from 'react';
|
||||||
import { css, cx } from 'emotion';
|
import { css, cx } from 'emotion';
|
||||||
import { LegendSeriesIcon } from '../Legend/LegendSeriesIcon';
|
import { VizLegendSeriesIcon } from './VizLegendSeriesIcon';
|
||||||
import { LegendItem } from '../Legend/Legend';
|
import { VizLegendItem } from './types';
|
||||||
import { SeriesColorChangeHandler } from './GraphWithLegend';
|
import { SeriesColorChangeHandler } from './types';
|
||||||
import { LegendStatsList } from '../Legend/LegendStatsList';
|
import { useStyles } from '../../themes/ThemeContext';
|
||||||
import { ThemeContext } from '../../themes/ThemeContext';
|
import { styleMixins } from '../../themes';
|
||||||
import { stylesFactory } from '../../themes';
|
|
||||||
import { GrafanaTheme, formattedValueToString } from '@grafana/data';
|
import { GrafanaTheme, formattedValueToString } from '@grafana/data';
|
||||||
|
|
||||||
export interface GraphLegendItemProps {
|
export interface Props {
|
||||||
key?: React.Key;
|
key?: React.Key;
|
||||||
item: LegendItem;
|
item: VizLegendItem;
|
||||||
className?: string;
|
className?: string;
|
||||||
onLabelClick?: (item: LegendItem, event: React.MouseEvent<HTMLDivElement>) => void;
|
onLabelClick?: (item: VizLegendItem, event: React.MouseEvent<HTMLDivElement>) => void;
|
||||||
onSeriesColorChange?: SeriesColorChangeHandler;
|
onSeriesColorChange?: SeriesColorChangeHandler;
|
||||||
onToggleAxis?: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GraphLegendListItem: React.FunctionComponent<GraphLegendItemProps> = ({
|
export const LegendTableItem: React.FunctionComponent<Props> = ({
|
||||||
item,
|
item,
|
||||||
onSeriesColorChange,
|
onSeriesColorChange,
|
||||||
onToggleAxis,
|
|
||||||
onLabelClick,
|
onLabelClick,
|
||||||
|
className,
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useContext(ThemeContext);
|
const styles = useStyles(getStyles);
|
||||||
const styles = getStyles(theme);
|
|
||||||
return (
|
return (
|
||||||
<>
|
<tr className={cx(styles.row, className)}>
|
||||||
<LegendSeriesIcon
|
<td>
|
||||||
|
<span className={styles.itemWrapper}>
|
||||||
|
<VizLegendSeriesIcon
|
||||||
disabled={!onSeriesColorChange}
|
disabled={!onSeriesColorChange}
|
||||||
color={item.color}
|
color={item.color}
|
||||||
onColorChange={color => {
|
onColorChange={color => {
|
||||||
@ -35,79 +35,6 @@ export const GraphLegendListItem: React.FunctionComponent<GraphLegendItemProps>
|
|||||||
onSeriesColorChange(item.label, color);
|
onSeriesColorChange(item.label, color);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onToggleAxis={onToggleAxis}
|
|
||||||
yAxis={item.yAxis}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
onClick={event => {
|
|
||||||
if (onLabelClick) {
|
|
||||||
onLabelClick(item, event);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className={cx(styles.label, item.disabled && styles.labelDisabled)}
|
|
||||||
>
|
|
||||||
{item.label}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{item.displayValues && <LegendStatsList stats={item.displayValues} />}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
|
||||||
return {
|
|
||||||
row: css`
|
|
||||||
label: LegendRow;
|
|
||||||
font-size: ${theme.typography.size.sm};
|
|
||||||
td {
|
|
||||||
padding: ${theme.spacing.xxs} ${theme.spacing.sm};
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
label: css`
|
|
||||||
label: LegendLabel;
|
|
||||||
cursor: pointer;
|
|
||||||
white-space: nowrap;
|
|
||||||
`,
|
|
||||||
labelDisabled: css`
|
|
||||||
label: LegendLabelDisabled;
|
|
||||||
color: ${theme.colors.linkDisabled};
|
|
||||||
`,
|
|
||||||
itemWrapper: css`
|
|
||||||
display: flex;
|
|
||||||
white-space: nowrap;
|
|
||||||
`,
|
|
||||||
value: css`
|
|
||||||
text-align: right;
|
|
||||||
`,
|
|
||||||
yAxisLabel: css`
|
|
||||||
color: ${theme.palette.gray2};
|
|
||||||
`,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export const GraphLegendTableRow: React.FunctionComponent<GraphLegendItemProps> = ({
|
|
||||||
item,
|
|
||||||
onSeriesColorChange,
|
|
||||||
onToggleAxis,
|
|
||||||
onLabelClick,
|
|
||||||
className,
|
|
||||||
}) => {
|
|
||||||
const theme = useContext(ThemeContext);
|
|
||||||
const styles = getStyles(theme);
|
|
||||||
return (
|
|
||||||
<tr className={cx(styles.row, className)}>
|
|
||||||
<td>
|
|
||||||
<span className={styles.itemWrapper}>
|
|
||||||
<LegendSeriesIcon
|
|
||||||
disabled={!!onSeriesColorChange}
|
|
||||||
color={item.color}
|
|
||||||
onColorChange={color => {
|
|
||||||
if (onSeriesColorChange) {
|
|
||||||
onSeriesColorChange(item.label, color);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onToggleAxis={onToggleAxis}
|
|
||||||
yAxis={item.yAxis}
|
yAxis={item.yAxis}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
@ -133,3 +60,45 @@ export const GraphLegendTableRow: React.FunctionComponent<GraphLegendItemProps>
|
|||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
LegendTableItem.displayName = 'LegendTableItem';
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme) => {
|
||||||
|
const rowHoverBg = styleMixins.hoverColor(theme.colors.bg1, theme);
|
||||||
|
|
||||||
|
return {
|
||||||
|
row: css`
|
||||||
|
label: LegendRow;
|
||||||
|
font-size: ${theme.typography.size.sm};
|
||||||
|
border-bottom: 1px solid ${theme.colors.border1};
|
||||||
|
td {
|
||||||
|
padding: ${theme.spacing.xxs} ${theme.spacing.sm};
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: ${rowHoverBg};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
label: css`
|
||||||
|
label: LegendLabel;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
`,
|
||||||
|
labelDisabled: css`
|
||||||
|
label: LegendLabelDisabled;
|
||||||
|
color: ${theme.colors.linkDisabled};
|
||||||
|
`,
|
||||||
|
itemWrapper: css`
|
||||||
|
display: flex;
|
||||||
|
white-space: nowrap;
|
||||||
|
align-items: center;
|
||||||
|
`,
|
||||||
|
value: css`
|
||||||
|
text-align: right;
|
||||||
|
`,
|
||||||
|
yAxisLabel: css`
|
||||||
|
color: ${theme.palette.gray2};
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
};
|
45
packages/grafana-ui/src/components/VizLegend/types.ts
Normal file
45
packages/grafana-ui/src/components/VizLegend/types.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { DataFrameFieldIndex, DisplayValue } from '@grafana/data';
|
||||||
|
|
||||||
|
export interface VizLegendBaseProps {
|
||||||
|
placement: LegendPlacement;
|
||||||
|
className?: string;
|
||||||
|
items: VizLegendItem[];
|
||||||
|
itemRenderer?: (item: VizLegendItem, index: number) => JSX.Element;
|
||||||
|
onSeriesColorChange?: SeriesColorChangeHandler;
|
||||||
|
onLabelClick?: (item: VizLegendItem, event: React.MouseEvent<HTMLElement>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VizLegendTableProps extends VizLegendBaseProps {
|
||||||
|
sortBy?: string;
|
||||||
|
sortDesc?: boolean;
|
||||||
|
onToggleSort?: (sortBy: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LegendProps extends VizLegendBaseProps, VizLegendTableProps {
|
||||||
|
displayMode: LegendDisplayMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VizLegendItem {
|
||||||
|
label: string;
|
||||||
|
color: string;
|
||||||
|
yAxis: number;
|
||||||
|
disabled?: boolean;
|
||||||
|
displayValues?: DisplayValue[];
|
||||||
|
fieldIndex?: DataFrameFieldIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum LegendDisplayMode {
|
||||||
|
List = 'list',
|
||||||
|
Table = 'table',
|
||||||
|
Hidden = 'hidden',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LegendPlacement = 'bottom' | 'right';
|
||||||
|
|
||||||
|
export interface VizLegendOptions {
|
||||||
|
displayMode: LegendDisplayMode;
|
||||||
|
placement: LegendPlacement;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SeriesOptionChangeHandler<TOption> = (label: string, option: TOption) => void;
|
||||||
|
export type SeriesColorChangeHandler = SeriesOptionChangeHandler<string>;
|
@ -69,7 +69,6 @@ export {
|
|||||||
|
|
||||||
export { Gauge } from './Gauge/Gauge';
|
export { Gauge } from './Gauge/Gauge';
|
||||||
export { Graph } from './Graph/Graph';
|
export { Graph } from './Graph/Graph';
|
||||||
export { GraphLegend } from './Graph/GraphLegend';
|
|
||||||
export { GraphWithLegend } from './Graph/GraphWithLegend';
|
export { GraphWithLegend } from './Graph/GraphWithLegend';
|
||||||
export { GraphContextMenu, GraphContextMenuHeader } from './Graph/GraphContextMenu';
|
export { GraphContextMenu, GraphContextMenuHeader } from './Graph/GraphContextMenu';
|
||||||
export { BarGauge, BarGaugeDisplayMode } from './BarGauge/BarGauge';
|
export { BarGauge, BarGaugeDisplayMode } from './BarGauge/BarGauge';
|
||||||
@ -77,17 +76,8 @@ export { GraphTooltipOptions } from './Graph/GraphTooltip/types';
|
|||||||
export { VizRepeater, VizRepeaterRenderValueProps } from './VizRepeater/VizRepeater';
|
export { VizRepeater, VizRepeaterRenderValueProps } from './VizRepeater/VizRepeater';
|
||||||
export { graphTimeFormat, graphTickFormatter } from './Graph/utils';
|
export { graphTimeFormat, graphTickFormatter } from './Graph/utils';
|
||||||
export { VizLayout, VizLayoutComponentType, VizLayoutLegendProps, VizLayoutProps } from './VizLayout/VizLayout';
|
export { VizLayout, VizLayoutComponentType, VizLayoutLegendProps, VizLayoutProps } from './VizLayout/VizLayout';
|
||||||
|
export { VizLegendItem, LegendPlacement, LegendDisplayMode, VizLegendOptions } from './VizLegend/types';
|
||||||
export {
|
export { VizLegend } from './VizLegend/VizLegend';
|
||||||
LegendOptions,
|
|
||||||
LegendBasicOptions,
|
|
||||||
LegendRenderOptions,
|
|
||||||
LegendList,
|
|
||||||
LegendTable,
|
|
||||||
LegendItem,
|
|
||||||
LegendPlacement,
|
|
||||||
LegendDisplayMode,
|
|
||||||
} from './Legend/Legend';
|
|
||||||
|
|
||||||
export { Alert, AlertVariant } from './Alert/Alert';
|
export { Alert, AlertVariant } from './Alert/Alert';
|
||||||
export { GraphSeriesToggler, GraphSeriesTogglerAPI } from './Graph/GraphSeriesToggler';
|
export { GraphSeriesToggler, GraphSeriesTogglerAPI } from './Graph/GraphSeriesToggler';
|
||||||
@ -109,7 +99,7 @@ export { WithContextMenu } from './ContextMenu/WithContextMenu';
|
|||||||
export { DataLinksInlineEditor } from './DataLinks/DataLinksInlineEditor/DataLinksInlineEditor';
|
export { DataLinksInlineEditor } from './DataLinks/DataLinksInlineEditor/DataLinksInlineEditor';
|
||||||
export { DataLinkInput } from './DataLinks/DataLinkInput';
|
export { DataLinkInput } from './DataLinks/DataLinkInput';
|
||||||
export { DataLinksContextMenu } from './DataLinks/DataLinksContextMenu';
|
export { DataLinksContextMenu } from './DataLinks/DataLinksContextMenu';
|
||||||
export { SeriesIcon } from './Legend/SeriesIcon';
|
export { SeriesIcon } from './VizLegend/SeriesIcon';
|
||||||
export { InfoBox } from './InfoBox/InfoBox';
|
export { InfoBox } from './InfoBox/InfoBox';
|
||||||
export { FeatureInfoBox } from './InfoBox/FeatureInfoBox';
|
export { FeatureInfoBox } from './InfoBox/FeatureInfoBox';
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { LegendOptions, GraphTooltipOptions, LegendDisplayMode } from '@grafana/ui';
|
import { GraphTooltipOptions, LegendDisplayMode, LegendPlacement } from '@grafana/ui';
|
||||||
import { YAxis } from '@grafana/data';
|
import { YAxis } from '@grafana/data';
|
||||||
|
|
||||||
export interface SeriesOptions {
|
export interface SeriesOptions {
|
||||||
@ -14,7 +14,10 @@ export interface GraphOptions {
|
|||||||
|
|
||||||
export interface Options {
|
export interface Options {
|
||||||
graph: GraphOptions;
|
graph: GraphOptions;
|
||||||
legend: LegendOptions & GraphLegendEditorLegendOptions;
|
legend: {
|
||||||
|
displayMode: LegendDisplayMode;
|
||||||
|
placement: LegendPlacement;
|
||||||
|
};
|
||||||
series: {
|
series: {
|
||||||
[alias: string]: SeriesOptions;
|
[alias: string]: SeriesOptions;
|
||||||
};
|
};
|
||||||
@ -35,7 +38,9 @@ export const defaults: Options = {
|
|||||||
tooltipOptions: { mode: 'single' },
|
tooltipOptions: { mode: 'single' },
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface GraphLegendEditorLegendOptions extends LegendOptions {
|
export interface GraphLegendEditorLegendOptions {
|
||||||
|
displayMode: LegendDisplayMode;
|
||||||
|
placement: LegendPlacement;
|
||||||
stats?: string[];
|
stats?: string[];
|
||||||
decimals?: number;
|
decimals?: number;
|
||||||
sortBy?: string;
|
sortBy?: string;
|
||||||
|
@ -197,7 +197,7 @@ class LegendSeriesIcon extends PureComponent<LegendSeriesIconProps, LegendSeries
|
|||||||
enableNamedColors
|
enableNamedColors
|
||||||
>
|
>
|
||||||
{({ ref, showColorPicker, hideColorPicker }) => (
|
{({ ref, showColorPicker, hideColorPicker }) => (
|
||||||
<span ref={ref} onClick={showColorPicker} onMouseLeave={hideColorPicker} className="graph-legend-icon">
|
<span ref={ref} onClick={showColorPicker} onMouseLeave={hideColorPicker}>
|
||||||
<SeriesIcon color={this.props.color} />
|
<SeriesIcon color={this.props.color} />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { LegendOptions, GraphTooltipOptions } from '@grafana/ui';
|
import { VizLegendOptions, GraphTooltipOptions } from '@grafana/ui';
|
||||||
|
|
||||||
export interface GraphOptions {
|
export interface GraphOptions {
|
||||||
// Redraw as time passes
|
// Redraw as time passes
|
||||||
@ -7,13 +7,6 @@ export interface GraphOptions {
|
|||||||
|
|
||||||
export interface Options {
|
export interface Options {
|
||||||
graph: GraphOptions;
|
graph: GraphOptions;
|
||||||
legend: LegendOptions;
|
legend: VizLegendOptions;
|
||||||
tooltipOptions: GraphTooltipOptions;
|
tooltipOptions: GraphTooltipOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GraphLegendEditorLegendOptions extends LegendOptions {
|
|
||||||
stats?: string[];
|
|
||||||
decimals?: number;
|
|
||||||
sortBy?: string;
|
|
||||||
sortDesc?: boolean;
|
|
||||||
}
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { LegendOptions, GraphTooltipOptions } from '@grafana/ui';
|
import { VizLegendOptions, GraphTooltipOptions } from '@grafana/ui';
|
||||||
|
|
||||||
export interface XYDimensionConfig {
|
export interface XYDimensionConfig {
|
||||||
frame: number;
|
frame: number;
|
||||||
@ -9,6 +9,6 @@ export interface XYDimensionConfig {
|
|||||||
|
|
||||||
export interface Options {
|
export interface Options {
|
||||||
dims: XYDimensionConfig;
|
dims: XYDimensionConfig;
|
||||||
legend: LegendOptions;
|
legend: VizLegendOptions;
|
||||||
tooltipOptions: GraphTooltipOptions;
|
tooltipOptions: GraphTooltipOptions;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user