billboard.js
Version:
Re-usable easy interface JavaScript chart library, based on D3 v4+
211 lines (208 loc) • 8.03 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 { $LEGEND } from '../../config/classes.js';
import { KEY } from '../../module/Cache.js';
import { endall } from '../../module/util/object.js';
/**
* Copyright (c) 2017 ~ present NAVER Corp.
* billboard.js project is licensed under the MIT license
*/
/**
* Call done callback with resize after transition
* @param {function} fn Callback function
* @param {boolean} resizeAfter Weather to resize chart after the load
* @private
*/
function callDone(fn, resizeAfter = false) {
const $$ = this;
const { api } = $$;
resizeAfter && $$.api.flush(true);
fn?.call(api);
}
var dataLoad = {
load(rawTargets, args) {
const $$ = this;
const { axis, data, org, scale } = $$;
const { append } = args;
const zoomState = {
domain: null,
currentDomain: null,
x: null
};
let targets = rawTargets;
if (targets) {
// filter loading targets if needed
if (args.filter) {
targets = targets.filter(args.filter);
}
// set type if args.types || args.type specified
if (args.type || args.types) {
targets.forEach(t => {
const type = args.types?.[t.id] || args.type;
$$.setTargetType(t.id, type);
});
}
// Update/Add data: index incoming targets by id to avoid O(n×m) scans
const incoming = new Map(targets.map(t => [t.id, t]));
data.targets.forEach(d => {
const t = incoming.get(d.id);
if (t) {
if (append) {
const values = t.values;
for (let j = 0; j < values.length; j++) {
d.values.push(values[j]);
}
}
else {
d.values = t.values;
}
incoming.delete(d.id);
}
});
// add remained
incoming.forEach(t => data.targets.push(t));
}
if ($$.state.isCanvasMode) {
$$.redraw({
withUpdateOrgXDomain: true,
withUpdateXDomain: true,
withLegend: true
});
$$.updateTypesElements();
callDone.call($$, args.done, args.resizeAfter);
return;
}
// Set targets
$$.updateTargets(data.targets);
if (scale.zoom) {
zoomState.x = axis.isCategorized() ?
scale.x.orgScale() :
(org.xScale || scale.x).copy();
zoomState.domain = $$.getXDomain(data.targets); // get updated xDomain
zoomState.x.domain(zoomState.domain);
zoomState.currentDomain = $$.zoom.getDomain(); // current zoomed domain
// reset zoom state when new data loaded is out of range
if (!$$.withinRange(zoomState.currentDomain, undefined, zoomState.domain)) {
scale.x.domain(zoomState.domain);
scale.zoom = null;
$$.$el.eventRect.property("__zoom", null);
}
}
// Redraw with new targets
$$.redraw({
withUpdateOrgXDomain: true,
withUpdateXDomain: true,
withLegend: true
});
// when load happens on zoom state
if (scale.zoom) {
// const x = (axis.isCategorized() ? scale.x.orgScale() : (org.xScale || scale.x)).copy();
org.xDomain = zoomState.domain;
org.xScale = zoomState.x;
if (axis.isCategorized()) {
zoomState.currentDomain = $$.getZoomDomainValue(zoomState.currentDomain);
org.xDomain = $$.getZoomDomainValue(org.xDomain);
org.xScale = zoomState.x.domain(org.xDomain);
}
$$.updateCurrentZoomTransform(zoomState.x, zoomState.currentDomain);
// https://github.com/naver/billboard.js/issues/3878
}
else if (org.xScale) {
org.xScale.domain(org.xDomain);
}
// Update current state chart type and elements list after redraw
$$.updateTypesElements();
callDone.call($$, args.done, args.resizeAfter);
},
loadFromArgs(args) {
const $$ = this;
// prevent load when chart is already destroyed
if (!$$.config) {
return;
}
// Reset non-generation-based caches only.
// Generation-based caches ($filteredTargets, $maxDataCountTarget, $valuesXIndexMap,
// $maxTickSize_*) are invalidated automatically by dataGeneration/redrawGeneration
// increments at the start of the next redraw — no need to delete them eagerly.
$$.cache.reset(false, [
KEY.filteredTargets,
KEY.maxDataCountTarget,
KEY.valuesXIndexMap,
KEY.maxTickSize
]);
$$.convertData(args, d => {
const data = args.data || d;
args.append && (data.__append__ = true);
data && $$.load($$.convertDataToTargets.call($$, data), args);
});
},
unload(rawTargetIds, customDoneCb) {
const $$ = this;
const { state, $el, $T } = $$;
const hasLegendDefsPoint = !!$$.hasLegendDefsPoint?.();
let done = customDoneCb;
let targetIds = rawTargetIds;
// Reset non-generation-based caches only (same rationale as loadFromArgs)
$$.cache.reset(false, [
KEY.filteredTargets,
KEY.maxDataCountTarget,
KEY.valuesXIndexMap,
KEY.maxTickSize
]);
if (!done) {
done = () => { };
}
// filter existing target
targetIds = targetIds.filter(id => $$.hasTarget($$.data.targets, id));
// If no target, call done and return
if (targetIds.length === 0) {
done();
return;
}
// remove in a single pass instead of re-filtering per id
const unloadIds = new Set(targetIds);
if (state.isCanvasMode) {
targetIds.forEach(id => {
state.withoutFadeIn[id] = false;
});
$$.data.targets = $$.data.targets.filter(t => !unloadIds.has(t.id));
$$.removeHiddenTargetIds(targetIds);
$$.removeHiddenLegendIds(targetIds);
$$.updateTypesElements();
done();
return;
}
targetIds.forEach(id => {
const suffixId = $$.getTargetSelectorSuffix(id);
// Reset fadein for future load
state.withoutFadeIn[id] = false;
// Remove target's elements
if ($el.legend) {
$el.legend.selectAll(`.${$LEGEND.legendItem}${suffixId}`).remove();
}
// Remove custom point def element
hasLegendDefsPoint && $el.defs?.select(`#${$$.getDefsPointId(suffixId)}`).remove();
});
// Remove targets
$$.data.targets = $$.data.targets.filter(t => !unloadIds.has(t.id));
// since treemap uses different data types, it needs to be transformed
state.hasFunnel && $$.updateFunnel($$.data.targets);
// since treemap uses different data types, it needs to be transformed
state.hasTreemap && $$.updateTargetsForTreemap($$.data.targets);
// Update current state chart type and elements list after redraw
$$.updateTypesElements();
const targets = $el.svg.selectAll(targetIds.map(id => $$.selectorTarget(id)));
$T(targets)
.style("opacity", "0")
.remove()
.call(endall, done);
}
};
export { callDone, dataLoad as default };