superset-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ccwilli...@apache.org
Subject [incubator-superset] branch master updated: [SIP-5] Refactor treemap (#5670)
Date Fri, 24 Aug 2018 21:15:47 GMT
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 54ae215  [SIP-5] Refactor treemap (#5670)
54ae215 is described below

commit 54ae215d1127532e3b115408bb9de9241d346a10
Author: Krist Wongsuphasawat <krist.wongz@gmail.com>
AuthorDate: Fri Aug 24 14:15:43 2018 -0700

    [SIP-5] Refactor treemap (#5670)
    
    * refactor treemap
    
    * refactor treemap code
    
    * Add proptypes
    
    * add proptypes for tree
    
    * Add margin prop
---
 superset/assets/src/visualizations/treemap.js | 297 ++++++++++++++++----------
 1 file changed, 190 insertions(+), 107 deletions(-)

diff --git a/superset/assets/src/visualizations/treemap.js b/superset/assets/src/visualizations/treemap.js
index b464d05..7834cd7 100644
--- a/superset/assets/src/visualizations/treemap.js
+++ b/superset/assets/src/visualizations/treemap.js
@@ -1,61 +1,115 @@
-/* eslint-disable no-shadow, no-param-reassign, no-underscore-dangle, no-use-before-define
*/
+/* eslint-disable no-shadow, no-param-reassign */
 import d3 from 'd3';
+import PropTypes from 'prop-types';
 import { getColorFromScheme } from '../modules/colors';
-
-require('./treemap.css');
+import './treemap.css';
+
+// Declare PropTypes for recursive data structures
+// https://github.com/facebook/react/issues/5676
+const lazyFunction = f => (() => f().apply(this, arguments));
+
+const leafType = PropTypes.shape({
+  name: PropTypes.string,
+  value: PropTypes.number.isRequired,
+});
+
+const parentShape = {
+  name: PropTypes.string,
+  children: PropTypes.arrayOf(PropTypes.oneOfType([
+    PropTypes.shape(lazyFunction(() => parentShape)),
+    leafType,
+  ])),
+};
+
+const nodeType = PropTypes.oneOfType([
+  PropTypes.shape(parentShape),
+  leafType,
+]);
+
+const propTypes = {
+  data: PropTypes.arrayOf(nodeType),
+  width: PropTypes.number,
+  height: PropTypes.number,
+  colorScheme: PropTypes.string,
+  margin: PropTypes.shape({
+    top: PropTypes.number,
+    right: PropTypes.number,
+    bottom: PropTypes.number,
+    left: PropTypes.number,
+  }),
+  numberFormat: PropTypes.string,
+  treemapRatio: PropTypes.number,
+};
+
+const DEFAULT_MARGIN = {
+  top: 0,
+  right: 0,
+  bottom: 0,
+  left: 0,
+};
 
 /* Modified from http://bl.ocks.org/ganeshv/6a8e9ada3ab7f2d88022 */
-function treemap(slice, payload) {
-  const div = d3.select(slice.selector);
-  const _draw = function (data, eltWidth, eltHeight, formData) {
-    const margin = { top: 0, right: 0, bottom: 0, left: 0 };
+function treemap(element, props) {
+  PropTypes.checkPropTypes(propTypes, props, 'prop', 'Treemap');
+
+  const {
+    data,
+    width,
+    height,
+    margin = DEFAULT_MARGIN,
+    numberFormat,
+    colorScheme,
+    treemapRatio,
+  } = props;
+  const div = d3.select(element);
+  const formatNumber = d3.format(numberFormat);
+
+  function draw(data, eltWidth, eltHeight) {
     const navBarHeight = 36;
     const navBarTitleSize = navBarHeight / 3;
     const navBarBuffer = 10;
     const width = eltWidth - margin.left - margin.right;
-    const height = (eltHeight - navBarHeight - navBarBuffer -
-                 margin.top - margin.bottom);
-    const formatNumber = d3.format(formData.number_format);
+    const height = (eltHeight - navBarHeight - navBarBuffer - margin.top - margin.bottom);
     let transitioning;
 
     const x = d3.scale.linear()
-        .domain([0, width])
-        .range([0, width]);
+      .domain([0, width])
+      .range([0, width]);
 
     const y = d3.scale.linear()
-        .domain([0, height])
-        .range([0, height]);
+      .domain([0, height])
+      .range([0, height]);
 
     const treemap = d3.layout.treemap()
-        .children(function (d, depth) { return depth ? null : d._children; })
-        .sort(function (a, b) { return a.value - b.value; })
-        .ratio(formData.treemap_ratio)
-        .mode('squarify')
-        .round(false);
+      .children((d, depth) => depth ? null : d.originalChildren)
+      .sort((a, b) => a.value - b.value)
+      .ratio(treemapRatio)
+      .mode('squarify')
+      .round(false);
 
     const svg = div.append('svg')
-        .attr('class', 'treemap')
-        .attr('width', eltWidth)
-        .attr('height', eltHeight);
+      .attr('class', 'treemap')
+      .attr('width', eltWidth)
+      .attr('height', eltHeight);
 
     const chartContainer = svg.append('g')
-        .attr('transform', 'translate(' + margin.left + ',' +
-                           (margin.top + navBarHeight + navBarBuffer) + ')')
-        .style('shape-rendering', 'crispEdges');
+      .attr('transform', 'translate(' + margin.left + ',' +
+        (margin.top + navBarHeight + navBarBuffer) + ')')
+      .style('shape-rendering', 'crispEdges');
 
     const grandparent = svg.append('g')
-        .attr('class', 'grandparent')
-        .attr('transform', 'translate(0,' + (margin.top + (navBarBuffer / 2)) + ')');
+      .attr('class', 'grandparent')
+      .attr('transform', 'translate(0,' + (margin.top + (navBarBuffer / 2)) + ')');
 
     grandparent.append('rect')
-        .attr('width', width)
-        .attr('height', navBarHeight);
+      .attr('width', width)
+      .attr('height', navBarHeight);
 
     grandparent.append('text')
-        .attr('x', width / 2)
-        .attr('y', (navBarHeight / 2) + (navBarTitleSize / 2))
-        .style('font-size', navBarTitleSize + 'px')
-        .style('text-anchor', 'middle');
+      .attr('x', width / 2)
+      .attr('y', (navBarHeight / 2) + (navBarTitleSize / 2))
+      .style('font-size', navBarTitleSize + 'px')
+      .style('text-anchor', 'middle');
 
     const initialize = function (root) {
       root.x = 0;
@@ -65,14 +119,51 @@ function treemap(slice, payload) {
       root.depth = 0;
     };
 
+    const text = function (selection) {
+      selection.selectAll('tspan')
+        .attr('x', d => x(d.x) + 6);
+      selection
+        .attr('x', d => x(d.x) + 6)
+        .attr('y', d => y(d.y) + 6)
+        .style('opacity', function (d) {
+          return this.getComputedTextLength() < x(d.x + d.dx) - x(d.x) ? 1 : 0;
+        });
+    };
+
+    const text2 = (selection) => {
+      selection
+        .attr('x', function (d) {
+          return x(d.x + d.dx) - this.getComputedTextLength() - 6;
+        })
+        .attr('y', d => y(d.y + d.dy) - 6)
+        .style('opacity', function (d) {
+          return this.getComputedTextLength() < x(d.x + d.dx) - x(d.x) ? 1 : 0;
+        });
+    };
+
+    const rect = (selection) => {
+      selection
+        .attr('x', d => x(d.x))
+        .attr('y', d => y(d.y))
+        .attr('width', d => x(d.x + d.dx) - x(d.x))
+        .attr('height', d => y(d.y + d.dy) - y(d.y));
+    };
+
+    const name = function (d) {
+      const value = formatNumber(d.value);
+      return d.parent ?
+        name(d.parent) + ' / ' + d.name + ' (' + value + ')' :
+        (d.name) + ' (' + value + ')';
+    };
+
     // Aggregate the values for internal nodes. This is normally done by the
     // treemap layout, but not here because of our custom implementation.
-    // We also take a snapshot of the original children (_children) to avoid
+    // We also take a snapshot of the original children (originalChildren) to avoid
     // the children being overwritten when when layout is computed.
     const accumulate = function (d) {
-      d._children = d.children;
-      if (d._children) {
-        d.value = d.children.reduce(function (p, v) { return p + accumulate(v); }, 0);
+      d.originalChildren = d.children;
+      if (d.originalChildren) {
+        d.value = d.children.reduce((p, v) => p + accumulate(v), 0);
       }
       return d.value;
     };
@@ -85,9 +176,11 @@ function treemap(slice, payload) {
     // of sibling was laid out in 1x1, we must rescale to fit using absolute
     // coordinates. This lets us use a viewport to zoom.
     const layout = function (d) {
-      if (d._children) {
-        treemap.nodes({ _children: d._children });
-        d._children.forEach(function (c) {
+      if (d.originalChildren) {
+        treemap.nodes({
+          originalChildren: d.originalChildren,
+        });
+        d.originalChildren.forEach(function (c) {
           c.x = d.x + (c.x * d.dx);
           c.y = d.y + (c.y * d.dy);
           c.dx *= d.dx;
@@ -99,8 +192,14 @@ function treemap(slice, payload) {
     };
 
     const display = function (d) {
+      const g1 = chartContainer.append('g')
+        .datum(d)
+        .attr('class', 'depth');
+
       const transition = function (d) {
-        if (transitioning || !d) { return; }
+        if (transitioning || !d) {
+          return;
+        }
         transitioning = true;
 
         const g2 = display(d);
@@ -115,7 +214,8 @@ function treemap(slice, payload) {
         chartContainer.style('shape-rendering', null);
 
         // Draw child nodes on top of parent nodes.
-        chartContainer.selectAll('.depth').sort(function (a, b) { return a.depth - b.depth;
});
+        chartContainer.selectAll('.depth')
+          .sort((a, b) => a.depth - b.depth);
 
         // Fade-in entering text.
         g2.selectAll('text').style('fill-opacity', 0);
@@ -136,104 +236,87 @@ function treemap(slice, payload) {
       };
 
       grandparent
-          .datum(d.parent)
-          .on('click', transition)
+        .datum(d.parent)
+        .on('click', transition)
         .select('text')
-          .text(name(d));
-
-      const g1 = chartContainer.append('g')
-          .datum(d)
-          .attr('class', 'depth');
+        .text(name(d));
 
       const g = g1.selectAll('g')
-          .data(d._children)
+        .data(d.originalChildren)
         .enter()
         .append('g');
 
-      g.filter(function (d) { return d._children; })
-          .classed('children', true)
-          .on('click', transition);
+      g.filter(d => d.originalChildren)
+        .classed('children', true)
+        .on('click', transition);
 
       const children = g.selectAll('.child')
-          .data(function (d) { return d._children || [d]; })
+        .data(d => d.originalChildren || [d])
         .enter()
         .append('g');
 
       children.append('rect')
-          .attr('class', 'child')
-          .call(rect)
+        .attr('class', 'child')
+        .call(rect)
         .append('title')
-          .text(function (d) { return d.name + ' (' + formatNumber(d.value) + ')'; });
+        .text(d => d.name + ' (' + formatNumber(d.value) + ')');
 
       children.append('text')
-          .attr('class', 'ctext')
-          .text(function (d) { return d.name; })
-          .call(text2);
+        .attr('class', 'ctext')
+        .text(d => d.name)
+        .call(text2);
 
       g.append('rect')
-          .attr('class', 'parent')
-          .call(rect);
+        .attr('class', 'parent')
+        .call(rect);
 
       const t = g.append('text')
-          .attr('class', 'ptext')
-          .attr('dy', '.75em');
+        .attr('class', 'ptext')
+        .attr('dy', '.75em');
 
       t.append('tspan')
-          .text(function (d) { return d.name; });
+        .text(d => d.name);
+
       t.append('tspan')
-          .attr('dy', '1.0em')
-          .text(function (d) { return formatNumber(d.value); });
+        .attr('dy', '1.0em')
+        .text(d => formatNumber(d.value));
       t.call(text);
       g.selectAll('rect')
-          .style('fill', function (d) { return getColorFromScheme(d.name, formData.color_scheme);
});
+        .style('fill', d => getColorFromScheme(d.name, colorScheme));
 
       return g;
     };
 
-    const text = function (selection) {
-      selection.selectAll('tspan')
-          .attr('x', function (d) { return x(d.x) + 6; });
-      selection.attr('x', function (d) { return x(d.x) + 6; })
-          .attr('y', function (d) { return y(d.y) + 6; })
-          .style('opacity', function (d) {
-            return this.getComputedTextLength() < x(d.x + d.dx) - x(d.x) ? 1 : 0;
-          });
-    };
-
-    const text2 = function (selection) {
-      selection.attr('x', function (d) { return x(d.x + d.dx) - this.getComputedTextLength()
- 6; })
-          .attr('y', function (d) { return y(d.y + d.dy) - 6; })
-          .style('opacity', function (d) {
-            return this.getComputedTextLength() < x(d.x + d.dx) - x(d.x) ? 1 : 0;
-          });
-    };
-
-    const rect = function (selection) {
-      selection.attr('x', function (d) { return x(d.x); })
-               .attr('y', function (d) { return y(d.y); })
-               .attr('width', function (d) { return x(d.x + d.dx) - x(d.x); })
-               .attr('height', function (d) { return y(d.y + d.dy) - y(d.y); });
-    };
-
-    const name = function (d) {
-      return d.parent
-          ? name(d.parent) + ' / ' + d.name + ' (' + formatNumber(d.value) + ')'
-          : (slice.datasource.verbose_map[d.name] || d.name) + ' (' + formatNumber(d.value)
+ ')';
-    };
-
     initialize(data);
     accumulate(data);
     layout(data);
     display(data);
-  };
-
+  }
 
   div.selectAll('*').remove();
-  const width = slice.width();
-  const height = slice.height() / payload.data.length;
-  for (let i = 0, l = payload.data.length; i < l; i += 1) {
-    _draw(payload.data[i], width, height, slice.formData);
-  }
+  const eachHeight = height / data.length;
+  data.forEach(d => draw(d, width, eachHeight));
+}
+
+treemap.propTypes = propTypes;
+
+function adaptor(slice, payload) {
+  const { selector, formData } = slice;
+  const {
+    number_format: numberFormat,
+    color_scheme: colorScheme,
+    treemap_ratio: treemapRatio,
+  } = formData;
+  const element = document.querySelector(selector);
+
+  return treemap(element, {
+    data: payload.data,
+    width: slice.width(),
+    height: slice.height(),
+    numberFormat,
+    colorScheme,
+    treemapRatio,
+  });
 }
 
-module.exports = treemap;
+export default adaptor;


Mime
View raw message