diff --git a/superset/assets/spec/javascripts/modules/utils_spec.jsx b/superset/assets/spec/javascripts/modules/utils_spec.jsx
index f227970ece8b1..10a4fbc0c11ef 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 ded0acc0341ad..d88214e319f6c 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 c5d4e75450ae9..0694cdcd0ae18 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 += "
"
- + `${tooltipTitle}`
- + ' |
';
-
- d.series.forEach((series, i) => {
- const yAxisFormatter = yAxisFormatters[i];
- const value = yAxisFormatter(series.value);
- tooltip += ""
- + ` | `
- + `${series.key} | `
- + `${value} |
`;
- });
-
- tooltip += '
';
-
- 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 1f46f5f9f9c89..31feffc2abba4 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 b8cd1a0785283..7309641433d8d 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 1baf5f576a0c1..73cb7b7cd6632 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 = ``;
- tooltip += tooltipGeneratorFunc(d);
- tooltip += '
';
- 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) => `${col1} | ${col2} |
`;
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 = '';
- s += (
- `` +
- `${p[fd.entity]} (${p.group})` +
- ' |
');
- 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 += '
';
- 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 += ""
- + `${formatDateVerbose(d.value)}`
- + ' |
';
- d.series.sort((a, b) => a.value >= b.value ? -1 : 1);
- d.series.forEach((series) => {
- tooltip += (
- `` +
- `` +
- '' +
- ' | ' +
- `${dompurify.sanitize(series.key)} | ` +
- `${yAxisFormatter(series.value)} | ` +
- '
'
- );
- });
- tooltip += '
';
- 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 '' + title + '
' +
- '' + body.join(', ') + '
';
- });
- 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 0000000000000..6c3d58daa365d
--- /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 0000000000000..47bd499f18a9b
--- /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 += ""
+ + `${formatDateVerbose(d.value)}`
+ + ' |
';
+ d.series.sort((a, b) => a.value >= b.value ? -1 : 1);
+ d.series.forEach((series) => {
+ tooltip += (
+ `` +
+ `` +
+ '' +
+ ' | ' +
+ `${dompurify.sanitize(series.key)} | ` +
+ `${valueFormatter(series.value)} | ` +
+ '
'
+ );
+ });
+ tooltip += '
';
+ return tooltip;
+}
+
+export function generateMultiLineTooltipContent(d, xFormatter, yFormatters) {
+ const tooltipTitle = xFormatter(d.value);
+ let tooltip = '';
+
+ tooltip += ""
+ + `${tooltipTitle}`
+ + ' |
';
+
+ d.series.forEach((series, i) => {
+ const yFormatter = yFormatters[i];
+ tooltip += ""
+ + ` | `
+ + `${series.key} | `
+ + `${yFormatter(series.value)} |
`;
+ });
+
+ tooltip += '
';
+
+ return tooltip;
+}
+
+function getLabel(stringOrObjectWithLabel) {
+ return stringOrObjectWithLabel.label || stringOrObjectWithLabel;
+}
+
+function createHTMLRow(col1, col2) {
+ return `${col1} | ${col2} |
`;
+}
+
+export function generateBubbleTooltipContent({
+ point,
+ entity,
+ xField,
+ yField,
+ sizeField,
+ xFormatter,
+ yFormatter,
+ sizeFormatter,
+}) {
+ let s = '';
+ s += (
+ `` +
+ `${point[entity]} (${point.group})` +
+ ' |
'
+ );
+ s += createHTMLRow(getLabel(xField), xFormatter(point.x));
+ s += createHTMLRow(getLabel(yField), yFormatter(point.y));
+ s += createHTMLRow(getLabel(sizeField), sizeFormatter(point.size));
+ s += '
';
+ 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 = ``;
+ tooltip += tooltipGeneratorFunc(d);
+ tooltip += '
';
+ 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 '' + title + '
' +
+ '' + body.join(', ') + '
';
+ });
+}
+
+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);
+ }
+}