text: /* 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 nv from 'nvd3-fork';
  import { parse as mathjsParse } from 'mathjs';
  import moment from 'moment';
  import PropTypes from 'prop-types';
  import {
    t,
    isDefined,
    getTimeFormatter,
    smartDateVerboseFormatter,
    getNumberFormatter,
    NumberFormats,
    CategoricalColorNamespace,
  } 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,
    generateMultiLineTooltipContent,
    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',
    'dual_line',
    'line_multi',
    'area',
    'compare',
    'bar',
    'time_pivot',
  ];
  
  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,
    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,
      yAxis2Format,
      yAxisBounds,
      yAxisLabel,
      yAxisShowMinMax = false,
      yField,
      yIsLogScale,
    } = props;
  
    const isExplore = document.querySelector('#explorer-container') !== null;
    const container = element;
    container.innerHTML = '';
    const activeAnnotationLayers = annotationLayers.filter(layer => layer.show);
    const chartId =
      container.parentElement && container.parentElement.id !== ''
        ? container.parentElement.id
        : null;
  
    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 'dual_line':
        case 'line_multi':
          chart = nv.models.multiChart();
          chart.interpolate(lineInterpolation);
          chart.xScale(d3.time.scale.utc());
          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 => {
              d.values.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, right: 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, 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])));
      }
  
      if (isVizTypes(['line', 'area']) && useRichTooltip) {
        chart.useInteractiveGuideline(true);
        if (vizType === 'line') {
          chart.interactiveLayer.tooltip.contentGenerator(d =>
            generateRichLineTooltipContent(d, smartDateVerboseFormatter, 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),
        );
      }
  
      if (isVizTypes(['dual_line', 'line_multi'])) {
        const yAxisFormatter1 = getNumberFormatter(yAxisFormat);
        const yAxisFormatter2 = getNumberFormatter(yAxis2Format);
        chart.yAxis1.tickFormat(yAxisFormatter1);
        chart.yAxis2.tickFormat(yAxisFormatter2);
        const yAxisFormatters = data.map(datum =>
          datum.yAxis === 1 ? yAxisFormatter1 : yAxisFormatter2,
        );
        chart.useInteractiveGuideline(true);
        chart.interactiveLayer.tooltip.contentGenerator(d =>
          generateMultiLineTooltipContent(d, xAxisFormatter, yAxisFormatters),
        );
        if (vizType === 'dual_line') {
          chart.showLegend(width > BREAKPOINTS.small);
        } else {
          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);
  
      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);
      }
  
      // align yAxis1 and yAxis2 ticks
      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);
  
        // match number of ticks in both axes
        const difference = ticks1.length - ticks2.length;
        if (ticks1.length > 0 && ticks2.length > 0 && difference !== 0) {
          const smallest = difference < 0 ? ticks1 : ticks2;
          const delta = smallest[1] - smallest[0];
          for (let i = 0; i < Math.abs(difference); i++) {
            if (i % 2 === 0) {
              smallest.unshift(smallest[0] - delta);
            } else {
              smallest.push(smallest[smallest.length - 1] + delta);
            }
          }
          chart.yDomain1([ticks1[0], ticks1[ticks1.length - 1]]);
          chart.yDomain2([ticks2[0], ticks2[ticks2.length - 1]]);
          chart.yAxis1.tickValues(ticks1);
          chart.yAxis2.tickValues(ticks2);
        }
      }
  
      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 (isVizTypes(['dual_line', 'line_multi'])) {
          const maxYAxis2LabelWidth = getMaxLabelSize(svg, 'nv-y2');
          margins.right = maxYAxis2LabelWidth + marginPad;
        }
        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)
            .map(a => ({ ...a, formula: mathjsParse(a.value) }));
  
          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 => ({
              key: fo.name,
              values: xValues.map(x => ({ y: fo.formula.evaluate({ x }), 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, maxWidth);
  
      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;
  