From 9f8fa4212ffbd090c350539c391048f58f516885 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Mon, 10 May 2021 14:24:23 +0200 Subject: [PATCH] GraphNG - shared cursor (#33433) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial work * WIP add cursor in debug panel * shared cursor.sync filter * explicit uplot events * explicit uplot events * uplot events * uplot events * depend on master uplot * sync sync sync * Fix merge * Get rid of PlotSyncContext and sync tooltip positions * make sync optional * Improve shared tooltip positioning * Plugins: add level and signature badges to plugin details page (#33553) * feat(grafana-ui): badge can accept react node for text, add shield-exclamation to icons * feat(plugins): add PluginSignatureType type * feat(pluginpage): introduce PluginSignatureDetailsBadge. Fix sidebar icon margin * feat(pluginlistpage): update filterinput placeholder, introduce filter by plugin type * Variables: Removes the never refresh option (#33533) * Variables: Removes the never refresh option * Tests: fixes DashboardModel repeat tests * Tests: fixs snapshots * Tests: fixes processVariable test * Tests: fixes DashboardModel tests * PageLayout: Fixes max-width breakpoint so that it triggers only when there is room for margin+ (#33558) * Alerting: Remove datasource (name) from migration (#33544) no longer needed as of https://github.com/grafana/grafana/pull/33416 for https://github.com/grafana/alerting-squad/issues/126 * Library panels: Adds description to library panels tab (#33428) * CodeOwners: Set owners of unified alerting migration (#33571) * ButtonSelect: updates component with the new theme model (#33565) * EmptySearchResult: updates component with the new theme model (#33573) * DashboardSettings: Slight design tweak to fix page toolbar padding and align design (#33575) * DashboardSettings: Slight design tweak to fix page toolbar padding and align design * Fixed font weight * Removed comment * Update * gitignore: Ignore files for accesscontrol provisioning (#33577) * Alerting/metrics (#33547) * moves alerting metrics to their own pkg * adds grafana_alerting_alerts (by state) metric * alerts_received_{total,invalid} * embed alertmanager alerting struct in ng metrics & remove duplicated notification metrics (already embed alertmanager notifier metrics) * use silence metrics from alertmanager lib * fix - manager has metrics * updates ngalert tests * comment lint Signed-off-by: Owen Diehl * cleaner prom registry code * removes ngalert global metrics * new registry use in all tests * ngalert metrics impl service, hack testinfra code to prevent duplicate metric registrations * nilmetrics unexported * Add note to Snapshot API doc to specify that user has to provide the entire dashboard model (#33572) * Added note as suggested by Macus E. * Update docs/sources/http_api/snapshot.md Co-authored-by: Ursula Kallio <73951760+osg-grafana@users.noreply.github.com> Co-authored-by: Ursula Kallio <73951760+osg-grafana@users.noreply.github.com> * Alerting: backend "ng" code cleanup (#33578) * AlertMigration: remove alert_rule UID db check (#33568) do not believe this is needed due to uniqueness promised by shortid lib since there is no provisioning yet. https://github.com/teris-io/shortid * Live: persisting last message in cache for broadcast scope (#32938) * Alerting: Load annotations from rule into State cache (#33542) for https://github.com/grafana/alerting-squad/issues/127 * add template for dashboard url parameters (#33549) * Update dashboard-links.md parameters with plain text like `var-something=value` can make confusion. template it to clarify . * describe way for template link. * AlertingMigration: Create alert_rule_version entry (#33585) Create the alert rule version entry during the migration so it is consistent with rules created via api. for https://github.com/grafana/alerting-squad/issues/123 * Build: Fix with cleanup call maybe? (#33590) * add selector for code editor (#33554) * broadcast over eventBus * broadcasting to eventbus (but not useing it yet) * merge master * moved to context * fix yarn.lock * update snapshot * Fix direct state mutation * Persist location state on partial updates * GraphNG- use getStream rather than subscribe * Sync LegacyGraphHoverEvent with GraphNG * Chenge plotRef signature * use subscription * subscription * one fewer file * Update types * Remove unnecessary filtering * Disable cursor sync when in edit mode * GraphNG - bring back logging Co-authored-by: Ryan McKinley Co-authored-by: Leon Sorokin Co-authored-by: Jack Westbrook Co-authored-by: Hugo Häggmark Co-authored-by: Torkel Ödegaard Co-authored-by: Kyle Brandt Co-authored-by: kay delaney <45561153+kaydelaney@users.noreply.github.com> Co-authored-by: Uchechukwu Obasi Co-authored-by: gamab Co-authored-by: Owen Diehl Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> Co-authored-by: Ursula Kallio <73951760+osg-grafana@users.noreply.github.com> Co-authored-by: Alexander Emelin Co-authored-by: Nagle Zhang Co-authored-by: Erik Sundell --- .../graph-shared-tooltip-cursor-position.json | 561 +++++++++++++ .../panel-graph/graph-shared-tooltips.json | 780 ++++++++++++++++++ packages/grafana-data/src/events/common.ts | 19 +- packages/grafana-data/src/events/types.ts | 1 + packages/grafana-data/src/types/dashboard.ts | 5 + packages/grafana-data/src/types/index.ts | 1 + .../grafana-data/src/types/legacyEvents.ts | 4 +- .../src/components/GraphNG/GraphNG.tsx | 95 ++- .../GraphNG/__snapshots__/utils.test.ts.snap | 18 +- .../src/components/GraphNG/utils.test.ts | 4 + .../src/components/GraphNG/utils.ts | 8 +- .../components/PanelChrome/PanelContext.ts | 7 +- .../src/components/TimeSeries/TimeSeries.tsx | 12 +- .../src/components/TimeSeries/utils.ts | 76 +- .../src/components/Timeline/TimelineChart.tsx | 8 + .../src/components/Timeline/utils.ts | 16 +- .../VizTooltip/VizTooltipContainer.tsx | 1 + .../grafana-ui/src/components/uPlot/Plot.tsx | 30 +- .../uPlot/config/UPlotConfigBuilder.ts | 5 +- .../uPlot/{context.ts => context.tsx} | 2 +- .../uPlot/plugins/TooltipPlugin.tsx | 105 ++- .../grafana-ui/src/components/uPlot/types.ts | 4 +- .../grafana-ui/src/components/uPlot/utils.ts | 51 ++ packages/grafana-ui/src/utils/logger.ts | 2 + .../DashboardSettings/GeneralSettings.tsx | 15 +- .../dashboard/dashgrid/PanelChrome.tsx | 20 +- .../dashboard/state/DashboardModel.ts | 3 +- public/app/plugins/panel/debug/CursorView.tsx | 69 ++ public/app/plugins/panel/debug/DebugPanel.tsx | 38 +- public/app/plugins/panel/debug/module.tsx | 1 + public/app/plugins/panel/debug/types.ts | 1 + .../app/plugins/panel/graph/graph_tooltip.ts | 6 +- public/app/plugins/panel/heatmap/rendering.ts | 9 +- .../panel/timeseries/TimeSeriesPanel.tsx | 9 +- 34 files changed, 1844 insertions(+), 142 deletions(-) create mode 100644 devenv/dev-dashboards/panel-graph/graph-shared-tooltip-cursor-position.json create mode 100644 devenv/dev-dashboards/panel-graph/graph-shared-tooltips.json create mode 100644 packages/grafana-data/src/types/dashboard.ts rename packages/grafana-ui/src/components/uPlot/{context.ts => context.tsx} (90%) create mode 100644 public/app/plugins/panel/debug/CursorView.tsx diff --git a/devenv/dev-dashboards/panel-graph/graph-shared-tooltip-cursor-position.json b/devenv/dev-dashboards/panel-graph/graph-shared-tooltip-cursor-position.json new file mode 100644 index 00000000000..85d1fa69a52 --- /dev/null +++ b/devenv/dev-dashboards/panel-graph/graph-shared-tooltip-cursor-position.json @@ -0,0 +1,561 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 2, + "id": 396, + "links": [], + "panels": [ + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "degree" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 8, + "x": 0, + "y": 0 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltipOptions": { + "mode": "single" + } + }, + "pluginVersion": "7.5.0-pre", + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "-9.4, -8.2, -8, -8, -7, -7.9, -5.9, -6.1, -5.5, -5.8, -4.7, -4.7, -4.1, -4.5, -3.5, -4, -2.4, -3.4, -1, -1, -0.3, -0.8, 0.8, 0.5, 1.7, 1.2, 2, 1.9, 2.4, 2.3, 3.3, 3.2, 3.9, 3.8, 4.5, 4.5, 5.6, 5.1, 7.1, 6.7, 7.2, 7.1, 7.6, 7.5, 8.4, 7.9, 9, 8.5, 9.4, 9.2" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Origin", + "type": "timeseries" + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "degree" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "B-series" + }, + "properties": [ + { + "id": "unit", + "value": "tflops" + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 8, + "x": 8, + "y": 0 + }, + "id": 9, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltipOptions": { + "mode": "single" + } + }, + "pluginVersion": "7.5.0-pre", + "targets": [ + { + "hide": false, + "max": 10, + "min": -10, + "noise": 1, + "refId": "A", + "scenarioId": "random_walk", + "spread": 2 + }, + { + "hide": false, + "max": 10, + "min": -10, + "noise": 1, + "refId": "B", + "scenarioId": "random_walk", + "spread": 2 + }, + { + "hide": false, + "max": 10, + "min": -10, + "noise": 1, + "refId": "C", + "scenarioId": "random_walk", + "spread": 2 + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Sensor 1, ranging as Origin panel", + "type": "timeseries" + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "degree" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "B-series" + }, + "properties": [ + { + "id": "unit", + "value": "tflops" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "A-series" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "purple", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 8, + "x": 16, + "y": 0 + }, + "id": 10, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltipOptions": { + "mode": "single" + } + }, + "pluginVersion": "7.5.0-pre", + "targets": [ + { + "hide": false, + "max": 4, + "min": -4, + "noise": 0, + "refId": "B", + "scenarioId": "random_walk", + "spread": 2 + }, + { + "hide": false, + "max": 4, + "min": -4, + "noise": 0, + "refId": "A", + "scenarioId": "random_walk", + "spread": 2 + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Sensor 2, ranging [-4, 4]", + "type": "timeseries" + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "degree" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "B-series" + }, + "properties": [ + { + "id": "unit", + "value": "tflops" + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 8, + "x": 8, + "y": 9 + }, + "id": 7, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltipOptions": { + "mode": "single" + } + }, + "pluginVersion": "7.5.0-pre", + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "5, 4.6, 4.4, 3.5, 3.3, 3, 2.6, 2.6, 2.4, 2.1, 1.8, 1.8, 1.7, 1.6, 1.5, 1.5, 1.4, 0.5, 0.5, 0, -0.2, -0.2, -0.4, -0.6, -1.2, -1.6, -2.4, -2.4, -2.8, -2.9, -2.9, -3.2, -3.3, -3.4, -4.3, -4.3, -4.6, -6.1, -6.3, -6.6, -6.7, -7, -7.2, -7.3, -7.5, -7.8, -8.2, -8.3, -9.1, -9.3" + }, + { + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "6,4,2" + }, + { + "hide": false, + "refId": "C", + "scenarioId": "csv_metric_values", + "stringInput": "-3,-4,-2" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Sensor 3", + "type": "timeseries" + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "degree" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "B-series" + }, + "properties": [ + { + "id": "unit", + "value": "tflops" + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 8, + "x": 16, + "y": 9 + }, + "id": 8, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltipOptions": { + "mode": "single" + } + }, + "pluginVersion": "7.5.0-pre", + "targets": [ + { + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "6,4,2" + }, + { + "hide": false, + "refId": "C", + "scenarioId": "csv_metric_values", + "stringInput": "-3,-4,-2" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Sensor 5", + "type": "timeseries" + } + ], + "refresh": false, + "schemaVersion": 28, + "style": "dark", + "tags": [ + "gdev", + "panel-tests", + "graph-ng" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Panel Tests - shared tooltips cursor positioning", + "uid": "ICiqZ1rGk", + "version": 51 +} diff --git a/devenv/dev-dashboards/panel-graph/graph-shared-tooltips.json b/devenv/dev-dashboards/panel-graph/graph-shared-tooltips.json new file mode 100644 index 00000000000..a311cc4caf7 --- /dev/null +++ b/devenv/dev-dashboards/panel-graph/graph-shared-tooltips.json @@ -0,0 +1,780 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 1, + "links": [], + "panels": [ + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "celsius" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Speed" + }, + "properties": [ + { + "id": "unit", + "value": "velocitykmh" + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 9, + "x": 0, + "y": 0 + }, + "id": 4, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltipOptions": { + "mode": "single" + } + }, + "pluginVersion": "7.5.0-pre", + "targets": [ + { + "alias": "Temperature", + "max": 25, + "min": 1, + "refId": "A", + "scenarioId": "random_walk" + }, + { + "alias": "Speed", + "hide": false, + "refId": "B", + "scenarioId": "random_walk" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "two units", + "type": "timeseries" + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "points", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "celsius" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Temperature" + }, + "properties": [ + { + "id": "unit", + "value": "celsius" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Speed" + }, + "properties": [ + { + "id": "unit", + "value": "velocitykmh" + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 9, + "y": 0 + }, + "id": 13, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltipOptions": { + "mode": "single" + } + }, + "pluginVersion": "7.5.0-pre", + "targets": [ + { + "max": 25, + "min": 1, + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 2 + } + ], + "title": "Speed vs Temperature (XY)", + "transformations": [ + { + "id": "seriesToColumns", + "options": { + "byField": "time" + } + }, + { + "id": "organize", + "options": { + "excludeByName": { + "time": true + }, + "indexByName": {}, + "renameByName": { + "A-series": "Temperature", + "A-series1": "Speed" + } + } + } + ], + "type": "xychart" + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 7, + "x": 17, + "y": 0 + }, + "id": 2, + "options": { + "counters": { + "dataChanged": true, + "render": true, + "schemaChanged": true + }, + "mode": "cursor" + }, + "pluginVersion": "7.5.0-pre", + "timeFrom": null, + "timeShift": null, + "title": "Cursor info", + "type": "debug" + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "celsius" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 9, + "x": 0, + "y": 8 + }, + "id": 5, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltipOptions": { + "mode": "single" + } + }, + "pluginVersion": "7.5.0-pre", + "targets": [ + { + "alias": "Temperature", + "max": 25, + "min": 5, + "refId": "A", + "scenarioId": "random_walk" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Only temperature", + "type": "timeseries" + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "velocitykmh" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Speed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "yellow", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 9, + "y": 8 + }, + "id": 9, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltipOptions": { + "mode": "single" + } + }, + "pluginVersion": "7.5.0-pre", + "targets": [ + { + "alias": "Speed", + "hide": false, + "refId": "B", + "scenarioId": "random_walk" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Only Speed", + "type": "timeseries" + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "accMS2" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Speed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "yellow", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Acceleration" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "purple", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 7, + "x": 17, + "y": 8 + }, + "id": 11, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltipOptions": { + "mode": "single" + } + }, + "pluginVersion": "7.5.0-pre", + "targets": [ + { + "alias": "Acceleration", + "hide": false, + "refId": "B", + "scenarioId": "random_walk" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Panel Title", + "type": "timeseries" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": null, + "fieldConfig": { + "defaults": { + "unit": "celsius" + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 6, + "w": 12, + "x": 0, + "y": 14 + }, + "hiddenSeries": false, + "id": 8, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.0-pre", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "max": 25, + "min": 1, + "refId": "A", + "scenarioId": "random_walk" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "flot panel (temperature)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "celsius", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": null, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 6, + "w": 12, + "x": 12, + "y": 14 + }, + "hiddenSeries": false, + "id": 10, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.0-pre", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "flot panel (no units)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "refresh": false, + "schemaVersion": 28, + "style": "dark", + "tags": [ + "gdev", + "panel-tests", + "graph-ng" + ], + "templating": { + "list": [] + }, + "time": { + "from": "2020-09-14T16:13:20.000Z", + "to": "2020-09-15T20:00:00.000Z" + }, + "timepicker": {}, + "timezone": "", + "title": "Panel Tests - shared tooltips", + "uid": "TX2VU59MZ", + "version": 2 +} diff --git a/packages/grafana-data/src/events/common.ts b/packages/grafana-data/src/events/common.ts index 071cbf25b7c..1dc5ba66993 100644 --- a/packages/grafana-data/src/events/common.ts +++ b/packages/grafana-data/src/events/common.ts @@ -1,20 +1,23 @@ import { DataFrame } from '../types'; import { BusEventWithPayload } from './types'; -/** @alpha */ +/** + * When hovering over an element this will identify + * + * For performance reasons, this object will usually be mutated between updates. This + * will avoid creating new objects for events that fire frequently (ie each mouse pixel) + * + * @alpha + */ export interface DataHoverPayload { - raw: any; // Original mouse event (includes pageX etc) - - x: Record; // { time: 5678 }, - y: Record; // { __fixed: 123, lengthft: 456 } // each axis|scale gets a value - data?: DataFrame; // source data rowIndex?: number; // the hover row columnIndex?: number; // the hover column dataId?: string; // identifying string to correlate data between publishers and subscribers - // When dragging, this will capture the original state - down?: Omit; + // When dragging, this will capture the point when the mouse was down + point: Record; // { time: 5678, lengthft: 456 } // each axis|scale gets a value + down?: Record; } /** @alpha */ diff --git a/packages/grafana-data/src/events/types.ts b/packages/grafana-data/src/events/types.ts index 7deead3970d..6f54cd65218 100644 --- a/packages/grafana-data/src/events/types.ts +++ b/packages/grafana-data/src/events/types.ts @@ -17,6 +17,7 @@ export interface BusEvent { export abstract class BusEventBase implements BusEvent { readonly type: string; readonly payload?: any; + readonly origin?: EventBus; constructor() { //@ts-ignore diff --git a/packages/grafana-data/src/types/dashboard.ts b/packages/grafana-data/src/types/dashboard.ts new file mode 100644 index 00000000000..85dbe95adab --- /dev/null +++ b/packages/grafana-data/src/types/dashboard.ts @@ -0,0 +1,5 @@ +export enum DashboardCursorSync { + Off, + Crosshair, + Tooltip, +} diff --git a/packages/grafana-data/src/types/index.ts b/packages/grafana-data/src/types/index.ts index 3fea83f134e..afc91463a8e 100644 --- a/packages/grafana-data/src/types/index.ts +++ b/packages/grafana-data/src/types/index.ts @@ -1,6 +1,7 @@ export * from './data'; export * from './dataFrame'; export * from './dataLink'; +export * from './dashboard'; export * from './annotations'; export * from './logs'; export * from './navModel'; diff --git a/packages/grafana-data/src/types/legacyEvents.ts b/packages/grafana-data/src/types/legacyEvents.ts index 10c0092b32c..d7e09095a1f 100644 --- a/packages/grafana-data/src/types/legacyEvents.ts +++ b/packages/grafana-data/src/types/legacyEvents.ts @@ -3,6 +3,7 @@ import { AngularPanelMenuItem } from './panel'; import { DataFrame } from './dataFrame'; import { eventFactory } from '../events/eventFactory'; import { BusEventBase, BusEventWithPayload } from '../events/types'; +import { DataHoverPayload } from '../events'; export type AlertPayload = [string, string?]; export type AlertErrorPayload = [string, (string | Error)?]; @@ -28,7 +29,7 @@ export const PanelEvents = { }; /** @public */ -export interface LegacyGraphHoverEventPayload { +export interface LegacyGraphHoverEventPayload extends DataHoverPayload { pos: any; panel: { id: number; @@ -43,4 +44,5 @@ export class LegacyGraphHoverEvent extends BusEventWithPayload { + static contextType = PanelContextRoot; + panelContext: PanelContext = {} as PanelContext; + private plotInstance: React.RefObject; + + private subscription = new Subscription(); + constructor(props: GraphNGProps) { super(props); this.state = this.prepState(props); + this.plotInstance = React.createRef(); } getTimeRange = () => this.props.timeRange; @@ -74,21 +94,76 @@ export class GraphNG extends React.Component { y: fieldMatchers.get(FieldMatcherID.numeric).get({}), } ); + pluginLog('GraphNG', false, 'data aligned', alignedFrame); if (alignedFrame) { state = { alignedFrame, alignedData: preparePlotData(alignedFrame, [FieldType.number]), }; + pluginLog('GraphNG', false, 'data prepared', state.alignedData); if (withConfig) { state.config = props.prepConfig(alignedFrame, this.getTimeRange); + pluginLog('GraphNG', false, 'config prepared', state.config); } } return state; } + componentDidMount() { + this.panelContext = this.context as PanelContext; + const { eventBus } = this.panelContext; + + this.subscription.add( + eventBus + .getStream(LegacyGraphHoverEvent) + .pipe(throttleTime(50)) + .subscribe({ + next: (evt) => { + const u = this.plotInstance?.current; + if (u) { + // Try finding left position on time axis + const left = u.valToPos(evt.payload.point.time, 'time'); + let top; + if (left) { + // find midpoint between points at current idx + top = findMidPointYPosition(u, u.posToIdx(left)); + } + + if (!top || !left) { + return; + } + + u.setCursor({ + left, + top, + }); + } + }, + }) + ); + + this.subscription.add( + eventBus + .getStream(LegacyGraphHoverClearEvent) + .pipe(throttleTime(50)) + .subscribe({ + next: () => { + const u = this.plotInstance?.current; + + if (u) { + u.setCursor({ + left: -10, + top: -10, + }); + } + }, + }) + ); + } + componentDidUpdate(prevProps: GraphNGProps) { const { frames, structureRev, timeZone, propsToDiff } = this.props; @@ -107,6 +182,7 @@ export class GraphNG extends React.Component { if (shouldReconfig) { newState.config = this.props.prepConfig(newState.alignedFrame, this.getTimeRange); + pluginLog('GraphNG', false, 'config recreated', newState.config); } } @@ -114,6 +190,10 @@ export class GraphNG extends React.Component { } } + componentWillUnmount() { + this.subscription.unsubscribe(); + } + render() { const { width, height, children, timeRange, renderLegend } = this.props; const { config, alignedFrame } = this.state; @@ -131,6 +211,7 @@ export class GraphNG extends React.Component { width={vizWidth} height={vizHeight} timeRange={timeRange} + plotRef={(u) => ((this.plotInstance as React.MutableRefObject).current = u)} > {children ? children(config, alignedFrame) : null} diff --git a/packages/grafana-ui/src/components/GraphNG/__snapshots__/utils.test.ts.snap b/packages/grafana-ui/src/components/GraphNG/__snapshots__/utils.test.ts.snap index 7a535bc3ec7..776002afd64 100644 --- a/packages/grafana-ui/src/components/GraphNG/__snapshots__/utils.test.ts.snap +++ b/packages/grafana-ui/src/components/GraphNG/__snapshots__/utils.test.ts.snap @@ -12,7 +12,7 @@ Object { "width": 1, }, "labelFont": "12px \\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif", - "scale": "x", + "scale": "time", "show": true, "side": 2, "size": [Function], @@ -79,6 +79,20 @@ Object { "stroke": [Function], "width": [Function], }, + "sync": Object { + "filters": Object { + "pub": [Function], + }, + "key": "__global_", + "match": Array [ + [Function], + [Function], + ], + "scales": Array [ + "time", + "__fixed", + ], + }, }, "hooks": Object {}, "scales": Object { @@ -91,7 +105,7 @@ Object { "range": [Function], "time": undefined, }, - "x": Object { + "time": Object { "auto": false, "dir": 1, "ori": 0, diff --git a/packages/grafana-ui/src/components/GraphNG/utils.test.ts b/packages/grafana-ui/src/components/GraphNG/utils.test.ts index e1db71c88a5..3d4751fbccb 100644 --- a/packages/grafana-ui/src/components/GraphNG/utils.test.ts +++ b/packages/grafana-ui/src/components/GraphNG/utils.test.ts @@ -2,7 +2,9 @@ import { preparePlotFrame } from './utils'; import { preparePlotConfigBuilder } from '../TimeSeries/utils'; import { createTheme, + DashboardCursorSync, DefaultTimeZone, + EventBusSrv, FieldConfig, FieldMatcherID, fieldMatchers, @@ -194,6 +196,8 @@ describe('GraphNG utils', () => { theme: createTheme(), timeZone: DefaultTimeZone, getTimeRange: getDefaultTimeRange, + eventBus: new EventBusSrv(), + sync: DashboardCursorSync.Tooltip, }).getConfig(); expect(result).toMatchSnapshot(); }); diff --git a/packages/grafana-ui/src/components/GraphNG/utils.ts b/packages/grafana-ui/src/components/GraphNG/utils.ts index d01b8309432..5941ccfccba 100644 --- a/packages/grafana-ui/src/components/GraphNG/utils.ts +++ b/packages/grafana-ui/src/components/GraphNG/utils.ts @@ -2,6 +2,7 @@ import { XYFieldMatchers } from './types'; import { ArrayVector, DataFrame, + EventBus, FieldType, GrafanaTheme2, outerJoinDataFrames, @@ -9,13 +10,14 @@ import { TimeZone, } from '@grafana/data'; import { nullToUndefThreshold } from './nullToUndefThreshold'; -export interface PrepConfigOpts { + +export type PrepConfigOpts = {}> = { frame: DataFrame; theme: GrafanaTheme2; timeZone: TimeZone; getTimeRange: () => TimeRange; - [prop: string]: any; -} + eventBus: EventBus; +} & T; function applySpanNullsThresholds(frames: DataFrame[]) { for (const frame of frames) { diff --git a/packages/grafana-ui/src/components/PanelChrome/PanelContext.ts b/packages/grafana-ui/src/components/PanelChrome/PanelContext.ts index 835c790442e..7fe9cb07bc2 100644 --- a/packages/grafana-ui/src/components/PanelChrome/PanelContext.ts +++ b/packages/grafana-ui/src/components/PanelChrome/PanelContext.ts @@ -1,4 +1,4 @@ -import { EventBusSrv, EventBus } from '@grafana/data'; +import { EventBusSrv, EventBus, DashboardCursorSync } from '@grafana/data'; import React from 'react'; import { SeriesVisibilityChangeMode } from '.'; @@ -6,6 +6,9 @@ import { SeriesVisibilityChangeMode } from '.'; export interface PanelContext { eventBus: EventBus; + /** Dashboard panels sync */ + sync?: DashboardCursorSync; + /** * Called when a component wants to change the color for a series * @@ -16,7 +19,7 @@ export interface PanelContext { onToggleSeriesVisibility?: (label: string, mode: SeriesVisibilityChangeMode) => void; } -const PanelContextRoot = React.createContext({ +export const PanelContextRoot = React.createContext({ eventBus: new EventBusSrv(), }); diff --git a/packages/grafana-ui/src/components/TimeSeries/TimeSeries.tsx b/packages/grafana-ui/src/components/TimeSeries/TimeSeries.tsx index 70af4458496..1dba6045dd9 100644 --- a/packages/grafana-ui/src/components/TimeSeries/TimeSeries.tsx +++ b/packages/grafana-ui/src/components/TimeSeries/TimeSeries.tsx @@ -6,18 +6,20 @@ import { PlotLegend } from '../uPlot/PlotLegend'; import { LegendDisplayMode } from '../VizLegend/models.gen'; import { preparePlotConfigBuilder } from './utils'; import { withTheme2 } from '../../themes/ThemeContext'; +import { PanelContext, PanelContextRoot } from '../PanelChrome/PanelContext'; const propsToDiff: string[] = []; type TimeSeriesProps = Omit; export class UnthemedTimeSeries extends React.Component { + static contextType = PanelContextRoot; + panelContext: PanelContext = {} as PanelContext; + prepConfig = (alignedFrame: DataFrame, getTimeRange: () => TimeRange) => { - return preparePlotConfigBuilder({ - frame: alignedFrame, - getTimeRange, - ...this.props, - }); + const { eventBus, sync } = this.context; + const { theme, timeZone } = this.props; + return preparePlotConfigBuilder({ frame: alignedFrame, theme, timeZone, getTimeRange, eventBus, sync }); }; renderLegend = (config: UPlotConfigBuilder) => { diff --git a/packages/grafana-ui/src/components/TimeSeries/utils.ts b/packages/grafana-ui/src/components/TimeSeries/utils.ts index 8f107b6ff41..0a51d8f7675 100644 --- a/packages/grafana-ui/src/components/TimeSeries/utils.ts +++ b/packages/grafana-ui/src/components/TimeSeries/utils.ts @@ -1,6 +1,10 @@ import { isNumber } from 'lodash'; import { + DashboardCursorSync, DataFrame, + DataHoverClearEvent, + DataHoverEvent, + DataHoverPayload, FieldConfig, FieldType, formattedValueToString, @@ -9,7 +13,6 @@ import { getFieldSeriesColor, } from '@grafana/data'; -import { PrepConfigOpts } from '../GraphNG/utils'; import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder'; import { FIXED_UNIT } from '../GraphNG/GraphNG'; import { @@ -22,6 +25,7 @@ import { ScaleOrientation, } from '../uPlot/config'; import { collectStackingGroups } from '../uPlot/utils'; +import { PrepConfigOpts } from '../GraphNG/utils'; const defaultFormatter = (v: any) => (v == null ? '-' : v.toFixed(1)); @@ -31,9 +35,9 @@ const defaultConfig: GraphFieldConfig = { axisPlacement: AxisPlacement.Auto, }; -type PrepConfig = (opts: PrepConfigOpts) => UPlotConfigBuilder; +type PrepConfig = (opts: PrepConfigOpts<{ sync: DashboardCursorSync }>) => UPlotConfigBuilder; -export const preparePlotConfigBuilder: PrepConfig = ({ frame, theme, timeZone, getTimeRange }) => { +export const preparePlotConfigBuilder: PrepConfig = ({ frame, theme, timeZone, getTimeRange, eventBus, sync }) => { const builder = new UPlotConfigBuilder(timeZone); // X is the first field in the aligned frame @@ -44,20 +48,24 @@ export const preparePlotConfigBuilder: PrepConfig = ({ frame, theme, timeZone, g let seriesIndex = 0; + let xScaleKey = '_x'; + let yScaleKey = ''; + if (xField.type === FieldType.time) { + xScaleKey = 'time'; builder.addScale({ - scaleKey: 'x', + scaleKey: xScaleKey, orientation: ScaleOrientation.Horizontal, direction: ScaleDirection.Right, isTime: true, range: () => { - const timeRange = getTimeRange(); - return [timeRange.from.valueOf(), timeRange.to.valueOf()]; + const r = getTimeRange(); + return [r.from.valueOf(), r.to.valueOf()]; }, }); builder.addAxis({ - scaleKey: 'x', + scaleKey: xScaleKey, isTime: true, placement: AxisPlacement.Bottom, timeZone, @@ -65,14 +73,18 @@ export const preparePlotConfigBuilder: PrepConfig = ({ frame, theme, timeZone, g }); } else { // Not time! + if (xField.config.unit) { + xScaleKey = xField.config.unit; + } + builder.addScale({ - scaleKey: 'x', + scaleKey: xScaleKey, orientation: ScaleOrientation.Horizontal, direction: ScaleDirection.Right, }); builder.addAxis({ - scaleKey: 'x', + scaleKey: xScaleKey, placement: AxisPlacement.Bottom, theme, }); @@ -82,7 +94,7 @@ export const preparePlotConfigBuilder: PrepConfig = ({ frame, theme, timeZone, g let indexByName: Map | undefined = undefined; - for (let i = 0; i < frame.fields.length; i++) { + for (let i = 1; i < frame.fields.length; i++) { const field = frame.fields[i]; const config = field.config as FieldConfig; const customConfig: GraphFieldConfig = { @@ -114,6 +126,10 @@ export const preparePlotConfigBuilder: PrepConfig = ({ frame, theme, timeZone, g softMax: customConfig.axisSoftMax, }); + if (!yScaleKey) { + yScaleKey = scaleKey; + } + if (customConfig.axisPlacement !== AxisPlacement.Hidden) { builder.addAxis({ scaleKey, @@ -183,7 +199,6 @@ export const preparePlotConfigBuilder: PrepConfig = ({ frame, theme, timeZone, g }); } } - collectStackingGroups(field, stackingGroups, seriesIndex); } @@ -197,6 +212,45 @@ export const preparePlotConfigBuilder: PrepConfig = ({ frame, theme, timeZone, g } } } + + builder.scaleKeys = [xScaleKey, yScaleKey]; + + if (sync !== DashboardCursorSync.Off) { + const payload: DataHoverPayload = { + point: { + [xScaleKey]: null, + [yScaleKey]: null, + }, + data: frame, + }; + const hoverEvent = new DataHoverEvent(payload); + builder.setCursor({ + sync: { + key: '__global_', + filters: { + pub: (type: string, src: uPlot, x: number, y: number, w: number, h: number, dataIdx: number) => { + payload.columnIndex = dataIdx; + if (x < 0 && y < 0) { + payload.point[xScaleKey] = null; + payload.point[yScaleKey] = null; + eventBus.publish(new DataHoverClearEvent(payload)); + } else { + // convert the points + payload.point[xScaleKey] = src.posToVal(x, xScaleKey); + payload.point[yScaleKey] = src.posToVal(y, yScaleKey); + eventBus.publish(hoverEvent); + hoverEvent.payload.down = undefined; + } + return true; + }, + }, + // ??? setSeries: syncMode === DashboardCursorSync.Tooltip, + scales: builder.scaleKeys, + match: [() => true, () => true], + }, + }); + } + return builder; }; diff --git a/packages/grafana-ui/src/components/Timeline/TimelineChart.tsx b/packages/grafana-ui/src/components/Timeline/TimelineChart.tsx index c023e3a844e..2253328f14e 100755 --- a/packages/grafana-ui/src/components/Timeline/TimelineChart.tsx +++ b/packages/grafana-ui/src/components/Timeline/TimelineChart.tsx @@ -5,6 +5,7 @@ import { GraphNG, GraphNGProps } from '../GraphNG/GraphNG'; import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder'; import { preparePlotConfigBuilder } from './utils'; import { BarValueVisibility, TimelineMode } from './types'; +import { PanelContext, PanelContextRoot } from '../PanelChrome/PanelContext'; /** * @alpha @@ -19,10 +20,17 @@ export interface TimelineProps extends Omit { + static contextType = PanelContextRoot; + panelContext: PanelContext = {} as PanelContext; + prepConfig = (alignedFrame: DataFrame, getTimeRange: () => TimeRange) => { + this.panelContext = this.context as PanelContext; + const { eventBus } = this.panelContext; + return preparePlotConfigBuilder({ frame: alignedFrame, getTimeRange, + eventBus, ...this.props, }); }; diff --git a/packages/grafana-ui/src/components/Timeline/utils.ts b/packages/grafana-ui/src/components/Timeline/utils.ts index 8e460ca0c0d..31f9289fd5a 100644 --- a/packages/grafana-ui/src/components/Timeline/utils.ts +++ b/packages/grafana-ui/src/components/Timeline/utils.ts @@ -42,14 +42,14 @@ export function preparePlotFrame(data: DataFrame[], dimFields: XYFieldMatchers) }); } -interface PrepConfigOptsTimeline extends PrepConfigOpts { - mode: TimelineMode; - rowHeight: number; - colWidth?: number; - showValue: BarValueVisibility; -} - -type PrepConfig = (opts: PrepConfigOptsTimeline) => UPlotConfigBuilder; +type PrepConfig = ( + opts: PrepConfigOpts<{ + mode: TimelineMode; + rowHeight: number; + colWidth?: number; + showValue: BarValueVisibility; + }> +) => UPlotConfigBuilder; export const preparePlotConfigBuilder: PrepConfig = ({ frame, diff --git a/packages/grafana-ui/src/components/VizTooltip/VizTooltipContainer.tsx b/packages/grafana-ui/src/components/VizTooltip/VizTooltipContainer.tsx index 613e879bc46..ce444577d13 100644 --- a/packages/grafana-ui/src/components/VizTooltip/VizTooltipContainer.tsx +++ b/packages/grafana-ui/src/components/VizTooltip/VizTooltipContainer.tsx @@ -94,6 +94,7 @@ export const VizTooltipContainer: React.FC = ({ left: 0, top: 0, transform: `translate3d(${placement.x}px, ${placement.y}px, 0)`, + transition: 'all ease-out 0.1s', }} {...otherProps} className={cx(styles.wrapper, className)} diff --git a/packages/grafana-ui/src/components/uPlot/Plot.tsx b/packages/grafana-ui/src/components/uPlot/Plot.tsx index 21f69104fa6..799f933d3e8 100755 --- a/packages/grafana-ui/src/components/uPlot/Plot.tsx +++ b/packages/grafana-ui/src/components/uPlot/Plot.tsx @@ -1,4 +1,4 @@ -import React, { createRef } from 'react'; +import React, { createRef, MutableRefObject } from 'react'; import uPlot, { Options } from 'uplot'; import { PlotContext, PlotContextType } from './context'; import { DEFAULT_PLOT_CONFIG } from './utils'; @@ -28,6 +28,7 @@ type UPlotChartState = { */ export class UPlotChart extends React.Component { plotContainer = createRef(); + plotCanvasBBox = createRef(); constructor(props: PlotProps) { super(props); @@ -35,20 +36,27 @@ export class UPlotChart extends React.Component { this.state = { ctx: { plot: null, + getCanvasBoundingBox: () => this.plotCanvasBBox.current, }, }; } reinitPlot() { let { ctx } = this.state; + let { width, height, plotRef } = this.props; ctx.plot?.destroy(); - let { width, height } = this.props; - if (width === 0 && height === 0) { return; } + this.props.config.addHook('setSize', (u) => { + const canvas = u.root.querySelector('.u-over'); + if (!canvas) { + return; + } + (this.plotCanvasBBox as MutableRefObject).current = canvas.getBoundingClientRect(); + }); const config: Options = { ...DEFAULT_PLOT_CONFIG, @@ -58,11 +66,19 @@ export class UPlotChart extends React.Component { ...this.props.config.getConfig(), }; - this.setState({ + const plot = new uPlot(config, this.props.data, this.plotContainer!.current!); + + if (plotRef) { + plotRef(plot); + } + + this.setState((s) => ({ + ...s, ctx: { - plot: new uPlot(config, this.props.data, this.plotContainer!.current!), + ...s.ctx, + plot, }, - }); + })); } componentDidMount() { @@ -82,7 +98,7 @@ export class UPlotChart extends React.Component { ); } - componentDidUpdate(prevProps: PlotProps, prevState: object) { + componentDidUpdate(prevProps: PlotProps) { let { ctx } = this.state; if (!sameDims(prevProps, this.props)) { diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts index 3b2b5b3ca27..d5fc5d3a7ee 100644 --- a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts @@ -28,6 +28,9 @@ export class UPlotConfigBuilder { this.tz = getTimeZoneInfo(timeZone, Date.now())?.ianaName; } + // Exposed to let the container know the primary scale keys + scaleKeys: [string, string] = ['', '']; + addHook(type: T, hook: Hooks.Defs[T]) { pluginLog('UPlotConfigBuilder', false, 'addHook', type); @@ -81,7 +84,7 @@ export class UPlotConfigBuilder { } setCursor(cursor?: Cursor) { - this.cursor = cursor; + this.cursor = { ...this.cursor, ...cursor }; } setSelect(select: Select) { diff --git a/packages/grafana-ui/src/components/uPlot/context.ts b/packages/grafana-ui/src/components/uPlot/context.tsx similarity index 90% rename from packages/grafana-ui/src/components/uPlot/context.ts rename to packages/grafana-ui/src/components/uPlot/context.tsx index 48a26df0084..ea6470a16e0 100644 --- a/packages/grafana-ui/src/components/uPlot/context.ts +++ b/packages/grafana-ui/src/components/uPlot/context.tsx @@ -1,8 +1,8 @@ import React, { useContext } from 'react'; import uPlot from 'uplot'; - export interface PlotContextType { plot: uPlot | null; + getCanvasBoundingBox: () => DOMRect | null; } /** diff --git a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin.tsx b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin.tsx index af6263ff82d..24cdc2bd032 100644 --- a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin.tsx +++ b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; +import React, { useEffect, useLayoutEffect, useState } from 'react'; import { Portal } from '../../Portal/Portal'; import { usePlotContext } from '../context'; import { @@ -12,8 +12,9 @@ import { } from '@grafana/data'; import { SeriesTable, SeriesTableRowProps, TooltipDisplayMode, VizTooltipContainer } from '../../VizTooltip'; import { UPlotConfigBuilder } from '../config/UPlotConfigBuilder'; -import { pluginLog } from '../utils'; +import { findMidPointYPosition, pluginLog } from '../utils'; import { useTheme2 } from '../../../themes/ThemeContext'; +import uPlot from 'uplot'; interface TooltipPluginProps { mode?: TooltipDisplayMode; @@ -22,6 +23,8 @@ interface TooltipPluginProps { config: UPlotConfigBuilder; } +const TOOLTIP_OFFSET = 10; + /** * @alpha */ @@ -33,46 +36,41 @@ export const TooltipPlugin: React.FC = ({ }) => { const theme = useTheme2(); const plotCtx = usePlotContext(); - const plotCanvas = useRef(); - const plotCanvasBBox = useRef({ left: 0, top: 0, right: 0, bottom: 0, width: 0, height: 0 }); const [focusedSeriesIdx, setFocusedSeriesIdx] = useState(null); const [focusedPointIdx, setFocusedPointIdx] = useState(null); - const [coords, setCoords] = useState<{ viewport: CartesianCoords2D; plotCanvas: CartesianCoords2D } | null>(null); + + const [coords, setCoords] = useState(null); + + const pluginId = `TooltipPlugin`; // Debug logs useEffect(() => { - pluginLog('TooltipPlugin', true, `Focused series: ${focusedSeriesIdx}, focused point: ${focusedPointIdx}`); + pluginLog(pluginId, true, `Focused series: ${focusedSeriesIdx}, focused point: ${focusedPointIdx}`); }, [focusedPointIdx, focusedSeriesIdx]); // Add uPlot hooks to the config, or re-add when the config changed useLayoutEffect(() => { - const onMouseCapture = (e: MouseEvent) => { - setCoords({ - plotCanvas: { - x: e.clientX - plotCanvasBBox.current.left, - y: e.clientY - plotCanvasBBox.current.top, - }, - viewport: { - x: e.clientX, - y: e.clientY, - }, - }); - }; - - config.addHook('init', (u) => { - const canvas = u.root.querySelector('.u-over'); - plotCanvas.current = canvas || undefined; - plotCanvas.current?.addEventListener('mousemove', onMouseCapture); - plotCanvas.current?.addEventListener('mouseleave', () => {}); - }); - config.addHook('setCursor', (u) => { - setFocusedPointIdx(u.cursor.idx === undefined ? null : u.cursor.idx); + const bbox = plotCtx.getCanvasBoundingBox(); + if (!bbox) { + return; + } + + const { x, y } = positionTooltip(u, bbox); + + if (x !== undefined && y !== undefined) { + setCoords({ x, y }); + } else { + setCoords(null); + } + + setFocusedPointIdx(u.cursor.idx === undefined ? u.posToIdx(u.cursor.left || 0) : u.cursor.idx); }); + config.addHook('setSeries', (_, idx) => { setFocusedSeriesIdx(idx); }); - }, [config]); + }, [plotCtx, config]); const plotInstance = plotCtx.plot; if (!plotInstance || focusedPointIdx === null) { @@ -142,15 +140,52 @@ export const TooltipPlugin: React.FC = ({ tooltip = ; } - if (!tooltip || !coords) { - return null; - } - return ( - - {tooltip} - + {tooltip && coords && ( + + {tooltip} + + )} ); }; + +function isCursourOutsideCanvas({ left, top }: uPlot.Cursor, canvas: DOMRect) { + if (left === undefined || top === undefined) { + return false; + } + return left < 0 || left > canvas.width || top < 0 || top > canvas.height; +} + +/** + * Given uPlot cursor position, figure out position of the tooltip withing the canvas bbox + * Tooltip is positioned relatively to a viewport + * @internal + **/ +export function positionTooltip(u: uPlot, bbox: DOMRect) { + let x, y; + const cL = u.cursor.left || 0; + const cT = u.cursor.top || 0; + + if (isCursourOutsideCanvas(u.cursor, bbox)) { + const idx = u.posToIdx(cL); + // when cursor outside of uPlot's canvas + if (cT < 0 || cT > bbox.height) { + let pos = findMidPointYPosition(u, idx); + + if (pos) { + y = bbox.top + pos; + if (cL >= 0 && cL <= bbox.width) { + // find x-scale position for a current cursor left position + x = bbox.left + u.valToPos(u.data[0][u.posToIdx(cL)], u.series[0].scale!); + } + } + } + } else { + x = bbox.left + cL; + y = bbox.top + cT; + } + + return { x, y }; +} diff --git a/packages/grafana-ui/src/components/uPlot/types.ts b/packages/grafana-ui/src/components/uPlot/types.ts index f0cf60f63c9..591bbe73b69 100755 --- a/packages/grafana-ui/src/components/uPlot/types.ts +++ b/packages/grafana-ui/src/components/uPlot/types.ts @@ -1,5 +1,5 @@ import React from 'react'; -import { Options, AlignedData } from 'uplot'; +import uPlot, { Options, AlignedData } from 'uplot'; import { TimeRange } from '@grafana/data'; import { UPlotConfigBuilder } from './config/UPlotConfigBuilder'; @@ -19,6 +19,8 @@ export interface PlotProps { config: UPlotConfigBuilder; timeRange: TimeRange; children?: React.ReactNode; + // Reference to uPlot instance + plotRef?: (u: uPlot) => void; } export abstract class PlotConfigBuilder { diff --git a/packages/grafana-ui/src/components/uPlot/utils.ts b/packages/grafana-ui/src/components/uPlot/utils.ts index 62f1c1e0962..203825213c2 100755 --- a/packages/grafana-ui/src/components/uPlot/utils.ts +++ b/packages/grafana-ui/src/components/uPlot/utils.ts @@ -106,6 +106,57 @@ export function collectStackingGroups(f: Field, groups: Map, s } } +/** + * Finds y axis midpoind for point at given idx (css pixels relative to uPlot canvas) + * @internal + **/ + +export function findMidPointYPosition(u: uPlot, idx: number) { + let y; + let sMaxIdx = 1; + let sMinIdx = 1; + // assume min/max being values of 1st series + let max = u.data[1][idx]; + let min = u.data[1][idx]; + + // find min max values AND ids of the corresponding series to get the scales + for (let i = 1; i < u.data.length; i++) { + const sData = u.data[i]; + const sVal = sData[idx]; + if (sVal !== null) { + if (max === null) { + max = sVal; + } else { + if (sVal > max) { + max = u.data[i][idx]; + sMaxIdx = i; + } + } + if (min === null) { + min = sVal; + } else { + if (sVal < min) { + min = u.data[i][idx]; + sMinIdx = i; + } + } + } + } + + if (min === null && max === null) { + // no tooltip to show + y = undefined; + } else if (min !== null && max !== null) { + // find median position + y = (u.valToPos(min, u.series[sMinIdx].scale!) + u.valToPos(max, u.series[sMaxIdx].scale!)) / 2; + } else { + // snap tooltip to min OR max point, one of thos is not null :) + y = u.valToPos((min || max)!, u.series[(sMaxIdx || sMinIdx)!].scale!); + } + + return y; +} + // Dev helpers /** @internal */ diff --git a/packages/grafana-ui/src/utils/logger.ts b/packages/grafana-ui/src/utils/logger.ts index d0b7dd4390a..157a517de25 100644 --- a/packages/grafana-ui/src/utils/logger.ts +++ b/packages/grafana-ui/src/utils/logger.ts @@ -14,6 +14,7 @@ export interface Logger { logger: (...t: any[]) => void; enable: () => void; disable: () => void; + isEnabled: () => boolean; } /** @internal */ @@ -29,5 +30,6 @@ export const createLogger = (name: string): Logger => { }, enable: () => (LOGGIN_ENABLED = true), disable: () => (LOGGIN_ENABLED = false), + isEnabled: () => LOGGIN_ENABLED, }; }; diff --git a/public/app/features/dashboard/components/DashboardSettings/GeneralSettings.tsx b/public/app/features/dashboard/components/DashboardSettings/GeneralSettings.tsx index 00384571e21..cc92988f7e6 100644 --- a/public/app/features/dashboard/components/DashboardSettings/GeneralSettings.tsx +++ b/public/app/features/dashboard/components/DashboardSettings/GeneralSettings.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; -import { SelectableValue, TimeZone } from '@grafana/data'; -import { Select, TagsInput, Input, Field, CollapsableSection, RadioButtonGroup } from '@grafana/ui'; +import { TimeZone } from '@grafana/data'; +import { TagsInput, Input, Field, CollapsableSection, RadioButtonGroup } from '@grafana/ui'; import { selectors } from '@grafana/e2e-selectors'; import { FolderPicker } from 'app/core/components/Select/FolderPicker'; import { DashboardModel } from '../../state/DashboardModel'; @@ -30,8 +30,8 @@ export const GeneralSettings: React.FC = ({ dashboard }) => { dashboard[event.currentTarget.name as 'title' | 'description'] = event.currentTarget.value; }; - const onTooltipChange = (graphTooltip: SelectableValue) => { - dashboard.graphTooltip = graphTooltip.value; + const onTooltipChange = (graphTooltip: number) => { + dashboard.graphTooltip = graphTooltip; setRenderCounter(renderCounter + 1); }; @@ -117,12 +117,7 @@ export const GeneralSettings: React.FC = ({ dashboard }) => { label="Graph tooltip" description="Controls tooltip and hover highlight behavior across different panels" > -