repo_description: Multi-Dimensional charting built to work natively with crossfilter rendered with d3.js
repo_topics: charting crossfilter visualization
code: import {BaseMixin} from '../base/base-mixin';
  import {BubbleMixin} from '../base/bubble-mixin';
  import {transition} from '../core/core';
  import {constants} from '../core/constants';
  import {utils} from '../core/utils';
  import {d3compat} from '../core/config';
  
  const BUBBLE_OVERLAY_CLASS = 'bubble-overlay';
  const BUBBLE_NODE_CLASS = 'node';
  const BUBBLE_CLASS = 'bubble';
  
  /**
   * The bubble overlay chart is quite different from the typical bubble chart. With the bubble overlay
   * chart you can arbitrarily place bubbles on an existing svg or bitmap image, thus changing the
   * typical x and y positioning while retaining the capability to visualize data using bubble radius
   * and coloring.
   *
   * Examples:
   * - {@link https://dc-js.github.io/dc.js/crime/index.html Canadian City Crime Stats}
   * @mixes BubbleMixin
   * @mixes BaseMixin
   */
  export class BubbleOverlay extends BubbleMixin(BaseMixin) {
      /**
       * Create a Bubble Overlay.
       *
       * @example
       * // create a bubble overlay chart on top of the '#chart-container1 svg' element using the default global chart group
       * var bubbleChart1 = BubbleOverlayChart('#chart-container1').svg(d3.select('#chart-container1 svg'));
       * // create a bubble overlay chart on top of the '#chart-container2 svg' element using chart group A
       * var bubbleChart2 = new CompositeChart('#chart-container2', 'chartGroupA').svg(d3.select('#chart-container2 svg'));
       * @param {String|node|d3.selection} 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.
       * @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();
  
          /**
           * **mandatory**
           *
           * Set the underlying svg image element. Unlike other dc charts this chart will not generate a svg
           * element; therefore the bubble overlay chart will not work if this function is not invoked. If the
           * underlying image is a bitmap, then an empty svg will need to be created on top of the image.
           * @example
           * // set up underlying svg element
           * chart.svg(d3.select('#chart svg'));
           * @param {SVGElement|d3.selection} [imageElement]
           * @returns {BubbleOverlay}
           */
          this._g = undefined;
          this._points = [];
          this._keyboardAccessible = false;
  
          this.transitionDuration(750);
  
          this.transitionDelay(0);
  
          this.radiusValueAccessor(d => d.value);
  
          this.anchor(parent, chartGroup);
      }
  
      /**
       * **mandatory**
       *
       * Set up a data point on the overlay. The name of a data point should match a specific 'key' among
       * data groups generated using keyAccessor.  If a match is found (point name <-> data group key)
       * then a bubble will be generated at the position specified by the function. x and y
       * value specified here are relative to the underlying svg.
       * @param {String} name
       * @param {Number} x
       * @param {Number} y
       * @returns {BubbleOverlay}
       */
      point (name, x, y) {
          this._points.push({name: name, x: x, y: y});
          return this;
      }
  
      _doRender () {
          this._g = this._initOverlayG();
  
          this.r().range([this.MIN_RADIUS, this.width() * this.maxBubbleRelativeSize()]);
  
          this._initializeBubbles();
  
          this.fadeDeselectedArea(this.filter());
  
          return this;
      }
  
      _initOverlayG () {
          this._g = this.select(`g.${BUBBLE_OVERLAY_CLASS}`);
          if (this._g.empty()) {
              this._g = this.svg().append('g').attr('class', BUBBLE_OVERLAY_CLASS);
          }
          return this._g;
      }
  
      _initializeBubbles () {
          const data = this._mapData();
          this.calculateRadiusDomain();
  
          this._points.forEach(point => {
              const nodeG = this._getNodeG(point, data);
  
              let circle = nodeG.select(`circle.${BUBBLE_CLASS}`);
  
              if (circle.empty()) {
                  circle = nodeG.append('circle')
                      .attr('class', BUBBLE_CLASS)
                      .classed('dc-tabbable', this._keyboardAccessible)
                      .attr('r', 0)
                      .attr('fill', this.getColor)
                      .on('click', d3compat.eventHandler(d => this.onClick(d)));
              }
  
              if (this._keyboardAccessible) {
                  this._makeKeyboardAccessible(this.onClick);
              }
  
              transition(circle, this.transitionDuration(), this.transitionDelay())
                  .attr('r', d => this.bubbleR(d));
  
              this._doRenderLabel(nodeG);
  
              this._doRenderTitles(nodeG);
          });
      }
  
      _mapData () {
          const data = {};
          this.data().forEach(datum => {
              data[this.keyAccessor()(datum)] = datum;
          });
          return data;
      }
  
      _getNodeG (point, data) {
          const bubbleNodeClass = `${BUBBLE_NODE_CLASS} ${utils.nameToId(point.name)}`;
  
          let nodeG = this._g.select(`g.${utils.nameToId(point.name)}`);
  
          if (nodeG.empty()) {
              nodeG = this._g.append('g')
                  .attr('class', bubbleNodeClass)
                  .attr('transform', `translate(${point.x},${point.y})`);
          }
  
          nodeG.datum(data[point.name]);
  
          return nodeG;
      }
  
      _doRedraw () {
          this._updateBubbles();
  
          this.fadeDeselectedArea(this.filter());
  
          return this;
      }
  
      _updateBubbles () {
          const data = this._mapData();
          this.calculateRadiusDomain();
  
          this._points.forEach(point => {
              const nodeG = this._getNodeG(point, data);
  
              const circle = nodeG.select(`circle.${BUBBLE_CLASS}`);
  
              transition(circle, this.transitionDuration(), this.transitionDelay())
                  .attr('r', d => this.bubbleR(d))
                  .attr('fill', this.getColor);
  
              this.doUpdateLabels(nodeG);
  
              this.doUpdateTitles(nodeG);
          });
      }
  
      debug (flag) {
          if (flag) {
              let debugG = this.select(`g.${constants.DEBUG_GROUP_CLASS}`);
  
              if (debugG.empty()) {
                  debugG = this.svg()
                      .append('g')
                      .attr('class', constants.DEBUG_GROUP_CLASS);
              }
  
              const debugText = debugG.append('text')
                  .attr('x', 10)
                  .attr('y', 20);
  
              debugG
                  .append('rect')
                  .attr('width', this.width())
                  .attr('height', this.height())
                  .on('mousemove', d3compat.eventHandler((d, evt) => {
                      const position = d3compat.pointer(evt, debugG.node());
                      const msg = `${position[0]}, ${position[1]}`;
                      debugText.text(msg);
                  }));
          } else {
              this.selectAll('.debug').remove();
          }
  
          return this;
      }
  
  }
  
  export const bubbleOverlay = (parent, chartGroup) => new BubbleOverlay(parent, chartGroup);
  