You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by cc...@apache.org on 2018/08/28 04:49:57 UTC

[incubator-superset] branch master updated: [SIP-5] Refactor and update heatmap (#5704)

This is an automated email from the ASF dual-hosted git repository.

ccwilliams 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 fcf2c75  [SIP-5] Refactor and update heatmap (#5704)
fcf2c75 is described below

commit fcf2c756c0cdebb8aad194ce0df9c6c1ec58deff
Author: Krist Wongsuphasawat <kr...@gmail.com>
AuthorDate: Mon Aug 27 21:49:54 2018 -0700

    [SIP-5] Refactor and update heatmap (#5704)
    
    * Extract slice and formData
    
    * Define data shape
    
    * update style
    
    * organize imports
    
    * fix heatmap axis labels
    
    * add new line
    
    * adjust indent
---
 superset/assets/src/visualizations/heatmap.css |  19 +-
 superset/assets/src/visualizations/heatmap.js  | 255 ++++++++++++++++++-------
 2 files changed, 198 insertions(+), 76 deletions(-)

diff --git a/superset/assets/src/visualizations/heatmap.css b/superset/assets/src/visualizations/heatmap.css
index 79542e2..597a48f 100644
--- a/superset/assets/src/visualizations/heatmap.css
+++ b/superset/assets/src/visualizations/heatmap.css
@@ -6,20 +6,24 @@
 }
 
 .heatmap .axis text {
-  font: 10px sans-serif;
+  font: 12px sans-serif;
   text-rendering: optimizeLegibility;
+  fill: #555;
+}
+
+.heatmap .background-rect {
+  stroke: #ddd;
+  fill-opacity: 0;
+  pointer-events: all;
 }
 
 .heatmap .axis path,
 .heatmap .axis line {
   fill: none;
-  stroke: #000;
+  stroke: #ddd;
   shape-rendering: crispEdges;
 }
 
-.heatmap svg {
-}
-
 .heatmap canvas, .heatmap img {
   image-rendering: optimizeSpeed;             /* Older versions of FF          */
   image-rendering: -moz-crisp-edges;          /* FF 6.0+                       */
@@ -41,3 +45,8 @@
 .heatmap .legendCells .cell:last-child text {
   opacity: 1;
 }
+
+.dashboard .heatmap .axis text {
+  font-size: 10px;
+  opacity: .75;
+}
diff --git a/superset/assets/src/visualizations/heatmap.js b/superset/assets/src/visualizations/heatmap.js
index c26291f..3a6b351 100644
--- a/superset/assets/src/visualizations/heatmap.js
+++ b/superset/assets/src/visualizations/heatmap.js
@@ -1,21 +1,85 @@
 import d3 from 'd3';
-// eslint-disable-next-line no-unused-vars
-import d3legend from 'd3-svg-legend';
+import PropTypes from 'prop-types';
+import 'd3-svg-legend';
 import d3tip from 'd3-tip';
 
 import { colorScalerFactory } from '../modules/colors';
 import '../../stylesheets/d3tip.css';
 import './heatmap.css';
 
+const propTypes = {
+  data: PropTypes.shape({
+    records: PropTypes.arrayOf(PropTypes.shape({
+      x: PropTypes.string,
+      y: PropTypes.string,
+      v: PropTypes.number,
+      perc: PropTypes.number,
+      rank: PropTypes.number,
+    })),
+    extents: PropTypes.arrayOf(PropTypes.number),
+  }),
+  width: PropTypes.number,
+  height: PropTypes.number,
+  bottomMargin: PropTypes.oneOfType([
+    PropTypes.string,
+    PropTypes.number,
+  ]),
+  colorScheme: PropTypes.string,
+  columnX: PropTypes.string,
+  columnY: PropTypes.string,
+  leftMargin: PropTypes.oneOfType([
+    PropTypes.string,
+    PropTypes.number,
+  ]),
+  metric: PropTypes.oneOfType([
+    PropTypes.string,
+    PropTypes.object,
+  ]),
+  normalized: PropTypes.bool,
+  numberFormat: PropTypes.string,
+  showLegend: PropTypes.bool,
+  showPercentage: PropTypes.bool,
+  showValues: PropTypes.bool,
+  sortXAxis: PropTypes.string,
+  sortYAxis: PropTypes.string,
+  xScaleInterval: PropTypes.number,
+  yScaleInterval: PropTypes.number,
+  yAxisBounds: PropTypes.arrayOf(PropTypes.number),
+};
+
 function cmp(a, b) {
   return a > b ? 1 : -1;
 }
 
 // Inspired from http://bl.ocks.org/mbostock/3074470
 // https://jsfiddle.net/cyril123/h0reyumq/
-function heatmapVis(slice, payload) {
-  const data = payload.data.records;
-  const fd = slice.formData;
+function Heatmap(element, props) {
+  PropTypes.checkPropTypes(propTypes, props, 'prop', 'Heatmap');
+
+  const {
+    data,
+    width,
+    height,
+    bottomMargin,
+    canvasImageRendering,
+    colorScheme,
+    columnX,
+    columnY,
+    leftMargin,
+    metric,
+    normalized,
+    numberFormat,
+    showLegend,
+    showPercentage,
+    showValues,
+    sortXAxis,
+    sortYAxis,
+    xScaleInterval,
+    yScaleInterval,
+    yAxisBounds,
+  } = props;
+
+  const { records, extents } = data;
 
   const margin = {
     top: 10,
@@ -23,7 +87,7 @@ function heatmapVis(slice, payload) {
     bottom: 35,
     left: 35,
   };
-  const valueFormatter = d3.format(fd.y_axis_format);
+  const valueFormatter = d3.format(numberFormat);
 
   // Dynamically adjusts  based on max x / y category lengths
   function adjustMargins() {
@@ -31,33 +95,32 @@ function heatmapVis(slice, payload) {
     const pixelsPerCharY = 6; // approx, depends on font size
     let longestX = 1;
     let longestY = 1;
-    let datum;
 
-    for (let i = 0; i < data.length; i++) {
-      datum = data[i];
+    for (let i = 0; i < records.length; i++) {
+      const datum = records[i];
       longestX = Math.max(longestX, datum.x.toString().length || 1);
       longestY = Math.max(longestY, datum.y.toString().length || 1);
     }
 
-    if (fd.left_margin === 'auto') {
+    if (leftMargin === 'auto') {
       margin.left = Math.ceil(Math.max(margin.left, pixelsPerCharY * longestY));
-      if (fd.show_legend) {
-        margin.left += 40;
-      }
     } else {
-      margin.left = fd.left_margin;
+      margin.left = leftMargin;
     }
-    if (fd.bottom_margin === 'auto') {
-      margin.bottom = Math.ceil(Math.max(margin.bottom, pixelsPerCharX * longestX));
-    } else {
-      margin.bottom = fd.bottom_margin;
+
+    if (showLegend) {
+      margin.right += 40;
     }
+
+    margin.bottom = (bottomMargin === 'auto')
+      ? Math.ceil(Math.max(margin.bottom, pixelsPerCharX * longestX))
+      : bottomMargin;
   }
 
   function ordScale(k, rangeBands, sortMethod) {
     let domain = {};
     const actualKeys = {};  // hack to preserve type of keys when number
-    data.forEach((d) => {
+    records.forEach((d) => {
       domain[d[k]] = (domain[d[k]] || 0) + d.v;
       actualKeys[d[k]] = d[k];
     });
@@ -83,46 +146,45 @@ function heatmapVis(slice, payload) {
     return d3.scale.ordinal().domain(domain).range(d3.range(domain.length));
   }
 
-  slice.container.html('');
+  // eslint-disable-next-line no-param-reassign
+  element.innerHTML = '';
   const matrix = {};
 
   adjustMargins();
 
-  const width = slice.width();
-  const height = slice.height();
   const hmWidth = width - (margin.left + margin.right);
   const hmHeight = height - (margin.bottom + margin.top);
   const fp = d3.format('.2%');
 
-  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 xScale = ordScale('x', null, sortXAxis);
+  const yScale = ordScale('y', null, sortYAxis);
+  const xRbScale = ordScale('x', [0, hmWidth], sortXAxis);
+  const yRbScale = ordScale('y', [hmHeight, 0], sortYAxis);
   const X = 0;
   const Y = 1;
   const heatmapDim = [xRbScale.domain().length, yRbScale.domain().length];
 
-  const minBound = fd.y_axis_bounds[0] || 0;
-  const maxBound = fd.y_axis_bounds[1] || 1;
-  const colorScaler = colorScalerFactory(fd.linear_color_scheme, null, null, [minBound, maxBound]);
+  const minBound = yAxisBounds[0] || 0;
+  const maxBound = yAxisBounds[1] || 1;
+  const colorScaler = colorScalerFactory(colorScheme, null, null, [minBound, maxBound]);
 
   const scale = [
     d3.scale.linear()
-    .domain([0, heatmapDim[X]])
-    .range([0, hmWidth]),
+      .domain([0, heatmapDim[X]])
+      .range([0, hmWidth]),
     d3.scale.linear()
-    .domain([0, heatmapDim[Y]])
-    .range([0, hmHeight]),
+      .domain([0, heatmapDim[Y]])
+      .range([0, hmHeight]),
   ];
 
-  const container = d3.select(slice.selector);
+  const container = d3.select(element);
 
   const canvas = container.append('canvas')
     .attr('width', heatmapDim[X])
     .attr('height', heatmapDim[Y])
     .style('width', hmWidth + 'px')
     .style('height', hmHeight + 'px')
-    .style('image-rendering', fd.canvas_image_rendering)
+    .style('image-rendering', canvasImageRendering)
     .style('left', margin.left + 'px')
     .style('top', margin.top + 'px')
     .style('position', 'absolute');
@@ -132,9 +194,9 @@ function heatmapVis(slice, payload) {
     .attr('height', height)
     .style('position', 'relative');
 
-  if (fd.show_values) {
+  if (showValues) {
     const cells = svg.selectAll('rect')
-      .data(data)
+      .data(records)
       .enter()
       .append('g')
       .attr('transform', `translate(${margin.left}, ${margin.top})`);
@@ -147,22 +209,22 @@ function heatmapVis(slice, payload) {
       .attr('dy', '.35em')
       .text(d => valueFormatter(d.v))
       .attr('font-size', Math.min(yRbScale.rangeBand(), xRbScale.rangeBand()) / 3 + 'px')
-      .attr('fill', d => d.v >= payload.data.extents[1] / 2 ? 'white' : 'black');
+      .attr('fill', d => d.v >= extents[1] / 2 ? 'white' : 'black');
   }
 
-  if (fd.show_legend) {
+  if (showLegend) {
     const colorLegend = d3.legend.color()
-    .labelFormat(valueFormatter)
-    .scale(colorScaler)
-    .shapePadding(0)
-    .cells(50)
-    .shapeWidth(10)
-    .shapeHeight(3)
-    .labelOffset(2);
+      .labelFormat(valueFormatter)
+      .scale(colorScaler)
+      .shapePadding(0)
+      .cells(10)
+      .shapeWidth(10)
+      .shapeHeight(10)
+      .labelOffset(3);
 
     svg.append('g')
-    .attr('transform', 'translate(10, 5)')
-    .call(colorLegend);
+      .attr('transform', `translate(${width - 40}, ${margin.top})`)
+      .call(colorLegend);
   }
 
   const tip = d3tip()
@@ -177,14 +239,14 @@ function heatmapVis(slice, payload) {
       const k = d3.mouse(this);
       const m = Math.floor(scale[0].invert(k[0]));
       const n = Math.floor(scale[1].invert(k[1]));
-      const metric = typeof fd.metric === 'object' ? fd.metric.label : fd.metric;
+      const metricLabel = typeof metric === 'object' ? metric.label : metric;
       if (m in matrix && n in matrix[m]) {
         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>' + metric + ': </b>' + valueFormatter(obj.v) + '<div>';
-        if (fd.show_perc) {
-          s += '<div><b>%: </b>' + fp(fd.normalized ? obj.rank : obj.perc) + '<div>';
+        s += '<div><b>' + columnX + ': </b>' + obj.x + '<div>';
+        s += '<div><b>' + columnY + ': </b>' + obj.y + '<div>';
+        s += '<div><b>' + metricLabel + ': </b>' + valueFormatter(obj.v) + '<div>';
+        if (showPercentage) {
+          s += '<div><b>%: </b>' + fp(normalized ? obj.rank : obj.perc) + '<div>';
         }
         tip.style('display', null);
       } else {
@@ -196,48 +258,50 @@ function heatmapVis(slice, payload) {
     });
 
   const rect = svg.append('g')
-    .attr('transform', `translate(${margin.left}, ${margin.top})`)
+      .attr('transform', `translate(${margin.left}, ${margin.top})`)
     .append('rect')
-    .attr('pointer-events', 'all')
-    .on('mousemove', tip.show)
-    .on('mouseout', tip.hide)
-    .style('fill-opacity', 0)
-    .attr('stroke', 'black')
-    .attr('width', hmWidth)
-    .attr('height', hmHeight);
+      .classed('background-rect', true)
+      .on('mousemove', tip.show)
+      .on('mouseout', tip.hide)
+      .attr('width', hmWidth)
+      .attr('height', hmHeight);
 
   rect.call(tip);
 
   const xAxis = d3.svg.axis()
     .scale(xRbScale)
+    .outerTickSize(0)
     .tickValues(xRbScale.domain().filter(
       function (d, i) {
-        return !(i % (parseInt(fd.xscale_interval, 10)));
+        return !(i % (xScaleInterval));
       }))
     .orient('bottom');
 
   const yAxis = d3.svg.axis()
     .scale(yRbScale)
+    .outerTickSize(0)
     .tickValues(yRbScale.domain().filter(
       function (d, i) {
-        return !(i % (parseInt(fd.yscale_interval, 10)));
+        return !(i % (yScaleInterval));
       }))
     .orient('left');
 
   svg.append('g')
     .attr('class', 'x axis')
     .attr('transform', 'translate(' + margin.left + ',' + (margin.top + hmHeight) + ')')
-    .call(xAxis)
+      .call(xAxis)
     .selectAll('text')
-    .style('text-anchor', 'end')
-    .attr('transform', 'rotate(-45)');
+      .attr('x', -4)
+      .attr('y', 10)
+      .attr('dy', '0.3em')
+      .style('text-anchor', 'end')
+      .attr('transform', 'rotate(-45)');
 
   svg.append('g')
     .attr('class', 'y axis')
     .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
     .call(yAxis);
 
-
   const context = canvas.node().getContext('2d');
   context.imageSmoothingEnabled = false;
 
@@ -246,8 +310,8 @@ function heatmapVis(slice, payload) {
     const imageObj = new Image();
     const image = context.createImageData(heatmapDim[0], heatmapDim[1]);
     const pixs = {};
-    data.forEach((d) => {
-      const c = d3.rgb(colorScaler(fd.normalized ? d.rank : d.perc));
+    records.forEach((d) => {
+      const c = d3.rgb(colorScaler(normalized ? d.rank : d.perc));
       const x = xScale(d.x);
       const y = yScale(d.y);
       pixs[x + (y * xScale.domain().length)] = c;
@@ -278,4 +342,53 @@ function heatmapVis(slice, payload) {
   createImageObj();
 }
 
-module.exports = heatmapVis;
+Heatmap.propTypes = propTypes;
+
+function adaptor(slice, payload) {
+  const { selector, formData } = slice;
+  const {
+    bottom_margin: bottomMargin,
+    canvas_image_rendering: canvasImageRendering,
+    all_columns_x: columnX,
+    all_columns_y: columnY,
+    linear_color_scheme: colorScheme,
+    left_margin: leftMargin,
+    metric,
+    normalized,
+    show_legend: showLegend,
+    show_perc: showPercentage,
+    show_values: showValues,
+    sort_x_axis: sortXAxis,
+    sort_y_axis: sortYAxis,
+    xscale_interval: xScaleInterval,
+    yscale_interval: yScaleInterval,
+    y_axis_bounds: yAxisBounds,
+    y_axis_format: numberFormat,
+  } = formData;
+  const element = document.querySelector(selector);
+
+  return Heatmap(element, {
+    data: payload.data,
+    width: slice.width(),
+    height: slice.height(),
+    bottomMargin,
+    canvasImageRendering,
+    colorScheme,
+    columnX,
+    columnY,
+    leftMargin,
+    metric,
+    normalized,
+    numberFormat,
+    showLegend,
+    showPercentage,
+    showValues,
+    sortXAxis,
+    sortYAxis,
+    xScaleInterval: parseInt(xScaleInterval, 10),
+    yScaleInterval: parseInt(yScaleInterval, 10),
+    yAxisBounds,
+  });
+}
+
+export default adaptor;