text: import {select} from 'd3-selection';
  
  import {StackMixin} from '../base/stack-mixin';
  import {transition} from '../core/core';
  import {constants} from '../core/constants';
  import {logger} from '../core/logger';
  import {pluck, utils} from '../core/utils';
  import {d3compat} from '../core/config';
  
  const MIN_BAR_WIDTH = 1;
  const DEFAULT_GAP_BETWEEN_BARS = 2;
  const LABEL_PADDING = 3;
  
  /**
   * Concrete bar chart/histogram implementation.
   *
   * Examples:
   * - {@link https://dc-js.github.io/dc.js/ Nasdaq 100 Index}
   * - {@link https://dc-js.github.io/dc.js/crime/index.html Canadian City Crime Stats}
   * @mixes StackMixin
   */
  export class BarChart extends StackMixin {
      /**
       * Create a Bar Chart
       * @example
       * // create a bar chart under #chart-container1 element using the default global chart group
       * var chart1 = new BarChart('#chart-container1');
       * // create a bar chart under #chart-container2 element using chart group A
       * var chart2 = new BarChart('#chart-container2', 'chartGroupA');
       * // create a sub-chart under a composite parent chart
       * var chart3 = new BarChart(compositeChart);
       * @param {String|node|d3.selection|CompositeChart} parent - Any valid
       * {@link https://github.com/d3/d3-selection/blob/master/README.md#select d3 single selector}
       * specifying a dom block element such as a div; or a dom element or d3 selection.  If the bar
       * chart is a sub-chart in a {@link CompositeChart Composite Chart} then pass in the parent
       * composite chart instance instead.
       * @param {String} [chartGroup] - The name of the chart group this chart instance should be placed in.
       * Interaction with a chart will only trigger events and redraws within the chart's group.
       */
      constructor (parent, chartGroup) {
          super();
  
          this._gap = DEFAULT_GAP_BETWEEN_BARS;
          this._centerBar = false;
          this._alwaysUseRounding = false;
  
          this._barWidth = undefined;
  
          this.label(d => utils.printSingleValue(d.y0 + d.y), false);
  
          this.anchor(parent, chartGroup);
      }
  
      /**
       * Get or set the outer padding on an ordinal bar chart. This setting has no effect on non-ordinal charts.
       * Will pad the width by `padding * barWidth` on each side of the chart.
       * @param {Number} [padding=0.5]
       * @returns {Number|BarChart}
       */
      outerPadding (padding) {
          if (!arguments.length) {
              return this._outerRangeBandPadding();
          }
          return this._outerRangeBandPadding(padding);
      }
  
      rescale () {
          super.rescale();
          this._barWidth = undefined;
          return this;
      }
  
      render () {
          if (this.round() && this._centerBar && !this._alwaysUseRounding) {
              logger.warn('By default, brush rounding is disabled if bars are centered. ' +
                  'See dc.js bar chart API documentation for details.');
          }
  
          return super.render();
      }
  
      plotData () {
          let layers = this.chartBodyG().selectAll('g.stack')
              .data(this.data());
  
          this._calculateBarWidth();
  
          layers = layers
              .enter()
              .append('g')
              .attr('class', (d, i) => `stack _${i}`)
              .merge(layers);
  
          const last = layers.size() - 1;
          {
              const chart = this;
              layers.each(function (d, i) {
                  const layer = select(this);
  
                  chart._renderBars(layer, i, d);
  
                  if (chart.renderLabel() && last === i) {
                      chart._renderLabels(layer, i, d);
                  }
              });
          }
      }
  
      _barHeight (d) {
          return utils.safeNumber(Math.abs(this.y()(d.y + d.y0) - this.y()(d.y0)));
      }
  
      _labelXPos (d) {
          let x = this.x()(d.x);
          if (!this._centerBar) {
              x += this._barWidth / 2;
          }
          if (this.isOrdinal() && this._gap !== undefined) {
              x += this._gap / 2;
          }
          return utils.safeNumber(x);
      }
  
      _labelYPos (d) {
          let y = this.y()(d.y + d.y0);
  
          if (d.y < 0) {
              y -= this._barHeight(d);
          }
  
          return utils.safeNumber(y - LABEL_PADDING);
      }
  
      _renderLabels (layer, layerIndex, data) {
          const labels = layer.selectAll('text.barLabel')
              .data(data.values, pluck('x'));
  
          const labelsEnterUpdate = labels
              .enter()
              .append('text')
              .attr('class', 'barLabel')
              .attr('text-anchor', 'middle')
              .attr('x', d => this._labelXPos(d))
              .attr('y', d => this._labelYPos(d))
              .merge(labels);
  
          if (this.isOrdinal()) {
              labelsEnterUpdate.on('click', d3compat.eventHandler(d => this.onClick(d)));
              labelsEnterUpdate.attr('cursor', 'pointer');
          }
  
          transition(labelsEnterUpdate, this.transitionDuration(), this.transitionDelay())
              .attr('x', d => this._labelXPos(d))
              .attr('y', d => this._labelYPos(d))
              .text(d => this.label()(d));
  
          transition(labels.exit(), this.transitionDuration(), this.transitionDelay())
              .attr('height', 0)
              .remove();
      }
  
      _barXPos (d) {
          let x = this.x()(d.x);
          if (this._centerBar) {
              x -= this._barWidth / 2;
          }
          if (this.isOrdinal() && this._gap !== undefined) {
              x += this._gap / 2;
          }
          return utils.safeNumber(x);
      }
  
      _renderBars (layer, layerIndex, data) {
          const bars = layer.selectAll('rect.bar')
              .data(data.values, pluck('x'));
  
          const enter = bars.enter()
              .append('rect')
              .attr('class', 'bar')
              .classed('dc-tabbable', this._keyboardAccessible)
              .attr('fill', pluck('data', this.getColor))
              .attr('x', d => this._barXPos(d))
              .attr('y', this.yAxisHeight())
              .attr('height', 0);
  
          const barsEnterUpdate = enter.merge(bars);
  
          if (this.renderTitle()) {
              enter.append('title').text(pluck('data', this.title(data.name)));
          }
  
          if (this.isOrdinal()) {
              barsEnterUpdate.on('click', d3compat.eventHandler(d => this.onClick(d)));
          }
  
          if (this._keyboardAccessible) {
              this._makeKeyboardAccessible(this.onClick);
          }
  
          transition(barsEnterUpdate, this.transitionDuration(), this.transitionDelay())
              .attr('x', d => this._barXPos(d))
              .attr('y', d => {
                  let y = this.y()(d.y + d.y0);
  
                  if (d.y < 0) {
                      y -= this._barHeight(d);
                  }
  
                  return utils.safeNumber(y);
              })
              .attr('width', this._barWidth)
              .attr('height', d => this._barHeight(d))
              .attr('fill', pluck('data', this.getColor))
              .select('title').text(pluck('data', this.title(data.name)));
  
          transition(bars.exit(), this.transitionDuration(), this.transitionDelay())
              .attr('x', d => this.x()(d.x))
              .attr('width', this._barWidth * 0.9)
              .remove();
      }
  
      _calculateBarWidth () {
          if (this._barWidth === undefined) {
              const numberOfBars = this.xUnitCount();
  
              // please can't we always use rangeBands for bar charts?
              if (this.isOrdinal() && this._gap === undefined) {
                  this._barWidth = Math.floor(this.x().bandwidth());
              } else if (this._gap) {
                  this._barWidth = Math.floor((this.xAxisLength() - (numberOfBars - 1) * this._gap) / numberOfBars);
              } else {
                  this._barWidth = Math.floor(this.xAxisLength() / (1 + this.barPadding()) / numberOfBars);
              }
  
              if (this._barWidth === Infinity || isNaN(this._barWidth) || this._barWidth < MIN_BAR_WIDTH) {
                  this._barWidth = MIN_BAR_WIDTH;
              }
          }
      }
  
      fadeDeselectedArea (brushSelection) {
          const bars = this.chartBodyG().selectAll('rect.bar');
  
          if (this.isOrdinal()) {
              if (this.hasFilter()) {
                  bars.classed(constants.SELECTED_CLASS, d => this.hasFilter(d.x));
                  bars.classed(constants.DESELECTED_CLASS, d => !this.hasFilter(d.x));
              } else {
                  bars.classed(constants.SELECTED_CLASS, false);
                  bars.classed(constants.DESELECTED_CLASS, false);
              }
          } else if (this.brushOn() || this.parentBrushOn()) {
              if (!this.brushIsEmpty(brushSelection)) {
                  const start = brushSelection[0];
                  const end = brushSelection[1];
  
                  bars.classed(constants.DESELECTED_CLASS, d => d.x < start || d.x >= end);
              } else {
                  bars.classed(constants.DESELECTED_CLASS, false);
              }
          }
      }
  
      /**
       * Whether the bar chart will render each bar centered around the data position on the x-axis.
       * @param {Boolean} [centerBar=false]
       * @returns {Boolean|BarChart}
       */
      centerBar (centerBar) {
          if (!arguments.length) {
              return this._centerBar;
          }
          this._centerBar = centerBar;
          return this;
      }
  
      onClick (d) {
          super.onClick(d.data);
      }
  
      /**
       * Get or set the spacing between bars as a fraction of bar size. Valid values are between 0-1.
       * Setting this value will also remove any previously set {@link BarChart#gap gap}. See the
       * {@link https://github.com/d3/d3-scale/blob/master/README.md#scaleBand d3 docs}
       * for a visual description of how the padding is applied.
       * @param {Number} [barPadding=0]
       * @returns {Number|BarChart}
       */
      barPadding (barPadding) {
          if (!arguments.length) {
              return this._rangeBandPadding();
          }
          this._rangeBandPadding(barPadding);
          this._gap = undefined;
          return this;
      }
  
      _useOuterPadding () {
          return this._gap === undefined;
      }
  
      /**
       * Manually set fixed gap (in px) between bars instead of relying on the default auto-generated
       * gap.  By default the bar chart implementation will calculate and set the gap automatically
       * based on the number of data points and the length of the x axis.
       * @param {Number} [gap=2]
       * @returns {Number|BarChart}
       */
      gap (gap) {
          if (!arguments.length) {
              return this._gap;
          }
          this._gap = gap;
          return this;
      }
  
      extendBrush (brushSelection) {
          if (brushSelection && this.round() && (!this._centerBar || this._alwaysUseRounding)) {
              brushSelection[0] = this.round()(brushSelection[0]);
              brushSelection[1] = this.round()(brushSelection[1]);
          }
          return brushSelection;
      }
  
      /**
       * Set or get whether rounding is enabled when bars are centered. If false, using
       * rounding with centered bars will result in a warning and rounding will be ignored.  This flag
       * has no effect if bars are not {@link BarChart#centerBar centered}.
       * When using standard d3.js rounding methods, the brush often doesn't align correctly with
       * centered bars since the bars are offset.  The rounding function must add an offset to
       * compensate, such as in the following example.
       * @example
       * chart.round(function(n) { return Math.floor(n) + 0.5; });
       * @param {Boolean} [alwaysUseRounding=false]
       * @returns {Boolean|BarChart}
       */
      alwaysUseRounding (alwaysUseRounding) {
          if (!arguments.length) {
              return this._alwaysUseRounding;
          }
          this._alwaysUseRounding = alwaysUseRounding;
          return this;
      }
  
      legendHighlight (d) {
          const colorFilter = (color, inv) => function () {
              const item = select(this);
              const match = item.attr('fill') === color;
              return inv ? !match : match;
          };
  
          if (!this.isLegendableHidden(d)) {
              this.g().selectAll('rect.bar')
                  .classed('highlight', colorFilter(d.color))
                  .classed('fadeout', colorFilter(d.color, true));
          }
      }
  
      legendReset () {
          this.g().selectAll('rect.bar')
              .classed('highlight', false)
              .classed('fadeout', false);
      }
  
      xAxisMax () {
          let max = super.xAxisMax();
          if ('resolution' in this.xUnits()) {
              const res = this.xUnits().resolution;
              max += res;
          }
          return max;
      }
  }
  
  export const barChart = (parent, chartGroup) => new BarChart(parent, chartGroup);
  