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 {
|
||||
FieldConfigEditorBuilder,
|
||||
FieldOverrideEditorProps,
|
||||
FieldType,
|
||||
identityOverrideProcessor,
|
||||
SelectableValue,
|
||||
StandardEditorProps,
|
||||
} from '@grafana/data';
|
||||
import { AxisConfig, AxisPlacement, ScaleDistribution, ScaleDistributionConfig } from '@grafana/schema';
|
||||
|
||||
@ -89,8 +89,8 @@ export function addAxisConfig(
|
||||
path: 'scaleDistribution',
|
||||
name: 'Scale',
|
||||
category,
|
||||
editor: ScaleDistributionEditor,
|
||||
override: ScaleDistributionEditor,
|
||||
editor: ScaleDistributionEditor as any,
|
||||
override: ScaleDistributionEditor as any,
|
||||
defaultValue: { type: ScaleDistribution.Linear },
|
||||
shouldApply: (f) => f.type === FieldType.number,
|
||||
process: identityOverrideProcessor,
|
||||
@ -121,19 +121,16 @@ const LOG_DISTRIBUTION_OPTIONS: Array<SelectableValue<number>> = [
|
||||
];
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
* @internal
|
||||
*/
|
||||
const ScaleDistributionEditor: React.FC<FieldOverrideEditorProps<ScaleDistributionConfig, any>> = ({
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
export const ScaleDistributionEditor = ({ value, onChange }: StandardEditorProps<ScaleDistributionConfig>) => {
|
||||
const type = value?.type ?? ScaleDistribution.Linear;
|
||||
return (
|
||||
<HorizontalGroup>
|
||||
<RadioButtonGroup
|
||||
value={value.type || ScaleDistribution.Linear}
|
||||
value={type}
|
||||
options={DISTRIBUTION_OPTIONS}
|
||||
onChange={(v) => {
|
||||
console.log(v, value);
|
||||
onChange({
|
||||
...value,
|
||||
type: v!,
|
||||
@ -141,9 +138,8 @@ const ScaleDistributionEditor: React.FC<FieldOverrideEditorProps<ScaleDistributi
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{value.type === ScaleDistribution.Log && (
|
||||
{type === ScaleDistribution.Log && (
|
||||
<Select
|
||||
allowCustomValue={false}
|
||||
options={LOG_DISTRIBUTION_OPTIONS}
|
||||
value={value.log || 2}
|
||||
prefix={'base'}
|
||||
|
@ -25,7 +25,7 @@ const supplier = (
|
||||
|
||||
export const HeatmapTransformerEditor: React.FC<TransformerUIProps<HeatmapTransformerOptions>> = (props) => {
|
||||
useEffect(() => {
|
||||
if (!props.options.xAxis?.mode) {
|
||||
if (!props.options.xBuckets?.mode) {
|
||||
const opts = getDefaultOptions(supplier);
|
||||
props.onChange({ ...opts, ...props.options });
|
||||
console.log('geometry useEffect', opts);
|
||||
|
@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
|
||||
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>> = [
|
||||
{
|
||||
@ -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,
|
||||
onChange,
|
||||
item,
|
||||
@ -27,7 +40,7 @@ export const AxisEditor: React.FC<StandardEditorProps<HeatmapCalculationAxisConf
|
||||
<HorizontalGroup>
|
||||
<RadioButtonGroup
|
||||
value={value?.mode || HeatmapCalculationMode.Size}
|
||||
options={modeOptions}
|
||||
options={value?.scale?.type === ScaleDistribution.Log ? logModeOptions : modeOptions}
|
||||
onChange={(mode) => {
|
||||
onChange({
|
||||
...value,
|
||||
|
@ -1,4 +1,6 @@
|
||||
import { PanelOptionsEditorBuilder } from '@grafana/data';
|
||||
import { ScaleDistribution } from '@grafana/schema';
|
||||
import { ScaleDistributionEditor } from '@grafana/ui/src/options/builder';
|
||||
|
||||
import { HeatmapCalculationMode, HeatmapCalculationOptions } from '../models.gen';
|
||||
|
||||
@ -11,9 +13,9 @@ export function addHeatmapCalculationOptions(
|
||||
category?: string[]
|
||||
) {
|
||||
builder.addCustomEditor({
|
||||
id: 'xAxis',
|
||||
path: `${prefix}xAxis`,
|
||||
name: 'X Buckets',
|
||||
id: 'xBuckets',
|
||||
path: `${prefix}xBuckets`,
|
||||
name: 'X Bucket',
|
||||
editor: AxisEditor,
|
||||
category,
|
||||
defaultValue: {
|
||||
@ -22,13 +24,22 @@ export function addHeatmapCalculationOptions(
|
||||
});
|
||||
|
||||
builder.addCustomEditor({
|
||||
id: 'yAxis',
|
||||
path: `${prefix}yAxis`,
|
||||
name: 'Y Buckets',
|
||||
id: 'yBuckets',
|
||||
path: `${prefix}yBuckets`,
|
||||
name: 'Y Bucket',
|
||||
editor: AxisEditor,
|
||||
category,
|
||||
defaultValue: {
|
||||
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 { toDataFrame } from '@grafana/data/src/dataframe/processDataFrame';
|
||||
|
||||
import { calculateHeatmapFromData } from './heatmap';
|
||||
import { bucketsToScanlines, calculateHeatmapFromData } from './heatmap';
|
||||
import { HeatmapCalculationOptions } from './models.gen';
|
||||
|
||||
describe('Heatmap transformer', () => {
|
||||
@ -13,12 +13,100 @@ describe('Heatmap transformer', () => {
|
||||
const data = toDataFrame({
|
||||
fields: [
|
||||
{ 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);
|
||||
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,
|
||||
Field,
|
||||
} 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';
|
||||
|
||||
export interface HeatmapTransformerOptions extends HeatmapCalculationOptions {
|
||||
@ -48,21 +49,39 @@ export function sortAscStrInf(aName?: string | null, bName?: string | null) {
|
||||
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 */
|
||||
// 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?
|
||||
const xField = frame.fields[0];
|
||||
const xField = opts.frame.fields[0];
|
||||
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
|
||||
const len = xValues.length * (frame.fields.length - 1);
|
||||
const len = xValues.length * yFields.length;
|
||||
const xs = new Array(len);
|
||||
const ys = 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
|
||||
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
|
||||
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];
|
||||
}
|
||||
|
||||
// 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 {
|
||||
length: xs.length,
|
||||
refId: opts.frame.refId,
|
||||
meta: {
|
||||
type: DataFrameType.HeatmapScanlines,
|
||||
custom,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
@ -97,19 +139,19 @@ export function bucketsToScanlines(frame: DataFrame): DataFrame {
|
||||
config: xField.config,
|
||||
},
|
||||
{
|
||||
// this name determines whether cells are drawn above, below, or centered on the values
|
||||
name: yField.labels?.le != null ? 'yMax' : 'y',
|
||||
name: ordinalFieldName,
|
||||
type: FieldType.number,
|
||||
values: new ArrayVector(ys),
|
||||
config: yField.config,
|
||||
config: {
|
||||
unit: 'short', // ordinal lookup
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'count',
|
||||
name: opts.name?.length ? opts.name : 'Value',
|
||||
type: FieldType.number,
|
||||
values: new ArrayVector(counts2),
|
||||
config: {
|
||||
unit: 'short',
|
||||
},
|
||||
config: yFields[0].config,
|
||||
display: yFields[0].display,
|
||||
},
|
||||
],
|
||||
};
|
||||
@ -195,13 +237,24 @@ export function calculateHeatmapFromData(frames: DataFrame[], options: HeatmapCa
|
||||
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, {
|
||||
xSorted: true,
|
||||
xTime: xField.type === FieldType.time,
|
||||
xMode: options.xAxis?.mode,
|
||||
xSize: +(options.xAxis?.value ?? 0),
|
||||
yMode: options.yAxis?.mode,
|
||||
ySize: +(options.yAxis?.value ?? 0),
|
||||
xMode: xBucketsCfg.mode,
|
||||
xSize: xBucketsCfg.value ? +xBucketsCfg.value : undefined,
|
||||
yMode: yBucketsCfg.mode,
|
||||
ySize: yBucketsCfg.value ? +yBucketsCfg.value : undefined,
|
||||
yLog: scaleDistribution?.type === ScaleDistribution.Log ? (scaleDistribution?.log as any) : undefined,
|
||||
});
|
||||
|
||||
const frame = {
|
||||
@ -221,10 +274,15 @@ export function calculateHeatmapFromData(frames: DataFrame[], options: HeatmapCa
|
||||
name: 'yMin',
|
||||
type: FieldType.number,
|
||||
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,
|
||||
values: new ArrayVector(heat2d.count),
|
||||
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 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 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 maxXBin = binX(maxX);
|
||||
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 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++) {
|
||||
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 xs = 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) {
|
||||
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) {
|
||||
x += xIncr;
|
||||
|
@ -1,18 +1,24 @@
|
||||
import { DataFrameType } from '@grafana/data';
|
||||
import { ScaleDistributionConfig } from '@grafana/schema';
|
||||
|
||||
export enum HeatmapCalculationMode {
|
||||
Size = 'size',
|
||||
Size = 'size', // When exponential, this is "splitFactor"
|
||||
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;
|
||||
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 {
|
||||
xAxis?: HeatmapCalculationAxisConfig;
|
||||
yAxis?: HeatmapCalculationAxisConfig;
|
||||
xAxisField?: string; // name of the x field
|
||||
encoding?: DataFrameType.HeatmapBuckets | DataFrameType.HeatmapScanlines;
|
||||
xBuckets?: HeatmapCalculationBucketConfig;
|
||||
yBuckets?: HeatmapCalculationBucketConfig;
|
||||
}
|
||||
|
@ -3,10 +3,12 @@ import React, { useEffect, useRef } from 'react';
|
||||
import { DataFrameType, Field, FieldType, formattedValueToString, getFieldDisplayName, LinkModel } from '@grafana/data';
|
||||
import { LinkButton, VerticalGroup } from '@grafana/ui';
|
||||
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 { BucketLayout, HeatmapData } from './fields';
|
||||
import { HeatmapData } from './fields';
|
||||
import { HeatmapHoverEvent } from './utils';
|
||||
|
||||
type Props = {
|
||||
@ -44,26 +46,15 @@ const HeatmapHoverCell = ({ data, hover, showHistogram }: Props) => {
|
||||
const yVals = yField?.values.toArray();
|
||||
const countVals = countField?.values.toArray();
|
||||
|
||||
let yDispSrc, yDisp;
|
||||
|
||||
// labeled buckets
|
||||
if (data.yAxisValues) {
|
||||
yDispSrc = data.yAxisValues;
|
||||
yDisp = (v: any) => v;
|
||||
} else {
|
||||
yDispSrc = yVals;
|
||||
yDisp = (v: any) => {
|
||||
if (yField?.display) {
|
||||
return formattedValueToString(yField.display(v));
|
||||
}
|
||||
return `${v}`;
|
||||
};
|
||||
}
|
||||
const meta = readHeatmapScanlinesCustomMeta(data.heatmap);
|
||||
const yDispSrc = meta.yOrdinalDisplay ?? yVals;
|
||||
const yDisp = yField?.display ? (v: any) => formattedValueToString(yField.display!(v)) : (v: any) => `${v}`;
|
||||
|
||||
const yValueIdx = index % data.yBucketCount! ?? 0;
|
||||
|
||||
const yMinIdx = data.yLayout === BucketLayout.le ? yValueIdx - 1 : yValueIdx;
|
||||
const yMaxIdx = data.yLayout === BucketLayout.le ? yValueIdx : yValueIdx + 1;
|
||||
const yMinIdx = data.yLayout === HeatmapBucketLayout.le ? yValueIdx - 1 : yValueIdx;
|
||||
const yMaxIdx = data.yLayout === HeatmapBucketLayout.le ? yValueIdx : yValueIdx + 1;
|
||||
|
||||
const yBucketMin = yDispSrc?.[yMinIdx];
|
||||
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 (
|
||||
<>
|
||||
<div>
|
||||
@ -186,15 +189,9 @@ const HeatmapHoverCell = ({ data, hover, showHistogram }: Props) => {
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
{data.yLayout === BucketLayout.unknown ? (
|
||||
<div>{yDisp(yBucketMin)}</div>
|
||||
) : (
|
||||
<div>
|
||||
Bucket: {yDisp(yBucketMin)} - {yDisp(yBucketMax)}
|
||||
</div>
|
||||
)}
|
||||
{renderYBuckets()}
|
||||
<div>
|
||||
{getFieldDisplayName(countField!, data.heatmap)}: {count}
|
||||
{getFieldDisplayName(countField!, data.heatmap)}: {data.display!(count)}
|
||||
</div>
|
||||
</div>
|
||||
{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 { 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 { ColorScale } from 'app/core/components/ColorScale/ColorScale';
|
||||
import { readHeatmapScanlinesCustomMeta } from 'app/features/transformers/calculateHeatmap/heatmap';
|
||||
|
||||
import { HeatmapHoverView } from './HeatmapHoverView';
|
||||
import { prepareHeatmapData } from './fields';
|
||||
@ -46,24 +56,25 @@ export const HeatmapPanel: React.FC<HeatmapPanelProps> = ({
|
||||
let exemplarsXFacet: number[] = []; // "Time" field
|
||||
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();
|
||||
|
||||
// ordinal/labeled heatmap-buckets?
|
||||
const hasLabeledY = info.yLabelValues != null;
|
||||
const hasLabeledY = meta.yOrdinalDisplay != null;
|
||||
|
||||
if (hasLabeledY) {
|
||||
let matchExemplarsBy = info.exemplars?.fields
|
||||
.find((field) => field.name === info.matchByLabel)!
|
||||
.find((field) => field.name === meta.yMatchWithLabel)!
|
||||
.values.toArray();
|
||||
exemplarsyFacet = matchExemplarsBy.map((label) => info.yLabelValues?.indexOf(label)) as number[];
|
||||
exemplarsyFacet = matchExemplarsBy.map((label) => meta.yOrdinalLabel?.indexOf(label)) as number[];
|
||||
} else {
|
||||
exemplarsyFacet = info.exemplars?.fields[1].values.toArray() as number[]; // "Value" field
|
||||
}
|
||||
}
|
||||
|
||||
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]);
|
||||
|
||||
@ -97,6 +108,8 @@ export const HeatmapPanel: React.FC<HeatmapPanelProps> = ({
|
||||
dataRef.current = info;
|
||||
|
||||
const builder = useMemo(() => {
|
||||
const scaleConfig = dataRef.current?.heatmap?.fields[1].config?.custom
|
||||
?.scaleDistribution as ScaleDistributionConfig;
|
||||
return prepConfig({
|
||||
dataRef,
|
||||
theme,
|
||||
@ -113,9 +126,10 @@ export const HeatmapPanel: React.FC<HeatmapPanelProps> = ({
|
||||
getTimeRange: () => timeRangeRef.current,
|
||||
palette,
|
||||
cellGap: options.cellGap,
|
||||
hideThreshold: options.hideThreshold,
|
||||
hideThreshold: options.filterValues?.min, // eventually a better range
|
||||
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
|
||||
}, [options, data.structureRev]);
|
||||
|
@ -1,42 +1,31 @@
|
||||
import {
|
||||
DataFrame,
|
||||
DataFrameType,
|
||||
FieldType,
|
||||
formattedValueToString,
|
||||
getDisplayProcessor,
|
||||
getFieldDisplayName,
|
||||
getValueFormat,
|
||||
GrafanaTheme2,
|
||||
outerJoinDataFrames,
|
||||
PanelData,
|
||||
} from '@grafana/data';
|
||||
import { calculateHeatmapFromData, bucketsToScanlines } from 'app/features/transformers/calculateHeatmap/heatmap';
|
||||
import { HeatmapBucketLayout } from 'app/features/transformers/calculateHeatmap/models.gen';
|
||||
|
||||
import { HeatmapMode, PanelOptions } from './models.gen';
|
||||
|
||||
export const enum BucketLayout {
|
||||
le = 'le',
|
||||
ge = 'ge',
|
||||
unknown = 'unknown', // unknown
|
||||
}
|
||||
import { PanelOptions } from './models.gen';
|
||||
|
||||
export interface HeatmapData {
|
||||
heatmap?: DataFrame; // data we will render
|
||||
exemplars?: DataFrame; // optionally linked exemplars
|
||||
exemplarColor?: string;
|
||||
|
||||
yAxisValues?: Array<number | string | null>;
|
||||
yLabelValues?: string[]; // matched ordinally to yAxisValues
|
||||
matchByLabel?: string; // e.g. le, pod, etc.
|
||||
|
||||
xBucketSize?: number;
|
||||
yBucketSize?: number;
|
||||
|
||||
xBucketCount?: number;
|
||||
yBucketCount?: number;
|
||||
|
||||
xLayout?: BucketLayout;
|
||||
yLayout?: BucketLayout;
|
||||
xLayout?: HeatmapBucketLayout;
|
||||
yLayout?: HeatmapBucketLayout;
|
||||
|
||||
// Print a heatmap cell value
|
||||
display?: (v: number) => string;
|
||||
@ -51,13 +40,11 @@ export function prepareHeatmapData(data: PanelData, options: PanelOptions, theme
|
||||
return {};
|
||||
}
|
||||
|
||||
const { mode } = options;
|
||||
|
||||
const exemplars = data.annotations?.find((f) => f.name === 'exemplar');
|
||||
|
||||
if (mode === HeatmapMode.Calculate) {
|
||||
if (options.calculate) {
|
||||
// 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
|
||||
@ -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
|
||||
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),
|
||||
};
|
||||
return getHeatmapData(bucketsToScanlines({ ...options.bucket, frame: bucketHeatmap }), exemplars, theme);
|
||||
}
|
||||
|
||||
const getSparseHeatmapData = (
|
||||
@ -173,8 +146,18 @@ const getHeatmapData = (frame: DataFrame, exemplars: DataFrame | undefined, them
|
||||
yBucketCount: yBinQty,
|
||||
|
||||
// TODO: improve heuristic
|
||||
xLayout: xName === 'xMax' ? BucketLayout.le : xName === 'xMin' ? BucketLayout.ge : BucketLayout.unknown,
|
||||
yLayout: yName === 'yMax' ? BucketLayout.le : yName === 'yMin' ? BucketLayout.ge : BucketLayout.unknown,
|
||||
xLayout:
|
||||
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)),
|
||||
};
|
||||
|
@ -25,14 +25,22 @@ describe('Heatmap Migrations', () => {
|
||||
"overrides": Array [],
|
||||
},
|
||||
"options": Object {
|
||||
"calculate": Object {
|
||||
"xAxis": Object {
|
||||
"bucket": Object {
|
||||
"layout": "auto",
|
||||
},
|
||||
"calculate": true,
|
||||
"calculation": Object {
|
||||
"xBuckets": Object {
|
||||
"mode": "count",
|
||||
"value": "100",
|
||||
},
|
||||
"yAxis": Object {
|
||||
"yBuckets": Object {
|
||||
"mode": "count",
|
||||
"value": "20",
|
||||
"scale": Object {
|
||||
"log": 2,
|
||||
"type": "log",
|
||||
},
|
||||
"value": "3",
|
||||
},
|
||||
},
|
||||
"cellGap": 2,
|
||||
@ -40,6 +48,8 @@ describe('Heatmap Migrations', () => {
|
||||
"color": Object {
|
||||
"exponent": 0.5,
|
||||
"fill": "dark-orange",
|
||||
"max": 100,
|
||||
"min": 5,
|
||||
"mode": "scheme",
|
||||
"scale": "exponential",
|
||||
"scheme": "BuGn",
|
||||
@ -48,17 +58,22 @@ describe('Heatmap Migrations', () => {
|
||||
"exemplars": Object {
|
||||
"color": "rgba(255,0,255,0.7)",
|
||||
},
|
||||
"filterValues": Object {
|
||||
"min": 1e-9,
|
||||
},
|
||||
"legend": Object {
|
||||
"show": true,
|
||||
},
|
||||
"mode": "calculate",
|
||||
"showValue": "never",
|
||||
"tooltip": Object {
|
||||
"show": true,
|
||||
"yHistogram": true,
|
||||
},
|
||||
"yAxisLabels": "auto",
|
||||
"yAxisReverse": false,
|
||||
"yAxis": Object {
|
||||
"axisPlacement": "left",
|
||||
"axisWidth": 400,
|
||||
"reverse": false,
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
@ -103,8 +118,8 @@ const oldHeatmap = {
|
||||
colorScale: 'sqrt',
|
||||
exponent: 0.5,
|
||||
colorScheme: 'interpolateBuGn',
|
||||
min: null,
|
||||
max: null,
|
||||
min: 5,
|
||||
max: 100,
|
||||
},
|
||||
legend: {
|
||||
show: true,
|
||||
@ -119,10 +134,11 @@ const oldHeatmap = {
|
||||
show: true,
|
||||
format: 'short',
|
||||
decimals: null,
|
||||
logBase: 1,
|
||||
splitFactor: null,
|
||||
logBase: 2,
|
||||
splitFactor: 3,
|
||||
min: null,
|
||||
max: null,
|
||||
width: '400',
|
||||
},
|
||||
xBucketSize: null,
|
||||
xBucketNumber: 100,
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { FieldConfigSource, PanelModel, PanelTypeChangedHandler } from '@grafana/data';
|
||||
import { VisibilityMode } from '@grafana/schema';
|
||||
import { AxisPlacement, ScaleDistribution, VisibilityMode } from '@grafana/schema';
|
||||
import {
|
||||
HeatmapBucketLayout,
|
||||
HeatmapCalculationMode,
|
||||
HeatmapCalculationOptions,
|
||||
} 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';
|
||||
|
||||
/**
|
||||
@ -29,36 +30,55 @@ export function angularToReactHeatmap(angular: any): { fieldConfig: FieldConfigS
|
||||
overrides: [],
|
||||
};
|
||||
|
||||
const mode = angular.dataFormat === 'tsbuckets' ? HeatmapMode.Aggregated : HeatmapMode.Calculate;
|
||||
const calculate: HeatmapCalculationOptions = {
|
||||
...defaultPanelOptions.calculate,
|
||||
const calculate = angular.dataFormat === 'tsbuckets' ? false : true;
|
||||
const calculation: HeatmapCalculationOptions = {
|
||||
...defaultPanelOptions.calculation,
|
||||
};
|
||||
|
||||
if (mode === HeatmapMode.Calculate) {
|
||||
const oldYAxis = { logBase: 1, ...angular.yAxis };
|
||||
|
||||
if (calculate) {
|
||||
if (angular.xBucketSize) {
|
||||
calculate.xAxis = { mode: HeatmapCalculationMode.Size, value: `${angular.xBucketSize}` };
|
||||
calculation.xBuckets = { mode: HeatmapCalculationMode.Size, value: `${angular.xBucketSize}` };
|
||||
} else if (angular.xBucketNumber) {
|
||||
calculate.xAxis = { mode: HeatmapCalculationMode.Count, value: `${angular.xBucketNumber}` };
|
||||
calculation.xBuckets = { mode: HeatmapCalculationMode.Count, value: `${angular.xBucketNumber}` };
|
||||
}
|
||||
|
||||
if (angular.yBucketSize) {
|
||||
calculate.yAxis = { mode: HeatmapCalculationMode.Size, value: `${angular.yBucketSize}` };
|
||||
calculation.yBuckets = { mode: HeatmapCalculationMode.Size, value: `${angular.yBucketSize}` };
|
||||
} 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 = {
|
||||
mode,
|
||||
calculate,
|
||||
calculation,
|
||||
color: {
|
||||
...defaultPanelOptions.color,
|
||||
steps: 128, // best match with existing colors
|
||||
},
|
||||
cellGap: asNumber(angular.cards?.cardPadding),
|
||||
cellSize: asNumber(angular.cards?.cardRound),
|
||||
yAxisLabels: angular.yBucketBound,
|
||||
yAxisReverse: angular.reverseYBuckets,
|
||||
yAxis: {
|
||||
axisPlacement: oldYAxis.show === false ? AxisPlacement.Hidden : AxisPlacement.Left,
|
||||
reverse: Boolean(angular.reverseYBuckets),
|
||||
axisWidth: oldYAxis.width ? +oldYAxis.width : undefined,
|
||||
},
|
||||
bucket: {
|
||||
layout: getHeatmapBucketLayout(angular.yBucketBound),
|
||||
},
|
||||
legend: {
|
||||
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
|
||||
const color = angular.color;
|
||||
switch (color?.mode) {
|
||||
@ -92,10 +116,24 @@ export function angularToReactHeatmap(angular: any): { fieldConfig: FieldConfigS
|
||||
break;
|
||||
}
|
||||
}
|
||||
options.color.min = color.min;
|
||||
options.color.max = color.max;
|
||||
|
||||
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 {
|
||||
const num = +v;
|
||||
return isNaN(num) ? undefined : num;
|
||||
|
@ -3,17 +3,11 @@
|
||||
// It is currenty hand written but will serve as the target for cuetsy
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
import { HideableFieldConfig, VisibilityMode } from '@grafana/schema';
|
||||
import { HeatmapCalculationOptions } from 'app/features/transformers/calculateHeatmap/models.gen';
|
||||
import { AxisConfig, AxisPlacement, HideableFieldConfig, ScaleDistributionConfig, VisibilityMode } from '@grafana/schema';
|
||||
import { HeatmapBucketLayout, HeatmapCalculationOptions } from 'app/features/transformers/calculateHeatmap/models.gen';
|
||||
|
||||
export const modelVersion = Object.freeze([1, 0]);
|
||||
|
||||
export enum HeatmapMode {
|
||||
Aggregated = 'agg',
|
||||
Calculate = 'calculate',
|
||||
Accumulated = 'acc', // accumulated
|
||||
}
|
||||
|
||||
export enum HeatmapColorMode {
|
||||
Opacity = 'opacity',
|
||||
Scheme = 'scheme',
|
||||
@ -36,6 +30,16 @@ export interface HeatmapColorOptions {
|
||||
min?: number;
|
||||
max?: number;
|
||||
}
|
||||
export interface YAxisConfig extends AxisConfig {
|
||||
unit?: string;
|
||||
reverse?: boolean;
|
||||
decimals?: number;
|
||||
}
|
||||
|
||||
export interface FilterValueRange {
|
||||
min?: number;
|
||||
max?: number;
|
||||
}
|
||||
|
||||
export interface HeatmapTooltip {
|
||||
show: boolean;
|
||||
@ -49,19 +53,24 @@ export interface ExemplarConfig {
|
||||
color: string;
|
||||
}
|
||||
|
||||
export interface BucketOptions {
|
||||
name?: string;
|
||||
layout?: HeatmapBucketLayout;
|
||||
}
|
||||
|
||||
export interface PanelOptions {
|
||||
mode: HeatmapMode;
|
||||
calculate?: boolean;
|
||||
calculation?: HeatmapCalculationOptions;
|
||||
|
||||
color: HeatmapColorOptions;
|
||||
calculate?: HeatmapCalculationOptions;
|
||||
filterValues?: FilterValueRange; // was hideZeroBuckets
|
||||
bucket?: BucketOptions;
|
||||
showValue: VisibilityMode;
|
||||
|
||||
cellGap?: number; // was cardPadding
|
||||
cellSize?: number; // was cardRadius
|
||||
|
||||
hideThreshold?: number; // was hideZeroBuckets
|
||||
yAxisLabels?: string;
|
||||
yAxisReverse?: boolean;
|
||||
yAxis: YAxisConfig;
|
||||
legend: HeatmapLegend;
|
||||
|
||||
tooltip: HeatmapTooltip;
|
||||
@ -69,7 +78,7 @@ export interface PanelOptions {
|
||||
}
|
||||
|
||||
export const defaultPanelOptions: PanelOptions = {
|
||||
mode: HeatmapMode.Aggregated,
|
||||
calculate: false,
|
||||
color: {
|
||||
mode: HeatmapColorMode.Scheme,
|
||||
scheme: 'Oranges',
|
||||
@ -78,6 +87,12 @@ export const defaultPanelOptions: PanelOptions = {
|
||||
exponent: 0.5,
|
||||
steps: 64,
|
||||
},
|
||||
bucket: {
|
||||
layout: HeatmapBucketLayout.auto,
|
||||
},
|
||||
yAxis: {
|
||||
axisPlacement: AxisPlacement.Left,
|
||||
},
|
||||
showValue: VisibilityMode.Auto,
|
||||
tooltip: {
|
||||
show: true,
|
||||
@ -89,13 +104,14 @@ export const defaultPanelOptions: PanelOptions = {
|
||||
exemplars: {
|
||||
color: 'rgba(255,0,255,0.7)',
|
||||
},
|
||||
filterValues: {
|
||||
min: 1e-9,
|
||||
},
|
||||
cellGap: 1,
|
||||
};
|
||||
|
||||
export interface PanelFieldConfig extends HideableFieldConfig {
|
||||
// TODO points vs lines etc
|
||||
scaleDistribution?: ScaleDistributionConfig;
|
||||
}
|
||||
|
||||
export const defaultPanelFieldConfig: PanelFieldConfig = {
|
||||
// default to points?
|
||||
};
|
||||
export const defaultPanelFieldConfig: PanelFieldConfig = {};
|
||||
|
@ -1,20 +1,45 @@
|
||||
import React from 'react';
|
||||
|
||||
import { FieldConfigProperty, PanelPlugin } from '@grafana/data';
|
||||
import { FieldConfigProperty, FieldType, identityOverrideProcessor, PanelPlugin } from '@grafana/data';
|
||||
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 { addHeatmapCalculationOptions } from 'app/features/transformers/calculateHeatmap/editor/helper';
|
||||
import { HeatmapBucketLayout } from 'app/features/transformers/calculateHeatmap/models.gen';
|
||||
|
||||
import { HeatmapPanel } from './HeatmapPanel';
|
||||
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 { HeatmapSuggestionsSupplier } from './suggestions';
|
||||
|
||||
export const plugin = new PanelPlugin<PanelOptions, GraphFieldConfig>(HeatmapPanel)
|
||||
.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)
|
||||
.setMigrationHandler(heatmapMigrationHandler)
|
||||
@ -24,23 +49,88 @@ export const plugin = new PanelPlugin<PanelOptions, GraphFieldConfig>(HeatmapPan
|
||||
let category = ['Heatmap'];
|
||||
|
||||
builder.addRadio({
|
||||
path: 'mode',
|
||||
name: 'Data',
|
||||
defaultValue: defaultPanelOptions.mode,
|
||||
path: 'calculate',
|
||||
name: 'Calculate from data',
|
||||
defaultValue: defaultPanelOptions.calculate,
|
||||
category,
|
||||
settings: {
|
||||
options: [
|
||||
{ label: 'Aggregated', value: HeatmapMode.Aggregated },
|
||||
{ label: 'Calculate', value: HeatmapMode.Calculate },
|
||||
// { label: 'Accumulated', value: HeatmapMode.Accumulated, description: 'The query response values are accumulated' },
|
||||
{ label: 'Yes', value: true },
|
||||
{ label: 'No', value: false },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
if (opts.mode === HeatmapMode.Calculate) {
|
||||
addHeatmapCalculationOptions('calculate.', builder, opts.calculate, category);
|
||||
if (opts.calculate) {
|
||||
addHeatmapCalculationOptions('calculation.', builder, opts.calculation, category);
|
||||
} else {
|
||||
builder.addTextInput({
|
||||
path: 'bucket.name',
|
||||
name: 'Cell value name',
|
||||
defaultValue: defaultPanelOptions.bucket?.name,
|
||||
settings: {
|
||||
placeholder: 'Value',
|
||||
},
|
||||
category,
|
||||
});
|
||||
builder.addRadio({
|
||||
path: 'bucket.layout',
|
||||
name: 'Layout',
|
||||
defaultValue: defaultPanelOptions.bucket?.layout ?? HeatmapBucketLayout.auto,
|
||||
category,
|
||||
settings: {
|
||||
options: [
|
||||
{ label: 'Auto', value: HeatmapBucketLayout.auto },
|
||||
{ label: 'Middle', value: HeatmapBucketLayout.unknown },
|
||||
{ label: 'Lower (LE)', value: HeatmapBucketLayout.le },
|
||||
{ label: 'Upper (GE)', value: HeatmapBucketLayout.ge },
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
category = ['Y Axis'];
|
||||
builder.addRadio({
|
||||
path: 'yAxis.axisPlacement',
|
||||
name: 'Placement',
|
||||
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'];
|
||||
|
||||
builder.addRadio({
|
||||
@ -152,9 +242,9 @@ export const plugin = new PanelPlugin<PanelOptions, GraphFieldConfig>(HeatmapPan
|
||||
// },
|
||||
// })
|
||||
.addNumberInput({
|
||||
path: 'hideThreshold',
|
||||
path: 'filterValues.min',
|
||||
name: 'Hide cell counts <=',
|
||||
defaultValue: 1e-9,
|
||||
defaultValue: defaultPanelOptions.filterValues?.min,
|
||||
category,
|
||||
})
|
||||
.addSliderInput({
|
||||
@ -166,37 +256,17 @@ export const plugin = new PanelPlugin<PanelOptions, GraphFieldConfig>(HeatmapPan
|
||||
min: 0,
|
||||
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'];
|
||||
|
||||
|
@ -1,13 +1,16 @@
|
||||
import { MutableRefObject, RefObject } from 'react';
|
||||
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 { 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 { BucketLayout, HeatmapData } from './fields';
|
||||
import { HeatmapData } from './fields';
|
||||
import { PanelFieldConfig, YAxisConfig } from './models.gen';
|
||||
|
||||
interface PathbuilderOpts {
|
||||
each: (u: uPlot, seriesIdx: number, dataIdx: number, lft: number, top: number, wid: number, hgt: number) => void;
|
||||
@ -15,6 +18,7 @@ interface PathbuilderOpts {
|
||||
hideThreshold?: number;
|
||||
xAlign?: -1 | 0 | 1;
|
||||
yAlign?: -1 | 0 | 1;
|
||||
ySizeDivisor?: number;
|
||||
disp: {
|
||||
fill: {
|
||||
values: (u: uPlot, seriesIndex: number) => number[];
|
||||
@ -52,7 +56,8 @@ interface PrepConfigOpts {
|
||||
exemplarColor: string;
|
||||
cellGap?: number | null; // in css pixels
|
||||
hideThreshold?: number;
|
||||
yAxisReverse?: boolean;
|
||||
yAxisConfig: YAxisConfig;
|
||||
ySizeDivisor?: number;
|
||||
}
|
||||
|
||||
export function prepConfig(opts: PrepConfigOpts) {
|
||||
@ -68,7 +73,8 @@ export function prepConfig(opts: PrepConfigOpts) {
|
||||
palette,
|
||||
cellGap,
|
||||
hideThreshold,
|
||||
yAxisReverse,
|
||||
yAxisConfig,
|
||||
ySizeDivisor,
|
||||
} = opts;
|
||||
|
||||
const pxRatio = devicePixelRatio;
|
||||
@ -205,7 +211,10 @@ export function prepConfig(opts: PrepConfigOpts) {
|
||||
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({
|
||||
scaleKey: 'y',
|
||||
@ -215,38 +224,84 @@ export function prepConfig(opts: PrepConfigOpts) {
|
||||
direction: yAxisReverse ? ScaleDirection.Down : ScaleDirection.Up,
|
||||
// should be tweakable manually
|
||||
distribution: shouldUseLogScale ? ScaleDistribution.Log : ScaleDistribution.Linear,
|
||||
log: 2,
|
||||
range: shouldUseLogScale
|
||||
? undefined
|
||||
: (u, dataMin, dataMax) => {
|
||||
let bucketSize = dataRef.current?.yBucketSize;
|
||||
log: yScale.log ?? 2,
|
||||
range:
|
||||
// sparse already accounts for le/ge by explicit yMin & yMax cell bounds, so use default log ranging
|
||||
heatmapType === DataFrameType.HeatmapSparse
|
||||
? 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) {
|
||||
bucketSize = 1;
|
||||
}
|
||||
let minExpanded = false;
|
||||
let maxExpanded = false;
|
||||
|
||||
if (bucketSize) {
|
||||
if (dataRef.current?.yLayout === BucketLayout.le) {
|
||||
dataMin -= bucketSize!;
|
||||
} else if (dataRef.current?.yLayout === BucketLayout.ge) {
|
||||
dataMax += bucketSize!;
|
||||
} else {
|
||||
dataMin -= bucketSize! / 2;
|
||||
dataMax += bucketSize! / 2;
|
||||
if (ySizeDivisor !== 1) {
|
||||
let log = yExp === 2 ? Math.log2 : Math.log10;
|
||||
|
||||
let minLog = log(dataMin);
|
||||
let maxLog = log(dataMax);
|
||||
|
||||
if (!Number.isInteger(minLog)) {
|
||||
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 {
|
||||
// how to expand scale range if inferred non-regular or log buckets?
|
||||
}
|
||||
// linear expansion
|
||||
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({
|
||||
scaleKey: 'y',
|
||||
placement: AxisPlacement.Left,
|
||||
show: yAxisConfig.axisPlacement !== AxisPlacement.Hidden,
|
||||
placement: yAxisConfig.axisPlacement || AxisPlacement.Left,
|
||||
size: yAxisConfig.axisWidth || null,
|
||||
label: yAxisConfig.axisLabel,
|
||||
theme: theme,
|
||||
splits: hasLabeledY
|
||||
? () => {
|
||||
@ -255,7 +310,7 @@ export function prepConfig(opts: PrepConfigOpts) {
|
||||
|
||||
const bucketSize = dataRef.current?.yBucketSize!;
|
||||
|
||||
if (dataRef.current?.yLayout === BucketLayout.le) {
|
||||
if (dataRef.current?.yLayout === HeatmapBucketLayout.le) {
|
||||
splits.unshift(ys[0] - bucketSize);
|
||||
} else {
|
||||
splits.push(ys[ys.length - 1] + bucketSize);
|
||||
@ -266,12 +321,14 @@ export function prepConfig(opts: PrepConfigOpts) {
|
||||
: undefined,
|
||||
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) {
|
||||
yAxisValues.unshift('0.0'); // assumes dense layout where lowest bucket's low bound is 0-ish
|
||||
} else if (dataRef.current?.yLayout === BucketLayout.ge) {
|
||||
yAxisValues.push('+Inf');
|
||||
if (dataRef.current?.yLayout === HeatmapBucketLayout.le) {
|
||||
yAxisValues.unshift(isFromBuckets ? '' : '0.0'); // assumes dense layout where lowest bucket's low bound is 0-ish
|
||||
} else if (dataRef.current?.yLayout === HeatmapBucketLayout.ge) {
|
||||
yAxisValues.push(isFromBuckets ? '' : '+Inf');
|
||||
}
|
||||
|
||||
return yAxisValues;
|
||||
@ -307,12 +364,18 @@ export function prepConfig(opts: PrepConfigOpts) {
|
||||
},
|
||||
gap: cellGap,
|
||||
hideThreshold,
|
||||
xAlign: dataRef.current?.xLayout === BucketLayout.le ? -1 : dataRef.current?.xLayout === BucketLayout.ge ? 1 : 0,
|
||||
yAlign: ((dataRef.current?.yLayout === BucketLayout.le
|
||||
xAlign:
|
||||
dataRef.current?.xLayout === HeatmapBucketLayout.le
|
||||
? -1
|
||||
: dataRef.current?.xLayout === HeatmapBucketLayout.ge
|
||||
? 1
|
||||
: 0,
|
||||
yAlign: ((dataRef.current?.yLayout === HeatmapBucketLayout.le
|
||||
? -1
|
||||
: dataRef.current?.yLayout === BucketLayout.ge
|
||||
: dataRef.current?.yLayout === HeatmapBucketLayout.ge
|
||||
? 1
|
||||
: 0) * (yAxisReverse ? -1 : 1)) as -1 | 0 | 1,
|
||||
ySizeDivisor,
|
||||
disp: {
|
||||
fill: {
|
||||
values: (u, seriesIdx) => {
|
||||
@ -402,7 +465,7 @@ export function prepConfig(opts: PrepConfigOpts) {
|
||||
const CRISP_EDGES_GAP_MIN = 4;
|
||||
|
||||
export function heatmapPathsDense(opts: PathbuilderOpts) {
|
||||
const { disp, each, gap = 1, hideThreshold = 0, xAlign = 1, yAlign = 1 } = opts;
|
||||
const { disp, each, gap = 1, hideThreshold = 0, xAlign = 1, yAlign = 1, ySizeDivisor = 1 } = opts;
|
||||
|
||||
const pxRatio = devicePixelRatio;
|
||||
|
||||
@ -451,8 +514,22 @@ export function heatmapPathsDense(opts: PathbuilderOpts) {
|
||||
let xBinIncr = xs[yBinQty] - xs[0];
|
||||
|
||||
// uniform tile sizes based on zoom level
|
||||
let xSize = Math.abs(valToPosX(xBinIncr, scaleX, xDim, xOff) - valToPosX(0, scaleX, xDim, xOff));
|
||||
let ySize = Math.abs(valToPosY(yBinIncr, scaleY, yDim, yOff) - valToPosY(0, scaleY, yDim, yOff));
|
||||
let xSize: number;
|
||||
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
|
||||
xSize = Math.max(1, round(xSize - cellGap));
|
||||
|
Loading…
Reference in New Issue
Block a user