diff --git a/client/app/services/query-result.js b/client/app/services/query-result.js index de734e25d3..10f91185b9 100644 --- a/client/app/services/query-result.js +++ b/client/app/services/query-result.js @@ -283,6 +283,7 @@ function QueryResultService($resource, $timeout, $q, QueryResultError) { const yValues = {}; let eValue = null; let sizeValue = null; + let zValue = null; forOwn(row, (v, definition) => { definition = '' + definition; @@ -320,6 +321,11 @@ function QueryResultService($resource, $timeout, $q, QueryResultError) { sizeValue = value; } + if (type === 'zVal') { + point[type] = value; + zValue = value; + } + if (type === 'multiFilter' || type === 'multi-filter') { seriesName = String(value); } @@ -335,6 +341,10 @@ function QueryResultService($resource, $timeout, $q, QueryResultError) { if (sizeValue !== null) { point.size = sizeValue; } + + if (zValue !== null) { + point.zVal = zValue; + } addPointToSeries(point, series, ySeriesName); }); } else { diff --git a/client/app/visualizations/chart/chart-editor.html b/client/app/visualizations/chart/chart-editor.html index e5ba6d6910..4fdb12826f 100644 --- a/client/app/visualizations/chart/chart-editor.html +++ b/client/app/visualizations/chart/chart-editor.html @@ -70,7 +70,7 @@ -
+
@@ -97,7 +97,19 @@
-
+
+ + + + {{$select.selected}} + + + + + +
+ +
@@ -110,7 +122,7 @@
-
+
-
+
@@ -190,6 +202,13 @@
+
+ +
+
+ +
+ +
+ +
+ +
+
@@ -272,7 +306,7 @@

{{$index == 0 ? 'Left' : 'Right'}} Y Axis

-
+
@@ -295,6 +329,50 @@

{{$index == 0 ? 'Left' : 'Right'}} Y Axis

+
+
+ + + + {{$select.selected | capitalize}} + +
+
+
+
+ +
+
+
+ + + + + + + + + + +
+
+
+
+ + + + + + + + + + +
+
+
+
+
@@ -319,7 +397,7 @@

{{$index == 0 ? 'Left' : 'Right'}} Y Axis

-
+
diff --git a/client/app/visualizations/chart/index.js b/client/app/visualizations/chart/index.js index 4946aeb431..3d8f734a6a 100644 --- a/client/app/visualizations/chart/index.js +++ b/client/app/visualizations/chart/index.js @@ -97,6 +97,7 @@ function ChartEditor(ColorPalette, clientConfig) { pie: { name: 'Pie', icon: 'pie-chart' }, scatter: { name: 'Scatter', icon: 'circle-o' }, bubble: { name: 'Bubble', icon: 'circle-o' }, + heatmap: { name: 'Heatmap', icon: 'th' }, box: { name: 'Box', icon: 'square-o' }, }; @@ -121,7 +122,12 @@ function ChartEditor(ColorPalette, clientConfig) { scope.$applyAsync(); }; + scope.colorScheme = ['Blackbody', 'Bluered', 'Blues', 'Earth', 'Electric', + 'Greens', 'Greys', 'Hot', 'Jet', 'Picnic', 'Portland', + 'Rainbow', 'RdBu', 'Reds', 'Viridis', 'YlGnBu', 'YlOrRd', 'Custom...']; + scope.showSizeColumnPicker = () => some(scope.options.seriesOptions, options => options.type === 'bubble'); + scope.showZColumnPicker = () => some(scope.options.seriesOptions, options => options.type === 'heatmap'); if (scope.options.customCode === undefined) { scope.options.customCode = `// Available variables are x, ys, element, and Plotly @@ -268,6 +274,14 @@ function ChartEditor(ColorPalette, clientConfig) { } }); + scope.$watch('form.zValColumn', (value, old) => { + if (old !== undefined) { + unsetColumn(old); + } + if (value !== undefined) { + setColumnRole('zVal', value); + } + }); scope.$watch('form.groupby', (value, old) => { if (old !== undefined) { @@ -297,6 +311,8 @@ function ChartEditor(ColorPalette, clientConfig) { scope.form.errorColumn = key; } else if (value === 'size') { scope.form.sizeColumn = key; + } else if (value === 'zVal') { + scope.form.zValColumn = key; } }); } diff --git a/client/app/visualizations/chart/plotly/index.js b/client/app/visualizations/chart/plotly/index.js index 38f5ed937a..a2f5008121 100644 --- a/client/app/visualizations/chart/plotly/index.js +++ b/client/app/visualizations/chart/plotly/index.js @@ -5,6 +5,7 @@ import bar from 'plotly.js/lib/bar'; import pie from 'plotly.js/lib/pie'; import histogram from 'plotly.js/lib/histogram'; import box from 'plotly.js/lib/box'; +import heatmap from 'plotly.js/lib/heatmap'; import { ColorPalette, @@ -15,7 +16,7 @@ import { normalizeValue, } from './utils'; -Plotly.register([bar, pie, histogram, box]); +Plotly.register([bar, pie, histogram, box, heatmap]); Plotly.setPlotConfig({ modeBarButtonsToRemove: ['sendDataToCloud'], }); diff --git a/client/app/visualizations/chart/plotly/utils.js b/client/app/visualizations/chart/plotly/utils.js index c7969fec68..c8dae2a60a 100644 --- a/client/app/visualizations/chart/plotly/utils.js +++ b/client/app/visualizations/chart/plotly/utils.js @@ -1,6 +1,6 @@ import { isArray, isNumber, isString, isUndefined, includes, min, max, has, find, - each, values, sortBy, identity, filter, map, extend, reduce, pick, + each, values, sortBy, identity, filter, map, extend, reduce, pick, flatten, uniq, } from 'lodash'; import moment from 'moment'; import d3 from 'd3'; @@ -113,6 +113,15 @@ export function normalizeValue(value, dateTimeFormat = 'YYYY-MM-DD HH:mm:ss') { return value; } +function naturalSort($a, $b) { + if ($a === $b) { + return 0; + } else if ($a < $b) { + return -1; + } + return 1; +} + function calculateAxisRange(seriesList, minValue, maxValue) { if (!isNumber(minValue)) { minValue = Math.min(0, min(map(seriesList, series => min(series.y)))); @@ -291,6 +300,109 @@ function preparePieData(seriesList, options) { }); } +function prepareHeatmapData(seriesList, options) { + const defaultColorScheme = [ + [0, '#356aff'], + [0.14, '#4a7aff'], + [0.28, '#5d87ff'], + [0.42, '#7398ff'], + [0.56, '#fb8c8c'], + [0.71, '#ec6463'], + [0.86, '#ec4949'], + [1, '#e92827'], + ]; + + const formatNumber = createFormatter({ + displayAs: 'number', + numberFormat: options.numberFormat, + }); + + let colorScheme = []; + + if (!options.colorScheme) { + colorScheme = defaultColorScheme; + } else if (options.colorScheme === 'Custom...') { + colorScheme = [[0, options.heatMinColor], [1, options.heatMaxColor]]; + } else { + colorScheme = options.colorScheme; + } + + return map(seriesList, (series) => { + const plotlySeries = { + x: [], + y: [], + z: [], + type: 'heatmap', + name: '', + colorscale: colorScheme, + }; + + plotlySeries.x = uniq(map(series.data, 'x')); + plotlySeries.y = uniq(map(series.data, 'y')); + + if (options.sortX) { + plotlySeries.x.sort(naturalSort); + } + + if (options.sortY) { + plotlySeries.y.sort(naturalSort); + } + + if (options.reverseX) { + plotlySeries.x.reverse(); + } + + if (options.reverseY) { + plotlySeries.y.reverse(); + } + + const zMax = max(map(series.data, 'zVal')); + + // Use text trace instead of default annotation for better performance + const dataLabels = { + x: [], + y: [], + mode: 'text', + hoverinfo: 'skip', + showlegend: false, + text: [], + textfont: { + color: [], + }, + }; + + for (let i = 0; i < plotlySeries.y.length; i += 1) { + const item = []; + for (let j = 0; j < plotlySeries.x.length; j += 1) { + const datum = find( + series.data, + { x: plotlySeries.x[j], y: plotlySeries.y[i] }, + ); + + const zValue = datum ? datum.zVal : 0; + item.push(zValue); + + if (isFinite(zMax) && options.showDataLabels) { + dataLabels.x.push(plotlySeries.x[j]); + dataLabels.y.push(plotlySeries.y[i]); + dataLabels.text.push(formatNumber(zValue)); + if (options.colorScheme && options.colorScheme === 'Custom...') { + dataLabels.textfont.color.push('white'); + } else { + dataLabels.textfont.color.push((zValue / zMax) < 0.25 ? 'white' : 'black'); + } + } + } + plotlySeries.z.push(item); + } + + if (isFinite(zMax) && options.showDataLabels) { + return [plotlySeries, dataLabels]; + } + return [plotlySeries]; + }); +} + function prepareChartData(seriesList, options) { const sortX = (options.sortX === true) || (options.sortX === undefined); @@ -401,6 +513,9 @@ export function prepareData(seriesList, options) { if (options.globalSeriesType === 'pie') { return preparePieData(seriesList, options); } + if (options.globalSeriesType === 'heatmap') { + return flatten(prepareHeatmapData(seriesList, options)); + } return prepareChartData(seriesList, options); } @@ -455,7 +570,11 @@ export function prepareLayout(element, seriesList, options, data) { }; if (options.sortX && result.xaxis.type === 'category') { - result.xaxis.categoryorder = 'category ascending'; + if (options.reverseX) { + result.xaxis.categoryorder = 'category descending'; + } else { + result.xaxis.categoryorder = 'category ascending'; + } } if (!isUndefined(options.xAxis.labels)) { @@ -597,6 +716,9 @@ export function updateData(seriesList, options) { updateSeriesText(seriesList, options); return seriesList; } + if (options.globalSeriesType === 'heatmap') { + return seriesList; + } // Use only visible series seriesList = filter(seriesList, s => s.visible === true);