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 nvd3 (#5838)
Date Mon, 17 Sep 2018 03:38:36 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 0e93a94  [SIP-5] Refactor nvd3 (#5838)
0e93a94 is described below

commit 0e93a94ae4febaef1401aa58ea4481342397a723
Author: Krist Wongsuphasawat <krist.wongz@gmail.com>
AuthorDate: Sun Sep 16 20:38:30 2018 -0700

    [SIP-5] Refactor nvd3 (#5838)
    
    * move into folder and scaffold adaptor
    
    * extract width and height
    
    * remove jquery
    
    * extract showBrush
    
    * extract lineInterpolation
    
    * extract xAxisFormat and yAxisFormat
    
    * extract annotationData
    
    * extract xTicksLayout and colorScheme
    
    * extract showXXX
    
    * extract x and y axis labels
    
    * extract showminmax
    
    * extract pie chart props
    
    * extract area chart props
    
    * extract logscale and yAxisBounds
    
    * extract margin
    
    * extract bubble props x,y,size
    
    * extract contribution, comparisonType and color_picker
    
    * remove the last fd.xxx
    
    * remove unnecessary variables
    
    * remove slice.container
    
    * fix unit test reference
    
    * Rewrite logic to compute max label lengths to use only d3, not jquery.
    
    * extract annotationLayers and no more slice.xxx in nvd3vis
    
    * rename x, y, size to xField, yField, sizeField
    
    * use arrow function
    
    * move tooltip function
    
    * extract helper functions into utils
    
    * remove unused argument
    
    * fix height calculation and show bar labels
    
    * rename function
    
    * update unit test
    
    * organize tooltip generator
    
    * update line_multi
    
    * Add proptypes for data
    
    * list proptypes for data
    
    * extract tooltip function for bubble chart
    
    * rename variables
    
    * remove console.log
    
    * enumerate vizTypes and pieLabelType
    
    * parse maxBubble
    
    * use new color scale
    
    * fix import"
    
    * remove line
---
 .../assets/spec/javascripts/modules/utils_spec.jsx |   7 -
 .../{nvd3_viz_spec.jsx => nvd3/utils_spec.js}      |  14 +-
 superset/assets/src/modules/utils.js               |  34 -
 superset/assets/src/visualizations/index.js        |   4 +-
 .../{line_multi.js => nvd3/LineMulti.js}           |   5 +-
 .../{nvd3_vis.css => nvd3/NVD3Vis.css}             |   0
 .../{nvd3_vis.js => nvd3/NVD3Vis.js}               | 898 ++++++++++++---------
 .../assets/src/visualizations/nvd3/PropTypes.js    |  63 ++
 superset/assets/src/visualizations/nvd3/utils.js   | 206 +++++
 9 files changed, 797 insertions(+), 434 deletions(-)

diff --git a/superset/assets/spec/javascripts/modules/utils_spec.jsx b/superset/assets/spec/javascripts/modules/utils_spec.jsx
index f227970..10a4fbc 100644
--- a/superset/assets/spec/javascripts/modules/utils_spec.jsx
+++ b/superset/assets/spec/javascripts/modules/utils_spec.jsx
@@ -1,6 +1,5 @@
 import { expect, assert } from 'chai';
 import {
-  tryNumify,
   slugify,
   formatSelectOptionsForRange,
   d3format,
@@ -11,12 +10,6 @@ import {
 } from '../../../src/modules/utils';
 
 describe('utils', () => {
-  it('tryNumify works as expected', () => {
-    expect(tryNumify(5)).to.equal(5);
-    expect(tryNumify('5')).to.equal(5);
-    expect(tryNumify('5.1')).to.equal(5.1);
-    expect(tryNumify('a string')).to.equal('a string');
-  });
   it('slugify slugifies', () => {
     expect(slugify('My Neat Label! ')).to.equal('my-neat-label');
     expect(slugify('Some Letters AnD a 5')).to.equal('some-letters-and-a-5');
diff --git a/superset/assets/spec/javascripts/visualizations/nvd3_viz_spec.jsx b/superset/assets/spec/javascripts/visualizations/nvd3/utils_spec.js
similarity index 68%
rename from superset/assets/spec/javascripts/visualizations/nvd3_viz_spec.jsx
rename to superset/assets/spec/javascripts/visualizations/nvd3/utils_spec.js
index ded0acc..d88214e 100644
--- a/superset/assets/spec/javascripts/visualizations/nvd3_viz_spec.jsx
+++ b/superset/assets/spec/javascripts/visualizations/nvd3/utils_spec.js
@@ -1,13 +1,13 @@
 import { expect } from 'chai';
 
-import { formatLabel } from '../../../src/visualizations/nvd3_vis';
+import { formatLabel, tryNumify } from '../../../../src/visualizations/nvd3/utils';
 
-describe('nvd3 viz', () => {
+describe('nvd3/utils', () => {
   const verboseMap = {
     foo: 'Foo',
     bar: 'Bar',
   };
-  describe('formatLabel', () => {
+  describe('formatLabel()', () => {
     it('formats simple labels', () => {
       expect(formatLabel('foo')).to.equal('foo');
       expect(formatLabel(['foo'])).to.equal('foo');
@@ -24,4 +24,12 @@ describe('nvd3 viz', () => {
       expect(formatLabel(['foo', 'bar', 'baz', '2 hours offset'], verboseMap)).to.equal('Foo, Bar, baz, 2 hours offset');
     });
   });
+  describe('tryNumify()', () => {
+    it('tryNumify works as expected', () => {
+      expect(tryNumify(5)).to.equal(5);
+      expect(tryNumify('5')).to.equal(5);
+      expect(tryNumify('5.1')).to.equal(5.1);
+      expect(tryNumify('a string')).to.equal('a string');
+    });
+  });
 });
diff --git a/superset/assets/src/modules/utils.js b/superset/assets/src/modules/utils.js
index c5d4e75..0694cdc 100644
--- a/superset/assets/src/modules/utils.js
+++ b/superset/assets/src/modules/utils.js
@@ -206,31 +206,6 @@ export function getDatasourceParameter(datasourceId, datasourceType) {
   return `${datasourceId}__${datasourceType}`;
 }
 
-export function customizeToolTip(chart, xAxisFormatter, yAxisFormatters) {
-  chart.useInteractiveGuideline(true);
-  chart.interactiveLayer.tooltip.contentGenerator(function (d) {
-    const tooltipTitle = xAxisFormatter(d.value);
-    let tooltip = '';
-
-    tooltip += "<table><thead><tr><td colspan='3'>"
-      + `<strong class='x-value'>${tooltipTitle}</strong>`
-      + '</td></tr></thead><tbody>';
-
-    d.series.forEach((series, i) => {
-      const yAxisFormatter = yAxisFormatters[i];
-      const value = yAxisFormatter(series.value);
-      tooltip += "<tr><td class='legend-color-guide'>"
-        + `<div style="background-color: ${series.color};"></div></td>`
-        + `<td class='key'>${series.key}</td>`
-        + `<td class='value'>${value}</td></tr>`;
-    });
-
-    tooltip += '</tbody></table>';
-
-    return tooltip;
-  });
-}
-
 export function initJQueryAjax() {
   // Works in conjunction with a Flask-WTF token as described here:
   // http://flask-wtf.readthedocs.io/en/stable/csrf.html#javascript-requests
@@ -246,15 +221,6 @@ export function initJQueryAjax() {
   }
 }
 
-export function tryNumify(s) {
-  // Attempts casting to Number, returns string when failing
-  const n = Number(s);
-  if (isNaN(n)) {
-    return s;
-  }
-  return n;
-}
-
 export function getParam(name) {
   /* eslint no-useless-escape: 0 */
   const formattedName = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
diff --git a/superset/assets/src/visualizations/index.js b/superset/assets/src/visualizations/index.js
index 1f46f5f..31feffc 100644
--- a/superset/assets/src/visualizations/index.js
+++ b/superset/assets/src/visualizations/index.js
@@ -59,7 +59,7 @@ const loadVis = promise =>
     // deckgl visualizations don't use esModules, fix it?
     return defaultExport.default || defaultExport;
   });
-const loadNvd3 = () => loadVis(import(/* webpackChunkName: "nvd3_vis" */ './nvd3_vis.js'));
+const loadNvd3 = () => loadVis(import(/* webpackChunkName: "nvd3_vis" */ './nvd3/NVD3Vis.js'));
 
 const vizMap = {
   [VIZ_TYPES.area]: loadNvd3,
@@ -87,7 +87,7 @@ const vizMap = {
   [VIZ_TYPES.iframe]: () => loadVis(import(/* webpackChunkName: "iframe" */ './iframe.js')),
   [VIZ_TYPES.line]: loadNvd3,
   [VIZ_TYPES.line_multi]: () =>
-    loadVis(import(/* webpackChunkName: "line_multi" */ './line_multi.js')),
+    loadVis(import(/* webpackChunkName: "line_multi" */ './nvd3/LineMulti.js')),
   [VIZ_TYPES.time_pivot]: loadNvd3,
   [VIZ_TYPES.mapbox]: () => loadVis(import(/* webpackChunkName: "mapbox" */ './MapBox/MapBox.jsx')),
   [VIZ_TYPES.markup]: () => loadVis(import(/* webpackChunkName: "markup" */ './markup.js')),
diff --git a/superset/assets/src/visualizations/line_multi.js b/superset/assets/src/visualizations/nvd3/LineMulti.js
similarity index 95%
rename from superset/assets/src/visualizations/line_multi.js
rename to superset/assets/src/visualizations/nvd3/LineMulti.js
index b8cd1a0..7309641 100644
--- a/superset/assets/src/visualizations/line_multi.js
+++ b/superset/assets/src/visualizations/nvd3/LineMulti.js
@@ -1,7 +1,6 @@
 import d3 from 'd3';
-
-import nvd3Vis from './nvd3_vis';
-import { getExploreLongUrl } from '../explore/exploreUtils';
+import nvd3Vis from './NVD3Vis';
+import { getExploreLongUrl } from '../../explore/exploreUtils';
 
 export default function lineMulti(slice, payload) {
   /*
diff --git a/superset/assets/src/visualizations/nvd3_vis.css b/superset/assets/src/visualizations/nvd3/NVD3Vis.css
similarity index 100%
rename from superset/assets/src/visualizations/nvd3_vis.css
rename to superset/assets/src/visualizations/nvd3/NVD3Vis.css
diff --git a/superset/assets/src/visualizations/nvd3_vis.js b/superset/assets/src/visualizations/nvd3/NVD3Vis.js
similarity index 50%
rename from superset/assets/src/visualizations/nvd3_vis.js
rename to superset/assets/src/visualizations/nvd3/NVD3Vis.js
index 1baf5f5..73cb7b7 100644
--- a/superset/assets/src/visualizations/nvd3_vis.js
+++ b/superset/assets/src/visualizations/nvd3/NVD3Vis.js
@@ -1,32 +1,48 @@
-// JS
-import $ from 'jquery';
 import throttle from 'lodash.throttle';
 import d3 from 'd3';
 import nv from 'nvd3';
-import 'nvd3/build/nv.d3.min.css';
 import mathjs from 'mathjs';
 import moment from 'moment';
-import d3tip from 'd3-tip';
-import dompurify from 'dompurify';
-
-import { getColorFromScheme } from '../modules/colors';
-import AnnotationTypes, {
-  applyNativeColumns,
-} from '../modules/AnnotationTypes';
-import { customizeToolTip, d3TimeFormatPreset, d3FormatPreset, tryNumify } from '../modules/utils';
-import { formatDateVerbose } from '../modules/dates';
-import { isTruthy, TIME_SHIFT_PATTERN } from '../utils/common';
-import { t } from '../locales';
-
-// CSS
-import './nvd3_vis.css';
-import { VIZ_TYPES } from './';
-
-const minBarWidth = 15;
+import PropTypes from 'prop-types';
+import 'nvd3/build/nv.d3.min.css';
+
+import { t } from '../../locales';
+import AnnotationTypes, { applyNativeColumns } from '../../modules/AnnotationTypes';
+import { getScale, getColor } from '../../modules/CategoricalColorNamespace';
+import { formatDateVerbose } from '../../modules/dates';
+import { d3TimeFormatPreset, d3FormatPreset } from '../../modules/utils';
+import { isTruthy } from '../../utils/common';
+import {
+  computeBarChartWidth,
+  drawBarValues,
+  formatLabel,
+  generateBubbleTooltipContent,
+  generateMultiLineTooltipContent,
+  generateRichLineTooltipContent,
+  getMaxLabelSize,
+  hideTooltips,
+  tipFactory,
+  tryNumify,
+  setAxisShowMaxMin,
+  stringifyTimeRange,
+  wrapTooltip,
+} from './utils';
+import {
+  annotationLayerType,
+  boxPlotValueType,
+  bulletDataType,
+  categoryAndValueXYType,
+  rgbObjectType,
+  numericXYType,
+  numberOrAutoType,
+  stringOrObjectWithLabelType,
+} from './PropTypes';
+import './NVD3Vis.css';
+
 // Limit on how large axes margins can grow as the chart window is resized
-const maxMarginPad = 30;
-const animationTime = 1000;
-const minHeightForBrush = 480;
+const MAX_MARGIN_PAD = 30;
+const ANIMATION_TIME = 1000;
+const MIN_HEIGHT_FOR_BRUSH = 480;
 
 const BREAKPOINTS = {
   small: 340,
@@ -42,163 +58,214 @@ const TIMESERIES_VIZ_TYPES = [
   'time_pivot',
 ];
 
-const addTotalBarValues = function (svg, chart, data, stacked, axisFormat) {
-  const format = d3.format(axisFormat || '.3s');
-  const countSeriesDisplayed = data.length;
-
-  const totalStackedValues = stacked && data.length !== 0 ?
-    data[0].values.map(function (bar, iBar) {
-      const bars = data.map(function (series) {
-        return series.values[iBar];
-      });
-      return d3.sum(bars, function (d) {
-        return d.y;
-      });
-    }) : [];
-
-  const rectsToBeLabeled = svg.selectAll('g.nv-group').filter(
-    function (d, i) {
-      if (!stacked) {
-        return true;
-      }
-      return i === countSeriesDisplayed - 1;
-    }).selectAll('rect');
-
-  const groupLabels = svg.select('g.nv-barsWrap').append('g');
-  rectsToBeLabeled.each(
-    function (d, index) {
-      const rectObj = d3.select(this);
-      if (rectObj.attr('class').includes('positive')) {
-        const transformAttr = rectObj.attr('transform');
-        const yPos = parseFloat(rectObj.attr('y'));
-        const xPos = parseFloat(rectObj.attr('x'));
-        const rectWidth = parseFloat(rectObj.attr('width'));
-        const textEls = groupLabels.append('text')
-          .attr('x', xPos) // rough position first, fine tune later
-          .attr('y', yPos - 5)
-          .text(format(stacked ? totalStackedValues[index] : d.y))
-          .attr('transform', transformAttr)
-          .attr('class', 'bar-chart-label');
-        const labelWidth = textEls.node().getBBox().width;
-        textEls.attr('x', xPos + rectWidth / 2 - labelWidth / 2); // fine tune
-      }
-    });
+const propTypes = {
+  data: PropTypes.oneOfType([
+    PropTypes.arrayOf(PropTypes.oneOfType([
+      // pie
+      categoryAndValueXYType,
+      // dist-bar
+      PropTypes.shape({
+        key: PropTypes.string,
+        values: PropTypes.arrayOf(categoryAndValueXYType),
+      }),
+      // area, line, compare, bar
+      PropTypes.shape({
+        key: PropTypes.arrayOf(PropTypes.string),
+        values: PropTypes.arrayOf(numericXYType),
+      }),
+      // dual-line
+      PropTypes.shape({
+        classed: PropTypes.string,
+        key: PropTypes.string,
+        type: PropTypes.string,
+        values: PropTypes.arrayOf(numericXYType),
+        yAxis: PropTypes.number,
+      }),
+      // box-plot
+      PropTypes.shape({
+        label: PropTypes.string,
+        values: PropTypes.arrayOf(boxPlotValueType),
+      }),
+      // bubble
+      PropTypes.shape({
+        key: PropTypes.string,
+        values: PropTypes.arrayOf(PropTypes.object),
+      }),
+    ])),
+    // bullet
+    bulletDataType,
+  ]),
+  width: PropTypes.number,
+  height: PropTypes.number,
+  annotationData: PropTypes.object,
+  annotationLayers: PropTypes.arrayOf(annotationLayerType),
+  bottomMargin: numberOrAutoType,
+  colorScheme: PropTypes.string,
+  comparisonType: PropTypes.string,
+  contribution: PropTypes.bool,
+  leftMargin: numberOrAutoType,
+  onError: PropTypes.func,
+  showLegend: PropTypes.bool,
+  showMarkers: PropTypes.bool,
+  useRichTooltip: PropTypes.bool,
+  vizType: PropTypes.oneOf([
+    'area',
+    'bar',
+    'box_plot',
+    'bubble',
+    'bullet',
+    'compare',
+    'column',
+    'dist_bar',
+    'line',
+    'line_multi',
+    'time_pivot',
+    'pie',
+    'dual_line',
+  ]),
+  xAxisFormat: PropTypes.string,
+  xAxisLabel: PropTypes.string,
+  xAxisShowMinMax: PropTypes.bool,
+  xIsLogScale: PropTypes.bool,
+  xTicksLayout: PropTypes.oneOf(['auto', 'staggered', '45°']),
+  yAxisFormat: PropTypes.string,
+  yAxisBounds: PropTypes.arrayOf(PropTypes.number),
+  yAxisLabel: PropTypes.string,
+  yAxisShowMinMax: PropTypes.bool,
+  yIsLogScale: PropTypes.bool,
+  // 'dist-bar' only
+  orderBars: PropTypes.bool,
+  // 'bar' or 'dist-bar'
+  isBarStacked: PropTypes.bool,
+  showBarValue: PropTypes.bool,
+  // 'bar', 'dist-bar' or 'column'
+  reduceXTicks: PropTypes.bool,
+  // 'bar', 'dist-bar' or 'area'
+  showControls: PropTypes.bool,
+  // 'line' only
+  showBrush: PropTypes.oneOf([true, false, 'auto']),
+  onBrushEnd: PropTypes.func,
+  // 'line-multi' or 'dual-line'
+  yAxis2Format: PropTypes.string,
+  // 'line', 'time-pivot', 'dual-line' or 'line-multi'
+  lineInterpolation: PropTypes.string,
+  // 'pie' only
+  isDonut: PropTypes.bool,
+  isPieLabelOutside: PropTypes.bool,
+  pieLabelType: PropTypes.oneOf([
+    'key',
+    'value',
+    'percent',
+    'key_value',
+    'key_percent',
+  ]),
+  showLabels: PropTypes.bool,
+  // 'area' only
+  areaStackedStyle: PropTypes.string,
+  // 'bubble' only
+  entity: PropTypes.string,
+  maxBubbleSize: PropTypes.number,
+  xField: stringOrObjectWithLabelType,
+  yField: stringOrObjectWithLabelType,
+  sizeField: stringOrObjectWithLabelType,
+  // time-pivot only
+  baseColor: rgbObjectType,
 };
 
-function hideTooltips() {
-  $('.nvtooltip').css({ opacity: 0 });
-}
+const NOOP = () => {};
+const formatter = d3.format('.3s');
+
+function nvd3Vis(element, props) {
+  PropTypes.checkPropTypes(propTypes, props, 'prop', 'NVD3Vis');
+
+  const {
+    data,
+    width: maxWidth,
+    height: maxHeight,
+    annotationData,
+    annotationLayers = [],
+    areaStackedStyle,
+    baseColor,
+    bottomMargin,
+    colorScheme,
+    comparisonType,
+    contribution,
+    entity,
+    isBarStacked,
+    isDonut,
+    isPieLabelOutside,
+    leftMargin,
+    lineInterpolation = 'linear',
+    maxBubbleSize,
+    onBrushEnd = NOOP,
+    onError = NOOP,
+    orderBars,
+    pieLabelType,
+    reduceXTicks = false,
+    showBarValue,
+    showBrush,
+    showControls,
+    showLabels,
+    showLegend,
+    showMarkers,
+    sizeField,
+    useRichTooltip,
+    vizType,
+    xAxisFormat,
+    xAxisLabel,
+    xAxisShowMinMax = false,
+    xField,
+    xIsLogScale,
+    xTicksLayout,
+    yAxisFormat,
+    yAxis2Format,
+    yAxisBounds,
+    yAxisLabel,
+    yAxisShowMinMax = false,
+    yField,
+    yIsLogScale,
+  } = props;
+
+  const isExplore = document.querySelector('#explorer-container') !== null;
+  const container = element;
+  container.innerHTML = '';
 
-function wrapTooltip(chart, container) {
-  const tooltipLayer = chart.useInteractiveGuideline && chart.useInteractiveGuideline() ?
-    chart.interactiveLayer : chart;
-  const tooltipGeneratorFunc = tooltipLayer.tooltip.contentGenerator();
-  tooltipLayer.tooltip.contentGenerator((d) => {
-    let tooltip = `<div style="max-width: ${container.width() * 0.5}px">`;
-    tooltip += tooltipGeneratorFunc(d);
-    tooltip += '</div>';
-    return tooltip;
-  });
-}
-
-function getMaxLabelSize(container, axisClass) {
-  // axis class = .nv-y2  // second y axis on dual line chart
-  // axis class = .nv-x  // x axis on time series line chart
-  const labelEls = container.find(`.${axisClass} text`).not('.nv-axislabel');
-  const labelDimensions = labelEls.map(i => labelEls[i].getComputedTextLength() * 0.75);
-  return Math.ceil(Math.max(...labelDimensions));
-}
-
-export function formatLabel(input, verboseMap = {}) {
-  // The input for label may be a string or an array of string
-  // When using the time shift feature, the label contains a '---' in the array
-  const verboseLkp = s => verboseMap[s] || s;
-  let label;
-  if (Array.isArray(input) && input.length) {
-    const verboseLabels = input.map(l => TIME_SHIFT_PATTERN.test(l) ? l : verboseLkp(l));
-    label = verboseLabels.join(', ');
-  } else {
-    label = verboseLkp(input);
-  }
-  return label;
-}
-
-export default function nvd3Vis(slice, payload) {
   let chart;
+  let width = maxWidth;
   let colorKey = 'key';
-  const isExplore = $('#explore-container').length === 1;
-
-  let data;
-  if (payload.data) {
-    if (Array.isArray(payload.data)) {
-        data = payload.data.map(x => ({
-            ...x, key: formatLabel(x.key, slice.datasource.verbose_map),
-        }));
-    } else {
-      data = payload.data;
-    }
-  } else {
-    data = [];
-  }
 
-  slice.container.html('');
-  slice.clearError();
-
-  let width = slice.width();
-  const fd = slice.formData;
-
-  const barchartWidth = function () {
-    let bars;
-    if (fd.bar_stacked) {
-      bars = d3.max(data, function (d) { return d.values.length; });
-    } else {
-      bars = d3.sum(data, function (d) { return d.values.length; });
-    }
-    if (bars * minBarWidth > width) {
-      return bars * minBarWidth;
-    }
-    return width;
-  };
-
-  const vizType = fd.viz_type;
-  const formatter = d3.format('.3s');
-  const reduceXTicks = fd.reduce_x_ticks || false;
-  let stacked = false;
-  let row;
+  function isVizTypes(types) {
+    return types.indexOf(vizType) >= 0;
+  }
 
   const drawGraph = function () {
-    let svg = d3.select(slice.selector).select('svg');
+    const d3Element = d3.select(element);
+    let svg = d3Element.select('svg');
     if (svg.empty()) {
-      svg = d3.select(slice.selector).append('svg');
+      svg = d3Element.append('svg');
     }
-    let height = slice.height();
-    const isTimeSeries = TIMESERIES_VIZ_TYPES.indexOf(vizType) >= 0;
+    const height = vizType === 'bullet' ? Math.min(maxHeight, 50) : maxHeight;
+    const isTimeSeries = isVizTypes(TIMESERIES_VIZ_TYPES);
 
     // Handling xAxis ticks settings
-    let xLabelRotation = 0;
-    let staggerLabels = false;
-    if (fd.x_ticks_layout === 'auto') {
-      if (['column', 'dist_bar'].indexOf(vizType) >= 0) {
-        xLabelRotation = 45;
-      }
-    } else if (fd.x_ticks_layout === 'staggered') {
-      staggerLabels = true;
-    } else if (fd.x_ticks_layout === '45°') {
-      if (isTruthy(fd.show_brush)) {
-        const error = t('You cannot use 45° tick layout along with the time range filter');
-        slice.error(error);
-        return null;
-      }
-      xLabelRotation = 45;
+    const staggerLabels = xTicksLayout === 'staggered';
+    const xLabelRotation =
+      ((xTicksLayout === 'auto' && isVizTypes(['column', 'dist_bar']))
+      || xTicksLayout === '45°')
+      ? 45 : 0;
+    if (xLabelRotation === 45 && isTruthy(showBrush)) {
+      onError(t('You cannot use 45° tick layout along with the time range filter'));
+      return null;
     }
-    const showBrush = (
-      isTruthy(fd.show_brush) ||
-      (fd.show_brush === 'auto' && height >= minHeightForBrush && fd.x_ticks_layout !== '45°')
+
+    const canShowBrush = (
+      isTruthy(showBrush) ||
+      (showBrush === 'auto' && maxHeight >= MIN_HEIGHT_FOR_BRUSH && xTicksLayout !== '45°')
     );
 
     switch (vizType) {
       case 'line':
-        if (showBrush) {
+        if (canShowBrush) {
           chart = nv.models.lineWithFocusChart();
           if (staggerLabels) {
             // Give a bit more room to focus area if X axis ticks are staggered
@@ -210,69 +277,60 @@ export default function nvd3Vis(slice, payload) {
           chart = nv.models.lineChart();
         }
         chart.xScale(d3.time.scale.utc());
-        chart.interpolate(fd.line_interpolation);
+        chart.interpolate(lineInterpolation);
         break;
 
       case 'time_pivot':
         chart = nv.models.lineChart();
         chart.xScale(d3.time.scale.utc());
-        chart.interpolate(fd.line_interpolation);
+        chart.interpolate(lineInterpolation);
         break;
 
       case 'dual_line':
-        chart = nv.models.multiChart();
-        chart.interpolate('linear');
-        break;
-
       case 'line_multi':
         chart = nv.models.multiChart();
-        chart.interpolate(fd.line_interpolation);
+        chart.interpolate(lineInterpolation);
         break;
 
       case 'bar':
         chart = nv.models.multiBarChart()
-        .showControls(fd.show_controls)
-        .groupSpacing(0.1);
+          .showControls(showControls)
+          .groupSpacing(0.1);
 
+        if (showBarValue) {
+          setTimeout(function () {
+            drawBarValues(svg, data, isBarStacked, yAxisFormat);
+          }, ANIMATION_TIME);
+        }
         if (!reduceXTicks) {
-          width = barchartWidth();
+          width = computeBarChartWidth(data, isBarStacked, maxWidth);
         }
         chart.width(width);
-        chart.xAxis
-        .showMaxMin(false);
-
-        stacked = fd.bar_stacked;
-        chart.stacked(stacked);
-
-        if (fd.show_bar_value) {
-          setTimeout(function () {
-            addTotalBarValues(svg, chart, data, stacked, fd.y_axis_format);
-          }, animationTime);
-        }
+        chart.xAxis.showMaxMin(false);
+        chart.stacked(isBarStacked);
         break;
 
       case 'dist_bar':
         chart = nv.models.multiBarChart()
-        .showControls(fd.show_controls)
-        .reduceXTicks(reduceXTicks)
-        .groupSpacing(0.1); // Distance between each group of bars.
+          .showControls(showControls)
+          .reduceXTicks(reduceXTicks)
+          .groupSpacing(0.1); // Distance between each group of bars.
 
         chart.xAxis.showMaxMin(false);
 
-        stacked = fd.bar_stacked;
-        chart.stacked(stacked);
-        if (fd.order_bars) {
+        chart.stacked(isBarStacked);
+        if (orderBars) {
           data.forEach((d) => {
             d.values.sort((a, b) => tryNumify(a.x) < tryNumify(b.x) ? -1 : 1);
           });
         }
-        if (fd.show_bar_value) {
+        if (showBarValue) {
           setTimeout(function () {
-            addTotalBarValues(svg, chart, data, stacked, fd.y_axis_format);
-          }, animationTime);
+            drawBarValues(svg, data, isBarStacked, yAxisFormat);
+          }, ANIMATION_TIME);
         }
         if (!reduceXTicks) {
-          width = barchartWidth();
+          width = computeBarChartWidth(data, isBarStacked, maxWidth);
         }
         chart.width(width);
         break;
@@ -281,24 +339,25 @@ export default function nvd3Vis(slice, payload) {
         chart = nv.models.pieChart();
         colorKey = 'x';
         chart.valueFormat(formatter);
-        if (fd.donut) {
+        if (isDonut) {
           chart.donut(true);
         }
-        chart.showLabels(fd.show_labels);
-        chart.labelsOutside(fd.labels_outside);
-        chart.labelThreshold(0.05);  // Configure the minimum slice size for labels to show up
-        if (fd.pie_label_type !== 'key_percent' && fd.pie_label_type !== 'key_value') {
-          chart.labelType(fd.pie_label_type);
-        } else if (fd.pie_label_type === 'key_value') {
+        chart.showLabels(showLabels);
+        chart.labelsOutside(isPieLabelOutside);
+        // Configure the minimum slice size for labels to show up
+        chart.labelThreshold(0.05);
+        chart.cornerRadius(true);
+
+        if (pieLabelType !== 'key_percent' && pieLabelType !== 'key_value') {
+          chart.labelType(pieLabelType);
+        } else if (pieLabelType === 'key_value') {
           chart.labelType(d => `${d.data.x}: ${d3.format('.3s')(d.data.y)}`);
         }
-        chart.cornerRadius(true);
 
-        if (fd.pie_label_type === 'percent' || fd.pie_label_type === 'key_percent') {
-          let total = 0;
-          data.forEach((d) => { total += d.y; });
+        if (pieLabelType === 'percent' || pieLabelType === 'key_percent') {
+          const total = d3.sum(data, d => d.y);
           chart.tooltip.valueFormatter(d => `${((d / total) * 100).toFixed()}%`);
-          if (fd.pie_label_type === 'key_percent') {
+          if (pieLabelType === 'key_percent') {
             chart.labelType(d => `${d.data.x}: ${((d.data.y / total) * 100).toFixed()}%`);
           }
         }
@@ -307,7 +366,7 @@ export default function nvd3Vis(slice, payload) {
 
       case 'column':
         chart = nv.models.multiBarChart()
-        .reduceXTicks(false);
+          .reduceXTicks(false);
         break;
 
       case 'compare':
@@ -318,33 +377,28 @@ export default function nvd3Vis(slice, payload) {
         break;
 
       case 'bubble':
-        row = (col1, col2) => `<tr><td>${col1}</td><td>${col2}</td></tr>`;
         chart = nv.models.scatterChart();
         chart.showDistX(true);
         chart.showDistY(true);
-        chart.tooltip.contentGenerator(function (obj) {
-          const p = obj.point;
-          const yAxisFormatter = d3FormatPreset(fd.y_axis_format);
-          const xAxisFormatter = d3FormatPreset(fd.x_axis_format);
-          let s = '<table>';
-          s += (
-            `<tr><td style="color: ${p.color};">` +
-              `<strong>${p[fd.entity]}</strong> (${p.group})` +
-            '</td></tr>');
-          s += row(fd.x.label || fd.x, xAxisFormatter(p.x));
-          s += row(fd.y.label || fd.y, yAxisFormatter(p.y));
-          s += row(fd.size.label || fd.size, formatter(p.size));
-          s += '</table>';
-          return s;
-        });
-        chart.pointRange([5, fd.max_bubble_size ** 2]);
+        chart.tooltip.contentGenerator(d =>
+          generateBubbleTooltipContent({
+            point: d.point,
+            entity,
+            xField,
+            yField,
+            sizeField,
+            xFormatter: d3FormatPreset(xAxisFormat),
+            yFormatter: d3FormatPreset(yAxisFormat),
+            sizeFormatter: formatter,
+          }));
+        chart.pointRange([5, maxBubbleSize ** 2]);
         chart.pointDomain([0, d3.max(data, d => d3.max(d.values, v => v.size))]);
         break;
 
       case 'area':
         chart = nv.models.stackedAreaChart();
-        chart.showControls(fd.show_controls);
-        chart.style(fd.stacked_style);
+        chart.showControls(showControls);
+        chart.style(areaStackedStyle);
         chart.xScale(d3.time.scale.utc());
         break;
 
@@ -363,14 +417,12 @@ export default function nvd3Vis(slice, payload) {
         throw new Error('Unrecognized visualization for nvd3' + vizType);
     }
 
-    if (isTruthy(fd.show_brush) && isTruthy(fd.send_time_range)) {
+    if (canShowBrush && onBrushEnd !== NOOP) {
       chart.focus.dispatch.on('brush', (event) => {
-        const extent = event.extent;
-        if (extent.some(d => d.toISOString === undefined)) {
-          return;
+        const timeRange = stringifyTimeRange(event.extent);
+        if (timeRange) {
+          event.brush.on('brushend', () => { onBrushEnd(timeRange); });
         }
-        const timeRange = extent.map(d => d.toISOString().slice(0, -1)).join(' : ');
-        event.brush.on('brushend', () => slice.addFilter('__time_range', timeRange, false, true));
       });
     }
 
@@ -387,47 +439,42 @@ export default function nvd3Vis(slice, payload) {
       chart.x2Axis.rotateLabels(xLabelRotation);
     }
 
-    if ('showLegend' in chart && typeof fd.show_legend !== 'undefined') {
+    if ('showLegend' in chart && typeof showLegend !== 'undefined') {
       if (width < BREAKPOINTS.small && vizType !== 'pie') {
         chart.showLegend(false);
       } else {
-        chart.showLegend(fd.show_legend);
+        chart.showLegend(showLegend);
       }
     }
 
-    if (vizType === 'bullet') {
-      height = Math.min(height, 50);
+    if (chart.forceY && yAxisBounds &&
+        (yAxisBounds[0] !== null || yAxisBounds[1] !== null)) {
+      chart.forceY(yAxisBounds);
     }
-
-    if (chart.forceY &&
-        fd.y_axis_bounds &&
-        (fd.y_axis_bounds[0] !== null || fd.y_axis_bounds[1] !== null)) {
-      chart.forceY(fd.y_axis_bounds);
-    }
-    if (fd.y_log_scale) {
+    if (yIsLogScale) {
       chart.yScale(d3.scale.log());
     }
-    if (fd.x_log_scale) {
+    if (xIsLogScale) {
       chart.xScale(d3.scale.log());
     }
 
-    let xAxisFormatter = d3FormatPreset(fd.x_axis_format);
+    let xAxisFormatter = d3FormatPreset(xAxisFormat);
     if (isTimeSeries) {
-      xAxisFormatter = d3TimeFormatPreset(fd.x_axis_format);
+      xAxisFormatter = d3TimeFormatPreset(xAxisFormat);
       // In tooltips, always use the verbose time format
       chart.interactiveLayer.tooltip.headerFormatter(formatDateVerbose);
     }
     if (chart.x2Axis && chart.x2Axis.tickFormat) {
       chart.x2Axis.tickFormat(xAxisFormatter);
     }
-    const isXAxisString = ['dist_bar', 'box_plot'].indexOf(vizType) >= 0;
+    const isXAxisString = isVizTypes(['dist_bar', 'box_plot']);
     if (!isXAxisString && chart.xAxis && chart.xAxis.tickFormat) {
       chart.xAxis.tickFormat(xAxisFormatter);
     }
 
-    let yAxisFormatter = d3FormatPreset(fd.y_axis_format);
+    let yAxisFormatter = d3FormatPreset(yAxisFormat);
     if (chart.yAxis && chart.yAxis.tickFormat) {
-      if (fd.contribution || fd.comparison_type === 'percentage') {
+      if (contribution || comparisonType === 'percentage') {
         // When computing a "Percentage" or "Contribution" selected, we force a percentage format
         yAxisFormatter = d3.format('.1%');
       }
@@ -444,93 +491,72 @@ export default function nvd3Vis(slice, payload) {
       chart.y2Axis.ticks(5);
     }
 
-
     // Set showMaxMin for all axis
-    function setAxisShowMaxMin(axis, showminmax) {
-      if (axis && axis.showMaxMin && showminmax !== undefined) {
-        axis.showMaxMin(showminmax);
-      }
-    }
-
-    // If these are undefined, they register as truthy
-    setAxisShowMaxMin(chart.xAxis, fd.x_axis_showminmax || false);
-    setAxisShowMaxMin(chart.x2Axis, fd.x_axis_showminmax || false);
-    setAxisShowMaxMin(chart.yAxis, fd.y_axis_showminmax || false);
-    setAxisShowMaxMin(chart.y2Axis, fd.y_axis_showminmax || false);
+    setAxisShowMaxMin(chart.xAxis, xAxisShowMinMax);
+    setAxisShowMaxMin(chart.x2Axis, xAxisShowMinMax);
+    setAxisShowMaxMin(chart.yAxis, yAxisShowMinMax);
+    setAxisShowMaxMin(chart.y2Axis, yAxisShowMinMax);
 
     if (vizType === 'time_pivot') {
-      chart.color((d) => {
-        const c = fd.color_picker;
-        let alpha = 1;
-        if (d.rank > 0) {
-          alpha = d.perc * 0.5;
-        }
-        return `rgba(${c.r}, ${c.g}, ${c.b}, ${alpha})`;
-      });
+      if (baseColor) {
+        const { r, g, b } = baseColor;
+        chart.color((d) => {
+          const alpha = d.rank > 0 ? d.perc * 0.5 : 1;
+          return `rgba(${r}, ${g}, ${b}, ${alpha})`;
+        });
+      }
     } else if (vizType !== 'bullet') {
-      chart.color(d => d.color || getColorFromScheme(d[colorKey], fd.color_scheme));
+      const colorFn = getScale(colorScheme).toFunction();
+      chart.color(d => d.color || colorFn(d[colorKey]));
     }
-    if ((vizType === 'line' || vizType === 'area') && fd.rich_tooltip) {
+
+    if (isVizTypes(['line', 'area']) && useRichTooltip) {
       chart.useInteractiveGuideline(true);
       if (vizType === 'line') {
-        // Custom sorted tooltip
-        // use a verbose formatter for times
-        chart.interactiveLayer.tooltip.contentGenerator((d) => {
-          let tooltip = '';
-          tooltip += "<table><thead><tr><td colspan='3'>"
-            + `<strong class='x-value'>${formatDateVerbose(d.value)}</strong>`
-            + '</td></tr></thead><tbody>';
-          d.series.sort((a, b) => a.value >= b.value ? -1 : 1);
-          d.series.forEach((series) => {
-            tooltip += (
-              `<tr class="${series.highlight ? 'emph' : ''}">` +
-                `<td class='legend-color-guide' style="opacity: ${series.highlight ? '1' : '0.75'};"">` +
-                  '<div ' +
-                    `style="border: 2px solid ${series.highlight ? 'black' : 'transparent'}; background-color: ${series.color};"` +
-                  '></div>' +
-                '</td>' +
-                `<td>${dompurify.sanitize(series.key)}</td>` +
-                `<td>${yAxisFormatter(series.value)}</td>` +
-              '</tr>'
-            );
-          });
-          tooltip += '</tbody></table>';
-          return tooltip;
-        });
+        chart.interactiveLayer.tooltip.contentGenerator(d =>
+          generateRichLineTooltipContent(d, yAxisFormatter));
       }
     }
 
-    if (['dual_line', 'line_multi'].indexOf(vizType) >= 0) {
-      const yAxisFormatter1 = d3.format(fd.y_axis_format);
-      const yAxisFormatter2 = d3.format(fd.y_axis_2_format);
+    if (isVizTypes(['dual_line', 'line_multi'])) {
+      const yAxisFormatter1 = d3.format(yAxisFormat);
+      const yAxisFormatter2 = d3.format(yAxis2Format);
       chart.yAxis1.tickFormat(yAxisFormatter1);
       chart.yAxis2.tickFormat(yAxisFormatter2);
       const yAxisFormatters = data.map(datum => (
         datum.yAxis === 1 ? yAxisFormatter1 : yAxisFormatter2));
-      customizeToolTip(chart, xAxisFormatter, yAxisFormatters);
+      chart.useInteractiveGuideline(true);
+      chart.interactiveLayer.tooltip.contentGenerator(d =>
+        generateMultiLineTooltipContent(d, xAxisFormatter, yAxisFormatters));
       if (vizType === 'dual_line') {
         chart.showLegend(width > BREAKPOINTS.small);
       } else {
-        chart.showLegend(fd.show_legend);
+        chart.showLegend(showLegend);
       }
     }
     // This is needed for correct chart dimensions if a chart is rendered in a hidden container
     chart.width(width);
     chart.height(height);
-    slice.container.css('height', height + 'px');
+    container.style.height = `${height}px`;
 
     svg
-    .datum(data)
-    .transition().duration(500)
-    .attr('height', height)
-    .attr('width', width)
-    .call(chart);
+      .datum(data)
+      .transition().duration(500)
+      .attr('height', height)
+      .attr('width', width)
+      .call(chart);
 
     // align yAxis1 and yAxis2 ticks
-    if (['dual_line', 'line_multi'].indexOf(vizType) >= 0) {
+    if (isVizTypes(['dual_line', 'line_multi'])) {
       const count = chart.yAxis1.ticks();
-      const ticks1 = chart.yAxis1.scale().domain(chart.yAxis1.domain()).nice(count).ticks(count);
-      const ticks2 = chart.yAxis2.scale().domain(chart.yAxis2.domain()).nice(count).ticks(count);
+      const ticks1 = chart.yAxis1.scale()
+        .domain(chart.yAxis1.domain())
+        .nice(count)
+        .ticks(count);
+      const ticks2 = chart.yAxis2.scale()
+        .domain(chart.yAxis2.domain())
+        .nice(count)
+        .ticks(count);
 
       // match number of ticks in both axes
       const difference = ticks1.length - ticks2.length;
@@ -551,23 +577,21 @@ export default function nvd3Vis(slice, payload) {
       }
     }
 
-    if (fd.show_markers) {
+    if (showMarkers) {
       svg.selectAll('.nv-point')
-      .style('stroke-opacity', 1)
-      .style('fill-opacity', 1);
+        .style('stroke-opacity', 1)
+        .style('fill-opacity', 1);
     }
 
     if (chart.yAxis !== undefined || chart.yAxis2 !== undefined) {
       // Hack to adjust y axis left margin to accommodate long numbers
-      const containerWidth = slice.container.width();
       const marginPad = Math.ceil(
-        Math.min(isExplore ? containerWidth * 0.01 : containerWidth * 0.03, maxMarginPad),
+        Math.min(maxWidth * (isExplore ? 0.01 : 0.03), MAX_MARGIN_PAD),
       );
-      const maxYAxisLabelWidth = chart.yAxis2 ? getMaxLabelSize(slice.container, 'nv-y1')
-                                              : getMaxLabelSize(slice.container, 'nv-y');
-      const maxXAxisLabelHeight = getMaxLabelSize(slice.container, 'nv-x');
+      const maxYAxisLabelWidth = getMaxLabelSize(svg, chart.yAxis2 ? 'nv-y1' : 'nv-y');
+      const maxXAxisLabelHeight = getMaxLabelSize(svg, 'nv-x');
       chart.margin({ left: maxYAxisLabelWidth + marginPad });
-      if (fd.y_axis_label && fd.y_axis_label !== '') {
+      if (yAxisLabel && yAxisLabel !== '') {
         chart.margin({ left: maxYAxisLabelWidth + marginPad + 25 });
       }
       // Hack to adjust margins to accommodate long axis tick labels.
@@ -577,7 +601,7 @@ export default function nvd3Vis(slice, payload) {
       // - adjust margins based on these measures and render again
       const margins = chart.margin();
       margins.bottom = 28;
-      if (fd.x_axis_showminmax) {
+      if (xAxisShowMinMax) {
         // If x bounds are shown, we need a right margin
         margins.right = Math.max(20, maxXAxisLabelHeight / 2) + marginPad;
       }
@@ -588,79 +612,81 @@ export default function nvd3Vis(slice, payload) {
         margins.bottom = 40;
       }
 
-      if (['dual_line', 'line_multi'].indexOf(vizType) >= 0) {
-        const maxYAxis2LabelWidth = getMaxLabelSize(slice.container, 'nv-y2');
+      if (isVizTypes(['dual_line', 'line_multi'])) {
+        const maxYAxis2LabelWidth = getMaxLabelSize(svg, 'nv-y2');
         margins.right = maxYAxis2LabelWidth + marginPad;
       }
-      if (fd.bottom_margin && fd.bottom_margin !== 'auto') {
-        margins.bottom = parseInt(fd.bottom_margin, 10);
+      if (bottomMargin && bottomMargin !== 'auto') {
+        margins.bottom = parseInt(bottomMargin, 10);
       }
-      if (fd.left_margin && fd.left_margin !== 'auto') {
-        margins.left = fd.left_margin;
+      if (leftMargin && leftMargin !== 'auto') {
+        margins.left = leftMargin;
       }
 
-      if (fd.x_axis_label && fd.x_axis_label !== '' && chart.xAxis) {
+      if (xAxisLabel && xAxisLabel !== '' && chart.xAxis) {
         margins.bottom += 25;
         let distance = 0;
-        if (margins.bottom && !isNaN(margins.bottom)) {
+        if (margins.bottom && !Number.isNaN(margins.bottom)) {
           distance = margins.bottom - 45;
         }
         // nvd3 bug axisLabelDistance is disregarded on xAxis
         // https://github.com/krispo/angular-nvd3/issues/90
-        chart.xAxis.axisLabel(fd.x_axis_label).axisLabelDistance(distance);
+        chart.xAxis.axisLabel(xAxisLabel).axisLabelDistance(distance);
       }
 
-      if (fd.y_axis_label && fd.y_axis_label !== '' && chart.yAxis) {
+      if (yAxisLabel && yAxisLabel !== '' && chart.yAxis) {
         let distance = 0;
-        if (margins.left && !isNaN(margins.left)) {
+        if (margins.left && !Number.isNaN(margins.left)) {
           distance = margins.left - 70;
         }
-        chart.yAxis.axisLabel(fd.y_axis_label).axisLabelDistance(distance);
+        chart.yAxis.axisLabel(yAxisLabel).axisLabelDistance(distance);
       }
 
-      const annotationLayers = (slice.formData.annotation_layers || []).filter(x => x.show);
-      if (isTimeSeries && annotationLayers && slice.annotationData) {
+      if (isTimeSeries && annotationData && annotationLayers.length > 0) {
         // Time series annotations add additional data
         const timeSeriesAnnotations = annotationLayers
-          .filter(a => a.annotationType === AnnotationTypes.TIME_SERIES).reduce((bushel, a) =>
-        bushel.concat((slice.annotationData[a.name] || []).map((series) => {
-          if (!series) {
-            return {};
-          }
-          const key = Array.isArray(series.key) ?
-            `${a.name}, ${series.key.join(', ')}` : `${a.name}, ${series.key}`;
-          return {
-            ...series,
-            key,
-            color: a.color,
-            strokeWidth: a.width,
-            classed: `${a.opacity} ${a.style} nv-timeseries-annotation-layer showMarkers${a.showMarkers} hideLine${a.hideLine}`,
-          };
-        })), []);
+          .filter(layer => layer.show)
+          .filter(layer => layer.annotationType === AnnotationTypes.TIME_SERIES)
+          .reduce((bushel, a) =>
+            bushel.concat((annotationData[a.name] || []).map((series) => {
+              if (!series) {
+                return {};
+              }
+              const key = Array.isArray(series.key) ?
+                `${a.name}, ${series.key.join(', ')}` : `${a.name}, ${series.key}`;
+              return {
+                ...series,
+                key,
+                color: a.color,
+                strokeWidth: a.width,
+                classed: `${a.opacity} ${a.style} nv-timeseries-annotation-layer showMarkers${a.showMarkers} hideLine${a.hideLine}`,
+              };
+            })), []);
         data.push(...timeSeriesAnnotations);
       }
 
       // render chart
       svg
-      .datum(data)
-      .transition().duration(500)
-      .attr('height', height)
-      .attr('width', width)
-      .call(chart);
+        .datum(data)
+        .transition().duration(500)
+        .attr('width', width)
+        .attr('height', height)
+        .call(chart);
 
       // on scroll, hide tooltips. throttle to only 4x/second.
-      $(window).scroll(throttle(hideTooltips, 250));
+      window.addEventListener('scroll', throttle(hideTooltips, 250));
 
       // The below code should be run AFTER rendering because chart is updated in call()
-      if (isTimeSeries && annotationLayers) {
+      if (isTimeSeries && annotationLayers.length > 0) {
         // Formula annotations
-        const formulas = annotationLayers.filter(a => a.annotationType === AnnotationTypes.FORMULA)
+        const formulas = annotationLayers
+          .filter(a => a.annotationType === AnnotationTypes.FORMULA)
           .map(a => ({ ...a, formula: mathjs.parse(a.value) }));
 
         let xMax;
         let xMin;
         let xScale;
-        if (vizType === VIZ_TYPES.bar) {
+        if (vizType === 'bar') {
           xMin = d3.min(data[0].values, d => (d.x));
           xMax = d3.max(data[0].values, d => (d.x));
           xScale = d3.scale.quantile()
@@ -681,9 +707,9 @@ export default function nvd3Vis(slice, payload) {
           xScale.clamp(true);
         }
 
-        if (Array.isArray(formulas) && formulas.length) {
+        if (formulas.length > 0) {
           const xValues = [];
-          if (vizType === VIZ_TYPES.bar) {
+          if (vizType === 'bar') {
             // For bar-charts we want one data point evaluated for every
             // data point that will be displayed.
             const distinct = data.reduce((xVals, d) => {
@@ -720,37 +746,23 @@ export default function nvd3Vis(slice, payload) {
         const yAxis = chart.yAxis1 ? chart.yAxis1 : chart.yAxis;
         const chartWidth = xAxis.scale().range()[1];
         const annotationHeight = yAxis.scale().range()[0];
-        const tipFactory = layer => d3tip()
-          .attr('class', 'd3-tip')
-          .direction('n')
-          .offset([-5, 0])
-          .html((d) => {
-            if (!d) {
-              return '';
-            }
-            const title = d[layer.titleColumn] && d[layer.titleColumn].length ?
-              d[layer.titleColumn] + ' - ' + layer.name :
-              layer.name;
-            const body = Array.isArray(layer.descriptionColumns) ?
-              layer.descriptionColumns.map(c => d[c]) : Object.values(d);
-            return '<div><strong>' + title + '</strong></div><br/>' +
-              '<div>' + body.join(', ') + '</div>';
-          });
 
-        if (slice.annotationData) {
+        if (annotationData) {
           // Event annotations
           annotationLayers.filter(x => (
             x.annotationType === AnnotationTypes.EVENT &&
-            slice.annotationData && slice.annotationData[x.name]
+            annotationData && annotationData[x.name]
           )).forEach((config, index) => {
             const e = applyNativeColumns(config);
             // Add event annotation layer
-            const annotations = d3.select(slice.selector).select('.nv-wrap').append('g')
+            const annotations = d3.select(element)
+              .select('.nv-wrap')
+              .append('g')
               .attr('class', `nv-event-annotation-layer-${index}`);
-            const aColor = e.color || getColorFromScheme(e.name, fd.color_scheme);
+            const aColor = e.color || getColor(e.name, colorScheme);
 
             const tip = tipFactory(e);
-            const records = (slice.annotationData[e.name].records || []).map((r) => {
+            const records = (annotationData[e.name].records || []).map((r) => {
               const timeValue = new Date(moment.utc(r[e.timeColumn]));
 
               return {
@@ -798,17 +810,19 @@ export default function nvd3Vis(slice, payload) {
           // Interval annotations
           annotationLayers.filter(x => (
             x.annotationType === AnnotationTypes.INTERVAL &&
-            slice.annotationData && slice.annotationData[x.name]
+            annotationData && annotationData[x.name]
           )).forEach((config, index) => {
             const e = applyNativeColumns(config);
             // Add interval annotation layer
-            const annotations = d3.select(slice.selector).select('.nv-wrap').append('g')
+            const annotations = d3.select(element)
+              .select('.nv-wrap')
+              .append('g')
               .attr('class', `nv-interval-annotation-layer-${index}`);
 
-            const aColor = e.color || getColorFromScheme(e.name, fd.color_scheme);
+            const aColor = e.color || getColor(e.name, colorScheme);
             const tip = tipFactory(e);
 
-            const records = (slice.annotationData[e.name].records || []).map((r) => {
+            const records = (annotationData[e.name].records || []).map((r) => {
               const timeValue = new Date(moment.utc(r[e.timeColumn]));
               const intervalEndValue = new Date(moment.utc(r[e.intervalEndColumn]));
               return {
@@ -875,7 +889,7 @@ export default function nvd3Vis(slice, payload) {
       }
     }
 
-    wrapTooltip(chart, slice.container);
+    wrapTooltip(chart, maxWidth);
     return chart;
   };
 
@@ -886,3 +900,117 @@ export default function nvd3Vis(slice, payload) {
 
   nv.addGraph(drawGraph);
 }
+
+nvd3Vis.propTypes = propTypes;
+
+function adaptor(slice, payload) {
+  const { formData, datasource, selector, annotationData } = slice;
+  const {
+    annotation_layers: annotationLayers,
+    bar_stacked: isBarStacked,
+    bottom_margin: bottomMargin,
+    color_picker: baseColor,
+    color_scheme: colorScheme,
+    comparison_type: comparisonType,
+    contribution,
+    donut: isDonut,
+    entity,
+    labels_outside: isPieLabelOutside,
+    left_margin: leftMargin,
+    line_interpolation: lineInterpolation,
+    max_bubble_size: maxBubbleSize,
+    order_bars: orderBars,
+    pie_label_type: pieLabelType,
+    reduce_x_ticks: reduceXTicks,
+    rich_tooltip: useRichTooltip,
+    send_time_range: hasBrushAction,
+    show_bar_value: showBarValue,
+    show_brush: showBrush,
+    show_controls: showControls,
+    show_labels: showLabels,
+    show_legend: showLegend,
+    show_markers: showMarkers,
+    size: sizeField,
+    stacked_style: areaStackedStyle,
+    viz_type: vizType,
+    x: xField,
+    x_axis_format: xAxisFormat,
+    x_axis_label: xAxisLabel,
+    x_axis_showminmax: xAxisShowMinMax,
+    x_log_scale: xIsLogScale,
+    x_ticks_layout: xTicksLayout,
+    y: yField,
+    y_axis_format: yAxisFormat,
+    y_axis_2_format: yAxis2Format,
+    y_axis_bounds: yAxisBounds,
+    y_axis_label: yAxisLabel,
+    y_axis_showminmax: yAxisShowMinMax,
+    y_log_scale: yIsLogScale,
+  } = formData;
+
+  const element = document.querySelector(selector);
+
+  const rawData = payload.data || [];
+  const data = Array.isArray(rawData)
+    ? rawData.map(row => ({
+      ...row,
+      key: formatLabel(row.key, datasource.verbose_map),
+    }))
+    : rawData;
+
+  const props = {
+    data,
+    width: slice.width(),
+    height: slice.height(),
+    annotationData,
+    annotationLayers,
+    areaStackedStyle,
+    baseColor,
+    bottomMargin,
+    colorScheme,
+    comparisonType,
+    contribution,
+    entity,
+    isBarStacked,
+    isDonut,
+    isPieLabelOutside,
+    leftMargin,
+    lineInterpolation,
+    maxBubbleSize: parseInt(maxBubbleSize, 10),
+    onBrushEnd: isTruthy(hasBrushAction) ? ((timeRange) => {
+      slice.addFilter('__time_range', timeRange, false, true);
+    }) : undefined,
+    onError(err) { slice.error(err); },
+    orderBars,
+    pieLabelType,
+    reduceXTicks,
+    showBarValue,
+    showBrush,
+    showControls,
+    showLabels,
+    showLegend,
+    showMarkers,
+    sizeField,
+    useRichTooltip,
+    vizType,
+    xAxisFormat,
+    xAxisLabel,
+    xAxisShowMinMax,
+    xField,
+    xIsLogScale,
+    xTicksLayout,
+    yAxisFormat,
+    yAxis2Format,
+    yAxisBounds,
+    yAxisLabel,
+    yAxisShowMinMax,
+    yField,
+    yIsLogScale,
+  };
+
+  slice.clearError();
+
+  return nvd3Vis(element, props);
+}
+
+export default adaptor;
diff --git a/superset/assets/src/visualizations/nvd3/PropTypes.js b/superset/assets/src/visualizations/nvd3/PropTypes.js
new file mode 100644
index 0000000..6c3d58d
--- /dev/null
+++ b/superset/assets/src/visualizations/nvd3/PropTypes.js
@@ -0,0 +1,63 @@
+import PropTypes from 'prop-types';
+import { ANNOTATION_TYPES } from '../../modules/AnnotationTypes';
+
+export const numberOrAutoType = PropTypes.oneOfType([
+  PropTypes.number,
+  PropTypes.oneOf(['auto']),
+]);
+
+export const stringOrObjectWithLabelType = PropTypes.oneOfType([
+  PropTypes.string,
+  PropTypes.shape({
+    label: PropTypes.string,
+  }),
+]);
+
+export const rgbObjectType = PropTypes.shape({
+  r: PropTypes.number.isRequired,
+  g: PropTypes.number.isRequired,
+  b: PropTypes.number.isRequired,
+});
+
+export const numericXYType = PropTypes.shape({
+  x: PropTypes.number,
+  y: PropTypes.number,
+});
+
+export const categoryAndValueXYType = PropTypes.shape({
+  x: PropTypes.string,
+  y: PropTypes.number,
+});
+
+export const boxPlotValueType = PropTypes.shape({
+  Q1: PropTypes.number,
+  Q2: PropTypes.number,
+  Q3: PropTypes.number,
+  outliers: PropTypes.arrayOf(PropTypes.number),
+  whisker_high: PropTypes.number,
+  whisker_low: PropTypes.number,
+});
+
+export const bulletDataType = PropTypes.shape({
+  markerLabels: PropTypes.arrayOf(PropTypes.string),
+  markerLineLabels: PropTypes.arrayOf(PropTypes.string),
+  markerLines: PropTypes.arrayOf(PropTypes.number),
+  markers: PropTypes.arrayOf(PropTypes.number),
+  measures: PropTypes.arrayOf(PropTypes.number),
+  rangeLabels: PropTypes.arrayOf(PropTypes.string),
+  ranges: PropTypes.arrayOf(PropTypes.number),
+});
+
+export const annotationLayerType = PropTypes.shape({
+  annotationType: PropTypes.oneOf(Object.keys(ANNOTATION_TYPES)),
+  color: PropTypes.string,
+  name: PropTypes.string,
+  hideLine: PropTypes.bool,
+  opacity: PropTypes.string,
+  show: PropTypes.bool,
+  showMarkers: PropTypes.bool,
+  sourceType: PropTypes.string,
+  style: PropTypes.string,
+  value: PropTypes.string,
+  width: PropTypes.number,
+});
diff --git a/superset/assets/src/visualizations/nvd3/utils.js b/superset/assets/src/visualizations/nvd3/utils.js
new file mode 100644
index 0000000..47bd499
--- /dev/null
+++ b/superset/assets/src/visualizations/nvd3/utils.js
@@ -0,0 +1,206 @@
+import d3 from 'd3';
+import d3tip from 'd3-tip';
+import dompurify from 'dompurify';
+import { formatDateVerbose } from '../../modules/dates';
+import { TIME_SHIFT_PATTERN } from '../../utils/common';
+
+export function drawBarValues(svg, data, stacked, axisFormat) {
+  const format = d3.format(axisFormat || '.3s');
+  const countSeriesDisplayed = data.length;
+
+  const totalStackedValues = stacked && data.length !== 0 ?
+    data[0].values.map(function (bar, iBar) {
+      const bars = data.map(series => series.values[iBar]);
+      return d3.sum(bars, d => d.y);
+    }) : [];
+
+  const groupLabels = svg.select('g.nv-barsWrap').append('g');
+
+  svg.selectAll('g.nv-group')
+    .filter((d, i) => !stacked || i === countSeriesDisplayed - 1)
+    .selectAll('rect')
+    .each(function (d, index) {
+      const rectObj = d3.select(this);
+      if (rectObj.attr('class').includes('positive')) {
+        const transformAttr = rectObj.attr('transform');
+        const yPos = parseFloat(rectObj.attr('y'));
+        const xPos = parseFloat(rectObj.attr('x'));
+        const rectWidth = parseFloat(rectObj.attr('width'));
+        const textEls = groupLabels.append('text')
+          .attr('y', yPos - 5)
+          .text(format(stacked ? totalStackedValues[index] : d.y))
+          .attr('transform', transformAttr)
+          .attr('class', 'bar-chart-label');
+        const labelWidth = textEls.node().getBBox().width;
+        textEls.attr('x', xPos + rectWidth / 2 - labelWidth / 2); // fine tune
+      }
+    });
+}
+
+// Custom sorted tooltip
+// use a verbose formatter for times
+export function generateRichLineTooltipContent(d, valueFormatter) {
+  let tooltip = '';
+  tooltip += "<table><thead><tr><td colspan='3'>"
+    + `<strong class='x-value'>${formatDateVerbose(d.value)}</strong>`
+    + '</td></tr></thead><tbody>';
+  d.series.sort((a, b) => a.value >= b.value ? -1 : 1);
+  d.series.forEach((series) => {
+    tooltip += (
+      `<tr class="${series.highlight ? 'emph' : ''}">` +
+        `<td class='legend-color-guide' style="opacity: ${series.highlight ? '1' : '0.75'};"">` +
+          '<div ' +
+            `style="border: 2px solid ${series.highlight ? 'black' : 'transparent'}; background-color: ${series.color};"` +
+          '></div>' +
+        '</td>' +
+        `<td>${dompurify.sanitize(series.key)}</td>` +
+        `<td>${valueFormatter(series.value)}</td>` +
+      '</tr>'
+    );
+  });
+  tooltip += '</tbody></table>';
+  return tooltip;
+}
+
+export function generateMultiLineTooltipContent(d, xFormatter, yFormatters) {
+  const tooltipTitle = xFormatter(d.value);
+  let tooltip = '';
+
+  tooltip += "<table><thead><tr><td colspan='3'>"
+    + `<strong class='x-value'>${tooltipTitle}</strong>`
+    + '</td></tr></thead><tbody>';
+
+  d.series.forEach((series, i) => {
+    const yFormatter = yFormatters[i];
+    tooltip += "<tr><td class='legend-color-guide'>"
+      + `<div style="background-color: ${series.color};"></div></td>`
+      + `<td class='key'>${series.key}</td>`
+      + `<td class='value'>${yFormatter(series.value)}</td></tr>`;
+  });
+
+  tooltip += '</tbody></table>';
+
+  return tooltip;
+}
+
+function getLabel(stringOrObjectWithLabel) {
+  return stringOrObjectWithLabel.label || stringOrObjectWithLabel;
+}
+
+function createHTMLRow(col1, col2) {
+  return `<tr><td>${col1}</td><td>${col2}</td></tr>`;
+}
+
+export function generateBubbleTooltipContent({
+  point,
+  entity,
+  xField,
+  yField,
+  sizeField,
+  xFormatter,
+  yFormatter,
+  sizeFormatter,
+}) {
+  let s = '<table>';
+  s += (
+    `<tr><td style="color: ${point.color};">` +
+      `<strong>${point[entity]}</strong> (${point.group})` +
+    '</td></tr>'
+  );
+  s += createHTMLRow(getLabel(xField), xFormatter(point.x));
+  s += createHTMLRow(getLabel(yField), yFormatter(point.y));
+  s += createHTMLRow(getLabel(sizeField), sizeFormatter(point.size));
+  s += '</table>';
+  return s;
+}
+
+export function hideTooltips() {
+  const target = document.querySelector('.nvtooltip');
+  if (target) {
+    target.style.opacity = 0;
+  }
+}
+
+export function wrapTooltip(chart, maxWidth) {
+  const tooltipLayer = chart.useInteractiveGuideline && chart.useInteractiveGuideline() ?
+    chart.interactiveLayer : chart;
+  const tooltipGeneratorFunc = tooltipLayer.tooltip.contentGenerator();
+  tooltipLayer.tooltip.contentGenerator((d) => {
+    let tooltip = `<div style="max-width: ${maxWidth * 0.5}px">`;
+    tooltip += tooltipGeneratorFunc(d);
+    tooltip += '</div>';
+    return tooltip;
+  });
+}
+
+export function tipFactory(layer) {
+  return d3tip()
+    .attr('class', 'd3-tip')
+    .direction('n')
+    .offset([-5, 0])
+    .html((d) => {
+      if (!d) {
+        return '';
+      }
+      const title = d[layer.titleColumn] && d[layer.titleColumn].length ?
+        d[layer.titleColumn] + ' - ' + layer.name :
+        layer.name;
+      const body = Array.isArray(layer.descriptionColumns) ?
+        layer.descriptionColumns.map(c => d[c]) : Object.values(d);
+      return '<div><strong>' + title + '</strong></div><br/>' +
+        '<div>' + body.join(', ') + '</div>';
+    });
+}
+
+export function getMaxLabelSize(svg, axisClass) {
+  // axis class = .nv-y2  // second y axis on dual line chart
+  // axis class = .nv-x  // x axis on time series line chart
+  const tickTexts = svg.selectAll(`.${axisClass} g.tick text`);
+  if (tickTexts.length > 0) {
+    const lengths = tickTexts[0].map(text => text.getComputedTextLength());
+    return Math.ceil(Math.max(...lengths));
+  }
+  return 0;
+}
+
+export function formatLabel(input, verboseMap = {}) {
+  // The input for label may be a string or an array of string
+  // When using the time shift feature, the label contains a '---' in the array
+  const verboseLookup = s => verboseMap[s] || s;
+  return (Array.isArray(input) && input.length)
+    ? input.map(l => TIME_SHIFT_PATTERN.test(l) ? l : verboseLookup(l))
+      .join(', ')
+    : verboseLookup(input);
+}
+
+const MIN_BAR_WIDTH = 15;
+
+export function computeBarChartWidth(data, stacked, maxWidth) {
+  const barCount = stacked
+    ? d3.max(data, d => d.values.length)
+    : d3.sum(data, d => d.values.length);
+
+  const barWidth = barCount * MIN_BAR_WIDTH;
+  return Math.max(barWidth, maxWidth);
+}
+
+export function tryNumify(s) {
+  // Attempts casting to Number, returns string when failing
+  const n = Number(s);
+  return Number.isNaN(n) ? s : n;
+}
+
+export function stringifyTimeRange(extent) {
+  if (extent.some(d => d.toISOString === undefined)) {
+    return null;
+  }
+  return extent.map(d => d.toISOString()
+    .slice(0, -1))
+    .join(' : ');
+}
+
+export function setAxisShowMaxMin(axis, showminmax) {
+  if (axis && axis.showMaxMin && showminmax !== undefined) {
+    axis.showMaxMin(showminmax);
+  }
+}


Mime
View raw message