Heatmap: Support heatmap rows with non-timeseries X axis (#60929)

Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
This commit is contained in:
Ryan McKinley 2023-02-01 15:16:34 -08:00 committed by GitHub
parent d4f4a83574
commit 4a8763d7b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 385 additions and 20 deletions

View File

@ -0,0 +1,326 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [],
"type": "dashboard"
},
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": 116,
"links": [],
"liveNow": false,
"panels": [
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "auto",
"displayMode": "auto",
"inspect": false
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 9,
"w": 10,
"x": 0,
"y": 0
},
"id": 2,
"options": {
"footer": {
"countRows": false,
"fields": "",
"reducer": [
"sum"
],
"show": false
},
"showHeader": true
},
"pluginVersion": "9.4.0-pre",
"targets": [
{
"csvContent": "x,y1,y2\n1,8,12\n2,6,13\n3,7,9\n5,9,7\n6,5,9",
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"refId": "A",
"scenarioId": "csv_content"
}
],
"title": "Raw heatmap rows",
"type": "table"
},
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"fieldConfig": {
"defaults": {
"custom": {
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"scaleDistribution": {
"type": "linear"
}
}
},
"overrides": []
},
"gridPos": {
"h": 9,
"w": 14,
"x": 10,
"y": 0
},
"id": 4,
"options": {
"calculate": false,
"cellGap": 1,
"color": {
"exponent": 0.5,
"fill": "dark-orange",
"mode": "scheme",
"reverse": false,
"scale": "exponential",
"scheme": "Oranges",
"steps": 64
},
"exemplars": {
"color": "rgba(255,0,255,0.7)"
},
"filterValues": {
"le": 1e-9
},
"legend": {
"show": true
},
"rowsFrame": {
"layout": "auto"
},
"tooltip": {
"show": true,
"yHistogram": false
},
"yAxis": {
"axisPlacement": "left",
"reverse": false
}
},
"pluginVersion": "9.4.0-pre",
"targets": [
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"panelId": 2,
"refId": "A"
}
],
"title": "Row heatmap",
"type": "heatmap"
},
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "auto",
"displayMode": "auto",
"inspect": false
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 10,
"x": 0,
"y": 9
},
"id": 5,
"options": {
"footer": {
"countRows": false,
"fields": "",
"reducer": [
"sum"
],
"show": false
},
"showHeader": true
},
"pluginVersion": "9.4.0-pre",
"targets": [
{
"csvContent": "x,y,count\n1,4,10\n1,6,11\n2,5,30\n2,4,22\n3,6,17",
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"refId": "A",
"scenarioId": "csv_content"
}
],
"title": "Raw heatmap cells",
"type": "table"
},
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"fieldConfig": {
"defaults": {
"custom": {
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"scaleDistribution": {
"type": "linear"
}
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 14,
"x": 10,
"y": 9
},
"id": 6,
"options": {
"calculate": false,
"cellGap": 1,
"color": {
"exponent": 0.5,
"fill": "dark-orange",
"mode": "scheme",
"reverse": false,
"scale": "exponential",
"scheme": "Oranges",
"steps": 64
},
"exemplars": {
"color": "rgba(255,0,255,0.7)"
},
"filterValues": {
"le": 1e-9
},
"legend": {
"show": true
},
"rowsFrame": {
"layout": "auto"
},
"tooltip": {
"show": true,
"yHistogram": false
},
"yAxis": {
"axisPlacement": "left",
"reverse": false
}
},
"pluginVersion": "9.4.0-pre",
"targets": [
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"panelId": 5,
"refId": "A"
}
],
"title": "Cells heatmap",
"type": "heatmap"
}
],
"revision": 1,
"schemaVersion": 37,
"style": "dark",
"tags": [],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "Heatmap X axis",
"uid": "5Y0jv6pVz",
"version": 3,
"weekStart": ""
}

View File

@ -303,6 +303,13 @@ local dashboard = grafana.dashboard;
id: 0,
}
},
dashboard.new('heatmap-x', import '../dev-dashboards/panel-heatmap/heatmap-x.json') +
resource.addMetadata('folder', 'dev-dashboards') +
{
spec+: {
id: 0,
}
},
dashboard.new('histogram_tests', import '../dev-dashboards/panel-histogram/histogram_tests.json') +
resource.addMetadata('folder', 'dev-dashboards') +
{

View File

@ -191,7 +191,7 @@ export function rowsToCellsHeatmap(opts: RowsHeatmapOptions): DataFrame {
},
fields: [
{
name: 'xMax',
name: xField.type === FieldType.time ? 'xMax' : 'x',
type: xField.type,
values: new ArrayVector(xs),
config: xField.config,

View File

@ -237,7 +237,7 @@ const HeatmapHoverCell = ({ data, hover, showHistogram }: Props) => {
<>
<div>
<div>{xDisp(xBucketMin)}</div>
<div>{xDisp(xBucketMax)}</div>
{data.xLayout !== HeatmapCellLayout.unknown && <div>{xDisp(xBucketMax)}</div>}
</div>
{showHistogram && (
<canvas

View File

@ -14,6 +14,7 @@ import {
incrRoundDn,
incrRoundUp,
TimeRange,
FieldType,
} from '@grafana/data';
import { AxisPlacement, ScaleDirection, ScaleDistribution, ScaleOrientation } from '@grafana/schema';
import { UPlotConfigBuilder } from '@grafana/ui';
@ -98,7 +99,13 @@ export function prepConfig(opts: PrepConfigOpts) {
} = opts;
const xScaleKey = 'x';
const xScaleUnit = 'time';
let xScaleUnit = 'time';
let isTime = true;
if (dataRef.current?.heatmap?.fields[0].type !== FieldType.time) {
xScaleUnit = dataRef.current?.heatmap?.fields[0].config?.unit ?? 'x';
isTime = false;
}
const pxRatio = devicePixelRatio;
@ -145,22 +152,24 @@ export function prepConfig(opts: PrepConfigOpts) {
u.setSelect({ left: 0, top: 0, width: 0, height: 0 }, false);
});
// this is a tmp hack because in mode: 2, uplot does not currently call scales.x.range() for setData() calls
// scales.x.range() typically reads back from drilled-down panelProps.timeRange via getTimeRange()
builder.addHook('setData', (u) => {
//let [min, max] = (u.scales!.x!.range! as uPlot.Range.Function)(u, 0, 100, xScaleKey);
if (isTime) {
// this is a tmp hack because in mode: 2, uplot does not currently call scales.x.range() for setData() calls
// scales.x.range() typically reads back from drilled-down panelProps.timeRange via getTimeRange()
builder.addHook('setData', (u) => {
//let [min, max] = (u.scales!.x!.range! as uPlot.Range.Function)(u, 0, 100, xScaleKey);
let { min: xMin, max: xMax } = u.scales!.x;
let { min: xMin, max: xMax } = u.scales!.x;
let min = getTimeRange().from.valueOf();
let max = getTimeRange().to.valueOf();
let min = getTimeRange().from.valueOf();
let max = getTimeRange().to.valueOf();
if (xMin !== min || xMax !== max) {
queueMicrotask(() => {
u.setScale(xScaleKey, { min, max });
});
}
});
if (xMin !== min || xMax !== max) {
queueMicrotask(() => {
u.setScale(xScaleKey, { min, max });
});
}
});
}
// rect of .u-over (grid area)
builder.addHook('syncRect', (u, r) => {
@ -236,19 +245,42 @@ export function prepConfig(opts: PrepConfigOpts) {
builder.addScale({
scaleKey: xScaleKey,
isTime: true,
isTime,
orientation: ScaleOrientation.Horizontal,
direction: ScaleDirection.Right,
// TODO: expand by x bucket size and layout
range: () => {
return [getTimeRange().from.valueOf(), getTimeRange().to.valueOf()];
range: (u, dataMin, dataMax) => {
if (isTime) {
return [getTimeRange().from.valueOf(), getTimeRange().to.valueOf()];
} else {
if (dataRef.current?.xLayout === HeatmapCellLayout.le) {
return [dataMin - dataRef.current?.xBucketSize!, dataMax];
} else if (dataRef.current?.xLayout === HeatmapCellLayout.ge) {
return [dataMin, dataMax + dataRef.current?.xBucketSize!];
} else {
let offset = dataRef.current?.xBucketSize! / 2;
return [dataMin - offset, dataMax + offset];
}
}
},
});
let incrs;
if (!isTime) {
incrs = [];
for (let i = 0; i < 10; i++) {
incrs.push(i * dataRef.current?.xBucketSize!);
}
}
builder.addAxis({
scaleKey: xScaleKey,
placement: AxisPlacement.Bottom,
isTime: true,
incrs,
isTime,
theme: theme,
timeZone,
});