mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Heatmap: add scale display to legend (#45571)
Co-authored-by: Adela Almasan <adela.almasan@grafana.com>
This commit is contained in:
parent
9067715d1d
commit
855979aac5
113
public/app/plugins/panel/heatmap-new/ColorScale.tsx
Normal file
113
public/app/plugins/panel/heatmap-new/ColorScale.tsx
Normal 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;
|
||||
`,
|
||||
});
|
@ -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>
|
||||
|
@ -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'];
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user