diff --git a/devenv/dev-dashboards/panel-heatmap/heatmap-calculate-log.json b/devenv/dev-dashboards/panel-heatmap/heatmap-calculate-log.json index b330ce03596..5481b136d9d 100644 --- a/devenv/dev-dashboards/panel-heatmap/heatmap-calculate-log.json +++ b/devenv/dev-dashboards/panel-heatmap/heatmap-calculate-log.json @@ -23,7 +23,7 @@ }, "editable": true, "fiscalYearStartMonth": 0, - "graphTooltip": 0, + "graphTooltip": 1, "links": [], "liveNow": false, "panels": [ @@ -172,7 +172,7 @@ "color": "rgba(255,0,255,0.7)" }, "filterValues": { - "min": 1e-9 + "le": 1e-9 }, "legend": { "show": true @@ -257,7 +257,7 @@ "color": "rgba(255,0,255,0.7)" }, "filterValues": { - "min": 1e-9 + "le": 1e-9 }, "legend": { "show": true @@ -333,7 +333,7 @@ "color": "rgba(255,0,255,0.7)" }, "filterValues": { - "min": 1e-9 + "le": 1e-9 }, "legend": { "show": true @@ -417,7 +417,7 @@ "color": "rgba(255,0,255,0.7)" }, "filterValues": { - "min": 1e-9 + "le": 1e-9 }, "legend": { "show": true @@ -502,7 +502,7 @@ "color": "rgba(255,0,255,0.7)" }, "filterValues": { - "min": 1e-9 + "le": 1e-9 }, "legend": { "show": true @@ -548,6 +548,6 @@ "timezone": "", "title": "Heatmap calculate (log)", "uid": "ZXYQTA97ZZ", - "version": 4, + "version": 1, "weekStart": "" } diff --git a/public/app/core/components/ColorScale/ColorScale.tsx b/public/app/core/components/ColorScale/ColorScale.tsx index ba9ece2581d..6da245922ac 100644 --- a/public/app/core/components/ColorScale/ColorScale.tsx +++ b/public/app/core/components/ColorScale/ColorScale.tsx @@ -26,7 +26,7 @@ const GRADIENT_STOPS = 10; export const ColorScale = ({ colorPalette, min, max, display, hoverValue, useStopsPercentage }: Props) => { const [colors, setColors] = useState([]); const [scaleHover, setScaleHover] = useState({ isShown: false, value: 0 }); - const [percent, setPercent] = useState(null); + const [percent, setPercent] = useState(null); // 0-100 for CSS percentage const theme = useTheme2(); const styles = getStyles(theme, colors); @@ -50,15 +50,12 @@ export const ColorScale = ({ colorPalette, min, max, display, hoverValue, useSto }; useEffect(() => { - if (hoverValue != null) { - const percent = hoverValue / (max - min); - setPercent(percent * 100); - } + setPercent(hoverValue == null ? null : clampPercent100((hoverValue - min) / (max - min))); }, [hoverValue, min, max]); return ( -
-
+
+
{display && (scaleHover.isShown || hoverValue !== undefined) && (
@@ -121,10 +118,19 @@ const getGradientStops = ({ return [...gradientStops]; }; +function clampPercent100(v: number) { + if (v > 1) { + return 100; + } + if (v < 0) { + return 0; + } + return v * 100; +} + const getStyles = (theme: GrafanaTheme2, colors: string[]) => ({ scaleWrapper: css` width: 100%; - max-width: 300px; font-size: 11px; opacity: 1; `, @@ -138,7 +144,7 @@ const getStyles = (theme: GrafanaTheme2, colors: string[]) => ({ `, hoverValue: css` position: absolute; - padding-top: 5px; + padding-top: 4px; `, followerContainer: css` position: relative; diff --git a/public/app/features/transformers/calculateHeatmap/editor/AxisEditor.tsx b/public/app/features/transformers/calculateHeatmap/editor/AxisEditor.tsx index ca55a149b9e..64b45a5d894 100644 --- a/public/app/features/transformers/calculateHeatmap/editor/AxisEditor.tsx +++ b/public/app/features/transformers/calculateHeatmap/editor/AxisEditor.tsx @@ -24,11 +24,6 @@ const logModeOptions: Array> = [ value: HeatmapCalculationMode.Size, description: 'Split the buckets based on size', }, - { - label: 'Count', - value: HeatmapCalculationMode.Count, - description: 'Split the buckets based on count', - }, ]; export const AxisEditor: React.FC> = ({ diff --git a/public/app/features/transformers/calculateHeatmap/heatmap.test.ts b/public/app/features/transformers/calculateHeatmap/heatmap.test.ts index 8eebf558ffa..160bc920ed8 100644 --- a/public/app/features/transformers/calculateHeatmap/heatmap.test.ts +++ b/public/app/features/transformers/calculateHeatmap/heatmap.test.ts @@ -58,7 +58,7 @@ describe('Heatmap transformer', () => { ], }); - const heatmap = bucketsToScanlines({ frame, name: 'Speed' }); + const heatmap = bucketsToScanlines({ frame, value: 'Speed' }); expect(heatmap.fields.map((f) => ({ name: f.name, type: f.type, config: f.config }))).toMatchInlineSnapshot(` Array [ Object { diff --git a/public/app/features/transformers/calculateHeatmap/heatmap.ts b/public/app/features/transformers/calculateHeatmap/heatmap.ts index 6ef7275c076..62416f358bb 100644 --- a/public/app/features/transformers/calculateHeatmap/heatmap.ts +++ b/public/app/features/transformers/calculateHeatmap/heatmap.ts @@ -63,7 +63,7 @@ export function readHeatmapScanlinesCustomMeta(frame?: DataFrame): HeatmapScanli export interface BucketsOptions { frame: DataFrame; - name?: string; + value?: string; // the field value name layout?: HeatmapBucketLayout; } @@ -147,7 +147,7 @@ export function bucketsToScanlines(opts: BucketsOptions): DataFrame { }, }, { - name: opts.name?.length ? opts.name : 'Value', + name: opts.value?.length ? opts.value : 'Value', type: FieldType.number, values: new ArrayVector(counts2), config: yFields[0].config, diff --git a/public/app/plugins/panel/heatmap-new/HeatmapPanel.tsx b/public/app/plugins/panel/heatmap-new/HeatmapPanel.tsx index 7516c203662..6d4e3ed1739 100644 --- a/public/app/plugins/panel/heatmap-new/HeatmapPanel.tsx +++ b/public/app/plugins/panel/heatmap-new/HeatmapPanel.tsx @@ -57,7 +57,7 @@ export const HeatmapPanel: React.FC = ({ let exemplarsyFacet: number[] = []; const meta = readHeatmapScanlinesCustomMeta(info.heatmap); - if (info.exemplars && meta.yMatchWithLabel) { + if (info.exemplars?.length && meta.yMatchWithLabel) { exemplarsXFacet = info.exemplars?.fields[0].values.toArray(); // ordinal/labeled heatmap-buckets? @@ -126,7 +126,10 @@ export const HeatmapPanel: React.FC = ({ getTimeRange: () => timeRangeRef.current, palette, cellGap: options.cellGap, - hideThreshold: options.filterValues?.min, // eventually a better range + hideLE: options.filterValues?.le, + hideGE: options.filterValues?.ge, + valueMin: options.color.min, + valueMax: options.color.max, exemplarColor: options.exemplars?.color ?? 'rgba(255,0,255,0.7)', yAxisConfig: options.yAxis, ySizeDivisor: scaleConfig?.type === ScaleDistribution.Log ? +(options.calculation?.yBuckets?.value || 1) : 1, @@ -143,7 +146,17 @@ export const HeatmapPanel: React.FC = ({ let countFieldIdx = heatmapType === DataFrameType.HeatmapScanlines ? 2 : 3; const countField = info.heatmap.fields[countFieldIdx]; - const { min, max } = reduceField({ field: countField, reducers: [ReducerID.min, ReducerID.max] }); + // TODO -- better would be to get the range from the real color scale! + let { min, max } = options.color; + if (min == null || max == null) { + const calc = reduceField({ field: countField, reducers: [ReducerID.min, ReducerID.max] }); + if (min == null) { + min = calc[ReducerID.min]; + } + if (max == null) { + max = calc[ReducerID.max]; + } + } let hoverValue: number | undefined = undefined; // seriesIdx: 1 is heatmap layer; 2 is exemplar layer @@ -154,7 +167,7 @@ export const HeatmapPanel: React.FC = ({ return (
- +
); @@ -209,5 +222,6 @@ const getStyles = (theme: GrafanaTheme2) => ({ colorScaleWrapper: css` margin-left: 25px; padding: 10px 0; + max-width: 300px; `, }); diff --git a/public/app/plugins/panel/heatmap-new/fields.ts b/public/app/plugins/panel/heatmap-new/fields.ts index bbbad5f1567..121df353783 100644 --- a/public/app/plugins/panel/heatmap-new/fields.ts +++ b/public/app/plugins/panel/heatmap-new/fields.ts @@ -75,7 +75,7 @@ export function prepareHeatmapData(data: PanelData, options: PanelOptions, theme } } - return getHeatmapData(bucketsToScanlines({ ...options.bucket, frame: bucketHeatmap }), exemplars, theme); + return getHeatmapData(bucketsToScanlines({ ...options.bucketFrame, frame: bucketHeatmap }), exemplars, theme); } const getSparseHeatmapData = ( @@ -139,7 +139,7 @@ const getHeatmapData = (frame: DataFrame, exemplars: DataFrame | undefined, them const data: HeatmapData = { heatmap: frame, - exemplars, + exemplars: exemplars?.length ? exemplars : undefined, xBucketSize: xBinIncr, yBucketSize: yBinIncr, xBucketCount: xBinQty, diff --git a/public/app/plugins/panel/heatmap-new/img/heatmap.svg b/public/app/plugins/panel/heatmap-new/img/icn-heatmap-panel.svg similarity index 100% rename from public/app/plugins/panel/heatmap-new/img/heatmap.svg rename to public/app/plugins/panel/heatmap-new/img/icn-heatmap-panel.svg diff --git a/public/app/plugins/panel/heatmap-new/migrations.test.ts b/public/app/plugins/panel/heatmap-new/migrations.test.ts index 86ff840d106..9ac89efd9ec 100644 --- a/public/app/plugins/panel/heatmap-new/migrations.test.ts +++ b/public/app/plugins/panel/heatmap-new/migrations.test.ts @@ -21,11 +21,14 @@ describe('Heatmap Migrations', () => { expect(panel).toMatchInlineSnapshot(` Object { "fieldConfig": Object { - "defaults": Object {}, + "defaults": Object { + "decimals": 6, + "unit": "short", + }, "overrides": Array [], }, "options": Object { - "bucket": Object { + "bucketFrame": Object { "layout": "auto", }, "calculate": true, @@ -44,7 +47,7 @@ describe('Heatmap Migrations', () => { }, }, "cellGap": 2, - "cellSize": 10, + "cellRadius": 10, "color": Object { "exponent": 0.5, "fill": "dark-orange", @@ -59,7 +62,7 @@ describe('Heatmap Migrations', () => { "color": "rgba(255,0,255,0.7)", }, "filterValues": Object { - "min": 1e-9, + "le": 1e-9, }, "legend": Object { "show": true, @@ -72,6 +75,8 @@ describe('Heatmap Migrations', () => { "yAxis": Object { "axisPlacement": "left", "axisWidth": 400, + "max": 22, + "min": 7, "reverse": false, }, }, @@ -133,11 +138,11 @@ const oldHeatmap = { yAxis: { show: true, format: 'short', - decimals: null, + decimals: 6, logBase: 2, splitFactor: 3, - min: null, - max: null, + min: 7, + max: 22, width: '400', }, xBucketSize: null, diff --git a/public/app/plugins/panel/heatmap-new/migrations.ts b/public/app/plugins/panel/heatmap-new/migrations.ts index d3d96a7945c..602f09bece7 100644 --- a/public/app/plugins/panel/heatmap-new/migrations.ts +++ b/public/app/plugins/panel/heatmap-new/migrations.ts @@ -60,6 +60,9 @@ export function angularToReactHeatmap(angular: any): { fieldConfig: FieldConfigS }, }; } + + fieldConfig.defaults.unit = oldYAxis.format; + fieldConfig.defaults.decimals = oldYAxis.decimals; } const options: PanelOptions = { @@ -69,14 +72,16 @@ export function angularToReactHeatmap(angular: any): { fieldConfig: FieldConfigS ...defaultPanelOptions.color, steps: 128, // best match with existing colors }, - cellGap: asNumber(angular.cards?.cardPadding), - cellSize: asNumber(angular.cards?.cardRound), + cellGap: asNumber(angular.cards?.cardPadding, 2), + cellRadius: asNumber(angular.cards?.cardRound), // just to keep it yAxis: { axisPlacement: oldYAxis.show === false ? AxisPlacement.Hidden : AxisPlacement.Left, reverse: Boolean(angular.reverseYBuckets), axisWidth: oldYAxis.width ? +oldYAxis.width : undefined, + min: oldYAxis.min, + max: oldYAxis.max, }, - bucket: { + bucketFrame: { layout: getHeatmapBucketLayout(angular.yBucketBound), }, legend: { @@ -134,9 +139,12 @@ function getHeatmapBucketLayout(v?: string): HeatmapBucketLayout { return HeatmapBucketLayout.auto; } -function asNumber(v: any): number | undefined { +function asNumber(v: any, defaultValue?: number): number | undefined { + if (v == null || v === '') { + return defaultValue; + } const num = +v; - return isNaN(num) ? undefined : num; + return isNaN(num) ? defaultValue : num; } export const heatmapMigrationHandler = (panel: PanelModel): Partial => { diff --git a/public/app/plugins/panel/heatmap-new/models.gen.ts b/public/app/plugins/panel/heatmap-new/models.gen.ts index c7450496fae..d5ff17e1fe3 100644 --- a/public/app/plugins/panel/heatmap-new/models.gen.ts +++ b/public/app/plugins/panel/heatmap-new/models.gen.ts @@ -34,11 +34,14 @@ export interface YAxisConfig extends AxisConfig { unit?: string; reverse?: boolean; decimals?: number; + // Only used when the axis is not ordinal + min?: number; + max?: number; } export interface FilterValueRange { - min?: number; - max?: number; + le?: number; + ge?: number; } export interface HeatmapTooltip { @@ -53,8 +56,8 @@ export interface ExemplarConfig { color: string; } -export interface BucketOptions { - name?: string; +export interface BucketFrameOptions { + value?: string; // value field name layout?: HeatmapBucketLayout; } @@ -64,11 +67,11 @@ export interface PanelOptions { color: HeatmapColorOptions; filterValues?: FilterValueRange; // was hideZeroBuckets - bucket?: BucketOptions; + bucketFrame?: BucketFrameOptions; showValue: VisibilityMode; cellGap?: number; // was cardPadding - cellSize?: number; // was cardRadius + cellRadius?: number; // was cardRadius (not used, but migrated from angular) yAxis: YAxisConfig; legend: HeatmapLegend; @@ -87,7 +90,7 @@ export const defaultPanelOptions: PanelOptions = { exponent: 0.5, steps: 64, }, - bucket: { + bucketFrame: { layout: HeatmapBucketLayout.auto, }, yAxis: { @@ -105,7 +108,7 @@ export const defaultPanelOptions: PanelOptions = { color: 'rgba(255,0,255,0.7)', }, filterValues: { - min: 1e-9, + le: 1e-9, }, cellGap: 1, }; diff --git a/public/app/plugins/panel/heatmap-new/module.tsx b/public/app/plugins/panel/heatmap-new/module.tsx index 0b583cf14a1..19cd36c2d64 100644 --- a/public/app/plugins/panel/heatmap-new/module.tsx +++ b/public/app/plugins/panel/heatmap-new/module.tsx @@ -63,33 +63,10 @@ export const plugin = new PanelPlugin(HeatmapPan if (opts.calculate) { addHeatmapCalculationOptions('calculation.', builder, opts.calculation, category); - } else { - builder.addTextInput({ - path: 'bucket.name', - name: 'Cell value name', - defaultValue: defaultPanelOptions.bucket?.name, - settings: { - placeholder: 'Value', - }, - category, - }); - builder.addRadio({ - path: 'bucket.layout', - name: 'Layout', - defaultValue: defaultPanelOptions.bucket?.layout ?? HeatmapBucketLayout.auto, - category, - settings: { - options: [ - { label: 'Auto', value: HeatmapBucketLayout.auto }, - { label: 'Middle', value: HeatmapBucketLayout.unknown }, - { label: 'Lower (LE)', value: HeatmapBucketLayout.le }, - { label: 'Upper (GE)', value: HeatmapBucketLayout.ge }, - ], - }, - }); } category = ['Y Axis']; + builder.addRadio({ path: 'yAxis.axisPlacement', name: 'Placement', @@ -104,6 +81,27 @@ export const plugin = new PanelPlugin(HeatmapPan }, }); + // TODO: support clamping the min/max range when there is a real axis + if (false && opts.calculate) { + builder + .addNumberInput({ + path: 'yAxis.min', + name: 'Min value', + settings: { + placeholder: 'Auto', + }, + category, + }) + .addTextInput({ + path: 'yAxis.max', + name: 'Max value', + settings: { + placeholder: 'Auto', + }, + category, + }); + } + builder .addNumberInput({ path: 'yAxis.axisWidth', @@ -123,13 +121,30 @@ export const plugin = new PanelPlugin(HeatmapPan placeholder: 'Auto', }, category, - }) - .addBooleanSwitch({ - path: 'yAxis.reverse', - name: 'Reverse', - defaultValue: defaultPanelOptions.yAxis.reverse === true, + }); + + if (!opts.calculate) { + builder.addRadio({ + path: 'bucketFrame.layout', + name: 'Tick alignment', + defaultValue: defaultPanelOptions.bucketFrame?.layout ?? HeatmapBucketLayout.auto, category, + settings: { + options: [ + { label: 'Auto', value: HeatmapBucketLayout.auto }, + { label: 'Top (LE)', value: HeatmapBucketLayout.le }, + { label: 'Middle', value: HeatmapBucketLayout.unknown }, + { label: 'Bottom (GE)', value: HeatmapBucketLayout.ge }, + ], + }, }); + } + builder.addBooleanSwitch({ + path: 'yAxis.reverse', + name: 'Reverse', + defaultValue: defaultPanelOptions.yAxis.reverse === true, + category, + }); category = ['Colors']; @@ -225,6 +240,26 @@ export const plugin = new PanelPlugin(HeatmapPan }, }); + builder + .addNumberInput({ + path: 'color.min', + name: 'Start color scale from value', + defaultValue: defaultPanelOptions.color.min, + settings: { + placeholder: 'Auto (min)', + }, + category, + }) + .addNumberInput({ + path: 'color.max', + name: 'End color scale at value', + defaultValue: defaultPanelOptions.color.max, + settings: { + placeholder: 'Auto (max)', + }, + category, + }); + category = ['Display']; builder @@ -241,12 +276,6 @@ export const plugin = new PanelPlugin(HeatmapPan // ], // }, // }) - .addNumberInput({ - path: 'filterValues.min', - name: 'Hide cell counts <=', - defaultValue: defaultPanelOptions.filterValues?.min, - category, - }) .addSliderInput({ name: 'Cell gap', path: 'cellGap', @@ -256,6 +285,24 @@ export const plugin = new PanelPlugin(HeatmapPan min: 0, max: 25, }, + }) + .addNumberInput({ + path: 'filterValues.le', + name: 'Hide cells with values <=', + defaultValue: defaultPanelOptions.filterValues?.le, + settings: { + placeholder: 'None', + }, + category, + }) + .addNumberInput({ + path: 'filterValues.ge', + name: 'Hide cells with values >=', + defaultValue: defaultPanelOptions.filterValues?.ge, + settings: { + placeholder: 'None', + }, + category, }); // .addSliderInput({ // name: 'Cell radius', @@ -277,6 +324,18 @@ export const plugin = new PanelPlugin(HeatmapPan category, }); + if (!opts.calculate) { + builder.addTextInput({ + path: 'bucketFrame.value', + name: 'Cell value name', + defaultValue: defaultPanelOptions.bucketFrame?.value, + settings: { + placeholder: 'Value', + }, + category, + }); + } + builder.addBooleanSwitch({ path: 'tooltip.yHistogram', name: 'Show histogram (Y axis)', diff --git a/public/app/plugins/panel/heatmap-new/plugin.json b/public/app/plugins/panel/heatmap-new/plugin.json index 4a320c089fb..66a6241b9f9 100644 --- a/public/app/plugins/panel/heatmap-new/plugin.json +++ b/public/app/plugins/panel/heatmap-new/plugin.json @@ -5,14 +5,14 @@ "state": "alpha", "info": { - "description": "Next generation heatmap visualization", + "description": "Like a histogram over time", "author": { "name": "Grafana Labs", "url": "https://grafana.com" }, "logos": { - "small": "img/heatmap.svg", - "large": "img/heatmap.svg" + "small": "img/icn-heatmap-panel.svg", + "large": "img/icn-heatmap-panel.svg" } } } diff --git a/public/app/plugins/panel/heatmap-new/utils.ts b/public/app/plugins/panel/heatmap-new/utils.ts index d75c6329720..e225d5451e9 100644 --- a/public/app/plugins/panel/heatmap-new/utils.ts +++ b/public/app/plugins/panel/heatmap-new/utils.ts @@ -1,7 +1,15 @@ import { MutableRefObject, RefObject } from 'react'; import uPlot from 'uplot'; -import { DataFrameType, GrafanaTheme2, incrRoundDn, incrRoundUp, TimeRange } from '@grafana/data'; +import { + DataFrameType, + formattedValueToString, + getValueFormat, + GrafanaTheme2, + incrRoundDn, + incrRoundUp, + TimeRange, +} from '@grafana/data'; import { AxisPlacement, ScaleDirection, ScaleDistribution, ScaleOrientation } from '@grafana/schema'; import { UPlotConfigBuilder } from '@grafana/ui'; import { readHeatmapScanlinesCustomMeta } from 'app/features/transformers/calculateHeatmap/heatmap'; @@ -15,7 +23,8 @@ import { PanelFieldConfig, YAxisConfig } from './models.gen'; interface PathbuilderOpts { each: (u: uPlot, seriesIdx: number, dataIdx: number, lft: number, top: number, wid: number, hgt: number) => void; gap?: number | null; - hideThreshold?: number; + hideLE?: number; + hideGE?: number; xAlign?: -1 | 0 | 1; yAlign?: -1 | 0 | 1; ySizeDivisor?: number; @@ -55,7 +64,10 @@ interface PrepConfigOpts { palette: string[]; exemplarColor: string; cellGap?: number | null; // in css pixels - hideThreshold?: number; + hideLE?: number; + hideGE?: number; + valueMin?: number; + valueMax?: number; yAxisConfig: YAxisConfig; ySizeDivisor?: number; } @@ -72,7 +84,10 @@ export function prepConfig(opts: PrepConfigOpts) { getTimeRange, palette, cellGap, - hideThreshold, + hideLE, + hideGE, + valueMin, + valueMax, yAxisConfig, ySizeDivisor, } = opts; @@ -289,12 +304,12 @@ export function prepConfig(opts: PrepConfigOpts) { // how to expand scale range if inferred non-regular or log buckets? } } - return [dataMin, dataMax]; }, }); - const hasLabeledY = readHeatmapScanlinesCustomMeta(dataRef.current?.heatmap).yOrdinalDisplay != null; + const isOrdianalY = readHeatmapScanlinesCustomMeta(dataRef.current?.heatmap).yOrdinalDisplay != null; + const disp = dataRef.current?.heatmap?.fields[1].display ?? getValueFormat('short'); builder.addAxis({ scaleKey: 'y', @@ -303,35 +318,51 @@ export function prepConfig(opts: PrepConfigOpts) { size: yAxisConfig.axisWidth || null, label: yAxisConfig.axisLabel, theme: theme, - splits: hasLabeledY - ? () => { - const ys = dataRef.current?.heatmap?.fields[1].values.toArray()!; - const splits = ys.slice(0, ys.length - ys.lastIndexOf(ys[0])); - - const bucketSize = dataRef.current?.yBucketSize!; + formatValue: (v: any) => formattedValueToString(disp(v)), + splits: isOrdianalY + ? (self: uPlot) => { + const meta = readHeatmapScanlinesCustomMeta(dataRef.current?.heatmap); + if (!meta.yOrdinalDisplay) { + return [0, 1]; //? + } + let splits = meta.yOrdinalDisplay.map((v, idx) => idx); + + switch (dataRef.current?.yLayout) { + case HeatmapBucketLayout.le: + splits.unshift(-1); + break; + case HeatmapBucketLayout.ge: + splits.push(splits.length); + break; + } - if (dataRef.current?.yLayout === HeatmapBucketLayout.le) { - splits.unshift(ys[0] - bucketSize); + // Skip labels when the height is too small + if (self.height < 60) { + splits = [splits[0], splits[splits.length - 1]]; } else { - splits.push(ys[ys.length - 1] + bucketSize); + while (splits.length > 3 && (self.height - 15) / splits.length < 10) { + splits = splits.filter((v, idx) => idx % 2 === 0); // remove half the items + } } - return splits; } : undefined, - values: hasLabeledY - ? () => { + values: isOrdianalY + ? (self: uPlot, splits) => { const meta = readHeatmapScanlinesCustomMeta(dataRef.current?.heatmap); - const yAxisValues = meta.yOrdinalDisplay?.slice()!; - const isFromBuckets = meta.yOrdinalDisplay?.length && !('le' === meta.yMatchWithLabel); - - if (dataRef.current?.yLayout === HeatmapBucketLayout.le) { - yAxisValues.unshift(isFromBuckets ? '' : '0.0'); // assumes dense layout where lowest bucket's low bound is 0-ish - } else if (dataRef.current?.yLayout === HeatmapBucketLayout.ge) { - yAxisValues.push(isFromBuckets ? '' : '+Inf'); + if (meta.yOrdinalDisplay) { + return splits.map((v) => { + const txt = meta.yOrdinalDisplay[v]; + if (!txt && v < 0) { + // Check prometheus style labels + if ('le' === meta.yMatchWithLabel) { + return '0.0'; + } + } + return txt; + }); } - - return yAxisValues; + return splits.map((v) => `${v}`); } : undefined, }); @@ -363,7 +394,8 @@ export function prepConfig(opts: PrepConfigOpts) { }); }, gap: cellGap, - hideThreshold, + hideLE, + hideGE, xAlign: dataRef.current?.xLayout === HeatmapBucketLayout.le ? -1 @@ -380,7 +412,7 @@ export function prepConfig(opts: PrepConfigOpts) { fill: { values: (u, seriesIdx) => { let countFacetIdx = heatmapType === DataFrameType.HeatmapScanlines ? 2 : 3; - return countsToFills(u.data[seriesIdx][countFacetIdx] as unknown as number[], palette); + return valuesToFills(u.data[seriesIdx][countFacetIdx] as unknown as number[], palette, valueMin, valueMax); }, index: palette, }, @@ -465,7 +497,7 @@ export function prepConfig(opts: PrepConfigOpts) { const CRISP_EDGES_GAP_MIN = 4; export function heatmapPathsDense(opts: PathbuilderOpts) { - const { disp, each, gap = 1, hideThreshold = 0, xAlign = 1, yAlign = 1, ySizeDivisor = 1 } = opts; + const { disp, each, gap = 1, hideLE = -Infinity, hideGE = Infinity, xAlign = 1, yAlign = 1, ySizeDivisor = 1 } = opts; const pxRatio = devicePixelRatio; @@ -549,14 +581,7 @@ export function heatmapPathsDense(opts: PathbuilderOpts) { ); for (let i = 0; i < dlen; i++) { - // filter out 0 counts and out of view - if ( - counts[i] > hideThreshold && - xs[i] + xBinIncr >= scaleX.min! && - xs[i] - xBinIncr <= scaleX.max! && - ys[i] + yBinIncr >= scaleY.min! && - ys[i] - yBinIncr <= scaleY.max! - ) { + if (counts[i] > hideLE && counts[i] < hideGE) { let cx = cxs[~~(i / yBinQty)]; let cy = cys[i % yBinQty]; @@ -646,7 +671,7 @@ export function heatmapPathsPoints(opts: PointsBuilderOpts, exemplarColor: strin // accepts xMax, yMin, yMax, count // xbinsize? x tile sizes are uniform? export function heatmapPathsSparse(opts: PathbuilderOpts) { - const { disp, each, gap = 1, hideThreshold = 0 } = opts; + const { disp, each, gap = 1, hideLE = -Infinity, hideGE = Infinity } = opts; const pxRatio = devicePixelRatio; @@ -717,7 +742,7 @@ export function heatmapPathsSparse(opts: PathbuilderOpts) { let xSizeUniform = xOffs.get(xMaxs.find((v) => v !== xMaxs[0])) - xOffs.get(xMaxs[0]); for (let i = 0; i < dlen; i++) { - if (counts[i] <= hideThreshold) { + if (counts[i] <= hideLE || counts[i] >= hideGE) { continue; } @@ -739,19 +764,11 @@ export function heatmapPathsSparse(opts: PathbuilderOpts) { let x = xMaxPx; let y = yMinPx; - // filter out 0 counts and out of view - // if ( - // xs[i] + xBinIncr >= scaleX.min! && - // xs[i] - xBinIncr <= scaleX.max! && - // ys[i] + yBinIncr >= scaleY.min! && - // ys[i] - yBinIncr <= scaleY.max! - // ) { let fillPath = fillPaths[fills[i]]; rect(fillPath, x, y, xSize, ySize); each(u, 1, i, x, y, xSize, ySize); - // } } u.ctx.save(); @@ -772,29 +789,36 @@ export function heatmapPathsSparse(opts: PathbuilderOpts) { }; } -export const countsToFills = (counts: number[], palette: string[]) => { - // TODO: integrate 1e-9 hideThreshold? - const hideThreshold = 0; +export const valuesToFills = (values: number[], palette: string[], minValue?: number, maxValue?: number) => { + if (minValue == null) { + minValue = Infinity; + + for (let i = 0; i < values.length; i++) { + minValue = Math.min(minValue, values[i]); + } + } - let minCount = Infinity; - let maxCount = -Infinity; + if (maxValue == null) { + maxValue = -Infinity; - for (let i = 0; i < counts.length; i++) { - if (counts[i] > hideThreshold) { - minCount = Math.min(minCount, counts[i]); - maxCount = Math.max(maxCount, counts[i]); + for (let i = 0; i < values.length; i++) { + maxValue = Math.max(maxValue, values[i]); } } - let range = maxCount - minCount; + let range = maxValue - minValue; let paletteSize = palette.length; - let indexedFills = Array(counts.length); + let indexedFills = Array(values.length); - for (let i = 0; i < counts.length; i++) { + for (let i = 0; i < values.length; i++) { indexedFills[i] = - counts[i] === 0 ? -1 : Math.min(paletteSize - 1, Math.floor((paletteSize * (counts[i] - minCount)) / range)); + values[i] < minValue + ? 0 + : values[i] > maxValue + ? paletteSize - 1 + : Math.min(paletteSize - 1, Math.floor((paletteSize * (values[i] - minValue)) / range)); } return indexedFills;