billboard.js
Version:
Re-usable easy interface JavaScript chart library, based on D3 v4+
263 lines (260 loc) • 9.68 kB
JavaScript
/*!
* Copyright (c) 2017 ~ present NAVER Corp.
* billboard.js project is licensed under the MIT license
*
* billboard.js, JavaScript chart library
* https://naver.github.io/billboard.js/
*
* @version 4.0.1
*/
import { drag } from 'd3-drag';
import { select } from 'd3-selection';
import { $AXIS, $SHAPE, $COMMON, $ARC } from '../../config/classes.js';
import { KEY } from '../../module/Cache.js';
import { getBoundingRect, hasViewBox, getTransformCTM, emulateEvent, getPointer } from '../../module/util/dom.js';
import { isObject, isNumber } from '../../module/util/type-checks.js';
/**
* Copyright (c) 2017 ~ present NAVER Corp.
* billboard.js project is licensed under the MIT license
*/
var interaction = {
/**
* Expand data shape/point
* @param {number} index Index number
* @param {string} id Data id
* @param {boolean} reset Reset expand state
* @private
*/
setExpand(index, id, reset) {
const $$ = this;
const { config, $el: { circle } } = $$;
circle && config.point_focus_expand_enabled &&
$$.expandCircles(index, id, reset);
// bar, candlestick
$$.expandBarTypeShapes(true, index, id, reset);
},
/**
* Expand/Unexpand bar type shapes
* @param {boolean} expand Expand or unexpand
* @param {number} i Shape index
* @param {string} id Data id
* @param {boolean} reset Reset expand style
* @private
*/
expandBarTypeShapes(expand = true, i, id, reset) {
const $$ = this;
["bar", "candlestick"]
.filter(v => $$.$el[v])
.forEach(v => {
reset && $$.$el[v].classed($COMMON.EXPANDED, false);
$$.getShapeByIndex(v, i, id).classed($COMMON.EXPANDED, expand);
});
},
/**
* Handle data.onover/out callback options
* @param {boolean} isOver Over or not
* @param {number|object} d data object
* @private
*/
setOverOut(isOver, d) {
const $$ = this;
const { config, state: { hasFunnel, hasRadar, hasTreemap }, $el: { main } } = $$;
const isArcishData = isObject(d);
// Call event handler
if (isArcishData || d !== -1) {
const callback = config[isOver ? "data_onover" : "data_onout"].bind($$.api);
config.color_onover && $$.setOverColor(isOver, d, isArcishData);
if (isArcishData) {
const suffix = $$.getTargetSelectorSuffix(d.id);
const selector = hasFunnel || hasTreemap ?
`${$COMMON.target + suffix} .${$SHAPE.shape}` :
$ARC.arc + suffix;
callback(d, main.select(`.${selector}`).node());
}
else if (!config.tooltip_grouped) {
const last = $$.cache.get(KEY.setOverOut) || [];
// select based on the index
const shapesAtIndex = main.selectAll(`.${$SHAPE.shape}-${d}`)
.filter(function (d) {
return $$.isWithinShape(this, d);
});
// filter if has new selection
const shape = shapesAtIndex.filter(function () {
return last.every(v => v !== this);
});
// call onout callback
if (!isOver || shapesAtIndex.empty() || (last.length === shape.size() && shape.nodes().every((v, i) => v !== last[i]))) {
while (last.length) {
const target = last.pop();
config.data_onout.bind($$.api)(select(target).datum(), target);
}
}
// call onover callback
shape.each(function () {
if (isOver) {
callback(select(this).datum(), this);
last.push(this);
}
});
$$.cache.add(KEY.setOverOut, last);
}
else {
if (isOver) {
hasRadar && $$.isPointFocusOnly() ?
$$.showCircleFocus($$.getAllValuesOnIndex(d, true)) :
$$.setExpand(d, null, true);
}
!$$.isMultipleX() && main.selectAll(`.${$SHAPE.shape}-${d}`)
.each(function (d) {
callback(d, this);
});
}
}
},
/**
* Call data.onover/out callback for touch event
* @param {number|object} d target index or data object for Arc type
* @private
*/
callOverOutForTouch(d) {
const $$ = this;
const last = $$.cache.get(KEY.callOverOutForTouch);
if (isObject(d) && last ? d.id !== last.id : (d !== last)) {
(last || isNumber(last)) && $$.setOverOut(false, last);
(d || isNumber(d)) && $$.setOverOut(true, d);
$$.cache.add(KEY.callOverOutForTouch, d);
}
},
/**
* Return draggable selection function
* @returns {function}
* @private
*/
getDraggableSelection() {
const $$ = this;
const { config, state } = $$;
return config.interaction_enabled && config.data_selection_draggable && $$.drag ?
drag()
.on("drag", function (event) {
state.event = event;
$$.drag(getPointer(event, this));
})
.on("start", function (event) {
state.event = event;
$$.dragstart(getPointer(event, this));
})
.on("end", event => {
state.event = event;
$$.dragend();
}) :
() => { };
},
/**
* Dispatch a mouse event.
* @private
* @param {string} type event type
* @param {number} index Index of eventRect
* @param {Array} mouse x and y coordinate value
*/
dispatchEvent(type, index, mouse) {
const $$ = this;
const { config, state: { eventReceiver, hasAxis, hasFunnel, hasRadar, hasTreemap }, $el: { eventRect, funnel, radar, svg, treemap } } = $$;
let element = (((hasFunnel || hasTreemap) && eventReceiver.rect) ||
(hasRadar && radar.axes.select(`.${$AXIS.axis}-${index} text`)) || (eventRect || $$.getArcElementByIdOrIndex?.(index)))?.node?.();
if (element) {
const isMultipleX = $$.isMultipleX();
const isRotated = config.axis_rotated;
let { width, left, top } = getBoundingRect(element);
if (hasAxis && !hasRadar && !isMultipleX) {
const coords = eventReceiver.coords[index];
if (coords) {
width = coords.w;
left += coords.x;
top += coords.y;
}
else {
width = 0;
left = 0;
top = 0;
}
}
let x = left + (mouse ? mouse[0] : 0) + (isMultipleX || isRotated ? 0 : (width / 2));
// value 4, is to adjust coordinate value set from: scale.ts - updateScales(): $$.getResettedPadding(1)
let y = top + (mouse ? mouse[1] : 0) + (isRotated ? 4 : 0);
// eventRect doesn't exist for radar/funnel/treemap charts
if (hasViewBox(svg) && $$.$el.eventRect) {
const ctm = getTransformCTM($$.$el.eventRect.node(), x, y, false);
x = ctm.x;
y = ctm.y;
}
const params = {
screenX: x,
screenY: y,
clientX: x,
clientY: y,
bubbles: hasRadar // radar type needs to bubble up event
};
// for funnel and treemap event bound to <g> node
if (hasFunnel || hasTreemap) {
element = (funnel ?? treemap).node();
}
emulateEvent[/^(mouse|click)/.test(type) ? "mouse" : "touch"](element, type, params);
}
},
setDragStatus(isDragging) {
this.state.dragging = isDragging;
},
/**
* Unbind zoom events
* @private
*/
unbindZoomEvent() {
const $$ = this;
const { $el: { canvas, eventRect, svg, zoomResetBtn } } = $$;
eventRect?.on(".zoom wheel.zoom .drag", null);
canvas?.on(".zoom wheel.zoom .drag", null);
// remove the Safari wheel workaround listener bound in bindZoomOnEventRect()
svg?.on("wheel", null);
zoomResetBtn?.on("click", null)
.style("display", "none");
},
/**
* Unbind all attached events
* @private
*/
unbindAllEvents() {
const $$ = this;
const { $el: { arcs, eventRect, legend, region, svg, treemap }, brush } = $$;
const list = [
"wheel",
"click",
"mouseover",
"mousemove",
"mouseout",
"touchstart",
"touchmove",
"touchend",
"touchstart.eventRect",
"touchmove.eventRect",
"touchend.eventRect",
".brush",
".drag",
".zoom",
"wheel.zoom",
"dblclick.zoom"
].join(" ");
// detach all possible event types
[
svg,
eventRect,
region?.list,
brush?.getSelection(),
arcs?.selectAll("path"),
legend?.selectAll("g"),
treemap
]
.forEach(v => v?.on(list, null));
$$.unbindZoomEvent?.();
}
};
export { interaction as default };