billboard.js
Version:
Re-usable easy interface JavaScript chart library, based on D3 v4+
325 lines (324 loc) • 13.2 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.1.1
*/
import { isObject, isUndefined, isValue } from "../../module/util/type-checks.js";
import { getRandom } from "../../module/util/object.js";
import { getBBox, getBoundingRect, getPointer } from "../../module/util/dom.js";
import { $CIRCLE, $COMMON, $SELECT } from "../../config/classes.js";
import { select } from "d3-selection";
//#region src/ChartInternal/shape/point.ts
/**
* Copyright (c) 2017 ~ present NAVER Corp.
* billboard.js project is licensed under the MIT license
*/
const getTransitionName = () => getRandom();
const pointBBoxCache = /* @__PURE__ */ new WeakMap();
/**
* Get cached dimensions for non-circle point elements.
* @param {SVGElement} node Point element
* @returns {object} Width/height pair
* @private
*/
function getPointBBox(node) {
const attrHeight = node.getAttribute("height");
const attrWidth = node.getAttribute("width");
const href = node.getAttribute("href") || node.getAttribute("xlink:href");
const cached = pointBBoxCache.get(node);
if (cached && cached.attrHeight === attrHeight && cached.attrWidth === attrWidth && cached.href === href) return cached;
const { width, height } = getBBox(node);
const bbox = {
attrHeight,
attrWidth,
height,
href,
width
};
pointBBoxCache.set(node, bbox);
return bbox;
}
var point_default = {
initialOpacityForCircle(d) {
const { config, state: { withoutFadeIn } } = this;
let opacity = config.point_opacity;
if (isUndefined(opacity)) opacity = this.getBaseValue(d) !== null && withoutFadeIn[d.id] ? this.opacityForCircle(d) : "0";
return opacity;
},
opacityForCircle(d) {
const { config } = this;
let opacity = config.point_opacity;
if (isUndefined(opacity)) {
opacity = config.point_show && !this.isPointFocusOnly?.() ? null : "0";
opacity = isValue(this.getBaseValue(d)) ? this.isBubbleType(d) || this.isScatterType(d) ? "0.5" : opacity : "0";
}
return opacity;
},
initCircle() {
const $$ = this;
const { $el: { main } } = $$;
!$$.point && ($$.point = $$.generatePoint());
if (($$.hasType("bubble") || $$.hasType("scatter")) && main.select(`.${$COMMON.chart} > .${$CIRCLE.chartCircles}`).empty()) main.select(`.${$COMMON.chart}`).append("g").attr("class", $CIRCLE.chartCircles);
},
updateTargetForCircle(targetsValue, enterNodeValue) {
const $$ = this;
const { config, data, $el } = $$;
const selectionEnabled = config.interaction_enabled && config.data_selection_enabled;
const isSelectable = selectionEnabled && config.data_selection_isselectable;
const classCircles = $$.getClass("circles", true);
if (!config.point_show) return;
$$.initCircle();
let targets = targetsValue;
let enterNode = enterNodeValue;
if (!targets) {
targets = $$.filterNullish(data.targets).filter((d) => this.isScatterType(d) || this.isBubbleType(d));
const mainCircle = $el.main.select(`.${$CIRCLE.chartCircles}`).style("pointer-events", "none").selectAll(`.${$CIRCLE.circles}`).data(targets);
mainCircle.exit().remove();
enterNode = mainCircle.enter();
}
selectionEnabled && enterNode.append("g").attr("class", (d) => $$.generateClass($SELECT.selectedCircles, d.id));
enterNode.append("g").attr("class", classCircles).call((selection) => {
$$.setCssRule(true, `.${$CIRCLE.circles}`, ["cursor:pointer"], isSelectable)(selection);
$$.setCssRule(true, ` .${$CIRCLE.circle}`, ["fill", "stroke"], $$.color)(selection);
}).style("opacity", function() {
return select(this.parentNode).classed($CIRCLE.chartCircles) ? "0" : null;
});
selectionEnabled && targets.forEach((t) => {
$el.main.selectAll(`.${$SELECT.selectedCircles}${$$.getTargetSelectorSuffix(t.id)}`).selectAll(`${$SELECT.selectedCircle}`).each((d) => {
d.value = t.values[d.index].value;
});
});
},
updateCircle(isSub = false) {
const $$ = this;
const { config, state, $el } = $$;
const focusOnly = $$.isPointFocusOnly();
const $root = isSub ? $el.subchart : $el;
if (config.point_show && !state.toggling) {
config.point_radialGradient && $$.updateLinearGradient();
const circles = $root.main.selectAll(`.${$CIRCLE.circles}`).selectAll(`.${$CIRCLE.circle}`).data((d) => {
const data = $$.isLineType(d) && $$.shouldDrawPointsForLine(d) || $$.isBubbleType(d) || $$.isRadarType(d) || $$.isScatterType(d) ? focusOnly ? [d.values[0]] : d.values : [];
return $$.filterNullish(data);
});
circles.exit().remove();
const pointR = $$.pointR.bind($$);
const updateCircleColor = $$.generateUpdateCircleColor();
const initialOpacityForCircle = $$.initialOpacityForCircle.bind($$);
circles.enter().filter(Boolean).append($$.point("create", this, pointR, updateCircleColor));
$root.circle = $root.main.selectAll(`.${$CIRCLE.circles} .${$CIRCLE.circle}`).style("stroke", $$.getStylePropValue($$.color)).style("opacity", initialOpacityForCircle);
}
},
/**
* Generate circle color accessor, hoisting the bound color function
* to be created once per call (not per datum)
* @returns {function} Color accessor
* @private
*/
generateUpdateCircleColor() {
const $$ = this;
const fn = $$.getStylePropValue($$.color);
return (d) => $$.config.point_radialGradient ? $$.getGradienColortUrl(d.id) : fn ? fn(d) : null;
},
/**
* Update circle color
* @param {object} d Data object
* @returns {string} Color string
* @private
*/
updateCircleColor(d) {
return this.generateUpdateCircleColor()(d);
},
redrawCircle(cx, cy, withTransition, flow, isSub = false) {
const $$ = this;
const { state: { rendered }, $el, $T } = $$;
const $root = isSub ? $el.subchart : $el;
const selectedCircles = $root.main.selectAll(`.${$SELECT.selectedCircle}`);
if (!$$.config.point_show) return [];
const posAttr = $$.isCirclePoint() ? "c" : "";
const t = getRandom();
const opacityStyleFn = $$.opacityForCircle.bind($$);
const updateCircleColor = $$.generateUpdateCircleColor();
if ($$.isCirclePoint()) {
const sel = $root.circle;
if ($$.hasType("bubble")) sel.attr("r", $$.pointR.bind($$));
if (withTransition) {
flow && sel.attr("cx", cx);
$T(sel.filter(function() {
return !!this.getAttribute("cx");
}), true, `${t}-pos`).attr("cx", cx).attr("cy", cy).style("fill", updateCircleColor);
sel.filter(function() {
return !this.getAttribute("cx");
}).attr("cx", cx).attr("cy", cy).style("fill", updateCircleColor);
} else sel.attr("cx", cx).attr("cy", cy).style("fill", updateCircleColor);
return [[$T(sel, withTransition || !rendered, t).style("opacity", opacityStyleFn)], $T(selectedCircles, withTransition).attr("cx", cx).attr("cy", cy)];
}
const fn = $$.point("update", $$, cx, cy, updateCircleColor, withTransition, flow, selectedCircles);
const mainCircles = [];
$root.circle.each(function(d) {
let result = fn.bind(this)(d);
result = $T(result, withTransition || !rendered, t).style("opacity", opacityStyleFn);
mainCircles.push(result);
});
return [mainCircles, $T(selectedCircles, withTransition).attr(`${posAttr}x`, cx).attr(`${posAttr}y`, cy)];
},
/**
* Show focused data point circle
* @param {object} d Selected data
* @private
*/
showCircleFocus(d) {
const $$ = this;
const { state: { hasRadar, resizing, toggling, transiting }, $el } = $$;
let { circle } = $el;
if (transiting === false && circle && $$.isPointFocusOnly()) {
const cx = (hasRadar ? $$.radarCircleX : $$.circleX).bind($$);
const cy = (hasRadar ? $$.radarCircleY : $$.circleY).bind($$);
const withTransition = toggling || isUndefined(d);
const fn = $$.point("update", $$, cx, cy, $$.getStylePropValue($$.color), resizing ? false : withTransition);
if (d) circle = circle.filter(function(t) {
const data = d.filter?.((v) => v.id === t.id);
return data.length ? select(this).datum(data[0]) : false;
});
circle.attr("class", this.updatePointClass.bind(this)).style("opacity", null).each(function(d) {
const { id, index, value } = d;
let visibility = "hidden";
if (isValue(value)) {
fn.bind(this)(d);
$$.expandCircles(index, id);
visibility = "";
}
this.style.visibility = visibility;
});
}
},
/**
* Hide focused data point circle
* @private
*/
hideCircleFocus() {
const $$ = this;
const { $el: { circle } } = $$;
if ($$.isPointFocusOnly() && circle) {
$$.unexpandCircles();
circle.style("visibility", "hidden");
}
},
expandCircles(i, id, reset) {
const $$ = this;
const r = $$.pointExpandedR.bind($$);
reset && $$.unexpandCircles();
const circles = $$.getShapeByIndex("circle", i, id).classed($COMMON.EXPANDED, true);
const scale = r(circles) / $$.config.point_r;
const ratio = 1 - scale;
if ($$.isCirclePoint()) circles.attr("r", r);
else circles.each(function() {
const point = select(this);
if (this.tagName === "circle") point.attr("r", r);
else {
const { width, height } = getPointBBox(this);
const x = ratio * (+point.attr("x") + width / 2);
const y = ratio * (+point.attr("y") + height / 2);
point.attr("transform", `translate(${x} ${y}) scale(${scale})`);
}
});
},
unexpandCircles(i) {
const $$ = this;
const r = $$.pointR.bind($$);
const circles = $$.getShapeByIndex("circle", i).filter(function() {
return select(this).classed($COMMON.EXPANDED);
}).classed($COMMON.EXPANDED, false);
circles.attr("r", r);
if (!$$.isCirclePoint()) {
const scale = r(circles) / $$.config.point_r;
circles.attr("transform", scale !== 1 ? `scale(${scale})` : null);
}
},
isWithinCircle(node, r) {
const { state } = this;
const mouse = getPointer(state.event, node);
const element = select(node);
const prefix = this.isCirclePoint(node) ? "c" : "";
const pointSensitivity = this.getPointSensitivity(element?.datum());
let cx = +element.attr(`${prefix}x`);
let cy = +element.attr(`${prefix}y`);
if (!(cx || cy) && node.nodeType === 1) {
const { x, y } = getBoundingRect(node);
cx = x;
cy = y;
}
const dx = cx - mouse[0];
const dy = cy - mouse[1];
return Math.sqrt(dx * dx + dy * dy) < (r || pointSensitivity);
},
updatePointClass(d) {
const $$ = this;
const { circle } = $$.$el;
let pointClass = false;
if (isObject(d) || circle) pointClass = d === true ? circle.each(function(d) {
let className = $$.getClass("circle", true)(d);
if (this.getAttribute("class").indexOf($COMMON.EXPANDED) > -1) className += ` ${$COMMON.EXPANDED}`;
this.setAttribute("class", className);
}) : $$.getClass("circle", true)(d);
return pointClass;
},
custom: {
create(element, id, fillStyleFn) {
return element.append("use").attr("xlink:href", `#${id}`).attr("class", this.updatePointClass.bind(this)).style("fill", fillStyleFn).node();
},
update(element, xPosFn, yPosFn, fillStyleFn, withTransition, flow, selectedCircles) {
const $$ = this;
const { width, height } = getPointBBox(element.node());
const xPosFn2 = (d) => isValue(d.value) ? xPosFn(d) - width / 2 : 0;
const yPosFn2 = (d) => isValue(d.value) ? yPosFn(d) - height / 2 : 0;
let mainCircles = element;
if (withTransition) {
flow && mainCircles.attr("x", xPosFn2);
mainCircles = $$.$T(mainCircles, withTransition, getTransitionName());
selectedCircles && $$.$T(selectedCircles, withTransition, getTransitionName());
}
return mainCircles.attr("x", xPosFn2).attr("y", yPosFn2).style("fill", fillStyleFn);
}
},
circle: {
create(element, sizeFn, fillStyleFn) {
return element.append("circle").attr("class", this.updatePointClass.bind(this)).attr("r", sizeFn).style("fill", fillStyleFn).node();
},
update(element, xPosFn, yPosFn, fillStyleFn, withTransition, flow, selectedCircles) {
const $$ = this;
let mainCircles = element;
if ($$.hasType("bubble")) mainCircles.attr("r", $$.pointR.bind($$));
if (withTransition) {
flow && mainCircles.attr("cx", xPosFn);
if (mainCircles.attr("cx")) mainCircles = $$.$T(mainCircles, withTransition, getTransitionName());
selectedCircles && $$.$T(selectedCircles, withTransition, getTransitionName());
}
return mainCircles.attr("cx", xPosFn).attr("cy", yPosFn).style("fill", fillStyleFn);
}
},
rectangle: {
create(element, sizeFn, fillStyleFn) {
const rectSizeFn = (d) => sizeFn(d) * 2;
return element.append("rect").attr("class", this.updatePointClass.bind(this)).attr("width", rectSizeFn).attr("height", rectSizeFn).style("fill", fillStyleFn).node();
},
update(element, xPosFn, yPosFn, fillStyleFn, withTransition, flow, selectedCircles) {
const $$ = this;
const r = $$.config.point_r;
const rectXPosFn = (d) => xPosFn(d) - r;
const rectYPosFn = (d) => yPosFn(d) - r;
let mainCircles = element;
if (withTransition) {
flow && mainCircles.attr("x", rectXPosFn);
mainCircles = $$.$T(mainCircles, withTransition, getTransitionName());
selectedCircles && $$.$T(selectedCircles, withTransition, getTransitionName());
}
return mainCircles.attr("x", rectXPosFn).attr("y", rectYPosFn).style("fill", fillStyleFn);
}
}
};
//#endregion
export { point_default as default };