diff --git a/packages/grafana-data/src/dataframe/processDataFrame.ts b/packages/grafana-data/src/dataframe/processDataFrame.ts
index 02f482c7cc9..6c2db82b2f3 100644
--- a/packages/grafana-data/src/dataframe/processDataFrame.ts
+++ b/packages/grafana-data/src/dataframe/processDataFrame.ts
@@ -262,7 +262,10 @@ export const isTableData = (data: any): data is DataFrame => data && data.hasOwn
export const isDataFrame = (data: any): data is DataFrame => data && data.hasOwnProperty('fields');
-export const toDataFrame = (data: any): DataFrame => {
+/**
+ * Inspect any object and return the results as a DataFrame
+ */
+export function toDataFrame(data: any): DataFrame {
if ('fields' in data) {
// DataFrameDTO does not have length
if ('length' in data) {
@@ -292,7 +295,7 @@ export const toDataFrame = (data: any): DataFrame => {
console.warn('Can not convert', data);
throw new Error('Unsupported data format');
-};
+}
export const toLegacyResponseData = (frame: DataFrame): TimeSeries | TableData => {
const { fields } = frame;
diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts
index fba507e2a4b..225a42af618 100644
--- a/public/app/plugins/panel/graph/graph.ts
+++ b/public/app/plugins/panel/graph/graph.ts
@@ -351,8 +351,15 @@ class GraphElement {
.appendTo(this.elem);
}
- if (this.ctrl.dataWarning) {
- $(`
${this.ctrl.dataWarning.title}
`).appendTo(this.elem);
+ const { dataWarning } = this.ctrl;
+ if (dataWarning) {
+ const msg = $(`${dataWarning.title}
`);
+ if (dataWarning.action) {
+ $(``)
+ .click(dataWarning.action)
+ .appendTo(msg);
+ }
+ msg.appendTo(this.elem);
}
this.thresholdManager.draw(plot);
diff --git a/public/app/plugins/panel/graph/graph_tooltip.ts b/public/app/plugins/panel/graph/graph_tooltip.ts
index e3319e5d6de..5a8931b3b9e 100644
--- a/public/app/plugins/panel/graph/graph_tooltip.ts
+++ b/public/app/plugins/panel/graph/graph_tooltip.ts
@@ -150,7 +150,7 @@ export default function GraphTooltip(this: any, elem: any, dashboard: any, scope
};
elem.mouseleave(() => {
- if (panel.tooltip.shared) {
+ if (panel.tooltip?.shared) {
const plot = elem.data().plot;
if (plot) {
$tooltip.detach();
diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts
index 4197092b929..bfc13eec892 100644
--- a/public/app/plugins/panel/graph/module.ts
+++ b/public/app/plugins/panel/graph/module.ts
@@ -20,6 +20,11 @@ import { getDataLinksVariableSuggestions } from 'app/features/panel/panellinks/l
import { auto } from 'angular';
import { AnnotationsSrv } from 'app/features/annotations/all';
import { CoreEvents } from 'app/types';
+import { DataWarning } from './types';
+import { getLocationSrv } from '@grafana/runtime';
+import { getDataTimeRange } from './utils';
+import { changePanelPlugin } from 'app/features/dashboard/state/actions';
+import { dispatch } from 'app/store/store';
class GraphCtrl extends MetricsPanelCtrl {
static template = template;
@@ -33,7 +38,7 @@ class GraphCtrl extends MetricsPanelCtrl {
alertState: any;
annotationsPromise: any;
- dataWarning: any;
+ dataWarning?: DataWarning;
colors: any = [];
subTabIndex: number;
processor: DataProcessor;
@@ -218,27 +223,7 @@ class GraphCtrl extends MetricsPanelCtrl {
this.linkVariableSuggestions = getDataLinksVariableSuggestions(data);
- this.dataWarning = null;
- const datapointsCount = this.seriesList.reduce((prev, series) => {
- return prev + series.datapoints.length;
- }, 0);
-
- if (datapointsCount === 0) {
- this.dataWarning = {
- title: 'No data',
- tip: 'No data returned from query',
- };
- } else {
- for (const series of this.seriesList) {
- if (series.isOutsideRange) {
- this.dataWarning = {
- title: 'Data outside time range',
- tip: 'Can be caused by timezone mismatch or missing time filter in query',
- };
- break;
- }
- }
- }
+ this.dataWarning = this.getDataWarning();
this.annotationsPromise.then(
(result: { alertState: any; annotations: any }) => {
@@ -261,6 +246,66 @@ class GraphCtrl extends MetricsPanelCtrl {
);
}
+ getDataWarning(): DataWarning {
+ const datapointsCount = this.seriesList.reduce((prev, series) => {
+ return prev + series.datapoints.length;
+ }, 0);
+
+ if (datapointsCount === 0) {
+ if (this.dataList) {
+ for (const frame of this.dataList) {
+ if (frame.length && frame.fields?.length) {
+ return {
+ title: 'Unable to graph data',
+ tip: 'Data exists, but is not timeseries',
+ actionText: 'Switch to table view',
+ action: () => {
+ console.log('Change from graph to table');
+ dispatch(changePanelPlugin(this.panel, 'table'));
+ },
+ };
+ }
+ }
+ }
+
+ return {
+ title: 'No data',
+ tip: 'No data returned from query',
+ };
+ }
+
+ // Look for data points outside time range
+ for (const series of this.seriesList) {
+ if (!series.isOutsideRange) {
+ continue;
+ }
+
+ const dataWarning: DataWarning = {
+ title: 'Data outside time range',
+ tip: 'Can be caused by timezone mismatch or missing time filter in query',
+ };
+
+ const range = getDataTimeRange(this.dataList);
+
+ if (range) {
+ dataWarning.actionText = 'Zoom to data';
+ dataWarning.action = () => {
+ getLocationSrv().update({
+ partial: true,
+ query: {
+ from: range.from,
+ to: range.to,
+ },
+ });
+ };
+ }
+
+ return dataWarning;
+ }
+
+ return null;
+ }
+
onRender() {
if (!this.seriesList) {
return;
diff --git a/public/app/plugins/panel/graph/types.ts b/public/app/plugins/panel/graph/types.ts
new file mode 100644
index 00000000000..5daa308f189
--- /dev/null
+++ b/public/app/plugins/panel/graph/types.ts
@@ -0,0 +1,6 @@
+export interface DataWarning {
+ title: string;
+ tip: string;
+ action?: () => void;
+ actionText?: string;
+}
diff --git a/public/app/plugins/panel/graph/utils.test.ts b/public/app/plugins/panel/graph/utils.test.ts
new file mode 100644
index 00000000000..daa1f03b01b
--- /dev/null
+++ b/public/app/plugins/panel/graph/utils.test.ts
@@ -0,0 +1,17 @@
+import { toDataFrame, FieldType } from '@grafana/data';
+import { getDataTimeRange } from './utils';
+
+describe('DataFrame utility functions', () => {
+ const frame = toDataFrame({
+ fields: [
+ { name: 'fist', type: FieldType.time, values: [2, 3, 5] },
+ { name: 'second', type: FieldType.time, values: [7, 8, 9] },
+ { name: 'third', type: FieldType.number, values: [2000, 3000, 1000] },
+ ],
+ });
+ it('Should find time range', () => {
+ const range = getDataTimeRange([frame]);
+ expect(range!.from).toEqual(2);
+ expect(range!.to).toEqual(9);
+ });
+});
diff --git a/public/app/plugins/panel/graph/utils.ts b/public/app/plugins/panel/graph/utils.ts
new file mode 100644
index 00000000000..4464fbcf650
--- /dev/null
+++ b/public/app/plugins/panel/graph/utils.ts
@@ -0,0 +1,24 @@
+import { DataFrame, ReducerID, reduceField, AbsoluteTimeRange, FieldType } from '@grafana/data';
+
+/**
+ * Find the min and max time that covers all data
+ */
+export function getDataTimeRange(frames: DataFrame[]): AbsoluteTimeRange | undefined {
+ const range: AbsoluteTimeRange = {
+ from: Number.MAX_SAFE_INTEGER,
+ to: Number.MIN_SAFE_INTEGER,
+ };
+ let found = false;
+ const reducers = [ReducerID.min, ReducerID.max];
+ for (const frame of frames) {
+ for (const field of frame.fields) {
+ if (field.type === FieldType.time) {
+ const calcs = reduceField({ field, reducers });
+ range.from = Math.min(range.from, calcs[ReducerID.min]);
+ range.to = Math.max(range.to, calcs[ReducerID.max]);
+ found = true;
+ }
+ }
+ }
+ return found ? range : undefined;
+}