UNPKG

billboard.js

Version:

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

455 lines (383 loc) 11.7 kB
/** * Copyright (c) 2017 ~ present NAVER Corp. * billboard.js project is licensed under the MIT license */ import { select as d3Select, event as d3Event } from "d3-selection"; import { brushX as d3BrushX, brushY as d3BrushY, brushSelection as d3BrushSelection } from "d3-brush"; import CLASS from "../../config/classes"; import {brushEmpty, capitalize, isArray, isFunction, getRandom, parseDate} from "../../module/util"; export default { /** * Initialize the brush. * @private */ initBrush(): void { const $$ = this; const {config, scale, $el: {subchart}} = $$; const isRotated = config.axis_rotated; // set the brush $$.brush = isRotated ? d3BrushY() : d3BrushX(); // set "brush" event const brushHandler = () => { $$.redrawForBrush(); }; const getBrushSize = () => { const brush = $$.$el.svg.select(`.${CLASS.brush} .overlay`); const brushSize = {width: 0, height: 0}; if (brush.size()) { brushSize.width = +brush.attr("width"); brushSize.height = +brush.attr("height"); } return brushSize[isRotated ? "width" : "height"]; }; let lastDomain; let timeout; $$.brush .on("start", () => { $$.state.inputType === "touch" && $$.hideTooltip(); brushHandler(); }) .on("brush", brushHandler) .on("end", () => { lastDomain = scale.x.orgDomain(); }); $$.brush.updateResize = function() { timeout && clearTimeout(timeout); timeout = setTimeout(() => { const selection = this.getSelection(); lastDomain && d3BrushSelection(selection.node()) && this.move(selection, lastDomain.map(scale.subX.orgScale())); }, 0); }; $$.brush.update = function() { const extent = this.extent()(); if (extent[1].filter(v => isNaN(v)).length === 0) { subchart.main && subchart.main.select(`.${CLASS.brush}`).call(this); } return this; }; // set the brush extent $$.brush.scale = function(scale) { const h = config.subchart_size_height || getBrushSize(); let extent = $$.getExtent(); if (!extent && scale.range) { extent = [[0, 0], [scale.range()[1], h]]; } else if (isArray(extent)) { extent = extent.map((v, i) => [v, i > 0 ? h : i]); } // [[x0, y0], [x1, y1]], where [x0, y0] is the top-left corner and [x1, y1] is the bottom-right corner isRotated && extent[1].reverse(); this.extent(extent); // when extent updates, brush selection also be re-applied // https://github.com/d3/d3/issues/2918 this.update(); }; $$.brush.getSelection = () => ( // @ts-ignore subchart.main ? subchart.main.select(`.${CLASS.brush}`) : d3Select([]) ); }, /** * Initialize the subchart. * @private */ initSubchart(): void { const $$ = this; const {config, state: {clip, hasAxis}, $el: {defs, svg, subchart, axis}} = $$; if (!hasAxis) { return; } const visibility = config.subchart_show ? "visible" : "hidden"; const clipId = `${clip.id}-subchart`; const clipPath = $$.getClipPath(clipId); clip.idSubchart = clipId; $$.appendClip(defs, clipId); $$.initBrush(); subchart.main = svg.append("g").attr("transform", $$.getTranslate("context")); const {main} = subchart; main.style("visibility", visibility); // Define g for chart area main.append("g") .attr("clip-path", clipPath) .attr("class", CLASS.chart); // Define g for bar chart area $$.hasType("bar") && main.select(`.${CLASS.chart}`) .append("g") .attr("class", CLASS.chartBars); // Define g for line chart area main.select(`.${CLASS.chart}`) .append("g") .attr("class", CLASS.chartLines); // Add extent rect for Brush main.append("g") .attr("clip-path", clipPath) .attr("class", CLASS.brush) .call($$.brush); // ATTENTION: This must be called AFTER chart added // Add Axis axis.subX = main.append("g") .attr("class", CLASS.axisX) .attr("transform", $$.getTranslate("subX")) .attr("clip-path", config.axis_rotated ? "" : clip.pathXAxis) .style("visibility", config.subchart_axis_x_show ? visibility : "hidden"); }, /** * Update sub chart * @param {object} targets $$.data.targets * @private */ updateTargetsForSubchart(targets): void { const $$ = this; const {config, state, $el: {subchart: {main}}} = $$; const classChartBar = $$.classChartBar.bind($$); const classBars = $$.classBars.bind($$); const classChartLine = $$.classChartLine.bind($$); const classLines = $$.classLines.bind($$); const classAreas = $$.classAreas.bind($$); if (config.subchart_show) { // -- Bar --// const barUpdate = main.select(`.${CLASS.chartBars}`) .selectAll(`.${CLASS.chartBar}`) .data(targets) .attr("class", classChartBar); const barEnter = barUpdate.enter() .append("g") .style("opacity", "0") .attr("class", classChartBar) .merge(barUpdate); // Bars for each data barEnter.append("g") .attr("class", classBars); // -- Line --// const lineUpdate = main.select(`.${CLASS.chartLines}`) .selectAll(`.${CLASS.chartLine}`) .data(targets) .attr("class", classChartLine); const lineEnter = lineUpdate.enter().append("g") .style("opacity", "0") .attr("class", classChartLine) .merge(lineUpdate); // Lines for each data lineEnter.append("g") .attr("class", classLines); // Area $$.hasType("area") && lineEnter.append("g") .attr("class", classAreas); // -- Brush --// main.selectAll(`.${CLASS.brush} rect`) .attr(config.axis_rotated ? "width" : "height", config.axis_rotated ? state.width2 : state.height2); } }, /** * Update the bar of the sub chart * @param {object} durationForExit Transition duration * @private */ updateBarForSubchart(durationForExit): void { const $$ = this; const {$el: {subchart}} = $$; subchart.bar = subchart.main.selectAll(`.${CLASS.bars}`).selectAll(`.${CLASS.bar}`) .data($$.barData.bind($$)); subchart.bar .exit() .transition() .duration(durationForExit) .style("opacity", "0") .remove(); subchart.bar = subchart.bar .enter() .append("path") .attr("class", $$.classBar.bind($$)) .style("stroke", "none") .style("fill", $$.color) .merge(subchart.bar) .style("opacity", $$.initialOpacity.bind($$)); }, /** * Redraw the bar of the subchart * @param {string} drawBarOnSub path in subchart line * @param {boolean} withTransition whether or not to transition * @param {number} duration transition duration * @private */ redrawBarForSubchart(drawBarOnSub: string, withTransition: boolean, duration: number): void { const {bar} = this.$el.subchart; (withTransition ? bar.transition(getRandom()).duration(duration) : bar) .attr("d", drawBarOnSub) .style("opacity", "1"); }, /** * Update the line of the sub chart * @param {number} durationForExit Fade-out transition duration * @private */ updateLineForSubchart(durationForExit): void { const $$ = this; const {$el: {subchart}} = $$; subchart.line = subchart.main.selectAll(`.${CLASS.lines}`) .selectAll(`.${CLASS.line}`) .data($$.lineData.bind($$)); subchart.line .exit() .transition() .duration(durationForExit) .style("opacity", "0") .remove(); subchart.line = subchart.line .enter() .append("path") .attr("class", $$.classLine.bind($$)) .style("stroke", $$.color) .merge(subchart.line) .style("opacity", $$.initialOpacity.bind($$)); }, /** * Redraw the line of the subchart * @private * @param {string} drawLineOnSub path in subchart line * @param {boolean} withTransition whether or not to transition * @param {number} duration transition duration */ redrawLineForSubchart(drawLineOnSub: string, withTransition: boolean, duration: number): void { const {line} = this.$el.subchart; (withTransition ? line.transition(getRandom()).duration(duration) : line) .attr("d", drawLineOnSub) .style("opacity", "1"); }, /** * Update the area of the sub chart * @param {number} durationForExit Fade-out transition duration * @private */ updateAreaForSubchart(durationForExit): void { const $$ = this; const {$el: {subchart}} = $$; subchart.area = subchart.main.selectAll(`.${CLASS.areas}`) .selectAll(`.${CLASS.area}`) .data($$.lineData.bind($$)); subchart.area .exit() .transition() .duration(durationForExit) .style("opacity", "0") .remove(); subchart.area = subchart.area .enter() .append("path") .attr("class", $$.classArea.bind($$)) .style("fill", $$.color) .style("opacity", function() { $$.state.orgAreaOpacity = d3Select(this).style("opacity"); return "0"; }) .merge(subchart.area) .style("opacity", "0"); }, /** * Redraw the area of the subchart * @private * @param {string} drawAreaOnSub path in subchart line * @param {boolean} withTransition whether or not to transition * @param {number} duration transition duration */ redrawAreaForSubchart(drawAreaOnSub: string, withTransition: boolean, duration: number): void { const {area} = this.$el.subchart; (withTransition ? area.transition(getRandom()).duration(duration) : area) .attr("d", drawAreaOnSub) .style("fill", this.color) .style("opacity", this.state.orgAreaOpacity); }, /** * Redraw subchart. * @private * @param {boolean} withSubchart whether or not to show subchart * @param {number} duration duration * @param {object} shape Shape's info */ redrawSubchart(withSubchart: boolean, duration: number, shape): void { const $$ = this; const {config, $el: {subchart: {main}}} = $$; main.style("visibility", config.subchart_show ? "visible" : "hidden"); // subchart if (config.subchart_show) { // reflect main chart to extent on subchart if zoomed if (d3Event && d3Event.type === "zoom") { $$.brush.update(); } // update subchart elements if needed if (withSubchart) { // extent rect !brushEmpty($$) && $$.brush.update(); Object.keys(shape.type).forEach(v => { const name = capitalize(v); const draw = $$[`generateDraw${name}`](shape.indices[v], true); $$[`update${name}ForSubchart`](duration); $$[`redraw${name}ForSubchart`](draw, duration, duration); }); } } }, /** * Redraw the brush. * @private */ redrawForBrush() { const $$ = this; const {config: {subchart_onbrush: onBrush, zoom_rescale: withY}, scale} = $$; $$.redraw({ withTransition: false, withY, withSubchart: false, withUpdateXDomain: true, withDimension: false }); onBrush.bind($$.api)(scale.x.orgDomain()); }, /** * Transform context * @param {boolean} withTransition indicates transition is enabled * @param {object} transitions The return value of the generateTransitions method of Axis. * @private */ transformContext(withTransition, transitions): void { const $$ = this; const {main} = $$.$el.subchart; let subXAxis; if (transitions && transitions.axisSubX) { subXAxis = transitions.axisSubX; } else { subXAxis = main.select(`.${CLASS.axisX}`); if (withTransition) { subXAxis = subXAxis.transition(); } } main.attr("transform", $$.getTranslate("context")); subXAxis.attr("transform", $$.getTranslate("subX")); }, /** * Get extent value * @returns {Array} default extent * @private */ getExtent(): number[] { const $$ = this; const {config, scale} = $$; let extent = config.axis_x_extent; if (extent) { if (isFunction(extent)) { extent = extent.bind($$.api)($$.getXDomain($$.data.targets), scale.subX); } else if ($$.axis.isTimeSeries() && extent.every(isNaN)) { const fn = parseDate.bind($$); extent = extent.map(v => scale.subX(fn(v))); } } return extent; } };