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);