Source: charts/bubble-overlay.js

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 http://dc-js.github.com/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);