mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
HeatmapNG: add log scale calculation (#49969)
Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
This commit is contained in:
parent
219e848e73
commit
fd34700225
554
devenv/dev-dashboards/panel-heatmap/heatmap-calculate-log.json
Normal file
554
devenv/dev-dashboards/panel-heatmap/heatmap-calculate-log.json
Normal file
@ -0,0 +1,554 @@
|
|||||||
|
{
|
||||||
|
"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,
|
||||||
|
"links": [],
|
||||||
|
"liveNow": false,
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "testdata",
|
||||||
|
"uid": "PD8C576611E62080A"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "palette-classic"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"axisLabel": "",
|
||||||
|
"axisPlacement": "auto",
|
||||||
|
"barAlignment": 0,
|
||||||
|
"drawStyle": "points",
|
||||||
|
"fillOpacity": 0,
|
||||||
|
"gradientMode": "none",
|
||||||
|
"hideFrom": {
|
||||||
|
"legend": false,
|
||||||
|
"tooltip": false,
|
||||||
|
"viz": false
|
||||||
|
},
|
||||||
|
"lineInterpolation": "linear",
|
||||||
|
"lineWidth": 1,
|
||||||
|
"pointSize": 2,
|
||||||
|
"scaleDistribution": {
|
||||||
|
"log": 2,
|
||||||
|
"type": "log"
|
||||||
|
},
|
||||||
|
"showPoints": "auto",
|
||||||
|
"spanNulls": false,
|
||||||
|
"stacking": {
|
||||||
|
"group": "A",
|
||||||
|
"mode": "none"
|
||||||
|
},
|
||||||
|
"thresholdsStyle": {
|
||||||
|
"mode": "off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"value": 80
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 14,
|
||||||
|
"w": 8,
|
||||||
|
"x": 0,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"id": 3,
|
||||||
|
"options": {
|
||||||
|
"legend": {
|
||||||
|
"calcs": [],
|
||||||
|
"displayMode": "list",
|
||||||
|
"placement": "bottom"
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"mode": "single",
|
||||||
|
"sort": "none"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pluginVersion": "9.0.0-pre",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "testdata",
|
||||||
|
"uid": "PD8C576611E62080A"
|
||||||
|
},
|
||||||
|
"max": 500000,
|
||||||
|
"min": 0.01,
|
||||||
|
"refId": "A",
|
||||||
|
"scenarioId": "random_walk",
|
||||||
|
"seriesCount": 2,
|
||||||
|
"spread": 1000,
|
||||||
|
"startValue": 0.01
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Time series",
|
||||||
|
"type": "timeseries"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "datasource",
|
||||||
|
"uid": "-- Dashboard --"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"custom": {
|
||||||
|
"hideFrom": {
|
||||||
|
"legend": false,
|
||||||
|
"tooltip": false,
|
||||||
|
"viz": false
|
||||||
|
},
|
||||||
|
"scaleDistribution": {
|
||||||
|
"type": "linear"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 14,
|
||||||
|
"w": 8,
|
||||||
|
"x": 8,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"id": 6,
|
||||||
|
"options": {
|
||||||
|
"bucket": {
|
||||||
|
"layout": "auto"
|
||||||
|
},
|
||||||
|
"calculate": true,
|
||||||
|
"calculation": {
|
||||||
|
"yBuckets": {
|
||||||
|
"scale": {
|
||||||
|
"log": 2,
|
||||||
|
"type": "log"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cellGap": 1,
|
||||||
|
"color": {
|
||||||
|
"exponent": 0.5,
|
||||||
|
"fill": "dark-orange",
|
||||||
|
"mode": "scheme",
|
||||||
|
"scale": "exponential",
|
||||||
|
"scheme": "Spectral",
|
||||||
|
"steps": 64
|
||||||
|
},
|
||||||
|
"exemplars": {
|
||||||
|
"color": "rgba(255,0,255,0.7)"
|
||||||
|
},
|
||||||
|
"filterValues": {
|
||||||
|
"min": 1e-9
|
||||||
|
},
|
||||||
|
"legend": {
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
"mode": "calculate",
|
||||||
|
"tooltip": {
|
||||||
|
"show": true,
|
||||||
|
"yHistogram": false
|
||||||
|
},
|
||||||
|
"yAxis": {
|
||||||
|
"axisPlacement": "left",
|
||||||
|
"reverse": false
|
||||||
|
},
|
||||||
|
"yAxisLabels": "auto",
|
||||||
|
"yAxisReverse": false
|
||||||
|
},
|
||||||
|
"pluginVersion": "9.0.0-pre",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "datasource",
|
||||||
|
"uid": "-- Dashboard --"
|
||||||
|
},
|
||||||
|
"panelId": 3,
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "log2",
|
||||||
|
"type": "heatmap-new"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "datasource",
|
||||||
|
"uid": "-- Dashboard --"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"custom": {
|
||||||
|
"hideFrom": {
|
||||||
|
"legend": false,
|
||||||
|
"tooltip": false,
|
||||||
|
"viz": false
|
||||||
|
},
|
||||||
|
"scaleDistribution": {
|
||||||
|
"type": "linear"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 14,
|
||||||
|
"w": 8,
|
||||||
|
"x": 16,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"id": 7,
|
||||||
|
"options": {
|
||||||
|
"bucket": {
|
||||||
|
"layout": "auto"
|
||||||
|
},
|
||||||
|
"calculate": true,
|
||||||
|
"calculation": {
|
||||||
|
"yBuckets": {
|
||||||
|
"scale": {
|
||||||
|
"log": 2,
|
||||||
|
"type": "log"
|
||||||
|
},
|
||||||
|
"value": "2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cellGap": 1,
|
||||||
|
"color": {
|
||||||
|
"exponent": 0.5,
|
||||||
|
"fill": "dark-orange",
|
||||||
|
"mode": "scheme",
|
||||||
|
"scale": "exponential",
|
||||||
|
"scheme": "Spectral",
|
||||||
|
"steps": 64
|
||||||
|
},
|
||||||
|
"exemplars": {
|
||||||
|
"color": "rgba(255,0,255,0.7)"
|
||||||
|
},
|
||||||
|
"filterValues": {
|
||||||
|
"min": 1e-9
|
||||||
|
},
|
||||||
|
"legend": {
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
"mode": "calculate",
|
||||||
|
"tooltip": {
|
||||||
|
"show": true,
|
||||||
|
"yHistogram": false
|
||||||
|
},
|
||||||
|
"yAxis": {
|
||||||
|
"axisPlacement": "left",
|
||||||
|
"reverse": false
|
||||||
|
},
|
||||||
|
"yAxisLabels": "auto",
|
||||||
|
"yAxisReverse": false
|
||||||
|
},
|
||||||
|
"pluginVersion": "9.0.0-pre",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "datasource",
|
||||||
|
"uid": "-- Dashboard --"
|
||||||
|
},
|
||||||
|
"panelId": 3,
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "log2 split 2",
|
||||||
|
"type": "heatmap-new"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "datasource",
|
||||||
|
"uid": "-- Dashboard --"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"custom": {
|
||||||
|
"hideFrom": {
|
||||||
|
"legend": false,
|
||||||
|
"tooltip": false,
|
||||||
|
"viz": false
|
||||||
|
},
|
||||||
|
"scaleDistribution": {
|
||||||
|
"type": "linear"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 14,
|
||||||
|
"w": 8,
|
||||||
|
"x": 0,
|
||||||
|
"y": 14
|
||||||
|
},
|
||||||
|
"id": 4,
|
||||||
|
"options": {
|
||||||
|
"bucket": {
|
||||||
|
"layout": "auto"
|
||||||
|
},
|
||||||
|
"calculate": true,
|
||||||
|
"cellGap": 1,
|
||||||
|
"color": {
|
||||||
|
"exponent": 0.5,
|
||||||
|
"fill": "dark-orange",
|
||||||
|
"mode": "scheme",
|
||||||
|
"scale": "exponential",
|
||||||
|
"scheme": "Spectral",
|
||||||
|
"steps": 64
|
||||||
|
},
|
||||||
|
"exemplars": {
|
||||||
|
"color": "rgba(255,0,255,0.7)"
|
||||||
|
},
|
||||||
|
"filterValues": {
|
||||||
|
"min": 1e-9
|
||||||
|
},
|
||||||
|
"legend": {
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
"mode": "calculate",
|
||||||
|
"tooltip": {
|
||||||
|
"show": true,
|
||||||
|
"yHistogram": false
|
||||||
|
},
|
||||||
|
"yAxis": {
|
||||||
|
"axisPlacement": "left",
|
||||||
|
"reverse": false
|
||||||
|
},
|
||||||
|
"yAxisLabels": "auto",
|
||||||
|
"yAxisReverse": false
|
||||||
|
},
|
||||||
|
"pluginVersion": "9.0.0-pre",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "datasource",
|
||||||
|
"uid": "-- Dashboard --"
|
||||||
|
},
|
||||||
|
"panelId": 3,
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "linear",
|
||||||
|
"type": "heatmap-new"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "datasource",
|
||||||
|
"uid": "-- Dashboard --"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"custom": {
|
||||||
|
"hideFrom": {
|
||||||
|
"legend": false,
|
||||||
|
"tooltip": false,
|
||||||
|
"viz": false
|
||||||
|
},
|
||||||
|
"scaleDistribution": {
|
||||||
|
"type": "linear"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 14,
|
||||||
|
"w": 8,
|
||||||
|
"x": 8,
|
||||||
|
"y": 14
|
||||||
|
},
|
||||||
|
"id": 5,
|
||||||
|
"options": {
|
||||||
|
"bucket": {
|
||||||
|
"layout": "auto"
|
||||||
|
},
|
||||||
|
"calculate": true,
|
||||||
|
"calculation": {
|
||||||
|
"yBuckets": {
|
||||||
|
"scale": {
|
||||||
|
"log": 10,
|
||||||
|
"type": "log"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cellGap": 1,
|
||||||
|
"color": {
|
||||||
|
"exponent": 0.5,
|
||||||
|
"fill": "dark-orange",
|
||||||
|
"mode": "scheme",
|
||||||
|
"scale": "exponential",
|
||||||
|
"scheme": "Spectral",
|
||||||
|
"steps": 64
|
||||||
|
},
|
||||||
|
"exemplars": {
|
||||||
|
"color": "rgba(255,0,255,0.7)"
|
||||||
|
},
|
||||||
|
"filterValues": {
|
||||||
|
"min": 1e-9
|
||||||
|
},
|
||||||
|
"legend": {
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
"mode": "calculate",
|
||||||
|
"tooltip": {
|
||||||
|
"show": true,
|
||||||
|
"yHistogram": false
|
||||||
|
},
|
||||||
|
"yAxis": {
|
||||||
|
"axisPlacement": "left",
|
||||||
|
"reverse": false
|
||||||
|
},
|
||||||
|
"yAxisLabels": "auto",
|
||||||
|
"yAxisReverse": false
|
||||||
|
},
|
||||||
|
"pluginVersion": "9.0.0-pre",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "datasource",
|
||||||
|
"uid": "-- Dashboard --"
|
||||||
|
},
|
||||||
|
"panelId": 3,
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "log10",
|
||||||
|
"type": "heatmap-new"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "datasource",
|
||||||
|
"uid": "-- Dashboard --"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"custom": {
|
||||||
|
"hideFrom": {
|
||||||
|
"legend": false,
|
||||||
|
"tooltip": false,
|
||||||
|
"viz": false
|
||||||
|
},
|
||||||
|
"scaleDistribution": {
|
||||||
|
"type": "linear"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 14,
|
||||||
|
"w": 8,
|
||||||
|
"x": 16,
|
||||||
|
"y": 14
|
||||||
|
},
|
||||||
|
"id": 8,
|
||||||
|
"options": {
|
||||||
|
"bucket": {
|
||||||
|
"layout": "auto"
|
||||||
|
},
|
||||||
|
"calculate": true,
|
||||||
|
"calculation": {
|
||||||
|
"yBuckets": {
|
||||||
|
"scale": {
|
||||||
|
"log": 10,
|
||||||
|
"type": "log"
|
||||||
|
},
|
||||||
|
"value": "2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cellGap": 1,
|
||||||
|
"color": {
|
||||||
|
"exponent": 0.5,
|
||||||
|
"fill": "dark-orange",
|
||||||
|
"mode": "scheme",
|
||||||
|
"scale": "exponential",
|
||||||
|
"scheme": "Spectral",
|
||||||
|
"steps": 64
|
||||||
|
},
|
||||||
|
"exemplars": {
|
||||||
|
"color": "rgba(255,0,255,0.7)"
|
||||||
|
},
|
||||||
|
"filterValues": {
|
||||||
|
"min": 1e-9
|
||||||
|
},
|
||||||
|
"legend": {
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
"mode": "calculate",
|
||||||
|
"tooltip": {
|
||||||
|
"show": true,
|
||||||
|
"yHistogram": false
|
||||||
|
},
|
||||||
|
"yAxis": {
|
||||||
|
"axisPlacement": "left",
|
||||||
|
"reverse": false
|
||||||
|
},
|
||||||
|
"yAxisLabels": "auto",
|
||||||
|
"yAxisReverse": false
|
||||||
|
},
|
||||||
|
"pluginVersion": "9.0.0-pre",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "datasource",
|
||||||
|
"uid": "-- Dashboard --"
|
||||||
|
},
|
||||||
|
"panelId": 3,
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "log10 split 2",
|
||||||
|
"type": "heatmap-new"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"schemaVersion": 36,
|
||||||
|
"style": "dark",
|
||||||
|
"tags": ["gdev", "panel-tests", "graph-ng"],
|
||||||
|
"templating": {
|
||||||
|
"list": []
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"from": "now-6h",
|
||||||
|
"to": "now"
|
||||||
|
},
|
||||||
|
"timepicker": {},
|
||||||
|
"timezone": "",
|
||||||
|
"title": "Heatmap calculate (log)",
|
||||||
|
"uid": "ZXYQTA97ZZ",
|
||||||
|
"version": 4,
|
||||||
|
"weekStart": ""
|
||||||
|
}
|
@ -2,10 +2,10 @@ import React from 'react';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
FieldConfigEditorBuilder,
|
FieldConfigEditorBuilder,
|
||||||
FieldOverrideEditorProps,
|
|
||||||
FieldType,
|
FieldType,
|
||||||
identityOverrideProcessor,
|
identityOverrideProcessor,
|
||||||
SelectableValue,
|
SelectableValue,
|
||||||
|
StandardEditorProps,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { AxisConfig, AxisPlacement, ScaleDistribution, ScaleDistributionConfig } from '@grafana/schema';
|
import { AxisConfig, AxisPlacement, ScaleDistribution, ScaleDistributionConfig } from '@grafana/schema';
|
||||||
|
|
||||||
@ -89,8 +89,8 @@ export function addAxisConfig(
|
|||||||
path: 'scaleDistribution',
|
path: 'scaleDistribution',
|
||||||
name: 'Scale',
|
name: 'Scale',
|
||||||
category,
|
category,
|
||||||
editor: ScaleDistributionEditor,
|
editor: ScaleDistributionEditor as any,
|
||||||
override: ScaleDistributionEditor,
|
override: ScaleDistributionEditor as any,
|
||||||
defaultValue: { type: ScaleDistribution.Linear },
|
defaultValue: { type: ScaleDistribution.Linear },
|
||||||
shouldApply: (f) => f.type === FieldType.number,
|
shouldApply: (f) => f.type === FieldType.number,
|
||||||
process: identityOverrideProcessor,
|
process: identityOverrideProcessor,
|
||||||
@ -121,19 +121,16 @@ const LOG_DISTRIBUTION_OPTIONS: Array<SelectableValue<number>> = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @alpha
|
* @internal
|
||||||
*/
|
*/
|
||||||
const ScaleDistributionEditor: React.FC<FieldOverrideEditorProps<ScaleDistributionConfig, any>> = ({
|
export const ScaleDistributionEditor = ({ value, onChange }: StandardEditorProps<ScaleDistributionConfig>) => {
|
||||||
value,
|
const type = value?.type ?? ScaleDistribution.Linear;
|
||||||
onChange,
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<HorizontalGroup>
|
<HorizontalGroup>
|
||||||
<RadioButtonGroup
|
<RadioButtonGroup
|
||||||
value={value.type || ScaleDistribution.Linear}
|
value={type}
|
||||||
options={DISTRIBUTION_OPTIONS}
|
options={DISTRIBUTION_OPTIONS}
|
||||||
onChange={(v) => {
|
onChange={(v) => {
|
||||||
console.log(v, value);
|
|
||||||
onChange({
|
onChange({
|
||||||
...value,
|
...value,
|
||||||
type: v!,
|
type: v!,
|
||||||
@ -141,9 +138,8 @@ const ScaleDistributionEditor: React.FC<FieldOverrideEditorProps<ScaleDistributi
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{value.type === ScaleDistribution.Log && (
|
{type === ScaleDistribution.Log && (
|
||||||
<Select
|
<Select
|
||||||
allowCustomValue={false}
|
|
||||||
options={LOG_DISTRIBUTION_OPTIONS}
|
options={LOG_DISTRIBUTION_OPTIONS}
|
||||||
value={value.log || 2}
|
value={value.log || 2}
|
||||||
prefix={'base'}
|
prefix={'base'}
|
||||||
|
@ -25,7 +25,7 @@ const supplier = (
|
|||||||
|
|
||||||
export const HeatmapTransformerEditor: React.FC<TransformerUIProps<HeatmapTransformerOptions>> = (props) => {
|
export const HeatmapTransformerEditor: React.FC<TransformerUIProps<HeatmapTransformerOptions>> = (props) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!props.options.xAxis?.mode) {
|
if (!props.options.xBuckets?.mode) {
|
||||||
const opts = getDefaultOptions(supplier);
|
const opts = getDefaultOptions(supplier);
|
||||||
props.onChange({ ...opts, ...props.options });
|
props.onChange({ ...opts, ...props.options });
|
||||||
console.log('geometry useEffect', opts);
|
console.log('geometry useEffect', opts);
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { SelectableValue, StandardEditorProps } from '@grafana/data';
|
import { SelectableValue, StandardEditorProps } from '@grafana/data';
|
||||||
import { HorizontalGroup, Input, RadioButtonGroup } from '@grafana/ui';
|
import { HorizontalGroup, Input, RadioButtonGroup, ScaleDistribution } from '@grafana/ui';
|
||||||
|
|
||||||
import { HeatmapCalculationAxisConfig, HeatmapCalculationMode } from '../models.gen';
|
import { HeatmapCalculationBucketConfig, HeatmapCalculationMode } from '../models.gen';
|
||||||
|
|
||||||
const modeOptions: Array<SelectableValue<HeatmapCalculationMode>> = [
|
const modeOptions: Array<SelectableValue<HeatmapCalculationMode>> = [
|
||||||
{
|
{
|
||||||
@ -18,7 +18,20 @@ const modeOptions: Array<SelectableValue<HeatmapCalculationMode>> = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const AxisEditor: React.FC<StandardEditorProps<HeatmapCalculationAxisConfig, any>> = ({
|
const logModeOptions: Array<SelectableValue<HeatmapCalculationMode>> = [
|
||||||
|
{
|
||||||
|
label: 'Split',
|
||||||
|
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<StandardEditorProps<HeatmapCalculationBucketConfig, any>> = ({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
item,
|
item,
|
||||||
@ -27,7 +40,7 @@ export const AxisEditor: React.FC<StandardEditorProps<HeatmapCalculationAxisConf
|
|||||||
<HorizontalGroup>
|
<HorizontalGroup>
|
||||||
<RadioButtonGroup
|
<RadioButtonGroup
|
||||||
value={value?.mode || HeatmapCalculationMode.Size}
|
value={value?.mode || HeatmapCalculationMode.Size}
|
||||||
options={modeOptions}
|
options={value?.scale?.type === ScaleDistribution.Log ? logModeOptions : modeOptions}
|
||||||
onChange={(mode) => {
|
onChange={(mode) => {
|
||||||
onChange({
|
onChange({
|
||||||
...value,
|
...value,
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import { PanelOptionsEditorBuilder } from '@grafana/data';
|
import { PanelOptionsEditorBuilder } from '@grafana/data';
|
||||||
|
import { ScaleDistribution } from '@grafana/schema';
|
||||||
|
import { ScaleDistributionEditor } from '@grafana/ui/src/options/builder';
|
||||||
|
|
||||||
import { HeatmapCalculationMode, HeatmapCalculationOptions } from '../models.gen';
|
import { HeatmapCalculationMode, HeatmapCalculationOptions } from '../models.gen';
|
||||||
|
|
||||||
@ -11,9 +13,9 @@ export function addHeatmapCalculationOptions(
|
|||||||
category?: string[]
|
category?: string[]
|
||||||
) {
|
) {
|
||||||
builder.addCustomEditor({
|
builder.addCustomEditor({
|
||||||
id: 'xAxis',
|
id: 'xBuckets',
|
||||||
path: `${prefix}xAxis`,
|
path: `${prefix}xBuckets`,
|
||||||
name: 'X Buckets',
|
name: 'X Bucket',
|
||||||
editor: AxisEditor,
|
editor: AxisEditor,
|
||||||
category,
|
category,
|
||||||
defaultValue: {
|
defaultValue: {
|
||||||
@ -22,13 +24,22 @@ export function addHeatmapCalculationOptions(
|
|||||||
});
|
});
|
||||||
|
|
||||||
builder.addCustomEditor({
|
builder.addCustomEditor({
|
||||||
id: 'yAxis',
|
id: 'yBuckets',
|
||||||
path: `${prefix}yAxis`,
|
path: `${prefix}yBuckets`,
|
||||||
name: 'Y Buckets',
|
name: 'Y Bucket',
|
||||||
editor: AxisEditor,
|
editor: AxisEditor,
|
||||||
category,
|
category,
|
||||||
defaultValue: {
|
defaultValue: {
|
||||||
mode: HeatmapCalculationMode.Size,
|
mode: HeatmapCalculationMode.Size,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
builder.addCustomEditor({
|
||||||
|
id: 'yBuckets-scale',
|
||||||
|
path: `${prefix}yBuckets.scale`,
|
||||||
|
name: 'Y Bucket scale',
|
||||||
|
category,
|
||||||
|
editor: ScaleDistributionEditor,
|
||||||
|
defaultValue: { type: ScaleDistribution.Linear },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { FieldType } from '@grafana/data';
|
import { FieldType } from '@grafana/data';
|
||||||
import { toDataFrame } from '@grafana/data/src/dataframe/processDataFrame';
|
import { toDataFrame } from '@grafana/data/src/dataframe/processDataFrame';
|
||||||
|
|
||||||
import { calculateHeatmapFromData } from './heatmap';
|
import { bucketsToScanlines, calculateHeatmapFromData } from './heatmap';
|
||||||
import { HeatmapCalculationOptions } from './models.gen';
|
import { HeatmapCalculationOptions } from './models.gen';
|
||||||
|
|
||||||
describe('Heatmap transformer', () => {
|
describe('Heatmap transformer', () => {
|
||||||
@ -13,12 +13,100 @@ describe('Heatmap transformer', () => {
|
|||||||
const data = toDataFrame({
|
const data = toDataFrame({
|
||||||
fields: [
|
fields: [
|
||||||
{ name: 'time', type: FieldType.time, values: [1, 2, 3, 4] },
|
{ name: 'time', type: FieldType.time, values: [1, 2, 3, 4] },
|
||||||
{ name: 'temp', type: FieldType.number, values: [1.1, 2.2, 3.3, 4.4] },
|
{ name: 'temp', type: FieldType.number, config: { unit: 'm2' }, values: [1.1, 2.2, 3.3, 4.4] },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const heatmap = calculateHeatmapFromData([data], options);
|
const heatmap = calculateHeatmapFromData([data], options);
|
||||||
|
expect(heatmap.fields.map((f) => ({ name: f.name, type: f.type, config: f.config }))).toMatchInlineSnapshot(`
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"config": Object {},
|
||||||
|
"name": "xMin",
|
||||||
|
"type": "time",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"config": Object {
|
||||||
|
"custom": Object {
|
||||||
|
"scaleDistribution": Object {
|
||||||
|
"type": "linear",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"unit": "m2",
|
||||||
|
},
|
||||||
|
"name": "yMin",
|
||||||
|
"type": "number",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"config": Object {
|
||||||
|
"unit": "short",
|
||||||
|
},
|
||||||
|
"name": "Count",
|
||||||
|
"type": "number",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
expect(heatmap).toBeDefined();
|
it('convert heatmap buckets to scanlines', async () => {
|
||||||
|
const frame = toDataFrame({
|
||||||
|
fields: [
|
||||||
|
{ name: 'time', type: FieldType.time, values: [1, 2, 3] },
|
||||||
|
{ name: 'A', type: FieldType.number, config: { unit: 'm2' }, values: [1.1, 1.2, 1.3] },
|
||||||
|
{ name: 'B', type: FieldType.number, config: { unit: 'm2' }, values: [2.1, 2.2, 2.3] },
|
||||||
|
{ name: 'C', type: FieldType.number, config: { unit: 'm2' }, values: [3.1, 3.2, 3.3] },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const heatmap = bucketsToScanlines({ frame, name: 'Speed' });
|
||||||
|
expect(heatmap.fields.map((f) => ({ name: f.name, type: f.type, config: f.config }))).toMatchInlineSnapshot(`
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"config": Object {},
|
||||||
|
"name": "xMax",
|
||||||
|
"type": "time",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"config": Object {
|
||||||
|
"unit": "short",
|
||||||
|
},
|
||||||
|
"name": "y",
|
||||||
|
"type": "number",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"config": Object {
|
||||||
|
"unit": "m2",
|
||||||
|
},
|
||||||
|
"name": "Speed",
|
||||||
|
"type": "number",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
expect(heatmap.meta).toMatchInlineSnapshot(`
|
||||||
|
Object {
|
||||||
|
"custom": Object {
|
||||||
|
"yMatchWithLabel": undefined,
|
||||||
|
"yOrdinalDisplay": Array [
|
||||||
|
"A",
|
||||||
|
"B",
|
||||||
|
"C",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"type": "heatmap-scanlines",
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
expect(heatmap.fields[1].values.toArray()).toMatchInlineSnapshot(`
|
||||||
|
Array [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
]
|
||||||
|
`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -12,8 +12,9 @@ import {
|
|||||||
getFieldDisplayName,
|
getFieldDisplayName,
|
||||||
Field,
|
Field,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
|
import { ScaleDistribution } from '@grafana/schema';
|
||||||
|
|
||||||
import { HeatmapCalculationMode, HeatmapCalculationOptions } from './models.gen';
|
import { HeatmapBucketLayout, HeatmapCalculationMode, HeatmapCalculationOptions } from './models.gen';
|
||||||
import { niceLinearIncrs, niceTimeIncrs } from './utils';
|
import { niceLinearIncrs, niceTimeIncrs } from './utils';
|
||||||
|
|
||||||
export interface HeatmapTransformerOptions extends HeatmapCalculationOptions {
|
export interface HeatmapTransformerOptions extends HeatmapCalculationOptions {
|
||||||
@ -48,21 +49,39 @@ export function sortAscStrInf(aName?: string | null, bName?: string | null) {
|
|||||||
return parseNumeric(aName) - parseNumeric(bName);
|
return parseNumeric(aName) - parseNumeric(bName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface HeatmapScanlinesCustomMeta {
|
||||||
|
/** This provides the lookup values */
|
||||||
|
yOrdinalDisplay: string[];
|
||||||
|
yOrdinalLabel?: string[];
|
||||||
|
yMatchWithLabel?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** simple utility to get heatmap metadata from a frame */
|
||||||
|
export function readHeatmapScanlinesCustomMeta(frame?: DataFrame): HeatmapScanlinesCustomMeta {
|
||||||
|
return (frame?.meta?.custom ?? {}) as HeatmapScanlinesCustomMeta;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BucketsOptions {
|
||||||
|
frame: DataFrame;
|
||||||
|
name?: string;
|
||||||
|
layout?: HeatmapBucketLayout;
|
||||||
|
}
|
||||||
|
|
||||||
/** Given existing buckets, create a values style frame */
|
/** Given existing buckets, create a values style frame */
|
||||||
// Assumes frames have already been sorted ASC and de-accumulated.
|
// Assumes frames have already been sorted ASC and de-accumulated.
|
||||||
export function bucketsToScanlines(frame: DataFrame): DataFrame {
|
export function bucketsToScanlines(opts: BucketsOptions): DataFrame {
|
||||||
// TODO: handle null-filling w/ fields[0].config.interval?
|
// TODO: handle null-filling w/ fields[0].config.interval?
|
||||||
const xField = frame.fields[0];
|
const xField = opts.frame.fields[0];
|
||||||
const xValues = xField.values.toArray();
|
const xValues = xField.values.toArray();
|
||||||
const yField = frame.fields[1];
|
const yFields = opts.frame.fields.filter((f, idx) => f.type === FieldType.number && idx > 0);
|
||||||
|
|
||||||
// similar to initBins() below
|
// similar to initBins() below
|
||||||
const len = xValues.length * (frame.fields.length - 1);
|
const len = xValues.length * yFields.length;
|
||||||
const xs = new Array(len);
|
const xs = new Array(len);
|
||||||
const ys = new Array(len);
|
const ys = new Array(len);
|
||||||
const counts2 = new Array(len);
|
const counts2 = new Array(len);
|
||||||
|
|
||||||
const counts = frame.fields.slice(1).map((field) => field.values.toArray().slice());
|
const counts = yFields.map((field) => field.values.toArray().slice());
|
||||||
|
|
||||||
// transpose
|
// transpose
|
||||||
counts.forEach((bucketCounts, bi) => {
|
counts.forEach((bucketCounts, bi) => {
|
||||||
@ -71,7 +90,7 @@ export function bucketsToScanlines(frame: DataFrame): DataFrame {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const bucketBounds = Array.from({ length: frame.fields.length - 1 }, (v, i) => i);
|
const bucketBounds = Array.from({ length: yFields.length }, (v, i) => i);
|
||||||
|
|
||||||
// fill flat/repeating array
|
// fill flat/repeating array
|
||||||
for (let i = 0, yi = 0, xi = 0; i < len; yi = ++i % bucketBounds.length) {
|
for (let i = 0, yi = 0, xi = 0; i < len; yi = ++i % bucketBounds.length) {
|
||||||
@ -84,10 +103,33 @@ export function bucketsToScanlines(frame: DataFrame): DataFrame {
|
|||||||
xs[i] = xValues[xi];
|
xs[i] = xValues[xi];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// this name determines whether cells are drawn above, below, or centered on the values
|
||||||
|
let ordinalFieldName = yFields[0].labels?.le != null ? 'yMax' : 'y';
|
||||||
|
switch (opts.layout) {
|
||||||
|
case HeatmapBucketLayout.le:
|
||||||
|
ordinalFieldName = 'yMax';
|
||||||
|
break;
|
||||||
|
case HeatmapBucketLayout.ge:
|
||||||
|
ordinalFieldName = 'yMin';
|
||||||
|
break;
|
||||||
|
case HeatmapBucketLayout.unknown:
|
||||||
|
ordinalFieldName = 'y';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const custom: HeatmapScanlinesCustomMeta = {
|
||||||
|
yOrdinalDisplay: yFields.map((f) => getFieldDisplayName(f, opts.frame)),
|
||||||
|
yMatchWithLabel: Object.keys(yFields[0].labels ?? {})[0],
|
||||||
|
};
|
||||||
|
if (custom.yMatchWithLabel) {
|
||||||
|
custom.yOrdinalLabel = yFields.map((f) => f.labels?.[custom.yMatchWithLabel!] ?? '');
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
length: xs.length,
|
length: xs.length,
|
||||||
|
refId: opts.frame.refId,
|
||||||
meta: {
|
meta: {
|
||||||
type: DataFrameType.HeatmapScanlines,
|
type: DataFrameType.HeatmapScanlines,
|
||||||
|
custom,
|
||||||
},
|
},
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
@ -97,19 +139,19 @@ export function bucketsToScanlines(frame: DataFrame): DataFrame {
|
|||||||
config: xField.config,
|
config: xField.config,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// this name determines whether cells are drawn above, below, or centered on the values
|
name: ordinalFieldName,
|
||||||
name: yField.labels?.le != null ? 'yMax' : 'y',
|
|
||||||
type: FieldType.number,
|
type: FieldType.number,
|
||||||
values: new ArrayVector(ys),
|
values: new ArrayVector(ys),
|
||||||
config: yField.config,
|
config: {
|
||||||
|
unit: 'short', // ordinal lookup
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'count',
|
name: opts.name?.length ? opts.name : 'Value',
|
||||||
type: FieldType.number,
|
type: FieldType.number,
|
||||||
values: new ArrayVector(counts2),
|
values: new ArrayVector(counts2),
|
||||||
config: {
|
config: yFields[0].config,
|
||||||
unit: 'short',
|
display: yFields[0].display,
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@ -195,13 +237,24 @@ export function calculateHeatmapFromData(frames: DataFrame[], options: HeatmapCa
|
|||||||
throw 'no values found';
|
throw 'no values found';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const xBucketsCfg = options.xBuckets ?? {};
|
||||||
|
const yBucketsCfg = options.yBuckets ?? {};
|
||||||
|
|
||||||
|
if (xBucketsCfg.scale?.type === ScaleDistribution.Log) {
|
||||||
|
throw 'X axis only supports linear buckets';
|
||||||
|
}
|
||||||
|
|
||||||
|
const scaleDistribution = options.yBuckets?.scale ?? {
|
||||||
|
type: ScaleDistribution.Linear,
|
||||||
|
};
|
||||||
const heat2d = heatmap(xs, ys, {
|
const heat2d = heatmap(xs, ys, {
|
||||||
xSorted: true,
|
xSorted: true,
|
||||||
xTime: xField.type === FieldType.time,
|
xTime: xField.type === FieldType.time,
|
||||||
xMode: options.xAxis?.mode,
|
xMode: xBucketsCfg.mode,
|
||||||
xSize: +(options.xAxis?.value ?? 0),
|
xSize: xBucketsCfg.value ? +xBucketsCfg.value : undefined,
|
||||||
yMode: options.yAxis?.mode,
|
yMode: yBucketsCfg.mode,
|
||||||
ySize: +(options.yAxis?.value ?? 0),
|
ySize: yBucketsCfg.value ? +yBucketsCfg.value : undefined,
|
||||||
|
yLog: scaleDistribution?.type === ScaleDistribution.Log ? (scaleDistribution?.log as any) : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const frame = {
|
const frame = {
|
||||||
@ -221,10 +274,15 @@ export function calculateHeatmapFromData(frames: DataFrame[], options: HeatmapCa
|
|||||||
name: 'yMin',
|
name: 'yMin',
|
||||||
type: FieldType.number,
|
type: FieldType.number,
|
||||||
values: new ArrayVector(heat2d.y),
|
values: new ArrayVector(heat2d.y),
|
||||||
config: yField.config, // keep units from the original source
|
config: {
|
||||||
|
...yField.config, // keep units from the original source
|
||||||
|
custom: {
|
||||||
|
scaleDistribution,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'count',
|
name: 'Count',
|
||||||
type: FieldType.number,
|
type: FieldType.number,
|
||||||
values: new ArrayVector(heat2d.count),
|
values: new ArrayVector(heat2d.count),
|
||||||
config: {
|
config: {
|
||||||
@ -294,6 +352,12 @@ function heatmap(xs: number[], ys: number[], opts?: HeatmapOpts) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let yExp = opts?.yLog;
|
||||||
|
|
||||||
|
if (yExp && (minY <= 0 || maxY <= 0)) {
|
||||||
|
throw 'Log Y axes cannot have values <= 0';
|
||||||
|
}
|
||||||
|
|
||||||
//let scaleX = opts?.xLog === 10 ? Math.log10 : opts?.xLog === 2 ? Math.log2 : (v: number) => v;
|
//let scaleX = opts?.xLog === 10 ? Math.log10 : opts?.xLog === 2 ? Math.log2 : (v: number) => v;
|
||||||
//let scaleY = opts?.yLog === 10 ? Math.log10 : opts?.yLog === 2 ? Math.log2 : (v: number) => v;
|
//let scaleY = opts?.yLog === 10 ? Math.log10 : opts?.yLog === 2 ? Math.log2 : (v: number) => v;
|
||||||
|
|
||||||
@ -338,6 +402,12 @@ function heatmap(xs: number[], ys: number[], opts?: HeatmapOpts) {
|
|||||||
let binX = opts?.xCeil ? (v: number) => incrRoundUp(v, xBinIncr) : (v: number) => incrRoundDn(v, xBinIncr);
|
let binX = opts?.xCeil ? (v: number) => incrRoundUp(v, xBinIncr) : (v: number) => incrRoundDn(v, xBinIncr);
|
||||||
let binY = opts?.yCeil ? (v: number) => incrRoundUp(v, yBinIncr) : (v: number) => incrRoundDn(v, yBinIncr);
|
let binY = opts?.yCeil ? (v: number) => incrRoundUp(v, yBinIncr) : (v: number) => incrRoundDn(v, yBinIncr);
|
||||||
|
|
||||||
|
if (yExp) {
|
||||||
|
yBinIncr = 1 / (opts?.ySize ?? 1); // sub-divides log exponents
|
||||||
|
let yLog = yExp === 2 ? Math.log2 : Math.log10;
|
||||||
|
binY = opts?.yCeil ? (v: number) => incrRoundUp(yLog(v), yBinIncr) : (v: number) => incrRoundDn(yLog(v), yBinIncr);
|
||||||
|
}
|
||||||
|
|
||||||
let minXBin = binX(minX);
|
let minXBin = binX(minX);
|
||||||
let maxXBin = binX(maxX);
|
let maxXBin = binX(maxX);
|
||||||
let minYBin = binY(minY);
|
let minYBin = binY(minY);
|
||||||
@ -346,7 +416,7 @@ function heatmap(xs: number[], ys: number[], opts?: HeatmapOpts) {
|
|||||||
let xBinQty = Math.round((maxXBin - minXBin) / xBinIncr) + 1;
|
let xBinQty = Math.round((maxXBin - minXBin) / xBinIncr) + 1;
|
||||||
let yBinQty = Math.round((maxYBin - minYBin) / yBinIncr) + 1;
|
let yBinQty = Math.round((maxYBin - minYBin) / yBinIncr) + 1;
|
||||||
|
|
||||||
let [xs2, ys2, counts] = initBins(xBinQty, yBinQty, minXBin, xBinIncr, minYBin, yBinIncr);
|
let [xs2, ys2, counts] = initBins(xBinQty, yBinQty, minXBin, xBinIncr, minYBin, yBinIncr, yExp);
|
||||||
|
|
||||||
for (let i = 0; i < len; i++) {
|
for (let i = 0; i < len; i++) {
|
||||||
const xi = (binX(xs[i]) - minXBin) / xBinIncr;
|
const xi = (binX(xs[i]) - minXBin) / xBinIncr;
|
||||||
@ -363,7 +433,7 @@ function heatmap(xs: number[], ys: number[], opts?: HeatmapOpts) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function initBins(xQty: number, yQty: number, xMin: number, xIncr: number, yMin: number, yIncr: number) {
|
function initBins(xQty: number, yQty: number, xMin: number, xIncr: number, yMin: number, yIncr: number, yExp?: number) {
|
||||||
const len = xQty * yQty;
|
const len = xQty * yQty;
|
||||||
const xs = new Array<number>(len);
|
const xs = new Array<number>(len);
|
||||||
const ys = new Array<number>(len);
|
const ys = new Array<number>(len);
|
||||||
@ -371,7 +441,12 @@ function initBins(xQty: number, yQty: number, xMin: number, xIncr: number, yMin:
|
|||||||
|
|
||||||
for (let i = 0, yi = 0, x = xMin; i < len; yi = ++i % yQty) {
|
for (let i = 0, yi = 0, x = xMin; i < len; yi = ++i % yQty) {
|
||||||
counts[i] = 0;
|
counts[i] = 0;
|
||||||
ys[i] = yMin + yi * yIncr;
|
|
||||||
|
if (yExp) {
|
||||||
|
ys[i] = yExp ** (yMin + yi * yIncr);
|
||||||
|
} else {
|
||||||
|
ys[i] = yMin + yi * yIncr;
|
||||||
|
}
|
||||||
|
|
||||||
if (yi === 0 && i >= yQty) {
|
if (yi === 0 && i >= yQty) {
|
||||||
x += xIncr;
|
x += xIncr;
|
||||||
|
@ -1,18 +1,24 @@
|
|||||||
import { DataFrameType } from '@grafana/data';
|
import { ScaleDistributionConfig } from '@grafana/schema';
|
||||||
|
|
||||||
export enum HeatmapCalculationMode {
|
export enum HeatmapCalculationMode {
|
||||||
Size = 'size',
|
Size = 'size', // When exponential, this is "splitFactor"
|
||||||
Count = 'count',
|
Count = 'count',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HeatmapCalculationAxisConfig {
|
export const enum HeatmapBucketLayout {
|
||||||
|
le = 'le',
|
||||||
|
ge = 'ge',
|
||||||
|
unknown = 'unknown', // unknown
|
||||||
|
auto = 'auto', // becomes unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HeatmapCalculationBucketConfig {
|
||||||
mode?: HeatmapCalculationMode;
|
mode?: HeatmapCalculationMode;
|
||||||
value?: string; // number or interval string ie 10s
|
value?: string; // number or interval string ie 10s, or log "split" divisor
|
||||||
|
scale?: ScaleDistributionConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HeatmapCalculationOptions {
|
export interface HeatmapCalculationOptions {
|
||||||
xAxis?: HeatmapCalculationAxisConfig;
|
xBuckets?: HeatmapCalculationBucketConfig;
|
||||||
yAxis?: HeatmapCalculationAxisConfig;
|
yBuckets?: HeatmapCalculationBucketConfig;
|
||||||
xAxisField?: string; // name of the x field
|
|
||||||
encoding?: DataFrameType.HeatmapBuckets | DataFrameType.HeatmapScanlines;
|
|
||||||
}
|
}
|
||||||
|
@ -3,10 +3,12 @@ import React, { useEffect, useRef } from 'react';
|
|||||||
import { DataFrameType, Field, FieldType, formattedValueToString, getFieldDisplayName, LinkModel } from '@grafana/data';
|
import { DataFrameType, Field, FieldType, formattedValueToString, getFieldDisplayName, LinkModel } from '@grafana/data';
|
||||||
import { LinkButton, VerticalGroup } from '@grafana/ui';
|
import { LinkButton, VerticalGroup } from '@grafana/ui';
|
||||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||||
|
import { readHeatmapScanlinesCustomMeta } from 'app/features/transformers/calculateHeatmap/heatmap';
|
||||||
|
import { HeatmapBucketLayout } from 'app/features/transformers/calculateHeatmap/models.gen';
|
||||||
|
|
||||||
import { DataHoverView } from '../geomap/components/DataHoverView';
|
import { DataHoverView } from '../geomap/components/DataHoverView';
|
||||||
|
|
||||||
import { BucketLayout, HeatmapData } from './fields';
|
import { HeatmapData } from './fields';
|
||||||
import { HeatmapHoverEvent } from './utils';
|
import { HeatmapHoverEvent } from './utils';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -44,26 +46,15 @@ const HeatmapHoverCell = ({ data, hover, showHistogram }: Props) => {
|
|||||||
const yVals = yField?.values.toArray();
|
const yVals = yField?.values.toArray();
|
||||||
const countVals = countField?.values.toArray();
|
const countVals = countField?.values.toArray();
|
||||||
|
|
||||||
let yDispSrc, yDisp;
|
|
||||||
|
|
||||||
// labeled buckets
|
// labeled buckets
|
||||||
if (data.yAxisValues) {
|
const meta = readHeatmapScanlinesCustomMeta(data.heatmap);
|
||||||
yDispSrc = data.yAxisValues;
|
const yDispSrc = meta.yOrdinalDisplay ?? yVals;
|
||||||
yDisp = (v: any) => v;
|
const yDisp = yField?.display ? (v: any) => formattedValueToString(yField.display!(v)) : (v: any) => `${v}`;
|
||||||
} else {
|
|
||||||
yDispSrc = yVals;
|
|
||||||
yDisp = (v: any) => {
|
|
||||||
if (yField?.display) {
|
|
||||||
return formattedValueToString(yField.display(v));
|
|
||||||
}
|
|
||||||
return `${v}`;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const yValueIdx = index % data.yBucketCount! ?? 0;
|
const yValueIdx = index % data.yBucketCount! ?? 0;
|
||||||
|
|
||||||
const yMinIdx = data.yLayout === BucketLayout.le ? yValueIdx - 1 : yValueIdx;
|
const yMinIdx = data.yLayout === HeatmapBucketLayout.le ? yValueIdx - 1 : yValueIdx;
|
||||||
const yMaxIdx = data.yLayout === BucketLayout.le ? yValueIdx : yValueIdx + 1;
|
const yMaxIdx = data.yLayout === HeatmapBucketLayout.le ? yValueIdx : yValueIdx + 1;
|
||||||
|
|
||||||
const yBucketMin = yDispSrc?.[yMinIdx];
|
const yBucketMin = yDispSrc?.[yMinIdx];
|
||||||
const yBucketMax = yDispSrc?.[yMaxIdx];
|
const yBucketMax = yDispSrc?.[yMaxIdx];
|
||||||
@ -171,6 +162,18 @@ const HeatmapHoverCell = ({ data, hover, showHistogram }: Props) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const renderYBuckets = () => {
|
||||||
|
switch (data.yLayout) {
|
||||||
|
case HeatmapBucketLayout.unknown:
|
||||||
|
return <div>{yDisp(yBucketMin)}</div>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
Bucket: {yDisp(yBucketMin)} - {yDisp(yBucketMax)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
@ -186,15 +189,9 @@ const HeatmapHoverCell = ({ data, hover, showHistogram }: Props) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
{data.yLayout === BucketLayout.unknown ? (
|
{renderYBuckets()}
|
||||||
<div>{yDisp(yBucketMin)}</div>
|
|
||||||
) : (
|
|
||||||
<div>
|
|
||||||
Bucket: {yDisp(yBucketMin)} - {yDisp(yBucketMax)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div>
|
<div>
|
||||||
{getFieldDisplayName(countField!, data.heatmap)}: {count}
|
{getFieldDisplayName(countField!, data.heatmap)}: {data.display!(count)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{links.length > 0 && (
|
{links.length > 0 && (
|
||||||
|
@ -3,9 +3,19 @@ import React, { useCallback, useMemo, useRef, useState } from 'react';
|
|||||||
|
|
||||||
import { DataFrameType, GrafanaTheme2, PanelProps, reduceField, ReducerID, TimeRange } from '@grafana/data';
|
import { DataFrameType, GrafanaTheme2, PanelProps, reduceField, ReducerID, TimeRange } from '@grafana/data';
|
||||||
import { PanelDataErrorView } from '@grafana/runtime';
|
import { PanelDataErrorView } from '@grafana/runtime';
|
||||||
import { Portal, UPlotChart, useStyles2, useTheme2, VizLayout, VizTooltipContainer } from '@grafana/ui';
|
import { ScaleDistributionConfig } from '@grafana/schema';
|
||||||
|
import {
|
||||||
|
Portal,
|
||||||
|
ScaleDistribution,
|
||||||
|
UPlotChart,
|
||||||
|
useStyles2,
|
||||||
|
useTheme2,
|
||||||
|
VizLayout,
|
||||||
|
VizTooltipContainer,
|
||||||
|
} from '@grafana/ui';
|
||||||
import { CloseButton } from 'app/core/components/CloseButton/CloseButton';
|
import { CloseButton } from 'app/core/components/CloseButton/CloseButton';
|
||||||
import { ColorScale } from 'app/core/components/ColorScale/ColorScale';
|
import { ColorScale } from 'app/core/components/ColorScale/ColorScale';
|
||||||
|
import { readHeatmapScanlinesCustomMeta } from 'app/features/transformers/calculateHeatmap/heatmap';
|
||||||
|
|
||||||
import { HeatmapHoverView } from './HeatmapHoverView';
|
import { HeatmapHoverView } from './HeatmapHoverView';
|
||||||
import { prepareHeatmapData } from './fields';
|
import { prepareHeatmapData } from './fields';
|
||||||
@ -46,24 +56,25 @@ export const HeatmapPanel: React.FC<HeatmapPanelProps> = ({
|
|||||||
let exemplarsXFacet: number[] = []; // "Time" field
|
let exemplarsXFacet: number[] = []; // "Time" field
|
||||||
let exemplarsyFacet: number[] = [];
|
let exemplarsyFacet: number[] = [];
|
||||||
|
|
||||||
if (info.exemplars && info.matchByLabel) {
|
const meta = readHeatmapScanlinesCustomMeta(info.heatmap);
|
||||||
|
if (info.exemplars && meta.yMatchWithLabel) {
|
||||||
exemplarsXFacet = info.exemplars?.fields[0].values.toArray();
|
exemplarsXFacet = info.exemplars?.fields[0].values.toArray();
|
||||||
|
|
||||||
// ordinal/labeled heatmap-buckets?
|
// ordinal/labeled heatmap-buckets?
|
||||||
const hasLabeledY = info.yLabelValues != null;
|
const hasLabeledY = meta.yOrdinalDisplay != null;
|
||||||
|
|
||||||
if (hasLabeledY) {
|
if (hasLabeledY) {
|
||||||
let matchExemplarsBy = info.exemplars?.fields
|
let matchExemplarsBy = info.exemplars?.fields
|
||||||
.find((field) => field.name === info.matchByLabel)!
|
.find((field) => field.name === meta.yMatchWithLabel)!
|
||||||
.values.toArray();
|
.values.toArray();
|
||||||
exemplarsyFacet = matchExemplarsBy.map((label) => info.yLabelValues?.indexOf(label)) as number[];
|
exemplarsyFacet = matchExemplarsBy.map((label) => meta.yOrdinalLabel?.indexOf(label)) as number[];
|
||||||
} else {
|
} else {
|
||||||
exemplarsyFacet = info.exemplars?.fields[1].values.toArray() as number[]; // "Value" field
|
exemplarsyFacet = info.exemplars?.fields[1].values.toArray() as number[]; // "Value" field
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return [null, info.heatmap?.fields.map((f) => f.values.toArray()), [exemplarsXFacet, exemplarsyFacet]];
|
return [null, info.heatmap?.fields.map((f) => f.values.toArray()), [exemplarsXFacet, exemplarsyFacet]];
|
||||||
}, [info.heatmap, info.exemplars, info.yLabelValues, info.matchByLabel]);
|
}, [info.heatmap, info.exemplars]);
|
||||||
|
|
||||||
const palette = useMemo(() => quantizeScheme(options.color, theme), [options.color, theme]);
|
const palette = useMemo(() => quantizeScheme(options.color, theme), [options.color, theme]);
|
||||||
|
|
||||||
@ -97,6 +108,8 @@ export const HeatmapPanel: React.FC<HeatmapPanelProps> = ({
|
|||||||
dataRef.current = info;
|
dataRef.current = info;
|
||||||
|
|
||||||
const builder = useMemo(() => {
|
const builder = useMemo(() => {
|
||||||
|
const scaleConfig = dataRef.current?.heatmap?.fields[1].config?.custom
|
||||||
|
?.scaleDistribution as ScaleDistributionConfig;
|
||||||
return prepConfig({
|
return prepConfig({
|
||||||
dataRef,
|
dataRef,
|
||||||
theme,
|
theme,
|
||||||
@ -113,9 +126,10 @@ export const HeatmapPanel: React.FC<HeatmapPanelProps> = ({
|
|||||||
getTimeRange: () => timeRangeRef.current,
|
getTimeRange: () => timeRangeRef.current,
|
||||||
palette,
|
palette,
|
||||||
cellGap: options.cellGap,
|
cellGap: options.cellGap,
|
||||||
hideThreshold: options.hideThreshold,
|
hideThreshold: options.filterValues?.min, // eventually a better range
|
||||||
exemplarColor: options.exemplars?.color ?? 'rgba(255,0,255,0.7)',
|
exemplarColor: options.exemplars?.color ?? 'rgba(255,0,255,0.7)',
|
||||||
yAxisReverse: options.yAxisReverse,
|
yAxisConfig: options.yAxis,
|
||||||
|
ySizeDivisor: scaleConfig?.type === ScaleDistribution.Log ? +(options.calculation?.yBuckets?.value || 1) : 1,
|
||||||
});
|
});
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [options, data.structureRev]);
|
}, [options, data.structureRev]);
|
||||||
|
@ -1,42 +1,31 @@
|
|||||||
import {
|
import {
|
||||||
DataFrame,
|
DataFrame,
|
||||||
DataFrameType,
|
DataFrameType,
|
||||||
FieldType,
|
|
||||||
formattedValueToString,
|
formattedValueToString,
|
||||||
getDisplayProcessor,
|
getDisplayProcessor,
|
||||||
getFieldDisplayName,
|
|
||||||
getValueFormat,
|
getValueFormat,
|
||||||
GrafanaTheme2,
|
GrafanaTheme2,
|
||||||
outerJoinDataFrames,
|
outerJoinDataFrames,
|
||||||
PanelData,
|
PanelData,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { calculateHeatmapFromData, bucketsToScanlines } from 'app/features/transformers/calculateHeatmap/heatmap';
|
import { calculateHeatmapFromData, bucketsToScanlines } from 'app/features/transformers/calculateHeatmap/heatmap';
|
||||||
|
import { HeatmapBucketLayout } from 'app/features/transformers/calculateHeatmap/models.gen';
|
||||||
|
|
||||||
import { HeatmapMode, PanelOptions } from './models.gen';
|
import { PanelOptions } from './models.gen';
|
||||||
|
|
||||||
export const enum BucketLayout {
|
|
||||||
le = 'le',
|
|
||||||
ge = 'ge',
|
|
||||||
unknown = 'unknown', // unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HeatmapData {
|
export interface HeatmapData {
|
||||||
heatmap?: DataFrame; // data we will render
|
heatmap?: DataFrame; // data we will render
|
||||||
exemplars?: DataFrame; // optionally linked exemplars
|
exemplars?: DataFrame; // optionally linked exemplars
|
||||||
exemplarColor?: string;
|
exemplarColor?: string;
|
||||||
|
|
||||||
yAxisValues?: Array<number | string | null>;
|
|
||||||
yLabelValues?: string[]; // matched ordinally to yAxisValues
|
|
||||||
matchByLabel?: string; // e.g. le, pod, etc.
|
|
||||||
|
|
||||||
xBucketSize?: number;
|
xBucketSize?: number;
|
||||||
yBucketSize?: number;
|
yBucketSize?: number;
|
||||||
|
|
||||||
xBucketCount?: number;
|
xBucketCount?: number;
|
||||||
yBucketCount?: number;
|
yBucketCount?: number;
|
||||||
|
|
||||||
xLayout?: BucketLayout;
|
xLayout?: HeatmapBucketLayout;
|
||||||
yLayout?: BucketLayout;
|
yLayout?: HeatmapBucketLayout;
|
||||||
|
|
||||||
// Print a heatmap cell value
|
// Print a heatmap cell value
|
||||||
display?: (v: number) => string;
|
display?: (v: number) => string;
|
||||||
@ -51,13 +40,11 @@ export function prepareHeatmapData(data: PanelData, options: PanelOptions, theme
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const { mode } = options;
|
|
||||||
|
|
||||||
const exemplars = data.annotations?.find((f) => f.name === 'exemplar');
|
const exemplars = data.annotations?.find((f) => f.name === 'exemplar');
|
||||||
|
|
||||||
if (mode === HeatmapMode.Calculate) {
|
if (options.calculate) {
|
||||||
// TODO, check for error etc
|
// TODO, check for error etc
|
||||||
return getHeatmapData(calculateHeatmapFromData(frames, options.calculate ?? {}), exemplars, theme);
|
return getHeatmapData(calculateHeatmapFromData(frames, options.calculation ?? {}), exemplars, theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for known heatmap types
|
// Check for known heatmap types
|
||||||
@ -88,21 +75,7 @@ export function prepareHeatmapData(data: PanelData, options: PanelOptions, theme
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Some datasources return values in ascending order and require math to know the deltas
|
return getHeatmapData(bucketsToScanlines({ ...options.bucket, frame: bucketHeatmap }), exemplars, theme);
|
||||||
if (mode === HeatmapMode.Accumulated) {
|
|
||||||
console.log('TODO, deaccumulate the values');
|
|
||||||
}
|
|
||||||
|
|
||||||
const yFields = bucketHeatmap.fields.filter((f) => f.type === FieldType.number);
|
|
||||||
const matchByLabel = Object.keys(yFields[0].labels ?? {})[0];
|
|
||||||
|
|
||||||
const scanlinesFrame = bucketsToScanlines(bucketHeatmap);
|
|
||||||
return {
|
|
||||||
matchByLabel,
|
|
||||||
yLabelValues: matchByLabel ? yFields.map((f) => f.labels?.[matchByLabel] ?? '') : undefined,
|
|
||||||
yAxisValues: yFields.map((f) => getFieldDisplayName(f, bucketHeatmap, frames)),
|
|
||||||
...getHeatmapData(scanlinesFrame, exemplars, theme),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSparseHeatmapData = (
|
const getSparseHeatmapData = (
|
||||||
@ -173,8 +146,18 @@ const getHeatmapData = (frame: DataFrame, exemplars: DataFrame | undefined, them
|
|||||||
yBucketCount: yBinQty,
|
yBucketCount: yBinQty,
|
||||||
|
|
||||||
// TODO: improve heuristic
|
// TODO: improve heuristic
|
||||||
xLayout: xName === 'xMax' ? BucketLayout.le : xName === 'xMin' ? BucketLayout.ge : BucketLayout.unknown,
|
xLayout:
|
||||||
yLayout: yName === 'yMax' ? BucketLayout.le : yName === 'yMin' ? BucketLayout.ge : BucketLayout.unknown,
|
xName === 'xMax'
|
||||||
|
? HeatmapBucketLayout.le
|
||||||
|
: xName === 'xMin'
|
||||||
|
? HeatmapBucketLayout.ge
|
||||||
|
: HeatmapBucketLayout.unknown,
|
||||||
|
yLayout:
|
||||||
|
yName === 'yMax'
|
||||||
|
? HeatmapBucketLayout.le
|
||||||
|
: yName === 'yMin'
|
||||||
|
? HeatmapBucketLayout.ge
|
||||||
|
: HeatmapBucketLayout.unknown,
|
||||||
|
|
||||||
display: (v) => formattedValueToString(disp(v)),
|
display: (v) => formattedValueToString(disp(v)),
|
||||||
};
|
};
|
||||||
|
@ -25,14 +25,22 @@ describe('Heatmap Migrations', () => {
|
|||||||
"overrides": Array [],
|
"overrides": Array [],
|
||||||
},
|
},
|
||||||
"options": Object {
|
"options": Object {
|
||||||
"calculate": Object {
|
"bucket": Object {
|
||||||
"xAxis": Object {
|
"layout": "auto",
|
||||||
|
},
|
||||||
|
"calculate": true,
|
||||||
|
"calculation": Object {
|
||||||
|
"xBuckets": Object {
|
||||||
"mode": "count",
|
"mode": "count",
|
||||||
"value": "100",
|
"value": "100",
|
||||||
},
|
},
|
||||||
"yAxis": Object {
|
"yBuckets": Object {
|
||||||
"mode": "count",
|
"mode": "count",
|
||||||
"value": "20",
|
"scale": Object {
|
||||||
|
"log": 2,
|
||||||
|
"type": "log",
|
||||||
|
},
|
||||||
|
"value": "3",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"cellGap": 2,
|
"cellGap": 2,
|
||||||
@ -40,6 +48,8 @@ describe('Heatmap Migrations', () => {
|
|||||||
"color": Object {
|
"color": Object {
|
||||||
"exponent": 0.5,
|
"exponent": 0.5,
|
||||||
"fill": "dark-orange",
|
"fill": "dark-orange",
|
||||||
|
"max": 100,
|
||||||
|
"min": 5,
|
||||||
"mode": "scheme",
|
"mode": "scheme",
|
||||||
"scale": "exponential",
|
"scale": "exponential",
|
||||||
"scheme": "BuGn",
|
"scheme": "BuGn",
|
||||||
@ -48,17 +58,22 @@ describe('Heatmap Migrations', () => {
|
|||||||
"exemplars": Object {
|
"exemplars": Object {
|
||||||
"color": "rgba(255,0,255,0.7)",
|
"color": "rgba(255,0,255,0.7)",
|
||||||
},
|
},
|
||||||
|
"filterValues": Object {
|
||||||
|
"min": 1e-9,
|
||||||
|
},
|
||||||
"legend": Object {
|
"legend": Object {
|
||||||
"show": true,
|
"show": true,
|
||||||
},
|
},
|
||||||
"mode": "calculate",
|
|
||||||
"showValue": "never",
|
"showValue": "never",
|
||||||
"tooltip": Object {
|
"tooltip": Object {
|
||||||
"show": true,
|
"show": true,
|
||||||
"yHistogram": true,
|
"yHistogram": true,
|
||||||
},
|
},
|
||||||
"yAxisLabels": "auto",
|
"yAxis": Object {
|
||||||
"yAxisReverse": false,
|
"axisPlacement": "left",
|
||||||
|
"axisWidth": 400,
|
||||||
|
"reverse": false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
@ -103,8 +118,8 @@ const oldHeatmap = {
|
|||||||
colorScale: 'sqrt',
|
colorScale: 'sqrt',
|
||||||
exponent: 0.5,
|
exponent: 0.5,
|
||||||
colorScheme: 'interpolateBuGn',
|
colorScheme: 'interpolateBuGn',
|
||||||
min: null,
|
min: 5,
|
||||||
max: null,
|
max: 100,
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
show: true,
|
show: true,
|
||||||
@ -119,10 +134,11 @@ const oldHeatmap = {
|
|||||||
show: true,
|
show: true,
|
||||||
format: 'short',
|
format: 'short',
|
||||||
decimals: null,
|
decimals: null,
|
||||||
logBase: 1,
|
logBase: 2,
|
||||||
splitFactor: null,
|
splitFactor: 3,
|
||||||
min: null,
|
min: null,
|
||||||
max: null,
|
max: null,
|
||||||
|
width: '400',
|
||||||
},
|
},
|
||||||
xBucketSize: null,
|
xBucketSize: null,
|
||||||
xBucketNumber: 100,
|
xBucketNumber: 100,
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { FieldConfigSource, PanelModel, PanelTypeChangedHandler } from '@grafana/data';
|
import { FieldConfigSource, PanelModel, PanelTypeChangedHandler } from '@grafana/data';
|
||||||
import { VisibilityMode } from '@grafana/schema';
|
import { AxisPlacement, ScaleDistribution, VisibilityMode } from '@grafana/schema';
|
||||||
import {
|
import {
|
||||||
|
HeatmapBucketLayout,
|
||||||
HeatmapCalculationMode,
|
HeatmapCalculationMode,
|
||||||
HeatmapCalculationOptions,
|
HeatmapCalculationOptions,
|
||||||
} from 'app/features/transformers/calculateHeatmap/models.gen';
|
} from 'app/features/transformers/calculateHeatmap/models.gen';
|
||||||
|
|
||||||
import { HeatmapMode, PanelOptions, defaultPanelOptions, HeatmapColorMode } from './models.gen';
|
import { PanelOptions, defaultPanelOptions, HeatmapColorMode } from './models.gen';
|
||||||
import { colorSchemes } from './palettes';
|
import { colorSchemes } from './palettes';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -29,36 +30,55 @@ export function angularToReactHeatmap(angular: any): { fieldConfig: FieldConfigS
|
|||||||
overrides: [],
|
overrides: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const mode = angular.dataFormat === 'tsbuckets' ? HeatmapMode.Aggregated : HeatmapMode.Calculate;
|
const calculate = angular.dataFormat === 'tsbuckets' ? false : true;
|
||||||
const calculate: HeatmapCalculationOptions = {
|
const calculation: HeatmapCalculationOptions = {
|
||||||
...defaultPanelOptions.calculate,
|
...defaultPanelOptions.calculation,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (mode === HeatmapMode.Calculate) {
|
const oldYAxis = { logBase: 1, ...angular.yAxis };
|
||||||
|
|
||||||
|
if (calculate) {
|
||||||
if (angular.xBucketSize) {
|
if (angular.xBucketSize) {
|
||||||
calculate.xAxis = { mode: HeatmapCalculationMode.Size, value: `${angular.xBucketSize}` };
|
calculation.xBuckets = { mode: HeatmapCalculationMode.Size, value: `${angular.xBucketSize}` };
|
||||||
} else if (angular.xBucketNumber) {
|
} else if (angular.xBucketNumber) {
|
||||||
calculate.xAxis = { mode: HeatmapCalculationMode.Count, value: `${angular.xBucketNumber}` };
|
calculation.xBuckets = { mode: HeatmapCalculationMode.Count, value: `${angular.xBucketNumber}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (angular.yBucketSize) {
|
if (angular.yBucketSize) {
|
||||||
calculate.yAxis = { mode: HeatmapCalculationMode.Size, value: `${angular.yBucketSize}` };
|
calculation.yBuckets = { mode: HeatmapCalculationMode.Size, value: `${angular.yBucketSize}` };
|
||||||
} else if (angular.xBucketNumber) {
|
} else if (angular.xBucketNumber) {
|
||||||
calculate.yAxis = { mode: HeatmapCalculationMode.Count, value: `${angular.yBucketNumber}` };
|
calculation.yBuckets = { mode: HeatmapCalculationMode.Count, value: `${angular.yBucketNumber}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldYAxis.logBase > 1) {
|
||||||
|
calculation.yBuckets = {
|
||||||
|
mode: HeatmapCalculationMode.Count,
|
||||||
|
value: +oldYAxis.splitFactor > 0 ? `${oldYAxis.splitFactor}` : undefined,
|
||||||
|
scale: {
|
||||||
|
type: ScaleDistribution.Log,
|
||||||
|
log: oldYAxis.logBase,
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const options: PanelOptions = {
|
const options: PanelOptions = {
|
||||||
mode,
|
|
||||||
calculate,
|
calculate,
|
||||||
|
calculation,
|
||||||
color: {
|
color: {
|
||||||
...defaultPanelOptions.color,
|
...defaultPanelOptions.color,
|
||||||
steps: 128, // best match with existing colors
|
steps: 128, // best match with existing colors
|
||||||
},
|
},
|
||||||
cellGap: asNumber(angular.cards?.cardPadding),
|
cellGap: asNumber(angular.cards?.cardPadding),
|
||||||
cellSize: asNumber(angular.cards?.cardRound),
|
cellSize: asNumber(angular.cards?.cardRound),
|
||||||
yAxisLabels: angular.yBucketBound,
|
yAxis: {
|
||||||
yAxisReverse: angular.reverseYBuckets,
|
axisPlacement: oldYAxis.show === false ? AxisPlacement.Hidden : AxisPlacement.Left,
|
||||||
|
reverse: Boolean(angular.reverseYBuckets),
|
||||||
|
axisWidth: oldYAxis.width ? +oldYAxis.width : undefined,
|
||||||
|
},
|
||||||
|
bucket: {
|
||||||
|
layout: getHeatmapBucketLayout(angular.yBucketBound),
|
||||||
|
},
|
||||||
legend: {
|
legend: {
|
||||||
show: Boolean(angular.legend.show),
|
show: Boolean(angular.legend.show),
|
||||||
},
|
},
|
||||||
@ -72,6 +92,10 @@ export function angularToReactHeatmap(angular: any): { fieldConfig: FieldConfigS
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (angular.hideZeroBuckets) {
|
||||||
|
options.filterValues = { ...defaultPanelOptions.filterValues }; // min: 1e-9
|
||||||
|
}
|
||||||
|
|
||||||
// Migrate color options
|
// Migrate color options
|
||||||
const color = angular.color;
|
const color = angular.color;
|
||||||
switch (color?.mode) {
|
switch (color?.mode) {
|
||||||
@ -92,10 +116,24 @@ export function angularToReactHeatmap(angular: any): { fieldConfig: FieldConfigS
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
options.color.min = color.min;
|
||||||
|
options.color.max = color.max;
|
||||||
|
|
||||||
return { fieldConfig, options };
|
return { fieldConfig, options };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getHeatmapBucketLayout(v?: string): HeatmapBucketLayout {
|
||||||
|
switch (v) {
|
||||||
|
case 'upper':
|
||||||
|
return HeatmapBucketLayout.ge;
|
||||||
|
case 'lower':
|
||||||
|
return HeatmapBucketLayout.le;
|
||||||
|
case 'middle':
|
||||||
|
return HeatmapBucketLayout.unknown;
|
||||||
|
}
|
||||||
|
return HeatmapBucketLayout.auto;
|
||||||
|
}
|
||||||
|
|
||||||
function asNumber(v: any): number | undefined {
|
function asNumber(v: any): number | undefined {
|
||||||
const num = +v;
|
const num = +v;
|
||||||
return isNaN(num) ? undefined : num;
|
return isNaN(num) ? undefined : num;
|
||||||
|
@ -3,17 +3,11 @@
|
|||||||
// It is currenty hand written but will serve as the target for cuetsy
|
// It is currenty hand written but will serve as the target for cuetsy
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
import { HideableFieldConfig, VisibilityMode } from '@grafana/schema';
|
import { AxisConfig, AxisPlacement, HideableFieldConfig, ScaleDistributionConfig, VisibilityMode } from '@grafana/schema';
|
||||||
import { HeatmapCalculationOptions } from 'app/features/transformers/calculateHeatmap/models.gen';
|
import { HeatmapBucketLayout, HeatmapCalculationOptions } from 'app/features/transformers/calculateHeatmap/models.gen';
|
||||||
|
|
||||||
export const modelVersion = Object.freeze([1, 0]);
|
export const modelVersion = Object.freeze([1, 0]);
|
||||||
|
|
||||||
export enum HeatmapMode {
|
|
||||||
Aggregated = 'agg',
|
|
||||||
Calculate = 'calculate',
|
|
||||||
Accumulated = 'acc', // accumulated
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum HeatmapColorMode {
|
export enum HeatmapColorMode {
|
||||||
Opacity = 'opacity',
|
Opacity = 'opacity',
|
||||||
Scheme = 'scheme',
|
Scheme = 'scheme',
|
||||||
@ -36,6 +30,16 @@ export interface HeatmapColorOptions {
|
|||||||
min?: number;
|
min?: number;
|
||||||
max?: number;
|
max?: number;
|
||||||
}
|
}
|
||||||
|
export interface YAxisConfig extends AxisConfig {
|
||||||
|
unit?: string;
|
||||||
|
reverse?: boolean;
|
||||||
|
decimals?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FilterValueRange {
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface HeatmapTooltip {
|
export interface HeatmapTooltip {
|
||||||
show: boolean;
|
show: boolean;
|
||||||
@ -49,19 +53,24 @@ export interface ExemplarConfig {
|
|||||||
color: string;
|
color: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BucketOptions {
|
||||||
|
name?: string;
|
||||||
|
layout?: HeatmapBucketLayout;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PanelOptions {
|
export interface PanelOptions {
|
||||||
mode: HeatmapMode;
|
calculate?: boolean;
|
||||||
|
calculation?: HeatmapCalculationOptions;
|
||||||
|
|
||||||
color: HeatmapColorOptions;
|
color: HeatmapColorOptions;
|
||||||
calculate?: HeatmapCalculationOptions;
|
filterValues?: FilterValueRange; // was hideZeroBuckets
|
||||||
|
bucket?: BucketOptions;
|
||||||
showValue: VisibilityMode;
|
showValue: VisibilityMode;
|
||||||
|
|
||||||
cellGap?: number; // was cardPadding
|
cellGap?: number; // was cardPadding
|
||||||
cellSize?: number; // was cardRadius
|
cellSize?: number; // was cardRadius
|
||||||
|
|
||||||
hideThreshold?: number; // was hideZeroBuckets
|
yAxis: YAxisConfig;
|
||||||
yAxisLabels?: string;
|
|
||||||
yAxisReverse?: boolean;
|
|
||||||
legend: HeatmapLegend;
|
legend: HeatmapLegend;
|
||||||
|
|
||||||
tooltip: HeatmapTooltip;
|
tooltip: HeatmapTooltip;
|
||||||
@ -69,7 +78,7 @@ export interface PanelOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const defaultPanelOptions: PanelOptions = {
|
export const defaultPanelOptions: PanelOptions = {
|
||||||
mode: HeatmapMode.Aggregated,
|
calculate: false,
|
||||||
color: {
|
color: {
|
||||||
mode: HeatmapColorMode.Scheme,
|
mode: HeatmapColorMode.Scheme,
|
||||||
scheme: 'Oranges',
|
scheme: 'Oranges',
|
||||||
@ -78,6 +87,12 @@ export const defaultPanelOptions: PanelOptions = {
|
|||||||
exponent: 0.5,
|
exponent: 0.5,
|
||||||
steps: 64,
|
steps: 64,
|
||||||
},
|
},
|
||||||
|
bucket: {
|
||||||
|
layout: HeatmapBucketLayout.auto,
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
axisPlacement: AxisPlacement.Left,
|
||||||
|
},
|
||||||
showValue: VisibilityMode.Auto,
|
showValue: VisibilityMode.Auto,
|
||||||
tooltip: {
|
tooltip: {
|
||||||
show: true,
|
show: true,
|
||||||
@ -89,13 +104,14 @@ export const defaultPanelOptions: PanelOptions = {
|
|||||||
exemplars: {
|
exemplars: {
|
||||||
color: 'rgba(255,0,255,0.7)',
|
color: 'rgba(255,0,255,0.7)',
|
||||||
},
|
},
|
||||||
|
filterValues: {
|
||||||
|
min: 1e-9,
|
||||||
|
},
|
||||||
cellGap: 1,
|
cellGap: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface PanelFieldConfig extends HideableFieldConfig {
|
export interface PanelFieldConfig extends HideableFieldConfig {
|
||||||
// TODO points vs lines etc
|
scaleDistribution?: ScaleDistributionConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultPanelFieldConfig: PanelFieldConfig = {
|
export const defaultPanelFieldConfig: PanelFieldConfig = {};
|
||||||
// default to points?
|
|
||||||
};
|
|
||||||
|
@ -1,20 +1,45 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { FieldConfigProperty, PanelPlugin } from '@grafana/data';
|
import { FieldConfigProperty, FieldType, identityOverrideProcessor, PanelPlugin } from '@grafana/data';
|
||||||
import { config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
import { GraphFieldConfig } from '@grafana/schema';
|
import { AxisPlacement, GraphFieldConfig, ScaleDistribution, ScaleDistributionConfig } from '@grafana/schema';
|
||||||
|
import { addHideFrom, ScaleDistributionEditor } from '@grafana/ui/src/options/builder';
|
||||||
import { ColorScale } from 'app/core/components/ColorScale/ColorScale';
|
import { ColorScale } from 'app/core/components/ColorScale/ColorScale';
|
||||||
import { addHeatmapCalculationOptions } from 'app/features/transformers/calculateHeatmap/editor/helper';
|
import { addHeatmapCalculationOptions } from 'app/features/transformers/calculateHeatmap/editor/helper';
|
||||||
|
import { HeatmapBucketLayout } from 'app/features/transformers/calculateHeatmap/models.gen';
|
||||||
|
|
||||||
import { HeatmapPanel } from './HeatmapPanel';
|
import { HeatmapPanel } from './HeatmapPanel';
|
||||||
import { heatmapChangedHandler, heatmapMigrationHandler } from './migrations';
|
import { heatmapChangedHandler, heatmapMigrationHandler } from './migrations';
|
||||||
import { PanelOptions, defaultPanelOptions, HeatmapMode, HeatmapColorMode, HeatmapColorScale } from './models.gen';
|
import { PanelOptions, defaultPanelOptions, HeatmapColorMode, HeatmapColorScale } from './models.gen';
|
||||||
import { colorSchemes, quantizeScheme } from './palettes';
|
import { colorSchemes, quantizeScheme } from './palettes';
|
||||||
import { HeatmapSuggestionsSupplier } from './suggestions';
|
import { HeatmapSuggestionsSupplier } from './suggestions';
|
||||||
|
|
||||||
export const plugin = new PanelPlugin<PanelOptions, GraphFieldConfig>(HeatmapPanel)
|
export const plugin = new PanelPlugin<PanelOptions, GraphFieldConfig>(HeatmapPanel)
|
||||||
.useFieldConfig({
|
.useFieldConfig({
|
||||||
disableStandardOptions: [FieldConfigProperty.Color, FieldConfigProperty.Thresholds],
|
// This keeps: unit, decimals, displayName
|
||||||
|
disableStandardOptions: [
|
||||||
|
FieldConfigProperty.Color,
|
||||||
|
FieldConfigProperty.Thresholds,
|
||||||
|
FieldConfigProperty.Min,
|
||||||
|
FieldConfigProperty.Max,
|
||||||
|
FieldConfigProperty.Mappings,
|
||||||
|
FieldConfigProperty.NoValue,
|
||||||
|
],
|
||||||
|
useCustomConfig: (builder) => {
|
||||||
|
builder.addCustomEditor<void, ScaleDistributionConfig>({
|
||||||
|
id: 'scaleDistribution',
|
||||||
|
path: 'scaleDistribution',
|
||||||
|
name: 'Y axis scale',
|
||||||
|
category: ['Heatmap'],
|
||||||
|
editor: ScaleDistributionEditor as any,
|
||||||
|
override: ScaleDistributionEditor as any,
|
||||||
|
defaultValue: { type: ScaleDistribution.Linear },
|
||||||
|
shouldApply: (f) => f.type === FieldType.number,
|
||||||
|
process: identityOverrideProcessor,
|
||||||
|
hideFromDefaults: true,
|
||||||
|
});
|
||||||
|
addHideFrom(builder); // for tooltip etc
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.setPanelChangeHandler(heatmapChangedHandler)
|
.setPanelChangeHandler(heatmapChangedHandler)
|
||||||
.setMigrationHandler(heatmapMigrationHandler)
|
.setMigrationHandler(heatmapMigrationHandler)
|
||||||
@ -24,23 +49,88 @@ export const plugin = new PanelPlugin<PanelOptions, GraphFieldConfig>(HeatmapPan
|
|||||||
let category = ['Heatmap'];
|
let category = ['Heatmap'];
|
||||||
|
|
||||||
builder.addRadio({
|
builder.addRadio({
|
||||||
path: 'mode',
|
path: 'calculate',
|
||||||
name: 'Data',
|
name: 'Calculate from data',
|
||||||
defaultValue: defaultPanelOptions.mode,
|
defaultValue: defaultPanelOptions.calculate,
|
||||||
category,
|
category,
|
||||||
settings: {
|
settings: {
|
||||||
options: [
|
options: [
|
||||||
{ label: 'Aggregated', value: HeatmapMode.Aggregated },
|
{ label: 'Yes', value: true },
|
||||||
{ label: 'Calculate', value: HeatmapMode.Calculate },
|
{ label: 'No', value: false },
|
||||||
// { label: 'Accumulated', value: HeatmapMode.Accumulated, description: 'The query response values are accumulated' },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (opts.mode === HeatmapMode.Calculate) {
|
if (opts.calculate) {
|
||||||
addHeatmapCalculationOptions('calculate.', builder, opts.calculate, category);
|
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',
|
||||||
|
defaultValue: defaultPanelOptions.yAxis.axisPlacement ?? AxisPlacement.Left,
|
||||||
|
category,
|
||||||
|
settings: {
|
||||||
|
options: [
|
||||||
|
{ label: 'Left', value: AxisPlacement.Left },
|
||||||
|
{ label: 'Right', value: AxisPlacement.Right },
|
||||||
|
{ label: 'Hidden', value: AxisPlacement.Hidden },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
builder
|
||||||
|
.addNumberInput({
|
||||||
|
path: 'yAxis.axisWidth',
|
||||||
|
name: 'Axis width',
|
||||||
|
defaultValue: defaultPanelOptions.yAxis.axisWidth,
|
||||||
|
settings: {
|
||||||
|
placeholder: 'Auto',
|
||||||
|
min: 5, // smaller should just be hidden
|
||||||
|
},
|
||||||
|
category,
|
||||||
|
})
|
||||||
|
.addTextInput({
|
||||||
|
path: 'yAxis.axisLabel',
|
||||||
|
name: 'Axis label',
|
||||||
|
defaultValue: defaultPanelOptions.yAxis.axisLabel,
|
||||||
|
settings: {
|
||||||
|
placeholder: 'Auto',
|
||||||
|
},
|
||||||
|
category,
|
||||||
|
})
|
||||||
|
.addBooleanSwitch({
|
||||||
|
path: 'yAxis.reverse',
|
||||||
|
name: 'Reverse',
|
||||||
|
defaultValue: defaultPanelOptions.yAxis.reverse === true,
|
||||||
|
category,
|
||||||
|
});
|
||||||
|
|
||||||
category = ['Colors'];
|
category = ['Colors'];
|
||||||
|
|
||||||
builder.addRadio({
|
builder.addRadio({
|
||||||
@ -152,9 +242,9 @@ export const plugin = new PanelPlugin<PanelOptions, GraphFieldConfig>(HeatmapPan
|
|||||||
// },
|
// },
|
||||||
// })
|
// })
|
||||||
.addNumberInput({
|
.addNumberInput({
|
||||||
path: 'hideThreshold',
|
path: 'filterValues.min',
|
||||||
name: 'Hide cell counts <=',
|
name: 'Hide cell counts <=',
|
||||||
defaultValue: 1e-9,
|
defaultValue: defaultPanelOptions.filterValues?.min,
|
||||||
category,
|
category,
|
||||||
})
|
})
|
||||||
.addSliderInput({
|
.addSliderInput({
|
||||||
@ -166,37 +256,17 @@ export const plugin = new PanelPlugin<PanelOptions, GraphFieldConfig>(HeatmapPan
|
|||||||
min: 0,
|
min: 0,
|
||||||
max: 25,
|
max: 25,
|
||||||
},
|
},
|
||||||
})
|
|
||||||
// .addSliderInput({
|
|
||||||
// name: 'Cell radius',
|
|
||||||
// path: 'cellRadius',
|
|
||||||
// defaultValue: defaultPanelOptions.cellRadius,
|
|
||||||
// category,
|
|
||||||
// settings: {
|
|
||||||
// min: 0,
|
|
||||||
// max: 100,
|
|
||||||
// },
|
|
||||||
// })
|
|
||||||
// .addRadio({
|
|
||||||
// path: 'yAxisLabels',
|
|
||||||
// name: 'Axis labels',
|
|
||||||
// defaultValue: 'auto',
|
|
||||||
// category,
|
|
||||||
// settings: {
|
|
||||||
// options: [
|
|
||||||
// { value: 'auto', label: 'Auto' },
|
|
||||||
// { value: 'middle', label: 'Middle' },
|
|
||||||
// { value: 'bottom', label: 'Bottom' },
|
|
||||||
// { value: 'top', label: 'Top' },
|
|
||||||
// ],
|
|
||||||
// },
|
|
||||||
// })
|
|
||||||
.addBooleanSwitch({
|
|
||||||
path: 'yAxisReverse',
|
|
||||||
name: 'Reverse buckets',
|
|
||||||
defaultValue: defaultPanelOptions.yAxisReverse === true,
|
|
||||||
category,
|
|
||||||
});
|
});
|
||||||
|
// .addSliderInput({
|
||||||
|
// name: 'Cell radius',
|
||||||
|
// path: 'cellRadius',
|
||||||
|
// defaultValue: defaultPanelOptions.cellRadius,
|
||||||
|
// category,
|
||||||
|
// settings: {
|
||||||
|
// min: 0,
|
||||||
|
// max: 100,
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
|
||||||
category = ['Tooltip'];
|
category = ['Tooltip'];
|
||||||
|
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
import { MutableRefObject, RefObject } from 'react';
|
import { MutableRefObject, RefObject } from 'react';
|
||||||
import uPlot from 'uplot';
|
import uPlot from 'uplot';
|
||||||
|
|
||||||
import { DataFrameType, GrafanaTheme2, TimeRange } from '@grafana/data';
|
import { DataFrameType, GrafanaTheme2, incrRoundDn, incrRoundUp, TimeRange } from '@grafana/data';
|
||||||
import { AxisPlacement, ScaleDirection, ScaleDistribution, ScaleOrientation } from '@grafana/schema';
|
import { AxisPlacement, ScaleDirection, ScaleDistribution, ScaleOrientation } from '@grafana/schema';
|
||||||
import { UPlotConfigBuilder } from '@grafana/ui';
|
import { UPlotConfigBuilder } from '@grafana/ui';
|
||||||
|
import { readHeatmapScanlinesCustomMeta } from 'app/features/transformers/calculateHeatmap/heatmap';
|
||||||
|
import { HeatmapBucketLayout } from 'app/features/transformers/calculateHeatmap/models.gen';
|
||||||
|
|
||||||
import { pointWithin, Quadtree, Rect } from '../barchart/quadtree';
|
import { pointWithin, Quadtree, Rect } from '../barchart/quadtree';
|
||||||
|
|
||||||
import { BucketLayout, HeatmapData } from './fields';
|
import { HeatmapData } from './fields';
|
||||||
|
import { PanelFieldConfig, YAxisConfig } from './models.gen';
|
||||||
|
|
||||||
interface PathbuilderOpts {
|
interface PathbuilderOpts {
|
||||||
each: (u: uPlot, seriesIdx: number, dataIdx: number, lft: number, top: number, wid: number, hgt: number) => void;
|
each: (u: uPlot, seriesIdx: number, dataIdx: number, lft: number, top: number, wid: number, hgt: number) => void;
|
||||||
@ -15,6 +18,7 @@ interface PathbuilderOpts {
|
|||||||
hideThreshold?: number;
|
hideThreshold?: number;
|
||||||
xAlign?: -1 | 0 | 1;
|
xAlign?: -1 | 0 | 1;
|
||||||
yAlign?: -1 | 0 | 1;
|
yAlign?: -1 | 0 | 1;
|
||||||
|
ySizeDivisor?: number;
|
||||||
disp: {
|
disp: {
|
||||||
fill: {
|
fill: {
|
||||||
values: (u: uPlot, seriesIndex: number) => number[];
|
values: (u: uPlot, seriesIndex: number) => number[];
|
||||||
@ -52,7 +56,8 @@ interface PrepConfigOpts {
|
|||||||
exemplarColor: string;
|
exemplarColor: string;
|
||||||
cellGap?: number | null; // in css pixels
|
cellGap?: number | null; // in css pixels
|
||||||
hideThreshold?: number;
|
hideThreshold?: number;
|
||||||
yAxisReverse?: boolean;
|
yAxisConfig: YAxisConfig;
|
||||||
|
ySizeDivisor?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function prepConfig(opts: PrepConfigOpts) {
|
export function prepConfig(opts: PrepConfigOpts) {
|
||||||
@ -68,7 +73,8 @@ export function prepConfig(opts: PrepConfigOpts) {
|
|||||||
palette,
|
palette,
|
||||||
cellGap,
|
cellGap,
|
||||||
hideThreshold,
|
hideThreshold,
|
||||||
yAxisReverse,
|
yAxisConfig,
|
||||||
|
ySizeDivisor,
|
||||||
} = opts;
|
} = opts;
|
||||||
|
|
||||||
const pxRatio = devicePixelRatio;
|
const pxRatio = devicePixelRatio;
|
||||||
@ -205,7 +211,10 @@ export function prepConfig(opts: PrepConfigOpts) {
|
|||||||
theme: theme,
|
theme: theme,
|
||||||
});
|
});
|
||||||
|
|
||||||
const shouldUseLogScale = heatmapType === DataFrameType.HeatmapSparse;
|
const yFieldConfig = dataRef.current?.heatmap?.fields[1]?.config?.custom as PanelFieldConfig | undefined;
|
||||||
|
const yScale = yFieldConfig?.scaleDistribution ?? { type: ScaleDistribution.Linear };
|
||||||
|
const yAxisReverse = Boolean(yAxisConfig.reverse);
|
||||||
|
const shouldUseLogScale = yScale.type !== ScaleDistribution.Linear || heatmapType === DataFrameType.HeatmapSparse;
|
||||||
|
|
||||||
builder.addScale({
|
builder.addScale({
|
||||||
scaleKey: 'y',
|
scaleKey: 'y',
|
||||||
@ -215,38 +224,84 @@ export function prepConfig(opts: PrepConfigOpts) {
|
|||||||
direction: yAxisReverse ? ScaleDirection.Down : ScaleDirection.Up,
|
direction: yAxisReverse ? ScaleDirection.Down : ScaleDirection.Up,
|
||||||
// should be tweakable manually
|
// should be tweakable manually
|
||||||
distribution: shouldUseLogScale ? ScaleDistribution.Log : ScaleDistribution.Linear,
|
distribution: shouldUseLogScale ? ScaleDistribution.Log : ScaleDistribution.Linear,
|
||||||
log: 2,
|
log: yScale.log ?? 2,
|
||||||
range: shouldUseLogScale
|
range:
|
||||||
? undefined
|
// sparse already accounts for le/ge by explicit yMin & yMax cell bounds, so use default log ranging
|
||||||
: (u, dataMin, dataMax) => {
|
heatmapType === DataFrameType.HeatmapSparse
|
||||||
let bucketSize = dataRef.current?.yBucketSize;
|
? undefined
|
||||||
|
: // dense and ordinal only have one of yMin|yMax|y, so expand range by one cell in the direction of le/ge/unknown
|
||||||
|
(u, dataMin, dataMax) => {
|
||||||
|
// logarithmic expansion
|
||||||
|
if (shouldUseLogScale) {
|
||||||
|
let yExp = u.scales['y'].log!;
|
||||||
|
|
||||||
if (bucketSize === 0) {
|
let minExpanded = false;
|
||||||
bucketSize = 1;
|
let maxExpanded = false;
|
||||||
}
|
|
||||||
|
|
||||||
if (bucketSize) {
|
if (ySizeDivisor !== 1) {
|
||||||
if (dataRef.current?.yLayout === BucketLayout.le) {
|
let log = yExp === 2 ? Math.log2 : Math.log10;
|
||||||
dataMin -= bucketSize!;
|
|
||||||
} else if (dataRef.current?.yLayout === BucketLayout.ge) {
|
let minLog = log(dataMin);
|
||||||
dataMax += bucketSize!;
|
let maxLog = log(dataMax);
|
||||||
} else {
|
|
||||||
dataMin -= bucketSize! / 2;
|
if (!Number.isInteger(minLog)) {
|
||||||
dataMax += bucketSize! / 2;
|
dataMin = yExp ** incrRoundDn(minLog, 1);
|
||||||
|
minExpanded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Number.isInteger(maxLog)) {
|
||||||
|
dataMax = yExp ** incrRoundUp(maxLog, 1);
|
||||||
|
maxExpanded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dataRef.current?.yLayout === HeatmapBucketLayout.le) {
|
||||||
|
if (!minExpanded) {
|
||||||
|
dataMin /= yExp;
|
||||||
|
}
|
||||||
|
} else if (dataRef.current?.yLayout === HeatmapBucketLayout.ge) {
|
||||||
|
if (!maxExpanded) {
|
||||||
|
dataMax *= yExp;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dataMin /= yExp / 2;
|
||||||
|
dataMax *= yExp / 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
// linear expansion
|
||||||
// how to expand scale range if inferred non-regular or log buckets?
|
else {
|
||||||
}
|
let bucketSize = dataRef.current?.yBucketSize;
|
||||||
|
|
||||||
return [dataMin, dataMax];
|
if (bucketSize === 0) {
|
||||||
},
|
bucketSize = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bucketSize) {
|
||||||
|
if (dataRef.current?.yLayout === HeatmapBucketLayout.le) {
|
||||||
|
dataMin -= bucketSize!;
|
||||||
|
} else if (dataRef.current?.yLayout === HeatmapBucketLayout.ge) {
|
||||||
|
dataMax += bucketSize!;
|
||||||
|
} else {
|
||||||
|
dataMin -= bucketSize! / 2;
|
||||||
|
dataMax += bucketSize! / 2;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// how to expand scale range if inferred non-regular or log buckets?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [dataMin, dataMax];
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasLabeledY = dataRef.current?.yAxisValues != null;
|
const hasLabeledY = readHeatmapScanlinesCustomMeta(dataRef.current?.heatmap).yOrdinalDisplay != null;
|
||||||
|
|
||||||
builder.addAxis({
|
builder.addAxis({
|
||||||
scaleKey: 'y',
|
scaleKey: 'y',
|
||||||
placement: AxisPlacement.Left,
|
show: yAxisConfig.axisPlacement !== AxisPlacement.Hidden,
|
||||||
|
placement: yAxisConfig.axisPlacement || AxisPlacement.Left,
|
||||||
|
size: yAxisConfig.axisWidth || null,
|
||||||
|
label: yAxisConfig.axisLabel,
|
||||||
theme: theme,
|
theme: theme,
|
||||||
splits: hasLabeledY
|
splits: hasLabeledY
|
||||||
? () => {
|
? () => {
|
||||||
@ -255,7 +310,7 @@ export function prepConfig(opts: PrepConfigOpts) {
|
|||||||
|
|
||||||
const bucketSize = dataRef.current?.yBucketSize!;
|
const bucketSize = dataRef.current?.yBucketSize!;
|
||||||
|
|
||||||
if (dataRef.current?.yLayout === BucketLayout.le) {
|
if (dataRef.current?.yLayout === HeatmapBucketLayout.le) {
|
||||||
splits.unshift(ys[0] - bucketSize);
|
splits.unshift(ys[0] - bucketSize);
|
||||||
} else {
|
} else {
|
||||||
splits.push(ys[ys.length - 1] + bucketSize);
|
splits.push(ys[ys.length - 1] + bucketSize);
|
||||||
@ -266,12 +321,14 @@ export function prepConfig(opts: PrepConfigOpts) {
|
|||||||
: undefined,
|
: undefined,
|
||||||
values: hasLabeledY
|
values: hasLabeledY
|
||||||
? () => {
|
? () => {
|
||||||
const yAxisValues = dataRef.current?.yAxisValues?.slice()!;
|
const meta = readHeatmapScanlinesCustomMeta(dataRef.current?.heatmap);
|
||||||
|
const yAxisValues = meta.yOrdinalDisplay?.slice()!;
|
||||||
|
const isFromBuckets = meta.yOrdinalDisplay?.length && !('le' === meta.yMatchWithLabel);
|
||||||
|
|
||||||
if (dataRef.current?.yLayout === BucketLayout.le) {
|
if (dataRef.current?.yLayout === HeatmapBucketLayout.le) {
|
||||||
yAxisValues.unshift('0.0'); // assumes dense layout where lowest bucket's low bound is 0-ish
|
yAxisValues.unshift(isFromBuckets ? '' : '0.0'); // assumes dense layout where lowest bucket's low bound is 0-ish
|
||||||
} else if (dataRef.current?.yLayout === BucketLayout.ge) {
|
} else if (dataRef.current?.yLayout === HeatmapBucketLayout.ge) {
|
||||||
yAxisValues.push('+Inf');
|
yAxisValues.push(isFromBuckets ? '' : '+Inf');
|
||||||
}
|
}
|
||||||
|
|
||||||
return yAxisValues;
|
return yAxisValues;
|
||||||
@ -307,12 +364,18 @@ export function prepConfig(opts: PrepConfigOpts) {
|
|||||||
},
|
},
|
||||||
gap: cellGap,
|
gap: cellGap,
|
||||||
hideThreshold,
|
hideThreshold,
|
||||||
xAlign: dataRef.current?.xLayout === BucketLayout.le ? -1 : dataRef.current?.xLayout === BucketLayout.ge ? 1 : 0,
|
xAlign:
|
||||||
yAlign: ((dataRef.current?.yLayout === BucketLayout.le
|
dataRef.current?.xLayout === HeatmapBucketLayout.le
|
||||||
|
? -1
|
||||||
|
: dataRef.current?.xLayout === HeatmapBucketLayout.ge
|
||||||
|
? 1
|
||||||
|
: 0,
|
||||||
|
yAlign: ((dataRef.current?.yLayout === HeatmapBucketLayout.le
|
||||||
? -1
|
? -1
|
||||||
: dataRef.current?.yLayout === BucketLayout.ge
|
: dataRef.current?.yLayout === HeatmapBucketLayout.ge
|
||||||
? 1
|
? 1
|
||||||
: 0) * (yAxisReverse ? -1 : 1)) as -1 | 0 | 1,
|
: 0) * (yAxisReverse ? -1 : 1)) as -1 | 0 | 1,
|
||||||
|
ySizeDivisor,
|
||||||
disp: {
|
disp: {
|
||||||
fill: {
|
fill: {
|
||||||
values: (u, seriesIdx) => {
|
values: (u, seriesIdx) => {
|
||||||
@ -402,7 +465,7 @@ export function prepConfig(opts: PrepConfigOpts) {
|
|||||||
const CRISP_EDGES_GAP_MIN = 4;
|
const CRISP_EDGES_GAP_MIN = 4;
|
||||||
|
|
||||||
export function heatmapPathsDense(opts: PathbuilderOpts) {
|
export function heatmapPathsDense(opts: PathbuilderOpts) {
|
||||||
const { disp, each, gap = 1, hideThreshold = 0, xAlign = 1, yAlign = 1 } = opts;
|
const { disp, each, gap = 1, hideThreshold = 0, xAlign = 1, yAlign = 1, ySizeDivisor = 1 } = opts;
|
||||||
|
|
||||||
const pxRatio = devicePixelRatio;
|
const pxRatio = devicePixelRatio;
|
||||||
|
|
||||||
@ -451,8 +514,22 @@ export function heatmapPathsDense(opts: PathbuilderOpts) {
|
|||||||
let xBinIncr = xs[yBinQty] - xs[0];
|
let xBinIncr = xs[yBinQty] - xs[0];
|
||||||
|
|
||||||
// uniform tile sizes based on zoom level
|
// uniform tile sizes based on zoom level
|
||||||
let xSize = Math.abs(valToPosX(xBinIncr, scaleX, xDim, xOff) - valToPosX(0, scaleX, xDim, xOff));
|
let xSize: number;
|
||||||
let ySize = Math.abs(valToPosY(yBinIncr, scaleY, yDim, yOff) - valToPosY(0, scaleY, yDim, yOff));
|
let ySize: number;
|
||||||
|
|
||||||
|
if (scaleX.distr === 3) {
|
||||||
|
xSize = Math.abs(valToPosX(xs[0] * scaleX.log!, scaleX, xDim, xOff) - valToPosX(xs[0], scaleX, xDim, xOff));
|
||||||
|
} else {
|
||||||
|
xSize = Math.abs(valToPosX(xBinIncr, scaleX, xDim, xOff) - valToPosX(0, scaleX, xDim, xOff));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scaleY.distr === 3) {
|
||||||
|
ySize =
|
||||||
|
Math.abs(valToPosY(ys[0] * scaleY.log!, scaleY, yDim, yOff) - valToPosY(ys[0], scaleY, yDim, yOff)) /
|
||||||
|
ySizeDivisor;
|
||||||
|
} else {
|
||||||
|
ySize = Math.abs(valToPosY(yBinIncr, scaleY, yDim, yOff) - valToPosY(0, scaleY, yDim, yOff)) / ySizeDivisor;
|
||||||
|
}
|
||||||
|
|
||||||
// clamp min tile size to 1px
|
// clamp min tile size to 1px
|
||||||
xSize = Math.max(1, round(xSize - cellGap));
|
xSize = Math.max(1, round(xSize - cellGap));
|
||||||
|
Loading…
Reference in New Issue
Block a user