diff --git a/public/app/plugins/panel/heatmap-new/ColorScale.tsx b/public/app/plugins/panel/heatmap-new/ColorScale.tsx new file mode 100644 index 00000000000..82064168b44 --- /dev/null +++ b/public/app/plugins/panel/heatmap-new/ColorScale.tsx @@ -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([]); + const [hover, setHover] = useState({ 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) => { + 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 ( +
+
+
+ {display && hover.isShown && ( + + {display(hover.value)} + + )} +
+ {display && ( +
+ {display(min)} + {display(max)} +
+ )} +
+
+ ); +}; + +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(); + + 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; + `, +}); diff --git a/public/app/plugins/panel/heatmap-new/HeatmapPanel.tsx b/public/app/plugins/panel/heatmap-new/HeatmapPanel.tsx index 7835e4d3d77..ea0ce691456 100644 --- a/public/app/plugins/panel/heatmap-new/HeatmapPanel.tsx +++ b/public/app/plugins/panel/heatmap-new/HeatmapPanel.tsx @@ -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 {} @@ -81,17 +90,30 @@ export const HeatmapPanel: React.FC = ({ // 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 ( + + + + ); + }; + if (info.warning || !info.heatmap) { return ; } return ( <> - + {(vizWidth: number, vizHeight: number) => ( - //
-          //   {JSON.stringify(scatterData, null, 2)}
-          // 
{/*children ? children(config, alignedFrame) : null*/} diff --git a/public/app/plugins/panel/heatmap-new/module.tsx b/public/app/plugins/panel/heatmap-new/module.tsx index d9f4d345540..ce31ca3a638 100644 --- a/public/app/plugins/panel/heatmap-new/module.tsx +++ b/public/app/plugins/panel/heatmap-new/module.tsx @@ -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(HeatmapPanel) .useFieldConfig() @@ -39,11 +42,6 @@ export const plugin = new PanelPlugin(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(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 ( +
+ +
+ ); + }, + }); category = ['Display'];