From 2e7ccf0e42795bec33460b9e1759ada4b96fe02e Mon Sep 17 00:00:00 2001 From: Andrej Ocenas Date: Tue, 18 May 2021 16:30:27 +0200 Subject: [PATCH] NodeGraph: Show gradient fields in legend (#34078) * Add gradient fields to legend * Fix test * Remove unnecessary mapping * Add tests --- .../src/utils/namedColorsPalette.ts | 2 +- .../components/VizLegend/SeriesIcon.test.tsx | 20 +++++++++ .../src/components/VizLegend/SeriesIcon.tsx | 44 ++++++++++++++----- .../VizLegend/VizLegendListItem.tsx | 2 +- .../VizLegend/VizLegendSeriesIcon.tsx | 9 ++-- .../src/components/VizLegend/types.ts | 3 +- .../datasource/jaeger/graphTransform.ts | 6 ++- .../datasource/tempo/graphTransform.ts | 6 ++- .../plugins/panel/nodeGraph/Legend.test.tsx | 30 +++++++++++++ public/app/plugins/panel/nodeGraph/Legend.tsx | 25 ++++++++--- public/app/plugins/panel/nodeGraph/Node.tsx | 29 +++++++++--- public/app/plugins/panel/nodeGraph/types.ts | 2 +- .../app/plugins/panel/nodeGraph/utils.test.ts | 18 ++++++-- public/app/plugins/panel/nodeGraph/utils.ts | 11 +---- 14 files changed, 161 insertions(+), 46 deletions(-) create mode 100644 packages/grafana-ui/src/components/VizLegend/SeriesIcon.test.tsx create mode 100644 public/app/plugins/panel/nodeGraph/Legend.test.tsx diff --git a/packages/grafana-data/src/utils/namedColorsPalette.ts b/packages/grafana-data/src/utils/namedColorsPalette.ts index f3b51e8c9ec..4d2424c1721 100644 --- a/packages/grafana-data/src/utils/namedColorsPalette.ts +++ b/packages/grafana-data/src/utils/namedColorsPalette.ts @@ -1,7 +1,7 @@ import { GrafanaTheme, GrafanaThemeType } from '../types/theme'; /** - * @deprecated use theme.vizColors.getByName + * @deprecated use theme.visualization.getColorByName */ export function getColorForTheme(color: string, theme: GrafanaTheme): string { return theme.visualization.getColorByName(color); diff --git a/packages/grafana-ui/src/components/VizLegend/SeriesIcon.test.tsx b/packages/grafana-ui/src/components/VizLegend/SeriesIcon.test.tsx new file mode 100644 index 00000000000..037eb0472a8 --- /dev/null +++ b/packages/grafana-ui/src/components/VizLegend/SeriesIcon.test.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { SeriesIcon } from './SeriesIcon'; + +describe('SeriesIcon', () => { + it('renders gradient correctly', () => { + const { container } = render(); + const div = container.firstChild! as HTMLDivElement; + // There is issue in JSDOM which means we cannot actually get the gradient value. I guess if it's empty at least + // we know it is setting some gradient instead of a single color. + // https://github.com/jsdom/jsdom/issues/2166 + expect(div.style.getPropertyValue('background')).toBe(''); + }); + + it('renders color correctly', () => { + const { container } = render(); + const div = container.firstChild! as HTMLDivElement; + expect(div.style.getPropertyValue('background')).toBe('red'); + }); +}); diff --git a/packages/grafana-ui/src/components/VizLegend/SeriesIcon.tsx b/packages/grafana-ui/src/components/VizLegend/SeriesIcon.tsx index 950dec6150a..89711d53307 100644 --- a/packages/grafana-ui/src/components/VizLegend/SeriesIcon.tsx +++ b/packages/grafana-ui/src/components/VizLegend/SeriesIcon.tsx @@ -1,20 +1,40 @@ import React, { CSSProperties } from 'react'; +import { useTheme2 } from '../../themes'; +import { fieldColorModeRegistry } from '@grafana/data'; export interface Props extends React.HTMLAttributes { - color: string; + color?: string; + gradient?: string; } -export const SeriesIcon = React.forwardRef(({ color, className, ...restProps }, ref) => { - const styles: CSSProperties = { - backgroundColor: color, - width: '14px', - height: '4px', - borderRadius: '1px', - display: 'inline-block', - marginRight: '8px', - }; +export const SeriesIcon = React.forwardRef( + ({ color, className, gradient, ...restProps }, ref) => { + const theme = useTheme2(); + let cssColor: string; - return
; -}); + if (gradient) { + const colors = fieldColorModeRegistry.get(gradient).getColors?.(theme); + if (colors?.length) { + cssColor = `linear-gradient(90deg, ${colors.join(', ')})`; + } else { + // Not sure what to default to, this will return gray, this should not happen though. + cssColor = theme.visualization.getColorByName(''); + } + } else { + cssColor = color!; + } + + const styles: CSSProperties = { + background: cssColor, + width: '14px', + height: '4px', + borderRadius: '1px', + display: 'inline-block', + marginRight: '8px', + }; + + return
; + } +); SeriesIcon.displayName = 'SeriesIcon'; diff --git a/packages/grafana-ui/src/components/VizLegend/VizLegendListItem.tsx b/packages/grafana-ui/src/components/VizLegend/VizLegendListItem.tsx index bef07f10cc6..d30f944fca5 100644 --- a/packages/grafana-ui/src/components/VizLegend/VizLegendListItem.tsx +++ b/packages/grafana-ui/src/components/VizLegend/VizLegendListItem.tsx @@ -59,7 +59,7 @@ export const VizLegendListItem = ({ className={cx(styles.itemWrapper, className)} aria-label={selectors.components.VizLegend.seriesName(item.label)} > - +
= ({ seriesName, color }) => { +export const VizLegendSeriesIcon: React.FunctionComponent = ({ seriesName, color, gradient }) => { const { onSeriesColorChange } = usePanelContext(); const onChange = useCallback( (color: string) => { @@ -20,7 +21,7 @@ export const VizLegendSeriesIcon: React.FunctionComponent = ({ seriesName [seriesName, onSeriesColorChange] ); - if (seriesName && onSeriesColorChange) { + if (seriesName && onSeriesColorChange && color) { return ( {({ ref, showColorPicker, hideColorPicker }) => ( @@ -35,7 +36,7 @@ export const VizLegendSeriesIcon: React.FunctionComponent = ({ seriesName ); } - return ; + return ; }; VizLegendSeriesIcon.displayName = 'VizLegendSeriesIcon'; diff --git a/packages/grafana-ui/src/components/VizLegend/types.ts b/packages/grafana-ui/src/components/VizLegend/types.ts index 55e03b6d97d..961615ae82e 100644 --- a/packages/grafana-ui/src/components/VizLegend/types.ts +++ b/packages/grafana-ui/src/components/VizLegend/types.ts @@ -31,7 +31,8 @@ export interface LegendProps extends VizLegendBaseProps, VizLegendTa export interface VizLegendItem { getItemKey?: () => string; label: string; - color: string; + color?: string; + gradient?: string; yAxis: number; disabled?: boolean; // displayValues?: DisplayValue[]; diff --git a/public/app/plugins/datasource/jaeger/graphTransform.ts b/public/app/plugins/datasource/jaeger/graphTransform.ts index 740492f42a2..e7a70992333 100644 --- a/public/app/plugins/datasource/jaeger/graphTransform.ts +++ b/public/app/plugins/datasource/jaeger/graphTransform.ts @@ -26,7 +26,11 @@ export function createGraphFrames(data: TraceResponse): DataFrame[] { { name: Fields.subTitle, type: FieldType.string }, { name: Fields.mainStat, type: FieldType.string, config: { displayName: 'Total time (% of trace)' } }, { name: Fields.secondaryStat, type: FieldType.string, config: { displayName: 'Self time (% of total)' } }, - { name: Fields.color, type: FieldType.number, config: { color: { mode: 'continuous-GrYlRd' } } }, + { + name: Fields.color, + type: FieldType.number, + config: { color: { mode: 'continuous-GrYlRd' }, displayName: 'Self time / Trace duration' }, + }, ], meta: { preferredVisualisationType: 'nodeGraph', diff --git a/public/app/plugins/datasource/tempo/graphTransform.ts b/public/app/plugins/datasource/tempo/graphTransform.ts index a67ba295afe..20eacecc271 100644 --- a/public/app/plugins/datasource/tempo/graphTransform.ts +++ b/public/app/plugins/datasource/tempo/graphTransform.ts @@ -44,7 +44,11 @@ export function createGraphFrames(data: DataFrame): DataFrame[] { { name: Fields.subTitle, type: FieldType.string }, { name: Fields.mainStat, type: FieldType.string, config: { displayName: 'Total time (% of trace)' } }, { name: Fields.secondaryStat, type: FieldType.string, config: { displayName: 'Self time (% of total)' } }, - { name: Fields.color, type: FieldType.number, config: { color: { mode: 'continuous-GrYlRd' } } }, + { + name: Fields.color, + type: FieldType.number, + config: { color: { mode: 'continuous-GrYlRd' }, displayName: 'Self time / Trace duration' }, + }, ], meta: { preferredVisualisationType: 'nodeGraph', diff --git a/public/app/plugins/panel/nodeGraph/Legend.test.tsx b/public/app/plugins/panel/nodeGraph/Legend.test.tsx new file mode 100644 index 00000000000..2fe9fddeb94 --- /dev/null +++ b/public/app/plugins/panel/nodeGraph/Legend.test.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { FieldColorModeId } from '@grafana/data'; +import { Legend } from './Legend'; +import { NodeDatum } from './types'; + +describe('Legend', () => { + it('renders ok without nodes', () => { + render( {}} sortable={false} />); + }); + + it('renders ok with color fields', () => { + const nodes: NodeDatum[] = [ + { + id: 'nodeId', + mainStat: { config: { displayName: 'stat1' } } as any, + secondaryStat: { config: { displayName: 'stat2' } } as any, + arcSections: [ + { config: { displayName: 'error', color: { mode: FieldColorModeId.Fixed, fixedColor: 'red' } } } as any, + ], + } as any, + ]; + render( {}} sortable={false} />); + const items = screen.getAllByLabelText(/VizLegend series/); + expect(items.length).toBe(3); + + const item = screen.getByLabelText(/VizLegend series error/); + expect((item.firstChild as HTMLDivElement).style.getPropertyValue('background')).toBe('rgb(242, 73, 92)'); + }); +}); diff --git a/public/app/plugins/panel/nodeGraph/Legend.tsx b/public/app/plugins/panel/nodeGraph/Legend.tsx index 968d7ae7145..37f435a8d17 100644 --- a/public/app/plugins/panel/nodeGraph/Legend.tsx +++ b/public/app/plugins/panel/nodeGraph/Legend.tsx @@ -62,6 +62,9 @@ interface ItemData { } function getColorLegendItems(nodes: NodeDatum[], theme: GrafanaTheme): Array> { + if (!nodes.length) { + return []; + } const fields = [nodes[0].mainStat, nodes[0].secondaryStat].filter(identity) as Field[]; const node = nodes.find((n) => n.arcSections.length > 0); @@ -71,18 +74,30 @@ function getColorLegendItems(nodes: NodeDatum[], theme: GrafanaTheme): Array n.arcSections).flat())); - } else { - // TODO: probably some sort of gradient which we will have to deal with later - return []; } } + if (nodes[0].color) { + fields.push(nodes[0].color); + } + return fields.map((f) => { - return { + const item: VizLegendItem = { label: f.config.displayName || f.name, - color: getColorForTheme(f.config.color?.fixedColor || '', theme), yAxis: 0, data: { field: f }, }; + if (f.config.color?.mode === FieldColorModeId.Fixed && f.config.color?.fixedColor) { + item.color = getColorForTheme(f.config.color?.fixedColor || '', theme); + } else if (f.config.color?.mode) { + item.gradient = f.config.color?.mode; + } + + if (!(item.color || item.gradient)) { + // Defaults to gray color + item.color = getColorForTheme('', theme); + } + + return item; }); } diff --git a/public/app/plugins/panel/nodeGraph/Node.tsx b/public/app/plugins/panel/nodeGraph/Node.tsx index e7978801cc8..d0c712f65ee 100644 --- a/public/app/plugins/panel/nodeGraph/Node.tsx +++ b/public/app/plugins/panel/nodeGraph/Node.tsx @@ -1,7 +1,7 @@ import React, { MouseEvent, memo } from 'react'; import cx from 'classnames'; -import { getColorForTheme, GrafanaTheme2 } from '@grafana/data'; -import { useStyles2, useTheme } from '@grafana/ui'; +import { Field, getFieldColorModeForField, GrafanaTheme2 } from '@grafana/data'; +import { useStyles2, useTheme2 } from '@grafana/ui'; import { NodeDatum } from './types'; import { css } from 'emotion'; import tinycolor from 'tinycolor2'; @@ -117,14 +117,14 @@ export const Node = memo(function Node(props: { function ColorCircle(props: { node: NodeDatum }) { const { node } = props; const fullStat = node.arcSections.find((s) => s.values.get(node.dataFrameRowIndex) === 1); - const theme = useTheme(); + const theme = useTheme2(); if (fullStat) { // Doing arc with path does not work well so it's better to just do a circle in that case return ( s.values.get(node.dataFrameRowIndex) !== 0); if (nonZero.length === 0) { // Fallback if no arc is defined - return ; + return ( + + ); } const { elements } = nonZero.reduce( @@ -151,7 +160,7 @@ function ColorCircle(props: { node: NodeDatum }) { y={node.y!} startPercent={acc.percent} percent={value} - color={getColorForTheme(color, theme)} + color={theme.visualization.getColorByName(color)} strokeWidth={2} /> ); @@ -197,3 +206,11 @@ function ArcSection({ /> ); } + +function getColor(field: Field, index: number, theme: GrafanaTheme2): string { + if (!field.config.color) { + return field.values.get(index); + } + + return getFieldColorModeForField(field).getCalculator(field, theme)(0, field.values.get(index)); +} diff --git a/public/app/plugins/panel/nodeGraph/types.ts b/public/app/plugins/panel/nodeGraph/types.ts index 03bce5f7df2..91b4103afee 100644 --- a/public/app/plugins/panel/nodeGraph/types.ts +++ b/public/app/plugins/panel/nodeGraph/types.ts @@ -12,7 +12,7 @@ export type NodeDatum = SimulationNodeDatum & { mainStat?: Field; secondaryStat?: Field; arcSections: Field[]; - color: string; + color?: Field; }; // This is the data we have before the graph is laid out with source and target being string IDs. diff --git a/public/app/plugins/panel/nodeGraph/utils.test.ts b/public/app/plugins/panel/nodeGraph/utils.test.ts index e214cad28d6..b2e25b0b98e 100644 --- a/public/app/plugins/panel/nodeGraph/utils.test.ts +++ b/public/app/plugins/panel/nodeGraph/utils.test.ts @@ -19,6 +19,18 @@ describe('processNodes', () => { theme ); + const colorField = { + config: { + color: { + mode: 'continuous-GrYlRd', + }, + }, + index: 7, + name: 'color', + type: 'number', + values: new ArrayVector([0.5, 0.5, 0.5]), + }; + expect(nodes).toEqual([ { arcSections: [ @@ -43,7 +55,7 @@ describe('processNodes', () => { values: new ArrayVector([0.5, 0.5, 0.5]), }, ], - color: 'rgb(226, 192, 61)', + color: colorField, dataFrameRowIndex: 0, id: '0', incoming: 0, @@ -87,7 +99,7 @@ describe('processNodes', () => { values: new ArrayVector([0.5, 0.5, 0.5]), }, ], - color: 'rgb(226, 192, 61)', + color: colorField, dataFrameRowIndex: 1, id: '1', incoming: 1, @@ -131,7 +143,7 @@ describe('processNodes', () => { values: new ArrayVector([0.5, 0.5, 0.5]), }, ], - color: 'rgb(226, 192, 61)', + color: colorField, dataFrameRowIndex: 2, id: '2', incoming: 2, diff --git a/public/app/plugins/panel/nodeGraph/utils.ts b/public/app/plugins/panel/nodeGraph/utils.ts index a12d367fbed..beb8e6dd857 100644 --- a/public/app/plugins/panel/nodeGraph/utils.ts +++ b/public/app/plugins/panel/nodeGraph/utils.ts @@ -4,7 +4,6 @@ import { Field, FieldCache, FieldType, - getFieldColorModeForField, GrafanaTheme2, MutableDataFrame, NodeGraphDataFrameFieldNames, @@ -100,7 +99,7 @@ export function processNodes( mainStat: nodeFields.mainStat, secondaryStat: nodeFields.secondaryStat, arcSections: nodeFields.arc, - color: nodeFields.color ? getColor(nodeFields.color, index, theme) : '', + color: nodeFields.color, }; return acc; }, {}) || {}; @@ -271,14 +270,6 @@ function edgesFrame() { }); } -function getColor(field: Field, index: number, theme: GrafanaTheme2): string { - if (!field.config.color) { - return field.values.get(index); - } - - return getFieldColorModeForField(field).getCalculator(field, theme)(0, field.values.get(index)); -} - export interface Bounds { top: number; right: number;