superset-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From maximebeauche...@apache.org
Subject [incubator-superset] branch master updated: [heatmap] numerous improvements (#3456)
Date Wed, 13 Sep 2017 23:13:47 GMT
This is an automated email from the ASF dual-hosted git repository.

maximebeauchemin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 49f24d1  [heatmap] numerous improvements (#3456)
49f24d1 is described below

commit 49f24d128b8b4a8ef8cbd1d5a9b01cdf32362930
Author: Maxime Beauchemin <maximebeauchemin@gmail.com>
AuthorDate: Wed Sep 13 16:13:45 2017 -0700

    [heatmap] numerous improvements (#3456)
    
    * [heatmap] numerous improvements
    
    * flexibility as to how to sort X and Y axis (alpha/value, desc/asc)
    * option to show a legend
    * fixed margins, maximize real estate
    * allowed users to define bounds
    
    * Tunning
---
 .../assets/javascripts/explore/stores/controls.jsx | 31 ++++++++
 .../assets/javascripts/explore/stores/visTypes.js  | 24 ++++--
 superset/assets/javascripts/modules/colors.js      | 11 ++-
 superset/assets/package.json                       |  1 +
 superset/assets/visualizations/heatmap.css         | 15 +++-
 superset/assets/visualizations/heatmap.js          | 85 +++++++++++++++-------
 superset/viz.py                                    | 16 +++-
 7 files changed, 141 insertions(+), 42 deletions(-)

diff --git a/superset/assets/javascripts/explore/stores/controls.jsx b/superset/assets/javascripts/explore/stores/controls.jsx
index f2d68a4..c885233 100644
--- a/superset/assets/javascripts/explore/stores/controls.jsx
+++ b/superset/assets/javascripts/explore/stores/controls.jsx
@@ -36,6 +36,12 @@ const timeColumnOption = {
     'A reference to the [Time] configuration, taking granularity into ' +
     'account'),
 };
+const sortAxisChoices = [
+  ['alpha_asc', 'Alphabetical ascending'],
+  ['alpha_desc', 'Alphabetical descending'],
+  ['value_asc', 'Value ascending'],
+  ['value_desc', 'Value descending'],
+];
 
 const groupByControl = {
   type: 'SelectControl',
@@ -156,6 +162,22 @@ export const controls = {
     description: '',
   },
 
+  sort_x_axis: {
+    type: 'SelectControl',
+    label: 'Sort X Axis',
+    choices: sortAxisChoices,
+    clearable: false,
+    default: 'alpha_asc',
+  },
+
+  sort_y_axis: {
+    type: 'SelectControl',
+    label: 'Sort Y Axis',
+    choices: sortAxisChoices,
+    clearable: false,
+    default: 'alpha_asc',
+  },
+
   linear_color_scheme: {
     type: 'ColorSchemeControl',
     label: 'Linear Color Scheme',
@@ -202,6 +224,7 @@ export const controls = {
   canvas_image_rendering: {
     type: 'SelectControl',
     label: 'Rendering',
+    renderTrigger: true,
     choices: [
       ['pixelated', 'pixelated (Sharp)'],
       ['auto', 'auto (Smooth)'],
@@ -236,6 +259,14 @@ export const controls = {
     default: false,
   },
 
+  show_perc: {
+    type: 'CheckboxControl',
+    label: 'Show percentage',
+    renderTrigger: true,
+    description: 'Whether to include the percentage in the tooltip',
+    default: true,
+  },
+
   bar_stacked: {
     type: 'CheckboxControl',
     label: 'Stacked Bars',
diff --git a/superset/assets/javascripts/explore/stores/visTypes.js b/superset/assets/javascripts/explore/stores/visTypes.js
index fad261c..4f9ebb8 100644
--- a/superset/assets/javascripts/explore/stores/visTypes.js
+++ b/superset/assets/javascripts/explore/stores/visTypes.js
@@ -929,10 +929,10 @@ export const visTypes = {
     label: 'Heatmap',
     controlPanelSections: [
       {
-        label: 'Axis & Metrics',
+        label: 'Query',
+        expanded: true,
         controlSetRows: [
-          ['all_columns_x'],
-          ['all_columns_y'],
+          ['all_columns_x', 'all_columns_y'],
           ['metric'],
         ],
       },
@@ -941,9 +941,11 @@ export const visTypes = {
         controlSetRows: [
           ['linear_color_scheme'],
           ['xscale_interval', 'yscale_interval'],
-          ['canvas_image_rendering'],
-          ['normalize_across'],
+          ['canvas_image_rendering', 'normalize_across'],
           ['left_margin', 'bottom_margin'],
+          ['y_axis_bounds', 'y_axis_format'],
+          ['show_legend', 'show_perc'],
+          ['sort_x_axis', 'sort_y_axis'],
         ],
       },
     ],
@@ -954,6 +956,18 @@ export const visTypes = {
       all_columns_y: {
         validators: [v.nonEmpty],
       },
+      y_axis_bounds: {
+        label: 'Value bounds',
+        renderTrigger: false,
+        description: (
+          'Hard value bounds applied for color coding. Is only relevant ' +
+          'and applied when the normalization is applied against the whole ' +
+          'heatmap.'
+        ),
+      },
+      y_axis_format: {
+        label: 'Value Format',
+      },
     },
   },
 
diff --git a/superset/assets/javascripts/modules/colors.js b/superset/assets/javascripts/modules/colors.js
index 8594e17..7fd585f 100644
--- a/superset/assets/javascripts/modules/colors.js
+++ b/superset/assets/javascripts/modules/colors.js
@@ -116,17 +116,20 @@ export const getColorFromScheme = (function () {
   };
 }());
 
-export const colorScalerFactory = function (colors, data, accessor) {
+export const colorScalerFactory = function (colors, data, accessor, extents) {
   // Returns a linear scaler our of an array of color
   if (!Array.isArray(colors)) {
     /* eslint no-param-reassign: 0 */
     colors = spectrums[colors];
   }
   let ext = [0, 1];
-  if (data !== undefined) {
+  if (extents) {
+    ext = extents;
+  }
+  if (data) {
     ext = d3.extent(data, accessor);
   }
   const chunkSize = (ext[1] - ext[0]) / (colors.length - 1);
-  const points = colors.map((col, i) => i * chunkSize);
-  return d3.scale.linear().domain(points).range(colors);
+  const points = colors.map((col, i) => ext[0] + (i * chunkSize));
+  return d3.scale.linear().domain(points).range(colors).clamp(true);
 };
diff --git a/superset/assets/package.json b/superset/assets/package.json
index 8733aba..002d0e8 100644
--- a/superset/assets/package.json
+++ b/superset/assets/package.json
@@ -49,6 +49,7 @@
     "d3": "^3.5.17",
     "d3-cloud": "^1.2.1",
     "d3-sankey": "^0.4.2",
+    "d3-svg-legend": "^1.x",
     "d3-tip": "^0.6.7",
     "datamaps": "^0.5.8",
     "datatables.net-bs": "^1.10.15",
diff --git a/superset/assets/visualizations/heatmap.css b/superset/assets/visualizations/heatmap.css
index bfcc327..79542e2 100644
--- a/superset/assets/visualizations/heatmap.css
+++ b/superset/assets/visualizations/heatmap.css
@@ -1,4 +1,4 @@
-.heatmap .slice_container {
+.heatmap {
   position: relative;
   top: 0;
   left: 0;
@@ -28,3 +28,16 @@
   image-rendering: pixelated;                 /* Awesome future-browsers       */
   -ms-interpolation-mode: nearest-neighbor;   /* IE                            */
 }
+
+.heatmap .legendCells text {
+  font-size: 10px;
+  font-weight: normal;
+  opacity: 0;
+}
+
+.heatmap .legendCells .cell:first-child text {
+  opacity: 1;
+}
+.heatmap .legendCells .cell:last-child text {
+  opacity: 1;
+}
diff --git a/superset/assets/visualizations/heatmap.js b/superset/assets/visualizations/heatmap.js
index af03739..1f76a3a 100644
--- a/superset/assets/visualizations/heatmap.js
+++ b/superset/assets/visualizations/heatmap.js
@@ -1,31 +1,30 @@
 import d3 from 'd3';
-import $ from 'jquery';
+// eslint-disable-next-line no-unused-vars
+import d3legend from 'd3-svg-legend';
 import d3tip from 'd3-tip';
 
 import { colorScalerFactory } from '../javascripts/modules/colors';
 import '../stylesheets/d3tip.css';
 import './heatmap.css';
 
-
 // Inspired from http://bl.ocks.org/mbostock/3074470
 // https://jsfiddle.net/cyril123/h0reyumq/
 function heatmapVis(slice, payload) {
-  // Header for panel in explore v2
-  const header = document.getElementById('slice-header');
-  const headerHeight = header ? 30 + header.getBoundingClientRect().height : 0;
+  const data = payload.data.records;
+  const fd = slice.formData;
+
   const margin = {
-    top: headerHeight,
+    top: 10,
     right: 10,
     bottom: 35,
     left: 35,
   };
+  const valueFormatter = d3.format(fd.y_axis_format);
 
-  const data = payload.data;
-  const fd = slice.formData;
   // Dynamically adjusts  based on max x / y category lengths
   function adjustMargins() {
     const pixelsPerCharX = 4.5; // approx, depends on font size
-    const pixelsPerCharY = 10; // approx, depends on font size
+    const pixelsPerCharY = 6; // approx, depends on font size
     let longestX = 1;
     let longestY = 1;
     let datum;
@@ -38,6 +37,9 @@ function heatmapVis(slice, payload) {
 
     if (fd.left_margin === 'auto') {
       margin.left = Math.ceil(Math.max(margin.left, pixelsPerCharY * longestY));
+      if (fd.show_legend) {
+        margin.left += 40;
+      }
     } else {
       margin.left = fd.left_margin;
     }
@@ -48,19 +50,29 @@ function heatmapVis(slice, payload) {
     }
   }
 
-  function ordScale(k, rangeBands, reverse = false) {
+  function ordScale(k, rangeBands, sortMethod) {
     let domain = {};
-    $.each(data, function (i, d) {
-      domain[d[k]] = true;
+    data.forEach((d) => {
+      domain[d[k]] = domain[d[k]] || 0 + d.v;
     });
-    domain = Object.keys(domain).sort();
-    if (reverse) {
+    if (sortMethod === 'alpha_asc') {
+      domain = Object.keys(domain).sort();
+    } else if (sortMethod === 'alpha_desc') {
+      domain = Object.keys(domain).sort().reverse();
+    } else if (sortMethod === 'value_desc') {
+      domain = Object.keys(domain).sort((d1, d2) => domain[d2] - domain[d1]);
+    } else if (sortMethod === 'value_asc') {
+      domain = Object.keys(domain).sort((d1, d2) => domain[d1] - domain[d2]);
+    }
+
+    if (k === 'y' && rangeBands) {
       domain.reverse();
     }
-    if (rangeBands === undefined) {
-      return d3.scale.ordinal().domain(domain).range(d3.range(domain.length));
+
+    if (rangeBands) {
+      return d3.scale.ordinal().domain(domain).rangeBands(rangeBands);
     }
-    return d3.scale.ordinal().domain(domain).rangeBands(rangeBands);
+    return d3.scale.ordinal().domain(domain).range(d3.range(domain.length));
   }
 
   slice.container.html('');
@@ -74,10 +86,10 @@ function heatmapVis(slice, payload) {
   const hmHeight = height - (margin.bottom + margin.top);
   const fp = d3.format('.3p');
 
-  const xScale = ordScale('x');
-  const yScale = ordScale('y', undefined, true);
-  const xRbScale = ordScale('x', [0, hmWidth]);
-  const yRbScale = ordScale('y', [hmHeight, 0]);
+  const xScale = ordScale('x', null, fd.sort_x_axis);
+  const yScale = ordScale('y', null, fd.sort_y_axis);
+  const xRbScale = ordScale('x', [0, hmWidth], fd.sort_x_axis);
+  const yRbScale = ordScale('y', [hmHeight, 0], fd.sort_y_axis);
   const X = 0;
   const Y = 1;
   const heatmapDim = [xRbScale.domain().length, yRbScale.domain().length];
@@ -102,15 +114,30 @@ function heatmapVis(slice, payload) {
     .style('height', hmHeight + 'px')
     .style('image-rendering', fd.canvas_image_rendering)
     .style('left', margin.left + 'px')
-    .style('top', margin.top + headerHeight + 'px')
+    .style('top', margin.top + 'px')
     .style('position', 'absolute');
 
   const svg = container.append('svg')
     .attr('width', width)
     .attr('height', height)
-    .style('left', '0px')
-    .style('top', headerHeight + 'px')
-    .style('position', 'absolute');
+    .style('position', 'relative');
+
+  if (fd.show_legend) {
+    const legendScaler = colorScalerFactory(
+      fd.linear_color_scheme, null, null, payload.data.extents);
+    const colorLegend = d3.legend.color()
+    .labelFormat(valueFormatter)
+    .scale(legendScaler)
+    .shapePadding(0)
+    .cells(50)
+    .shapeWidth(10)
+    .shapeHeight(3)
+    .labelOffset(2);
+
+    svg.append('g')
+    .attr('transform', 'translate(10, 5)')
+    .call(colorLegend);
+  }
 
   const tip = d3tip()
     .attr('class', 'd3-tip')
@@ -128,8 +155,10 @@ function heatmapVis(slice, payload) {
         const obj = matrix[m][n];
         s += '<div><b>' + fd.all_columns_x + ': </b>' + obj.x + '<div>';
         s += '<div><b>' + fd.all_columns_y + ': </b>' + obj.y + '<div>';
-        s += '<div><b>' + fd.metric + ': </b>' + obj.v + '<div>';
-        s += '<div><b>%: </b>' + fp(obj.perc) + '<div>';
+        s += '<div><b>' + fd.metric + ': </b>' + valueFormatter(obj.v)
+ '<div>';
+        if (fd.show_perc) {
+          s += '<div><b>%: </b>' + fp(obj.perc) + '<div>';
+        }
         tip.style('display', null);
       } else {
         // this is a hack to hide the tooltip because we have map it to a single <rect>
@@ -190,7 +219,7 @@ function heatmapVis(slice, payload) {
     const imageObj = new Image();
     const image = context.createImageData(heatmapDim[0], heatmapDim[1]);
     const pixs = {};
-    $.each(data, function (i, d) {
+    data.forEach((d) => {
       const c = d3.rgb(color(d.perc));
       const x = xScale(d.x);
       const y = yScale(d.y);
diff --git a/superset/viz.py b/superset/viz.py
index 7a2b22a..bb4c594 100644
--- a/superset/viz.py
+++ b/superset/viz.py
@@ -1479,6 +1479,13 @@ class HeatmapViz(BaseViz):
             df.columns = ['x', 'y', 'v']
         norm = fd.get('normalize_across')
         overall = False
+        max_ = df.v.max()
+        min_ = df.v.min()
+        bounds = fd.get('y_axis_bounds')
+        if bounds and bounds[0]:
+            min_ = bounds[0]
+        if bounds and bounds[1]:
+            max_ = bounds[1]
         if norm == 'heatmap':
             overall = True
         else:
@@ -1491,10 +1498,11 @@ class HeatmapViz(BaseViz):
                         lambda x: (x.v - x.v.min()) / (x.v.max() - x.v.min()))
                 )
         if overall:
-            v = df.v
-            min_ = v.min()
-            df['perc'] = (v - min_) / (v.max() - min_)
-        return df.to_dict(orient="records")
+            df['perc'] = (df.v - min_) / (max_ - min_)
+        return {
+            'records': df.to_dict(orient="records"),
+            'extents': [min_, max_],
+        }
 
 
 class HorizonViz(NVD3TimeSeriesViz):

-- 
To stop receiving notification emails like this one, please contact
['"commits@superset.apache.org" <commits@superset.apache.org>'].

Mime
View raw message