UNPKG

billboard.js

Version:

Re-usable easy interface JavaScript chart library, based on D3 v4+

359 lines (287 loc) 10.7 kB
/** * 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); } };