diff --git a/README.md b/README.md index bf18ca9..1f38a7f 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,24 @@ Works with grafana 4, 5, and 6 ![Screenshot of scatter plot](https://raw.githubusercontent.com/NatelEnergy/grafana-plotly-panel/master/src/img/screenshot-multiple-trace.png) ![Screenshot of 3d scatter plot](https://raw.githubusercontent.com/NatelEnergy/grafana-plotly-panel/master/src/img/screenshot-scatter-3d.png) ![Screenshot of the options screen](https://raw.githubusercontent.com/NatelEnergy/grafana-plotly-panel/master/src/img/screenshot-options-new.png) +![Screenshot of horizontal bar chart](https://raw.githubusercontent.com/NatelEnergy/grafana-plotly-panel/master/src/img/screenshot-hbar-single-trace.png) +![Screenshot of horizontal bar chart](https://raw.githubusercontent.com/NatelEnergy/grafana-plotly-panel/master/src/img/screenshot-hbar-multi-trace.png) +![Screenshot of stacked vertical bar chart](https://raw.githubusercontent.com/NatelEnergy/grafana-plotly-panel/master/src/img/screenshot-vbar-stacked-auto-trace.png) + +### Auto-trace + +Sometimes the number and identity of the traces on a plot varies depending +on a property of the data being plotted. Auto-trace mode allows traces +to be constructed from the query result. The query must return exactly +3 columns. The column contents must be: + +1. The trace name. This must be a string. +1. An X value for the named trace. +1. The corresponding Y value. ### Building -To complie, run: +To compile, run: ``` npm install -g yarn diff --git a/package.json b/package.json index 84f65fa..aba6e43 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "natel-plotly-panel", + "name": "sinodun-natel-plotly-panel", "version": "0.0.7-dev", "description": "Plot.ly Panel Plugin for Grafana", "scripts": { diff --git a/src/SeriesWrapper.ts b/src/SeriesWrapper.ts index 086031a..c4d0e2a 100644 --- a/src/SeriesWrapper.ts +++ b/src/SeriesWrapper.ts @@ -126,7 +126,7 @@ export class SeriesWrapperTable extends SeriesWrapper { const col = table.columns[index]; if (!col) { - throw new Error('Unkonwn Column: ' + index); + throw new Error('Unknown Column: ' + index); } this.name = col.text; diff --git a/src/img/screenshot-hbar-multi-trace.png b/src/img/screenshot-hbar-multi-trace.png new file mode 100644 index 0000000..acf482c Binary files /dev/null and b/src/img/screenshot-hbar-multi-trace.png differ diff --git a/src/img/screenshot-hbar-single-trace.png b/src/img/screenshot-hbar-single-trace.png new file mode 100644 index 0000000..88f05ab Binary files /dev/null and b/src/img/screenshot-hbar-single-trace.png differ diff --git a/src/img/screenshot-vbar-stacked-auto-trace.png b/src/img/screenshot-vbar-stacked-auto-trace.png new file mode 100644 index 0000000..2c97b2d Binary files /dev/null and b/src/img/screenshot-vbar-stacked-auto-trace.png differ diff --git a/src/libLoader.ts b/src/libLoader.ts index c23280a..33f7cdd 100644 --- a/src/libLoader.ts +++ b/src/libLoader.ts @@ -10,14 +10,14 @@ export function loadPlotly(cfg: any): Promise { return Promise.resolve(loaded); } - const needsFull = cfg.settings.type !== 'scatter'; - let url = 'public/plugins/natel-plotly-panel/lib/plotly-cartesian.min.js'; + const needsFull = cfg.settings.type === 'scatter3d'; + let url = 'public/plugins/sinodun-natel-plotly-panel/lib/plotly-cartesian.min.js'; if (cfg.loadFromCDN) { url = needsFull ? 'https://cdn.plot.ly/plotly-latest.min.js' : 'https://cdn.plot.ly/plotly-cartesian-latest.min.js'; } else if (needsFull) { - url = 'public/plugins/natel-plotly-panel/lib/plotly.min.js'; + url = 'public/plugins/sinodun-natel-plotly-panel/lib/plotly.min.js'; } return new Promise((resolve, reject) => { $script(url, resolve); @@ -40,7 +40,7 @@ export function loadIfNecessary(cfg: any): Promise { return loadPlotly(cfg); } - const needsFull = cfg.settings.type !== 'scatter'; + const needsFull = cfg.settings.type === 'scatter3d'; if (needsFull && !isFull) { console.log('Switching to the full plotly library'); loaded = null; diff --git a/src/module.ts b/src/module.ts index 3c52a39..ef988e3 100644 --- a/src/module.ts +++ b/src/module.ts @@ -63,6 +63,10 @@ class PlotlyPanelCtrl extends MetricsPanelCtrl { }, showscale: false, }, + barmarker: { + color: '#33B5E5', + width: 1, + }, color_option: 'ramp', }, }; @@ -83,34 +87,73 @@ class PlotlyPanelCtrl extends MetricsPanelCtrl { settings: { type: 'scatter', displayModeBar: false, + orientation: 'v', + autotrace: false, }, layout: { + barmode: 'group', showlegend: false, legend: { orientation: 'h', + traceorder: 'normal', + font: { + family: 'Roboto, Helvetica, Arial, sans-serif', + size: 11, + }, }, dragmode: 'lasso', // (enumerated: "zoom" | "pan" | "select" | "lasso" | "orbit" | "turntable" ) hovermode: 'closest', font: { - family: '"Open Sans", Helvetica, Arial, sans-serif', + family: 'Roboto, Helvetica, Arial, sans-serif', + size: 10.8, }, xaxis: { showgrid: true, zeroline: false, type: 'auto', - rangemode: 'normal', // (enumerated: "normal" | "tozero" | "nonnegative" ) + rangemode: 'normal', // (enumerated: "normal" | "tozero" | "nonnegative" ), + tickangle: 0, + tickmargin: 30, + autotick: true, + ticks: 'outside', + tick0: 0, + dtick: 1, + titlefont: { + family: 'Roboto, Helvetica, Arial, sans-serif', + size: 12, + }, }, yaxis: { showgrid: true, zeroline: false, type: 'linear', rangemode: 'normal', // (enumerated: "normal" | "tozero" | "nonnegative" ), + tickangle: 0, + tickmargin: 45, + autotick: true, + ticks: 'outside', + tick0: 0, + dtick: 1, + titlefont: { + family: 'Roboto, Helvetica, Arial, sans-serif', + size: 12, + }, }, zaxis: { showgrid: true, zeroline: false, type: 'linear', - rangemode: 'normal', // (enumerated: "normal" | "tozero" | "nonnegative" ) + rangemode: 'normal', // (enumerated: "normal" | "tozero" | "nonnegative" ), + tickangle: 0, + tickmargin: 30, + autotick: true, + ticks: 'outside', + tick0: 0, + dtick: 1, + titlefont: { + family: 'Roboto, Helvetica, Arial, sans-serif', + size: 12, + }, }, }, }, @@ -121,6 +164,7 @@ class PlotlyPanelCtrl extends MetricsPanelCtrl { series: SeriesWrapper[]; seriesByKey: Map = new Map(); seriesHash = '?'; + seriesIsTimeseries = true; traces: any[]; // The data sent directly to Plotly -- with a special __copy element layout: any; // The layout used by Plotly @@ -235,8 +279,8 @@ class PlotlyPanelCtrl extends MetricsPanelCtrl { onInitEditMode() { this.editor = new EditorHelper(this); - this.addEditorTab('Display', 'public/plugins/natel-plotly-panel/partials/tab_display.html', 2); - this.addEditorTab('Traces', 'public/plugins/natel-plotly-panel/partials/tab_traces.html', 3); + this.addEditorTab('Display', 'public/plugins/sinodun-natel-plotly-panel/partials/tab_display.html', 2); + this.addEditorTab('Traces', 'public/plugins/sinodun-natel-plotly-panel/partials/tab_traces.html', 3); // this.editorTabIndex = 1; this.onConfigChanged(); // Sets up the axis info @@ -324,6 +368,12 @@ class PlotlyPanelCtrl extends MetricsPanelCtrl { layout.yaxis = {}; } + // If tickangle is 0, remove it to enable default behaviour, + // which is to use 0 if it fits and otherwise 90. + if (layout.tickangle === 0) { + delete layout.tickangle; + } + // Fixed scales if (this.cfg.fixScale) { if ('x' === this.cfg.fixScale) { @@ -371,16 +421,22 @@ class PlotlyPanelCtrl extends MetricsPanelCtrl { } } - const isDate = layout.xaxis.type === 'date'; layout.margin = { - l: layout.yaxis.title ? 50 : 35, + l: layout.yaxis.tickmargin, r: 5, t: 0, - b: layout.xaxis.title ? 65 : isDate ? 40 : 30, + b: layout.xaxis.tickmargin, pad: 2, }; + // Add a bit more margin if there is an axis title. + if (layout.xaxis.title) + layout.margin.b += 25; + if (layout.yaxis.title) + layout.margin.l += 25; + // Set the range to the query window + const isDate = layout.xaxis.type === 'date'; if (isDate && !layout.xaxis.range) { const range = this.timeSrv.timeRange(); layout.xaxis.range = [range.from.valueOf(), range.to.valueOf()]; @@ -480,6 +536,11 @@ class PlotlyPanelCtrl extends MetricsPanelCtrl { return; } + if (!this.seriesIsTimeseries) { + console.log('Not timeseries data, time zoom disabled'); + return; + } + if (data.points.length === 0) { console.log('Nothing Selected', data); return; @@ -533,15 +594,52 @@ class PlotlyPanelCtrl extends MetricsPanelCtrl { onDataReceived(dataList) { const finfo: SeriesWrapper[] = []; + const autotrace = this.cfg.settings.autotrace; let seriesHash = '/'; + let autotraceDataSeries = {}; if (dataList && dataList.length > 0) { + if (autotrace) { + // Check input is of expected form. + dataList.forEach((series, sidx) => { + if (series.columns && series.columns.length === 3 && series.type === 'table') { + series.rows.forEach((val) => { + const sname = val[0]; + if (!(sname in autotraceDataSeries)) { + autotraceDataSeries[sname] = { + columns: series.columns, + rows: [], + type: 'table', + name: sname, + } + }; + autotraceDataSeries[sname].rows.push(val); + }); + } else { + console.error('Autotrace needs table input with 3 columns', sidx, series); + throw new Error('Autotrace needs table input with 3 columns'); + } + }); + + let autotraceDataList: any = []; + for (var key in autotraceDataSeries) { + autotraceDataList.push(autotraceDataSeries[key]); + } + this._updateAutoTraces(autotraceDataSeries); + dataList = autotraceDataList; + } + const useRefID = dataList.length === this.panel.targets.length; + this.seriesIsTimeseries = true; dataList.forEach((series, sidx) => { let refId = ''; - if (useRefID) { - refId = _.get(this.panel, 'targets[' + sidx + '].refId'); - if (!refId) { - refId = String.fromCharCode('A'.charCodeAt(0) + sidx); + if (autotrace) { + refId = series.name; + } else { + if (useRefID) { + refId = _.get(this.panel, 'targets[' + sidx + '].refId'); + if (!refId) { + refId = String.fromCharCode('A'.charCodeAt(0) + sidx); + } } } if (series.columns) { @@ -556,6 +654,8 @@ class PlotlyPanelCtrl extends MetricsPanelCtrl { } else { console.error('Unsupported Series response', sidx, series); } + if (series.type === 'table') + this.seriesIsTimeseries = false; }); } this.seriesByKey.clear(); @@ -631,6 +731,10 @@ class PlotlyPanelCtrl extends MetricsPanelCtrl { // This will update all trace settings *except* the data _updateTracesFromConfigs() { + // If we're in autotrace mode, trace creation happens elsewhere. + if (this.cfg.settings.autotrace) + return; + this.dataWarnings = []; // Make sure we have a trace @@ -639,6 +743,7 @@ class PlotlyPanelCtrl extends MetricsPanelCtrl { } const is3D = this.is3d(); + const isBar = this.isBar(); this.traces = this.cfg.traces.map((tconfig, idx) => { const config = this.deepCopyWithTemplates(tconfig) || {}; _.defaults(config, PlotlyPanelCtrl.defaults); @@ -647,30 +752,43 @@ class PlotlyPanelCtrl extends MetricsPanelCtrl { const trace: any = { name: config.name || EditorHelper.createTraceName(idx), type: this.cfg.settings.type, - mode: 'markers+lines', // really depends on config settings + orientation: this.cfg.settings.orientation, __set: [], // { key:? property:? } }; - let mode = ''; - if (config.show.markers) { - mode += '+markers'; - trace.marker = config.settings.marker; + if (isBar) { + trace.marker = config.settings.barmarker; + } else { + let mode = ''; + if (config.show.markers) { + mode += '+markers'; + trace.marker = config.settings.marker; - delete trace.marker.sizemin; - delete trace.marker.sizemode; - delete trace.marker.sizeref; + delete trace.marker.sizemin; + delete trace.marker.sizemode; + delete trace.marker.sizeref; - if (config.settings.color_option === 'ramp') { - this.__addCopyPath(trace, mapping.color, 'marker.color'); - } else { - delete trace.marker.colorscale; - delete trace.marker.showscale; + if (config.settings.color_option === 'ramp') { + this.__addCopyPath(trace, mapping.color, 'marker.color'); + } else { + delete trace.marker.colorscale; + delete trace.marker.showscale; + } } - } - if (config.show.lines) { - mode += '+lines'; - trace.line = config.settings.line; + if (config.show.lines) { + mode += '+lines'; + trace.line = config.settings.line; + } + + if (is3D) { + this.__addCopyPath(trace, mapping.z, 'z'); + } + + // Set the trace mode + if (mode) { + trace.mode = mode.substring(1); + } } // Set the text @@ -678,28 +796,52 @@ class PlotlyPanelCtrl extends MetricsPanelCtrl { this.__addCopyPath(trace, mapping.x, 'x'); this.__addCopyPath(trace, mapping.y, 'y'); - if (is3D) { - this.__addCopyPath(trace, mapping.z, 'z'); - } - - // Set the trace mode - if (mode) { - trace.mode = mode.substring(1); - } return trace; }); } + // Autotrace. Recreate the traces to those found in the data. + _updateAutoTraces(series) { + this.dataWarnings = []; + + delete this.traces; + + this.traces = []; + + for (var key in series) { + const trace: any = { + name: key, + type: this.cfg.settings.type, + orientation: this.cfg.settings.orientation, + __set: [], // { key:? property:? } + }; + + // Set the text + this.__addCopyPath(trace, key + '/' + series[key].columns[1].text, 'x'); + this.__addCopyPath(trace, key + '/' + series[key].columns[2].text, 'y'); + if (trace.orientation === 'v') + this.__addCopyPath(trace, key + '/' + series[key].columns[2].text, 'text'); + else + this.__addCopyPath(trace, key + '/' + series[key].columns[1].text, 'text'); + + this.traces.push(trace); + } + + if (this.traces.length < 1) { + this.traces = [_.cloneDeep(PlotlyPanelCtrl.defaultTrace)]; + } + } + // Fills in the required data into the trace values _updateTraceData(force = false): boolean { if (!this.series) { - // console.log('NO Series data yet!'); + // console.log('No series data yet!'); return false; } if (force || !this.traces) { this._updateTracesFromConfigs(); - } else if (this.traces.length !== this.cfg.traces.length) { + } else if (!this.cfg.settings.autotrace && this.traces.length !== this.cfg.traces.length) { console.log( 'trace number mismatch. Found: ' + this.traces.length + @@ -776,7 +918,6 @@ class PlotlyPanelCtrl extends MetricsPanelCtrl { if (this.annotations.shapes.length > 0) { traces = this.traces.concat(this.annotations.trace); } - console.log('ConfigChanged (traces)', traces); Plotly.react(this.graphDiv, traces, this.layout, options); } @@ -788,6 +929,10 @@ class PlotlyPanelCtrl extends MetricsPanelCtrl { return this.cfg.settings.type === 'scatter3d'; } + isBar() { + return this.cfg.settings.type === 'bar'; + } + link(scope, elem, attrs, ctrl) { this.graphDiv = elem.find('.plotly-spot')[0]; this.initialized = false; diff --git a/src/partials/tab_display.html b/src/partials/tab_display.html index 709bb92..8248997 100644 --- a/src/partials/tab_display.html +++ b/src/partials/tab_display.html @@ -9,14 +9,38 @@
Options
ng-change="ctrl.editor.onConfigChanged()"> - +
+ +
+ +
+
+ +
+ +
+ +
+
+
@@ -65,6 +89,13 @@
Options
checked="ctrl.cfg.settings.displayModeBar" on-change="ctrl.editor.onConfigChanged()"> + + Options on-change="ctrl.editor.onConfigChanged()">
- -
- -
+ +
+ +
+
+ +
+ +
+
+
@@ -104,6 +145,8 @@
{{axis.label}}
+ +
@@ -141,8 +184,62 @@
{{axis.label}}
/> - - + + + +
+ + +
+ +
+ + +
+ + + +
+ +
+ +
+
+ +
+ + +
+ +
+ + +
diff --git a/src/partials/tab_traces.html b/src/partials/tab_traces.html index 750fb58..0fdb83e 100644 --- a/src/partials/tab_traces.html +++ b/src/partials/tab_traces.html @@ -4,7 +4,7 @@ -
+
-
+
Markers
Markers
-
+
Lines
Lines
+
+
Bars
+
+ + + + + +
+
+
Text
diff --git a/src/plugin.json b/src/plugin.json index fc699a0..3d41bba 100644 --- a/src/plugin.json +++ b/src/plugin.json @@ -1,21 +1,22 @@ { "type": "panel", - "name": "Plotly", - "id": "natel-plotly-panel", + "name": "Plotly (Sinodun)", + "id": "sinodun-natel-plotly-panel", "info": { - "description": "Scatter plots and more", + "description": "Bar charts, scatter plots and more", "author": { - "name": "Natel Energy" + "name": "Sinodun and Natel Energy" }, - "keywords": ["plotly", "scatter", "panel"], + "keywords": ["plotly", "scatter", "bar", "panel"], "logos": { "small": "img/plotly_logo.svg", "large": "img/plotly_logo.svg" }, "links": [ {"name": "Plot.ly", "url": "https://plot.ly/javascript/"}, - {"name": "Project Page", "url": "https://github.com/NatelEnergy/grafana-plotly-panel"}, + {"name": "Project Page", "url": "https://github.com/Sinodun/grafana-plotly-panel"}, + {"name": "Sinodun", "url": "http://www.sinodun.com/"}, {"name": "Natel Energy", "url": "http://www.natelenergy.com/"} ], "screenshots": [