airbnb/superset

View on GitHub
superset-frontend/plugins/legacy-preset-chart-nvd3/src/NVD3Vis.js

Summary

Maintainability
F
2 wks
Test Coverage
/* eslint-disable react/sort-prop-types */
/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
import { kebabCase, throttle } from 'lodash';
import d3 from 'd3';
import moment from 'moment';
import nv from 'nvd3-fork';
import PropTypes from 'prop-types';
import {
  CategoricalColorNamespace,
  evalExpression,
  getNumberFormatter,
  getTimeFormatter,
  isDefined,
  NumberFormats,
  smartDateVerboseFormatter,
  t,
} from '@superset-ui/core';

import 'nvd3-fork/build/nv.d3.css';

/* eslint-disable-next-line */
import ANNOTATION_TYPES, {
  applyNativeColumns,
} from './vendor/superset/AnnotationTypes';
import isTruthy from './utils/isTruthy';
import {
  cleanColorInput,
  computeBarChartWidth,
  computeYDomain,
  computeStackedYDomain,
  drawBarValues,
  generateBubbleTooltipContent,
  generateCompareTooltipContent,
  generateRichLineTooltipContent,
  generateTimePivotTooltip,
  generateTooltipClassName,
  generateAreaChartTooltipContent,
  getMaxLabelSize,
  getTimeOrNumberFormatter,
  hideTooltips,
  tipFactory,
  tryNumify,
  removeTooltip,
  setAxisShowMaxMin,
  stringifyTimeRange,
  wrapTooltip,
} from './utils';
import {
  annotationLayerType,
  boxPlotValueType,
  bulletDataType,
  categoryAndValueXYType,
  rgbObjectType,
  numericXYType,
  numberOrAutoType,
  stringOrObjectWithLabelType,
} from './PropTypes';

const NO_DATA_RENDER_DATA = [
  { text: 'No data', dy: '-.75em', class: 'header' },
  {
    text: 'Adjust filters or check the Datasource.',
    dy: '.75em',
    class: 'body',
  },
];

// Override the noData render function to make a prettier UX
// Code adapted from https://github.com/novus/nvd3/blob/master/src/utils.js#L653
nv.utils.noData = function noData(chart, container) {
  const opt = chart.options();
  const margin = opt.margin();
  const height = nv.utils.availableHeight(null, container, margin);
  const width = nv.utils.availableWidth(null, container, margin);
  const x = margin.left + width / 2;
  const y = margin.top + height / 2;

  // Remove any previously created chart components
  container.selectAll('g').remove();

  const noDataText = container
    .selectAll('.nv-noData')
    .data(NO_DATA_RENDER_DATA);

  noDataText
    .enter()
    .append('text')
    .attr('class', d => `nvd3 nv-noData ${d.class}`)
    .attr('dy', d => d.dy)
    .style('text-anchor', 'middle');

  noDataText
    .attr('x', x)
    .attr('y', y)
    .text(d => d.text);
};

const { getColor, getScale } = CategoricalColorNamespace;

// Limit on how large axes margins can grow as the chart window is resized
const MAX_MARGIN_PAD = 30;
const MIN_HEIGHT_FOR_BRUSH = 480;
const MAX_NO_CHARACTERS_IN_LABEL = 40;

const BREAKPOINTS = {
  small: 340,
};

const TIMESERIES_VIZ_TYPES = ['line', 'area', 'compare', 'bar', 'time_pivot'];

const CHART_ID_PREFIX = 'chart-id-';

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',
    'time_pivot',
    'pie',
  ]),
  xAxisFormat: PropTypes.string,
  numberFormat: 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, 'yes', false, 'no', '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',
    'key_value_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,
};

const NOOP = () => {};
const formatter = getNumberFormatter();

function nvd3Vis(element, props) {
  const {
    data,
    width: maxWidth,
    height: maxHeight,
    annotationData,
    annotationLayers = [],
    areaStackedStyle,
    baseColor,
    bottomMargin,
    colorScheme,
    comparisonType,
    contribution,
    entity,
    isBarStacked,
    isDonut,
    isPieLabelOutside,
    leftMargin,
    lineInterpolation = 'linear',
    markerLabels,
    markerLines,
    markerLineLabels,
    markers,
    maxBubbleSize,
    onBrushEnd = NOOP,
    onError = NOOP,
    orderBars,
    pieLabelType,
    rangeLabels,
    ranges,
    reduceXTicks = false,
    showBarValue,
    showBrush,
    showControls,
    showLabels,
    showLegend,
    showMarkers,
    sizeField,
    useRichTooltip,
    vizType,
    xAxisFormat,
    numberFormat,
    xAxisLabel,
    xAxisShowMinMax = false,
    xField,
    xIsLogScale,
    xTicksLayout,
    yAxisFormat,
    yAxisBounds,
    yAxisLabel,
    yAxisShowMinMax = false,
    yAxis2ShowMinMax = false,
    yField,
    yIsLogScale,
    sliceId,
  } = props;

  const isExplore = document.querySelector('#explorer-container') !== null;
  const container = element;
  container.innerHTML = '';
  const activeAnnotationLayers = annotationLayers.filter(layer => layer.show);

  // Search for the chart id in a parent div from the nvd3 chart
  let chartContainer = container;
  let chartId = null;
  while (chartContainer.parentElement) {
    if (chartContainer.parentElement.id.startsWith(CHART_ID_PREFIX)) {
      chartId = chartContainer.parentElement.id;
      break;
    }

    chartContainer = chartContainer.parentElement;
  }

  let chart;
  let width = maxWidth;
  let colorKey = 'key';

  container.style.width = `${maxWidth}px`;
  container.style.height = `${maxHeight}px`;

  function isVizTypes(types) {
    return types.includes(vizType);
  }

  const drawGraph = function drawGraph() {
    const d3Element = d3.select(element);
    d3Element.classed('superset-legacy-chart-nvd3', true);
    d3Element.classed(`superset-legacy-chart-nvd3-${kebabCase(vizType)}`, true);
    let svg = d3Element.select('svg');
    if (svg.empty()) {
      svg = d3Element.append('svg');
    }
    const height = vizType === 'bullet' ? Math.min(maxHeight, 50) : maxHeight;
    const isTimeSeries = isVizTypes(TIMESERIES_VIZ_TYPES);

    // Handling xAxis ticks settings
    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 canShowBrush =
      isTruthy(showBrush) ||
      (showBrush === 'auto' &&
        maxHeight >= MIN_HEIGHT_FOR_BRUSH &&
        xTicksLayout !== '45°');
    const numberFormatter = getNumberFormatter(numberFormat);

    switch (vizType) {
      case 'line':
        if (canShowBrush) {
          chart = nv.models.lineWithFocusChart();
          if (staggerLabels) {
            // Give a bit more room to focus area if X axis ticks are staggered
            chart.focus.margin({ bottom: 40 });
            chart.focusHeight(80);
          }
          chart.focus.xScale(d3.time.scale.utc());
        } else {
          chart = nv.models.lineChart();
        }
        chart.xScale(d3.time.scale.utc());
        chart.interpolate(lineInterpolation);
        chart.clipEdge(false);
        break;

      case 'time_pivot':
        chart = nv.models.lineChart();
        chart.xScale(d3.time.scale.utc());
        chart.interpolate(lineInterpolation);
        break;

      case 'bar':
        chart = nv.models
          .multiBarChart()
          .showControls(showControls)
          .groupSpacing(0.1);

        if (!reduceXTicks) {
          width = computeBarChartWidth(data, isBarStacked, maxWidth);
        }
        chart.width(width);
        chart.xAxis.showMaxMin(false);
        chart.stacked(isBarStacked);
        break;

      case 'dist_bar':
        chart = nv.models
          .multiBarChart()
          .showControls(showControls)
          .reduceXTicks(reduceXTicks)
          .groupSpacing(0.1); // Distance between each group of bars.

        chart.xAxis.showMaxMin(false);

        chart.stacked(isBarStacked);
        if (orderBars) {
          data.forEach(d => {
            const newValues = [...d.values]; // need to copy values to avoid redux store changed.
            // eslint-disable-next-line no-param-reassign
            d.values = newValues.sort((a, b) =>
              tryNumify(a.x) < tryNumify(b.x) ? -1 : 1,
            );
          });
        }
        if (!reduceXTicks) {
          width = computeBarChartWidth(data, isBarStacked, maxWidth);
        }
        chart.width(width);
        break;

      case 'pie':
        chart = nv.models.pieChart();
        colorKey = 'x';
        chart.valueFormat(numberFormatter);
        if (isDonut) {
          chart.donut(true);
        }
        chart.showLabels(showLabels);
        chart.labelsOutside(isPieLabelOutside);
        // Configure the minimum slice size for labels to show up
        chart.labelThreshold(0.05);
        chart.cornerRadius(true);

        if (['key', 'value', 'percent'].includes(pieLabelType)) {
          chart.labelType(pieLabelType);
        } else if (pieLabelType === 'key_value') {
          chart.labelType(d => `${d.data.x}: ${numberFormatter(d.data.y)}`);
        } else {
          // pieLabelType in ['key_percent', 'key_value_percent']
          const total = d3.sum(data, d => d.y);
          const percentFormatter = getNumberFormatter(
            NumberFormats.PERCENT_2_POINT,
          );
          if (pieLabelType === 'key_percent') {
            chart.tooltip.valueFormatter(d => percentFormatter(d));
            chart.labelType(
              d => `${d.data.x}: ${percentFormatter(d.data.y / total)}`,
            );
          } else {
            // pieLabelType === 'key_value_percent'
            chart.tooltip.valueFormatter(
              d => `${numberFormatter(d)} (${percentFormatter(d / total)})`,
            );
            chart.labelType(
              d =>
                `${d.data.x}: ${numberFormatter(d.data.y)} (${percentFormatter(
                  d.data.y / total,
                )})`,
            );
          }
        }
        // Pie chart does not need top margin
        chart.margin({ top: 0 });
        break;

      case 'column':
        chart = nv.models.multiBarChart().reduceXTicks(false);
        break;

      case 'compare':
        chart = nv.models.cumulativeLineChart();
        chart.xScale(d3.time.scale.utc());
        chart.useInteractiveGuideline(true);
        chart.xAxis.showMaxMin(false);
        break;

      case 'bubble':
        chart = nv.models.scatterChart();
        chart.showDistX(false);
        chart.showDistY(false);
        chart.tooltip.contentGenerator(d =>
          generateBubbleTooltipContent({
            point: d.point,
            entity,
            xField,
            yField,
            sizeField,
            xFormatter: getTimeOrNumberFormatter(xAxisFormat),
            yFormatter: getTimeOrNumberFormatter(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(showControls);
        chart.style(areaStackedStyle);
        chart.xScale(d3.time.scale.utc());
        break;

      case 'box_plot':
        colorKey = 'label';
        chart = nv.models.boxPlotChart();
        chart.x(d => d.label);
        chart.maxBoxWidth(75); // prevent boxes from being incredibly wide
        break;

      case 'bullet':
        chart = nv.models.bulletChart();
        data.rangeLabels = rangeLabels;
        data.ranges = ranges;
        data.markerLabels = markerLabels;
        data.markerLines = markerLines;
        data.markerLineLabels = markerLineLabels;
        data.markers = markers;
        break;

      default:
        throw new Error(`Unrecognized visualization for nvd3${vizType}`);
    }
    // Assuming the container has padding already other than for top margin
    chart.margin({ left: 0, bottom: 0 });

    if (showBarValue) {
      drawBarValues(svg, data, isBarStacked, yAxisFormat);
      chart.dispatch.on('stateChange.drawBarValues', () => {
        drawBarValues(svg, data, isBarStacked, yAxisFormat);
      });
    }

    if (canShowBrush && onBrushEnd !== NOOP) {
      if (chart.focus) {
        chart.focus.dispatch.on('brush', event => {
          const timeRange = stringifyTimeRange(event.extent);
          if (timeRange) {
            event.brush.on('brushend', () => {
              onBrushEnd(timeRange);
            });
          }
        });
      }
    }

    if (chart.xAxis && chart.xAxis.staggerLabels) {
      chart.xAxis.staggerLabels(staggerLabels);
    }
    if (chart.xAxis && chart.xAxis.rotateLabels) {
      chart.xAxis.rotateLabels(xLabelRotation);
    }
    if (chart.x2Axis && chart.x2Axis.staggerLabels) {
      chart.x2Axis.staggerLabels(staggerLabels);
    }
    if (chart.x2Axis && chart.x2Axis.rotateLabels) {
      chart.x2Axis.rotateLabels(xLabelRotation);
    }

    if ('showLegend' in chart && typeof showLegend !== 'undefined') {
      if (width < BREAKPOINTS.small && vizType !== 'pie') {
        chart.showLegend(false);
      } else {
        chart.showLegend(showLegend);
      }
    }

    if (yIsLogScale) {
      chart.yScale(d3.scale.log());
    }
    if (xIsLogScale) {
      chart.xScale(d3.scale.log());
    }

    let xAxisFormatter;
    if (isTimeSeries) {
      xAxisFormatter = getTimeFormatter(xAxisFormat);
      // In tooltips, always use the verbose time format
      chart.interactiveLayer.tooltip.headerFormatter(smartDateVerboseFormatter);
    } else {
      xAxisFormatter = getTimeOrNumberFormatter(xAxisFormat);
    }
    if (chart.x2Axis && chart.x2Axis.tickFormat) {
      chart.x2Axis.tickFormat(xAxisFormatter);
    }
    if (chart.xAxis && chart.xAxis.tickFormat) {
      const isXAxisString = isVizTypes(['dist_bar', 'box_plot']);
      if (isXAxisString) {
        chart.xAxis.tickFormat(d =>
          d.length > MAX_NO_CHARACTERS_IN_LABEL
            ? `${d.slice(0, Math.max(0, MAX_NO_CHARACTERS_IN_LABEL))}…`
            : d,
        );
      } else {
        chart.xAxis.tickFormat(xAxisFormatter);
      }
    }

    let yAxisFormatter = getTimeOrNumberFormatter(yAxisFormat);
    if (chart.yAxis && chart.yAxis.tickFormat) {
      if (
        (contribution || comparisonType === 'percentage') &&
        (!yAxisFormat ||
          yAxisFormat === NumberFormats.SMART_NUMBER ||
          yAxisFormat === NumberFormats.SMART_NUMBER_SIGNED)
      ) {
        // When computing a "Percentage" or "Contribution" selected,
        // force a percentage format if no custom formatting set
        yAxisFormatter = getNumberFormatter(NumberFormats.PERCENT_1_POINT);
      }
      chart.yAxis.tickFormat(yAxisFormatter);
    }
    if (chart.y2Axis && chart.y2Axis.tickFormat) {
      chart.y2Axis.tickFormat(yAxisFormatter);
    }

    if (chart.yAxis) {
      chart.yAxis.ticks(5);
    }
    if (chart.y2Axis) {
      chart.y2Axis.ticks(5);
    }

    // Set showMaxMin for all axis
    setAxisShowMaxMin(chart.xAxis, xAxisShowMinMax);
    setAxisShowMaxMin(chart.x2Axis, xAxisShowMinMax);
    setAxisShowMaxMin(chart.yAxis, yAxisShowMinMax);
    setAxisShowMaxMin(chart.y2Axis, yAxis2ShowMinMax || yAxisShowMinMax);

    if (vizType === 'time_pivot') {
      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})`;
        });
      }

      chart.useInteractiveGuideline(true);
      chart.interactiveLayer.tooltip.contentGenerator(d =>
        generateTimePivotTooltip(d, xAxisFormatter, yAxisFormatter),
      );
    } else if (vizType !== 'bullet') {
      const colorFn = getScale(colorScheme);
      chart.color(
        d => d.color || colorFn(cleanColorInput(d[colorKey]), sliceId),
      );
    }

    if (isVizTypes(['line', 'area', 'bar', 'dist_bar']) && useRichTooltip) {
      chart.useInteractiveGuideline(true);
      if (vizType === 'line' || vizType === 'bar') {
        chart.interactiveLayer.tooltip.contentGenerator(d =>
          generateRichLineTooltipContent(
            d,
            smartDateVerboseFormatter,
            yAxisFormatter,
          ),
        );
      } else if (vizType === 'dist_bar') {
        chart.interactiveLayer.tooltip.contentGenerator(d =>
          generateCompareTooltipContent(d, yAxisFormatter),
        );
      } else {
        // area chart
        chart.interactiveLayer.tooltip.contentGenerator(d =>
          generateAreaChartTooltipContent(
            d,
            smartDateVerboseFormatter,
            yAxisFormatter,
            chart,
          ),
        );
      }
    }

    if (isVizTypes(['compare'])) {
      chart.interactiveLayer.tooltip.contentGenerator(d =>
        generateCompareTooltipContent(d, yAxisFormatter),
      );
    }

    // This is needed for correct chart dimensions if a chart is rendered in a hidden container
    chart.width(width);
    chart.height(height);

    svg
      .datum(data)
      .transition()
      .duration(500)
      .attr('height', height)
      .attr('width', width)
      .call(chart);

    // For log scale, only show 1, 10, 100, 1000, ...
    if (yIsLogScale) {
      chart.yAxis.tickFormat(d =>
        d !== 0 && Math.log10(d) % 1 === 0 ? yAxisFormatter(d) : '',
      );
    }

    if (xLabelRotation > 0) {
      // shift labels to the left so they look better
      const xTicks = svg.select('.nv-x.nv-axis > g').selectAll('g');
      xTicks.selectAll('text').attr('dx', -6.5);
    }

    const applyYAxisBounds = () => {
      if (
        chart.yDomain &&
        Array.isArray(yAxisBounds) &&
        yAxisBounds.length === 2
      ) {
        const [customMin, customMax] = yAxisBounds;
        const hasCustomMin = isDefined(customMin) && !Number.isNaN(customMin);
        const hasCustomMax = isDefined(customMax) && !Number.isNaN(customMax);

        if (
          (hasCustomMin || hasCustomMax) &&
          vizType === 'area' &&
          chart.style() === 'expand'
        ) {
          // Because there are custom bounds, we need to override them back to 0%-100% since this
          // is an expanded area chart
          chart.yDomain([0, 1]);
        } else if (
          (hasCustomMin || hasCustomMax) &&
          vizType === 'area' &&
          chart.style() === 'stream'
        ) {
          // Because there are custom bounds, we need to override them back to the domain of the
          // data since this is a stream area chart
          chart.yDomain(computeStackedYDomain(data));
        } else if (hasCustomMin && hasCustomMax) {
          // Override the y domain if there's both a custom min and max
          chart.yDomain([customMin, customMax]);
          chart.clipEdge(true);
        } else if (hasCustomMin || hasCustomMax) {
          // Only one of the bounds has been set, so we need to manually calculate the other one
          let [trueMin, trueMax] = [0, 1];

          // These viz types can be stacked
          // They correspond to the nvd3 stackedAreaChart and multiBarChart
          if (
            vizType === 'area' ||
            (isVizTypes(['bar', 'dist_bar']) && chart.stacked())
          ) {
            // This is a stacked area chart or a stacked bar chart
            [trueMin, trueMax] = computeStackedYDomain(data);
          } else {
            [trueMin, trueMax] = computeYDomain(data);
          }

          const min = hasCustomMin ? customMin : trueMin;
          const max = hasCustomMax ? customMax : trueMax;
          chart.yDomain([min, max]);
          chart.clipEdge(true);
        }
      }
    };
    applyYAxisBounds();

    // Also reapply on each state change to account for enabled/disabled series
    if (chart.dispatch && chart.dispatch.stateChange) {
      chart.dispatch.on('stateChange.applyYAxisBounds', applyYAxisBounds);
    }

    if (showMarkers) {
      svg
        .selectAll('.nv-point')
        .style('stroke-opacity', 1)
        .style('fill-opacity', 1);

      // redo on legend toggle; nvd3 calls the callback *before* the line is
      // drawn, so we need to add a small delay here
      chart.dispatch.on('stateChange.showMarkers', () => {
        setTimeout(() => {
          svg
            .selectAll('.nv-point')
            .style('stroke-opacity', 1)
            .style('fill-opacity', 1);
        }, 10);
      });
    }

    if (chart.yAxis !== undefined || chart.yAxis2 !== undefined) {
      // Hack to adjust y axis left margin to accommodate long numbers
      const marginPad = Math.ceil(
        Math.min(maxWidth * (isExplore ? 0.01 : 0.03), MAX_MARGIN_PAD),
      );
      // Hack to adjust margins to accommodate long axis tick labels.
      // - has to be done only after the chart has been rendered once
      // - measure the width or height of the labels
      // ---- (x axis labels are rotated 45 degrees so we use height),
      // - adjust margins based on these measures and render again
      const margins = chart.margin();
      if (chart.xAxis) {
        margins.bottom = 28;
      }
      const maxYAxisLabelWidth = getMaxLabelSize(
        svg,
        chart.yAxis2 ? 'nv-y1' : 'nv-y',
      );
      const maxXAxisLabelHeight = getMaxLabelSize(svg, 'nv-x');
      margins.left = maxYAxisLabelWidth + marginPad;

      if (yAxisLabel && yAxisLabel !== '') {
        margins.left += 25;
      }
      if (showBarValue) {
        // Add more margin to avoid label colliding with legend.
        margins.top += 24;
      }
      if (xAxisShowMinMax) {
        // If x bounds are shown, we need a right margin
        margins.right = Math.max(20, maxXAxisLabelHeight / 2) + marginPad;
      }
      if (xLabelRotation === 45) {
        margins.bottom =
          maxXAxisLabelHeight * Math.sin((Math.PI * xLabelRotation) / 180) +
          marginPad +
          30;
        margins.right =
          maxXAxisLabelHeight * Math.cos((Math.PI * xLabelRotation) / 180) +
          marginPad;
      } else if (staggerLabels) {
        margins.bottom = 40;
      }

      if (bottomMargin && bottomMargin !== 'auto') {
        margins.bottom = parseInt(bottomMargin, 10);
      }
      if (leftMargin && leftMargin !== 'auto') {
        margins.left = leftMargin;
      }

      if (xAxisLabel && xAxisLabel !== '' && chart.xAxis) {
        margins.bottom += 25;
        let distance = 0;
        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(xAxisLabel).axisLabelDistance(distance);
      }

      if (yAxisLabel && yAxisLabel !== '' && chart.yAxis) {
        let distance = 0;
        if (margins.left && !Number.isNaN(margins.left)) {
          distance = margins.left - 70;
        }
        chart.yAxis.axisLabel(yAxisLabel).axisLabelDistance(distance);
      }
      if (isTimeSeries && annotationData && activeAnnotationLayers.length > 0) {
        // Time series annotations add additional data
        const timeSeriesAnnotations = activeAnnotationLayers
          .filter(
            layer => layer.annotationType === ANNOTATION_TYPES.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);
      }

      // Uniquely identify tooltips based on chartId so this chart instance only
      // controls its own tooltips
      if (chartId) {
        if (chart && chart.interactiveLayer && chart.interactiveLayer.tooltip) {
          chart.interactiveLayer.tooltip.classes([
            generateTooltipClassName(chartId),
          ]);
        }

        if (chart && chart.tooltip) {
          chart.tooltip.classes([generateTooltipClassName(chartId)]);
        }
      }

      // render chart
      chart.margin(margins);
      svg
        .datum(data)
        .transition()
        .duration(500)
        .attr('width', width)
        .attr('height', height)
        .call(chart);

      // On scroll, hide (not remove) tooltips so they can reappear on hover.
      // Throttle to only 4x/second.
      window.addEventListener(
        'scroll',
        throttle(() => hideTooltips(false), 250),
      );

      // The below code should be run AFTER rendering because chart is updated in call()
      if (isTimeSeries && activeAnnotationLayers.length > 0) {
        // Formula annotations
        const formulas = activeAnnotationLayers.filter(
          a => a.annotationType === ANNOTATION_TYPES.FORMULA,
        );

        let xMax;
        let xMin;
        let xScale;
        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()
            .domain([xMin, xMax])
            .range(chart.xAxis.range());
        } else {
          xMin = chart.xAxis.scale().domain()[0].valueOf();
          xMax = chart.xAxis.scale().domain()[1].valueOf();
          if (chart.xScale) {
            xScale = chart.xScale();
          } else if (chart.xAxis.scale) {
            xScale = chart.xAxis.scale();
          } else {
            xScale = d3.scale.linear();
          }
        }
        if (xScale && xScale.clamp) {
          xScale.clamp(true);
        }

        if (formulas.length > 0) {
          const xValues = [];
          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) => {
              d.values.forEach(x => xVals.add(x.x));

              return xVals;
            }, new Set());
            xValues.push(...distinct.values());
            xValues.sort();
          } else {
            // For every other time visualization it should be ok, to have a
            // data points in even intervals.
            let period = Math.min(
              ...data.map(d =>
                Math.min(
                  ...d.values.slice(1).map((v, i) => v.x - d.values[i].x),
                ),
              ),
            );
            const dataPoints = (xMax - xMin) / (period || 1);
            // make sure that there are enough data points and not too many
            period = dataPoints < 100 ? (xMax - xMin) / 100 : period;
            period = dataPoints > 500 ? (xMax - xMin) / 500 : period;
            xValues.push(xMin);
            for (let x = xMin; x < xMax; x += period) {
              xValues.push(x);
            }
            xValues.push(xMax);
          }
          const formulaData = formulas.map(fo => {
            const { value: expression } = fo;
            return {
              key: fo.name,
              values: xValues.map(x => ({
                x,
                y: evalExpression(expression, x),
              })),
              color: fo.color,
              strokeWidth: fo.width,
              classed: `${fo.opacity} ${fo.style}`,
            };
          });
          data.push(...formulaData);
        }
        const xAxis = chart.xAxis1 ? chart.xAxis1 : chart.xAxis;
        const yAxis = chart.yAxis1 ? chart.yAxis1 : chart.yAxis;
        const chartWidth = xAxis.scale().range()[1];
        const annotationHeight = yAxis.scale().range()[0];

        if (annotationData) {
          // Event annotations
          activeAnnotationLayers
            .filter(
              x =>
                x.annotationType === ANNOTATION_TYPES.EVENT &&
                annotationData &&
                annotationData[x.name],
            )
            .forEach((config, index) => {
              const e = applyNativeColumns(config);
              // Add event annotation layer
              const annotations = d3
                .select(element)
                .select('.nv-wrap')
                .append('g')
                .attr('class', `nv-event-annotation-layer-${index}`);
              const aColor =
                e.color || getColor(cleanColorInput(e.name), colorScheme);

              const tip = tipFactory({
                ...e,
                annotationTipClass: `arrow-down nv-event-annotation-layer-${config.sourceType}`,
              });
              const records = (annotationData[e.name].records || [])
                .map(r => {
                  const timeValue = new Date(moment.utc(r[e.timeColumn]));

                  return {
                    ...r,
                    [e.timeColumn]: timeValue,
                  };
                })
                .filter(
                  record =>
                    !Number.isNaN(record[e.timeColumn].getMilliseconds()),
                );

              if (records.length > 0) {
                annotations
                  .selectAll('line')
                  .data(records)
                  .enter()
                  .append('line')
                  .attr({
                    x1: d => xScale(new Date(d[e.timeColumn])),
                    y1: 0,
                    x2: d => xScale(new Date(d[e.timeColumn])),
                    y2: annotationHeight,
                  })
                  .attr('class', `${e.opacity} ${e.style}`)
                  .style('stroke', aColor)
                  .style('stroke-width', e.width)
                  .on('mouseover', tip.show)
                  .on('mouseout', tip.hide)
                  .call(tip);
              }

              // update annotation positions on brush event
              if (chart.focus) {
                chart.focus.dispatch.on('onBrush.event-annotation', () => {
                  annotations
                    .selectAll('line')
                    .data(records)
                    .attr({
                      x1: d => xScale(new Date(d[e.timeColumn])),
                      y1: 0,
                      x2: d => xScale(new Date(d[e.timeColumn])),
                      y2: annotationHeight,
                      opacity: d => {
                        const x = xScale(new Date(d[e.timeColumn]));

                        return x > 0 && x < chartWidth ? 1 : 0;
                      },
                    });
                });
              }
            });

          // Interval annotations
          activeAnnotationLayers
            .filter(
              x =>
                x.annotationType === ANNOTATION_TYPES.INTERVAL &&
                annotationData &&
                annotationData[x.name],
            )
            .forEach((config, index) => {
              const e = applyNativeColumns(config);
              // Add interval annotation layer
              const annotations = d3
                .select(element)
                .select('.nv-wrap')
                .append('g')
                .attr('class', `nv-interval-annotation-layer-${index}`);

              const aColor =
                e.color || getColor(cleanColorInput(e.name), colorScheme);
              const tip = tipFactory(e);

              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 {
                    ...r,
                    [e.timeColumn]: timeValue,
                    [e.intervalEndColumn]: intervalEndValue,
                  };
                })
                .filter(
                  record =>
                    !Number.isNaN(record[e.timeColumn].getMilliseconds()) &&
                    !Number.isNaN(
                      record[e.intervalEndColumn].getMilliseconds(),
                    ),
                );

              if (records.length > 0) {
                annotations
                  .selectAll('rect')
                  .data(records)
                  .enter()
                  .append('rect')
                  .attr({
                    x: d =>
                      Math.min(
                        xScale(new Date(d[e.timeColumn])),
                        xScale(new Date(d[e.intervalEndColumn])),
                      ),
                    y: 0,
                    width: d =>
                      Math.max(
                        Math.abs(
                          xScale(new Date(d[e.intervalEndColumn])) -
                            xScale(new Date(d[e.timeColumn])),
                        ),
                        1,
                      ),
                    height: annotationHeight,
                  })
                  .attr('class', `${e.opacity} ${e.style}`)
                  .style('stroke-width', e.width)
                  .style('stroke', aColor)
                  .style('fill', aColor)
                  .style('fill-opacity', 0.2)
                  .on('mouseover', tip.show)
                  .on('mouseout', tip.hide)
                  .call(tip);
              }

              // update annotation positions on brush event
              if (chart.focus) {
                chart.focus.dispatch.on('onBrush.interval-annotation', () => {
                  annotations
                    .selectAll('rect')
                    .data(records)
                    .attr({
                      x: d => xScale(new Date(d[e.timeColumn])),
                      width: d => {
                        const x1 = xScale(new Date(d[e.timeColumn]));
                        const x2 = xScale(new Date(d[e.intervalEndColumn]));

                        return x2 - x1;
                      },
                    });
                });
              }
            });
        }

        // rerender chart appended with annotation layer
        svg.datum(data).attr('height', height).attr('width', width).call(chart);

        // Display styles for Time Series Annotations
        chart.dispatch.on('renderEnd.timeseries-annotation', () => {
          d3.selectAll(
            '.slice_container .nv-timeseries-annotation-layer.showMarkerstrue .nv-point',
          )
            .style('stroke-opacity', 1)
            .style('fill-opacity', 1);
          d3.selectAll(
            '.slice_container .nv-timeseries-annotation-layer.hideLinetrue',
          ).style('stroke-width', 0);
        });
      }
    }

    wrapTooltip(chart);

    return chart;
  };

  // Remove tooltips before rendering chart, if the chart is being re-rendered sometimes
  // there are left over tooltips in the dom,
  // this will clear them before rendering the chart again.
  if (chartId) {
    removeTooltip(chartId);
  } else {
    hideTooltips(true);
  }

  nv.addGraph(drawGraph);
}

nvd3Vis.displayName = 'NVD3';
nvd3Vis.propTypes = propTypes;
export default nvd3Vis;