From 79c39e790792a39c356bc792478e04127728beb4 Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Tue, 1 Jul 2025 15:03:53 -0500 Subject: [PATCH] TimeSeries/Trend: Fix sparse points rendering heuristic (#107279) --- .../panel-timeseries/timeseries-nulls.json | 368 ++++++++++++++---- .../app/core/components/TimeSeries/utils.ts | 26 +- 2 files changed, 309 insertions(+), 85 deletions(-) diff --git a/devenv/dev-dashboards/panel-timeseries/timeseries-nulls.json b/devenv/dev-dashboards/panel-timeseries/timeseries-nulls.json index a107a35423f..bae4ac5b6b7 100644 --- a/devenv/dev-dashboards/panel-timeseries/timeseries-nulls.json +++ b/devenv/dev-dashboards/panel-timeseries/timeseries-nulls.json @@ -21,10 +21,10 @@ } ] }, - "description": "", "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 0, + "id": 21273, "links": [ { "asDropdown": true, @@ -37,9 +37,9 @@ "type": "dashboards" } ], - "liveNow": false, "panels": [ { + "collapsed": false, "gridPos": { "h": 1, "w": 24, @@ -47,6 +47,7 @@ "y": 0 }, "id": 44, + "panels": [], "title": "Enums", "type": "row" }, @@ -99,7 +100,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -162,6 +163,7 @@ "sort": "none" } }, + "pluginVersion": "12.1.0-pre", "targets": [ { "datasource": { @@ -209,6 +211,7 @@ "mode": "thresholds" }, "custom": { + "axisPlacement": "auto", "fillOpacity": 70, "hideFrom": { "legend": false, @@ -225,7 +228,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -282,6 +285,7 @@ "sort": "none" } }, + "pluginVersion": "12.1.0-pre", "targets": [ { "datasource": { @@ -318,6 +322,7 @@ "type": "state-timeline" }, { + "collapsed": false, "gridPos": { "h": 1, "w": 24, @@ -325,6 +330,7 @@ "y": 17 }, "id": 43, + "panels": [], "title": "Gaps and connected", "type": "row" }, @@ -375,7 +381,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -394,7 +400,6 @@ "y": 18 }, "id": 3, - "links": [], "options": { "legend": { "calcs": [], @@ -407,7 +412,7 @@ "sort": "none" } }, - "pluginVersion": "7.4.0-pre", + "pluginVersion": "12.1.0-pre", "targets": [ { "alias": "", @@ -490,7 +495,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -509,7 +514,6 @@ "y": 18 }, "id": 2, - "links": [], "options": { "legend": { "calcs": [], @@ -522,7 +526,7 @@ "sort": "none" } }, - "pluginVersion": "7.4.0-pre", + "pluginVersion": "12.1.0-pre", "targets": [ { "alias": "", @@ -613,7 +617,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -632,7 +636,6 @@ "y": 18 }, "id": 6, - "links": [], "options": { "legend": { "calcs": [], @@ -645,7 +648,7 @@ "sort": "none" } }, - "pluginVersion": "7.4.0-pre", + "pluginVersion": "12.1.0-pre", "targets": [ { "alias": "", @@ -754,7 +757,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -773,7 +776,6 @@ "y": 25 }, "id": 4, - "links": [], "options": { "legend": { "calcs": [], @@ -786,7 +788,7 @@ "sort": "none" } }, - "pluginVersion": "7.4.0-pre", + "pluginVersion": "12.1.0-pre", "targets": [ { "alias": "", @@ -869,7 +871,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -888,7 +890,6 @@ "y": 25 }, "id": 5, - "links": [], "options": { "legend": { "calcs": [], @@ -901,7 +902,7 @@ "sort": "none" } }, - "pluginVersion": "7.4.0-pre", + "pluginVersion": "12.1.0-pre", "targets": [ { "alias": "", @@ -992,7 +993,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -1011,7 +1012,6 @@ "y": 25 }, "id": 7, - "links": [], "options": { "legend": { "calcs": [], @@ -1024,7 +1024,7 @@ "sort": "none" } }, - "pluginVersion": "7.4.0-pre", + "pluginVersion": "12.1.0-pre", "targets": [ { "alias": "", @@ -1133,7 +1133,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -1197,7 +1197,7 @@ "sort": "none" } }, - "pluginVersion": "7.2.0-pre", + "pluginVersion": "12.1.0-pre", "targets": [ { "refId": "A", @@ -1232,7 +1232,6 @@ } ], "title": "Null values in first series & show gaps ", - "transformations": [], "type": "timeseries" }, { @@ -1282,7 +1281,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -1346,7 +1345,7 @@ "sort": "none" } }, - "pluginVersion": "7.2.0-pre", + "pluginVersion": "12.1.0-pre", "targets": [ { "alias": "", @@ -1381,11 +1380,12 @@ } ], "title": "Null values in second series show gaps (bugged)", - "transformations": [], "type": "timeseries" }, { - "datasource": { "type": "testdata" }, + "datasource": { + "type": "testdata" + }, "fieldConfig": { "defaults": { "color": { @@ -1432,7 +1432,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -1462,6 +1462,7 @@ "sort": "none" } }, + "pluginVersion": "12.1.0-pre", "targets": [ { "refId": "A", @@ -1473,7 +1474,9 @@ "type": "timeseries" }, { - "datasource": { "type": "testdata" }, + "datasource": { + "type": "testdata" + }, "fieldConfig": { "defaults": { "color": { @@ -1520,7 +1523,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -1550,6 +1553,7 @@ "sort": "none" } }, + "pluginVersion": "12.1.0-pre", "targets": [ { "datasource": { @@ -1565,7 +1569,9 @@ "type": "timeseries" }, { - "datasource": { "type": "testdata" }, + "datasource": { + "type": "testdata" + }, "fieldConfig": { "defaults": { "color": { @@ -1609,7 +1615,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -1640,6 +1646,7 @@ "sort": "none" } }, + "pluginVersion": "12.1.0-pre", "targets": [ { "datasource": { @@ -1655,7 +1662,9 @@ "type": "timeseries" }, { - "datasource": { "type": "testdata" }, + "datasource": { + "type": "testdata" + }, "fieldConfig": { "defaults": { "color": { @@ -1699,7 +1708,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -1730,6 +1739,7 @@ "sort": "none" } }, + "pluginVersion": "12.1.0-pre", "targets": [ { "datasource": { @@ -1745,7 +1755,9 @@ "type": "timeseries" }, { - "datasource": { "type": "testdata" }, + "datasource": { + "type": "testdata" + }, "fieldConfig": { "defaults": { "color": { @@ -1789,7 +1801,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -1820,6 +1832,7 @@ "sort": "none" } }, + "pluginVersion": "12.1.0-pre", "targets": [ { "datasource": { @@ -1835,7 +1848,9 @@ "type": "timeseries" }, { - "datasource": { "type": "testdata" }, + "datasource": { + "type": "testdata" + }, "fieldConfig": { "defaults": { "color": { @@ -1879,7 +1894,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -1891,7 +1906,7 @@ "overrides": [] }, "gridPos": { - "h": 10, + "h": 5, "w": 8, "x": 0, "y": 61 @@ -1910,6 +1925,7 @@ "sort": "none" } }, + "pluginVersion": "12.1.0-pre", "targets": [ { "datasource": { @@ -1925,7 +1941,9 @@ "type": "timeseries" }, { - "datasource": { "type": "testdata" }, + "datasource": { + "type": "testdata" + }, "fieldConfig": { "defaults": { "color": { @@ -1969,7 +1987,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -1981,7 +1999,7 @@ "overrides": [] }, "gridPos": { - "h": 10, + "h": 5, "w": 8, "x": 8, "y": 61 @@ -2000,6 +2018,7 @@ "sort": "none" } }, + "pluginVersion": "12.1.0-pre", "targets": [ { "datasource": { @@ -2015,7 +2034,9 @@ "type": "timeseries" }, { - "datasource": { "type": "testdata" }, + "datasource": { + "type": "testdata" + }, "fieldConfig": { "defaults": { "color": { @@ -2059,7 +2080,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -2071,7 +2092,7 @@ "overrides": [] }, "gridPos": { - "h": 10, + "h": 5, "w": 8, "x": 16, "y": 61 @@ -2090,6 +2111,7 @@ "sort": "none" } }, + "pluginVersion": "12.1.0-pre", "targets": [ { "datasource": { @@ -2104,6 +2126,192 @@ "title": "Higher density (mostly nulls, no leading/trailing)", "type": "timeseries" }, + { + "datasource": { + "type": "testdata" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 8, + "x": 0, + "y": 66 + }, + "id": 47, + "maxDataPoints": 150, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0-pre", + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "8" + } + ], + "title": "Higher density (mostly nulls, one leading point)", + "type": "timeseries" + }, + { + "datasource": { + "type": "testdata" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 8, + "x": 8, + "y": 66 + }, + "id": 48, + "maxDataPoints": 150, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0-pre", + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": ",,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,8" + } + ], + "title": "Higher density (mostly nulls, one trailing point)", + "type": "timeseries" + }, { "collapsed": false, "gridPos": { @@ -2165,7 +2373,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -2196,7 +2404,7 @@ "sort": "none" } }, - "pluginVersion": "7.5.0-pre", + "pluginVersion": "12.1.0-pre", "targets": [ { "alias": "", @@ -2213,7 +2421,9 @@ "type": "timeseries" }, { - "datasource": { "type": "testdata" }, + "datasource": { + "type": "testdata" + }, "fieldConfig": { "defaults": { "color": { @@ -2257,7 +2467,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -2288,7 +2498,7 @@ "sort": "none" } }, - "pluginVersion": "7.5.0-pre", + "pluginVersion": "12.1.0-pre", "targets": [ { "alias": "", @@ -2305,13 +2515,16 @@ "type": "trend" }, { - "datasource": { "type": "testdata" }, + "datasource": { + "type": "testdata" + }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "custom": { + "axisPlacement": "auto", "fillOpacity": 70, "hideFrom": { "legend": false, @@ -2328,7 +2541,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "#EAB839", @@ -2374,7 +2587,7 @@ "sort": "none" } }, - "pluginVersion": "7.5.0-pre", + "pluginVersion": "12.1.0-pre", "targets": [ { "alias": "", @@ -2438,7 +2651,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -2469,7 +2682,7 @@ "sort": "none" } }, - "pluginVersion": "7.5.0-pre", + "pluginVersion": "12.1.0-pre", "targets": [ { "datasource": { @@ -2531,7 +2744,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -2562,7 +2775,7 @@ "sort": "none" } }, - "pluginVersion": "7.5.0-pre", + "pluginVersion": "12.1.0-pre", "targets": [ { "datasource": { @@ -2587,6 +2800,7 @@ "mode": "thresholds" }, "custom": { + "axisPlacement": "auto", "fillOpacity": 70, "hideFrom": { "legend": false, @@ -2603,7 +2817,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "#EAB839", @@ -2649,7 +2863,7 @@ "sort": "none" } }, - "pluginVersion": "7.5.0-pre", + "pluginVersion": "12.1.0-pre", "targets": [ { "datasource": { @@ -2711,7 +2925,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -2742,7 +2956,7 @@ "sort": "none" } }, - "pluginVersion": "7.5.0-pre", + "pluginVersion": "12.1.0-pre", "targets": [ { "datasource": { @@ -2804,7 +3018,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -2835,7 +3049,7 @@ "sort": "none" } }, - "pluginVersion": "7.5.0-pre", + "pluginVersion": "12.1.0-pre", "targets": [ { "datasource": { @@ -2860,6 +3074,7 @@ "mode": "thresholds" }, "custom": { + "axisPlacement": "auto", "fillOpacity": 70, "hideFrom": { "legend": false, @@ -2876,7 +3091,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "#EAB839", @@ -2922,7 +3137,7 @@ "sort": "none" } }, - "pluginVersion": "7.5.0-pre", + "pluginVersion": "12.1.0-pre", "targets": [ { "datasource": { @@ -2984,7 +3199,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -3015,7 +3230,7 @@ "sort": "none" } }, - "pluginVersion": "7.5.0-pre", + "pluginVersion": "12.1.0-pre", "targets": [ { "datasource": { @@ -3077,7 +3292,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "red", @@ -3108,7 +3323,7 @@ "sort": "none" } }, - "pluginVersion": "7.5.0-pre", + "pluginVersion": "12.1.0-pre", "targets": [ { "datasource": { @@ -3133,6 +3348,7 @@ "mode": "thresholds" }, "custom": { + "axisPlacement": "auto", "fillOpacity": 70, "hideFrom": { "legend": false, @@ -3149,7 +3365,7 @@ "steps": [ { "color": "green", - "value": null + "value": 0 }, { "color": "#EAB839", @@ -3195,7 +3411,7 @@ "sort": "none" } }, - "pluginVersion": "7.5.0-pre", + "pluginVersion": "12.1.0-pre", "targets": [ { "datasource": { @@ -3210,8 +3426,9 @@ "type": "state-timeline" } ], + "preload": false, "refresh": false, - "schemaVersion": 39, + "schemaVersion": 41, "tags": [ "gdev", "panel-tests", @@ -3232,6 +3449,5 @@ "timezone": "", "title": "Panel Tests - Graph NG - Discrete panels", "uid": "8mmCAF1Mz", - "version": 25, - "weekStart": "" + "version": 1 } \ No newline at end of file diff --git a/public/app/core/components/TimeSeries/utils.ts b/public/app/core/components/TimeSeries/utils.ts index b23a6a3b46d..d71e3246dab 100644 --- a/public/app/core/components/TimeSeries/utils.ts +++ b/public/app/core/components/TimeSeries/utils.ts @@ -343,7 +343,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ let pointsFilter: uPlot.Series.Points.Filter = () => null; - if (customConfig.spanNulls !== true) { + if (customConfig.spanNulls !== true && showPoints === VisibilityMode.Auto) { pointsFilter = (u, seriesIdx, show, gaps) => { let filtered = []; @@ -390,15 +390,23 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ filtered.push(lastIdx); } } - // single point surrounded by nulls + // single point else { - // meh heuristic - if (yData[0] == null && yData[yData.length - 1] == null) { - for (let i = 0; i < yData.length; i++) { - if (yData[i] != null) { - filtered.push(i); - } - } + // scan right + let leftIdx = 0; + while (yData[leftIdx] == null) { + leftIdx++; + } + + // scan left + let rightIdx = yData.length - 1; + while (rightIdx >= leftIdx && yData[rightIdx] == null) { + rightIdx--; + } + + // render if same + if (leftIdx === rightIdx) { + filtered.push(leftIdx); } } }