billboard.js
Version:
Re-usable easy interface JavaScript chart library, based on D3 v4+
359 lines (287 loc) • 10.7 kB
text/typescript
/**
* Copyright (c) 2017 ~ present NAVER Corp.
* billboard.js project is licensed under the MIT license
*/
import {transition as d3Transition} from "d3-transition";
import {$COMMON, $SELECT, $TEXT} from "../../config/classes";
import {KEY} from "../../module/Cache";
import {generateWait} from "../../module/generator";
import {callFn, capitalize, getOption, isTabVisible, notEmpty} from "../../module/util";
export default {
redraw(options: any = {}): void {
const $$ = this;
const {config, state, $el} = $$;
const {main, treemap} = $el;
state.redrawing = true;
// Increment generation counters for cache invalidation
state.redrawGeneration++;
if (state.dirty.data || state.dirty.visibility || options.initializing) {
state.dataGeneration++;
}
// Invalidate per-redraw caches (only when size changed or initializing)
if (options.initializing || state.dirty.size || state.dirty.data || !state.rendered) {
$$.cache.remove([KEY.svgLeft]);
}
const targetsToShow = $$.filterTargetsToShow($$.data.targets);
// Cache for reuse by sub-methods within this redraw cycle
state._targetsToShow = targetsToShow;
// Capture and reset dirty flags immediately to avoid race conditions
// (async afterRedraw could wipe flags set by a subsequent redraw)
const dirtySnapshot = {
data: state.dirty.data,
visibility: state.dirty.visibility,
size: state.dirty.size
};
state.dirty.data = false;
state.dirty.visibility = false;
state.dirty.size = false;
const {flow, initializing} = options;
const wth = $$.getWithOption(options);
const duration = wth.Transition ? config.transition_duration : 0;
const durationForExit = wth.TransitionForExit ? duration : 0;
const durationForAxis = wth.TransitionForAxis ? duration : 0;
const transitions = $$.axis?.generateTransitions(durationForAxis);
// Shape DOM updates (D3 data join: enter/update/exit) are only needed when data or
// visibility changes. Zoom/brush only need redraw*() (position recalculation via
// getRedrawList), not update*(). New APIs that modify data must set dirty flags.
const needShapeUpdate = dirtySnapshot.data || dirtySnapshot.visibility || initializing;
if (state.isCanvasMode) {
$$.setContainerSize();
$$.updateHtmlLegend?.();
}
$$.updateSizes(initializing);
if (state.isCanvasMode) {
const zoomScale = $$.scale.zoom;
const zoomDomain = zoomScale?.domain();
zoomScale && ($$.scale.zoom = null);
$$.updateScales(initializing, wth.UpdateXDomain);
if (zoomScale) {
zoomScale.range($$.scale.x.range());
zoomDomain && zoomScale.domain(zoomDomain);
$$.scale.zoom = $$.getCustomizedXScale(zoomScale);
$$.axis.x.scale($$.scale.zoom);
$$.scale.x.domain(config.zoom_rescale ? zoomDomain : $$.org.xDomain);
$$.scale.subX.domain($$.org.xDomain);
}
state.hasAxis && $$.axis.syncAxisDomains(targetsToShow, wth, flow);
state.hasAxis && $$.applyCanvasSubchartDomain?.();
const shape = needShapeUpdate ?
$$.getDrawShape() :
(state._cachedDrawShape || $$.getDrawShape());
if (needShapeUpdate) {
state._cachedDrawShape = shape;
}
$$.updateHtmlLegend?.();
$$.resizeCanvas?.();
state.canvasFocusKey = null;
$$.renderCanvasFrame(shape, null, true);
initializing && $$.updateTypesElements();
$$.callPluginHook("$redraw", options, 0);
state.redrawing = false;
state._targetsToShow = null;
state._cachedDrawShape = null;
$$.mapToIds($$.data.targets).forEach(id => {
state.withoutFadeIn[id] = true;
});
callFn(config.onrendered, $$.api);
return;
}
// update legend and transform each g
if (wth.Legend && config.legend_show) {
options.withTransition = !!duration;
!treemap && $$.updateLegend($$.mapToIds($$.data.targets), options, transitions);
} else if (wth.Dimension) {
// need to update dimension (e.g. axis.y.tick.values) because y tick values should change
// no need to update axis in it because they will be updated in redraw()
$$.updateDimension(true);
}
// Data empty label positioning and text.
config.data_empty_label_text && main.select(`text.${$TEXT.text}.${$COMMON.empty}`)
.attr("x", state.width / 2)
.attr("y", state.height / 2)
.text(config.data_empty_label_text)
.style("display", targetsToShow.length ? "none" : null);
// title - position early so other elements can calculate correct padding
$$.redrawTitle?.();
// update axis
if (state.hasAxis) {
// @TODO: Make 'init' state to be accessible everywhere not passing as argument.
$$.axis.redrawAxis(targetsToShow, wth, transitions, flow, initializing);
// grid (optional module — guarded for tree-shakable grid resolver)
$$.hasGrid?.() && $$.updateGrid();
// rect for regions (optional module — updateRegion installed by regions resolver)
config.regions.length && $$.updateRegion?.();
["bar", "candlestick", "line", "area"].forEach(v => {
const name = capitalize(v);
if ((/^(line|area)$/.test(v) && $$.hasTypeOf(name)) || $$.hasType(v)) {
if (needShapeUpdate) {
$$[`update${name}`](wth.TransitionForExit);
}
}
});
// circles for select
$el.text && main.selectAll(`.${$SELECT.selectedCircles}`)
.filter($$.isBarType.bind($$))
.selectAll("circle")
.remove();
// event rects will redrawn when flow called
if (config.interaction_enabled && !flow && wth.EventRect) {
$$.redrawEventRect();
$$.bindZoomEvent?.();
}
} else {
// arc
$el.arcs && $$.redrawArc(duration, durationForExit, wth.Transform);
// radar
$el.radar && $$.redrawRadar();
// polar
$el.polar && $$.redrawPolar();
// funnel
$el.funnel && $$.redrawFunnel();
// treemap
treemap && $$.updateTreemap(durationForExit);
}
if (!state.resizing && !treemap && ($$.hasPointType() || state.hasRadar)) {
if (needShapeUpdate) {
$$.updateCircle();
}
} else if ($$.hasLegendDefsPoint?.()) {
$$.data.targets.forEach($$.point("create", this));
}
// text
if ($$.hasDataLabel() && !$$.hasArcType(null, ["radar"])) {
if (needShapeUpdate) {
$$.updateText();
}
}
initializing && $$.updateTypesElements();
$$.generateRedrawList(targetsToShow, flow, duration, wth.Subchart, needShapeUpdate);
$$.updateTooltipOnRedraw();
$$.callPluginHook("$redraw", options, duration);
},
/**
* Generate redraw list
* @param {object} targets targets data to be shown
* @param {object} flow flow object
* @param {number} duration duration value
* @param {boolean} withSubchart whether or not to show subchart
* @param {boolean} needShapeRegen whether to regenerate draw shape
* @private
*/
generateRedrawList(targets, flow: any, duration: number, withSubchart: boolean,
needShapeRegen: boolean = true): void {
const $$ = this;
const {config, state} = $$;
const shape = needShapeRegen ?
$$.getDrawShape() :
(state._cachedDrawShape || $$.getDrawShape());
if (needShapeRegen) {
state._cachedDrawShape = shape;
}
if (state.hasAxis) {
// subchart
config.subchart_show && $$.redrawSubchart(withSubchart, duration, shape);
}
// generate flow
const flowFn = flow && $$.generateFlow({
targets,
flow,
duration: flow.duration,
shape,
xv: $$.xv.bind($$)
});
const withTransition = (duration || flowFn) && isTabVisible();
// redraw list
const redrawList = $$.getRedrawList(shape, flow, flowFn, withTransition);
// callback function after redraw ends
const afterRedraw = () => {
flowFn && flowFn();
state.redrawing = false;
state._targetsToShow = null;
state._cachedDrawShape = null;
callFn(config.onrendered, $$.api);
};
// Only use transition when current tab is visible.
if (withTransition && redrawList.length) {
// Wait for end of transitions for callback
const waitForDraw = generateWait();
// transition should be derived from one transition
d3Transition().duration(duration)
.each(() => {
redrawList
.flatMap(t1 => t1)
.forEach(t => waitForDraw.add(t));
})
.call(waitForDraw, afterRedraw);
} else if (!state.transiting) {
afterRedraw();
}
// update fadein condition
$$.mapToIds($$.data.targets).forEach(id => {
state.withoutFadeIn[id] = true;
});
},
getRedrawList(shape, flow, flowFn, withTransition: boolean): Function[] {
const $$ = <any>this;
const {config, state: {hasAxis, hasRadar, hasTreemap}, $el: {grid}} = $$;
const {cx, cy, xForText, yForText} = shape.pos;
const list: Function[] = [];
if (hasAxis) {
if ($$.redrawGrid && (config.grid_x_lines.length || config.grid_y_lines.length)) {
list.push($$.redrawGrid(withTransition));
}
if ($$.redrawRegion && config.regions.length) {
list.push($$.redrawRegion(withTransition));
}
for (const v in shape.type) {
const name = capitalize(v);
const drawFn = shape.type[v];
if ((/^(area|line)$/.test(v) && $$.hasTypeOf(name)) || $$.hasType(v)) {
list.push($$[`redraw${name}`](drawFn, withTransition));
}
}
!flow && grid.main && $$.updateGridFocus && list.push($$.updateGridFocus());
}
if (!$$.hasArcType() || hasRadar) {
notEmpty(config.data_labels) && config.data_labels !== false &&
list.push($$.redrawText(xForText, yForText, flow, withTransition));
}
if (($$.hasPointType() || hasRadar) && !$$.isPointFocusOnly()) {
$$.redrawCircle && list.push($$.redrawCircle(cx, cy, withTransition, flowFn));
}
if (hasTreemap) {
list.push($$.redrawTreemap(withTransition));
}
return list;
},
updateAndRedraw(options: any = {}): void {
const $$ = this;
const {config, state} = $$;
let transitions;
// same with redraw
options.withTransition = getOption(options, "withTransition", true);
options.withTransform = getOption(options, "withTransform", false);
options.withLegend = getOption(options, "withLegend", false);
// NOT same with redraw
options.withUpdateXDomain = true;
options.withUpdateOrgXDomain = true;
options.withTransitionForExit = false;
options.withTransitionForTransform = getOption(options, "withTransitionForTransform",
options.withTransition);
// MEMO: called in updateLegend in redraw if withLegend
if (!(options.withLegend && config.legend_show)) {
if (state.hasAxis) {
transitions = $$.axis.generateTransitions(
options.withTransitionForAxis ? config.transition_duration : 0
);
}
// Update scales
$$.updateScales();
$$.updateSvgSize();
// Update g positions
$$.transformAll(options.withTransitionForTransform, transitions);
}
// Draw with new sizes & scales
$$.redraw(options);
}
};