Heatmap: add scale display to legend (#45571)

Co-authored-by: Adela Almasan <adela.almasan@grafana.com>
This commit is contained in:
Ryan McKinley 2022-03-02 09:04:19 -08:00 committed by GitHub
parent 9067715d1d
commit 855979aac5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 171 additions and 23 deletions

View File

@ -0,0 +1,113 @@
import React, { useState, useEffect } from 'react';
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { useTheme2, VizTooltipContainer } from '@grafana/ui';
type Props = {
colorPalette: string[];
min: number;
max: number;
// Show a value as string -- when not defined, the raw values will not be shown
display?: (v: number) => string;
};
type HoverState = {
isShown: boolean;
value: number;
};
export const ColorScale = ({ colorPalette, min, max, display }: Props) => {
const [colors, setColors] = useState<string[]>([]);
const [hover, setHover] = useState<HoverState>({ isShown: false, value: 0 });
const [cursor, setCursor] = useState({ clientX: 0, clientY: 0 });
useEffect(() => {
setColors(getGradientStops({ colorArray: colorPalette }));
}, [colorPalette]);
const theme = useTheme2();
const styles = getStyles(theme, colors);
const onScaleMouseMove = (event: React.MouseEvent<HTMLDivElement>) => {
const divOffset = event.nativeEvent.offsetX;
const offsetWidth = (event.target as any).offsetWidth as number;
const normPercentage = Math.floor((divOffset * 100) / offsetWidth + 1);
const scaleValue = Math.floor(((max - min) * normPercentage) / 100 + min);
setHover({ isShown: true, value: scaleValue });
setCursor({ clientX: event.clientX, clientY: event.clientY });
};
const onScaleMouseLeave = () => {
setHover({ isShown: false, value: 0 });
};
return (
<div className={styles.scaleWrapper}>
<div>
<div className={styles.scaleGradient} onMouseMove={onScaleMouseMove} onMouseLeave={onScaleMouseLeave}>
{display && hover.isShown && (
<VizTooltipContainer position={{ x: cursor.clientX, y: cursor.clientY }} offset={{ x: 10, y: 10 }}>
{display(hover.value)}
</VizTooltipContainer>
)}
</div>
{display && (
<div>
<span>{display(min)}</span>
<span className={styles.maxDisplay}>{display(max)}</span>
</div>
)}
</div>
</div>
);
};
const getGradientStops = ({ colorArray, stops = 10 }: { colorArray: string[]; stops?: number }): string[] => {
const colorCount = colorArray.length;
if (colorCount <= 20) {
const incr = (1 / colorCount) * 100;
let per = 0;
const stops: string[] = [];
for (const color of colorArray) {
if (per > 0) {
stops.push(`${color} ${per}%`);
} else {
stops.push(color);
}
per += incr;
stops.push(`${color} ${per}%`);
}
return stops;
}
const gradientEnd = colorArray[colorCount - 1];
const skip = Math.ceil(colorCount / stops);
const gradientStops = new Set<string>();
for (let i = 0; i < colorCount; i += skip) {
gradientStops.add(colorArray[i]);
}
gradientStops.add(gradientEnd);
return [...gradientStops];
};
const getStyles = (theme: GrafanaTheme2, colors: string[]) => ({
scaleWrapper: css`
margin: 0 16px;
padding-top: 4px;
width: 100%;
max-width: 300px;
color: #ccccdc;
font-size: 11px;
`,
scaleGradient: css`
background: linear-gradient(90deg, ${colors.join()});
height: 6px;
`,
maxDisplay: css`
float: right;
`,
});

View File

@ -1,7 +1,15 @@
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { css } from '@emotion/css';
import { GrafanaTheme2, PanelProps } from '@grafana/data';
import { Portal, UPlotChart, useStyles2, useTheme2, VizLayout, VizTooltipContainer } from '@grafana/ui';
import { formattedValueToString, GrafanaTheme2, PanelProps, reduceField, ReducerID } from '@grafana/data';
import {
Portal,
UPlotChart,
useStyles2,
useTheme2,
VizLayout,
VizTooltipContainer,
LegendDisplayMode,
} from '@grafana/ui';
import { PanelDataErrorView } from '@grafana/runtime';
import { HeatmapData, prepareHeatmapData } from './fields';
@ -10,6 +18,7 @@ import { quantizeScheme } from './palettes';
import { HeatmapHoverEvent, prepConfig } from './utils';
import { HeatmapHoverView } from './HeatmapHoverView';
import { CloseButton } from 'app/core/components/CloseButton/CloseButton';
import { ColorScale } from './ColorScale';
interface HeatmapPanelProps extends PanelProps<PanelOptions> {}
@ -81,17 +90,30 @@ export const HeatmapPanel: React.FC<HeatmapPanelProps> = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [options, data.structureRev]);
const renderLegend = () => {
if (options.legend.displayMode === LegendDisplayMode.Hidden || !info.heatmap) {
return null;
}
const field = info.heatmap.fields[2];
const { min, max } = reduceField({ field, reducers: [ReducerID.min, ReducerID.max] });
const display = field.display ? (v: number) => formattedValueToString(field.display!(v)) : (v: number) => `${v}`;
return (
<VizLayout.Legend placement="bottom" maxHeight="20%">
<ColorScale colorPalette={palette} min={min} max={max} display={display} />
</VizLayout.Legend>
);
};
if (info.warning || !info.heatmap) {
return <PanelDataErrorView panelId={id} data={data} needsNumberField={true} message={info.warning} />;
}
return (
<>
<VizLayout width={width} height={height}>
<VizLayout width={width} height={height} legend={renderLegend()}>
{(vizWidth: number, vizHeight: number) => (
// <pre style={{ width: vizWidth, height: vizHeight, border: '1px solid green', margin: '0px' }}>
// {JSON.stringify(scatterData, null, 2)}
// </pre>
<UPlotChart config={builder} data={facets as any} width={vizWidth} height={vizHeight} timeRange={timeRange}>
{/*children ? children(config, alignedFrame) : null*/}
</UPlotChart>

View File

@ -1,3 +1,4 @@
import React from 'react';
import { GraphFieldConfig, VisibilityMode } from '@grafana/schema';
import { Field, FieldType, PanelPlugin } from '@grafana/data';
import { commonOptionsBuilder } from '@grafana/ui';
@ -12,7 +13,9 @@ import {
import { HeatmapSuggestionsSupplier } from './suggestions';
import { heatmapChangedHandler } from './migrations';
import { addHeatmapCalculationOptions } from 'app/features/transformers/calculateHeatmap/editor/helper';
import { colorSchemes } from './palettes';
import { colorSchemes, quantizeScheme } from './palettes';
import { config } from '@grafana/runtime';
import { ColorScale } from './ColorScale';
export const plugin = new PanelPlugin<PanelOptions, GraphFieldConfig>(HeatmapPanel)
.useFieldConfig()
@ -39,11 +42,6 @@ export const plugin = new PanelPlugin<PanelOptions, GraphFieldConfig>(HeatmapPan
if (opts.source === HeatmapSourceMode.Calculate) {
addHeatmapCalculationOptions('heatmap.', builder, opts.heatmap, category);
} else if (opts.source === HeatmapSourceMode.Data) {
// builder.addSliderInput({
// name: 'heatmap from the data...',
// path: 'xxx',
// });
}
category = ['Colors'];
@ -125,17 +123,32 @@ export const plugin = new PanelPlugin<PanelOptions, GraphFieldConfig>(HeatmapPan
showIf: (opts) => opts.color.mode !== HeatmapColorMode.Opacity,
});
builder.addSliderInput({
path: 'color.steps',
name: 'Max steps',
defaultValue: defaultPanelOptions.color.steps,
category,
settings: {
min: 2, // 1 for on/off?
max: 128,
step: 1,
},
});
builder
.addSliderInput({
path: 'color.steps',
name: 'Steps',
defaultValue: defaultPanelOptions.color.steps,
category,
settings: {
min: 2,
max: 128,
step: 1,
},
})
.addCustomEditor({
id: '__scale__',
path: `__scale__`,
name: 'Scale',
category,
editor: () => {
const palette = quantizeScheme(opts.color, config.theme2);
return (
<div>
<ColorScale colorPalette={palette} min={1} max={100} />
</div>
);
},
});
category = ['Display'];