Skip to main content
Module

x/billboardjs/ChartInternal/interactions/subchart.ts

πŸ“Š Re-usable, easy interface JavaScript chart library based on D3.js
Go to Latest
File
/** * Copyright (c) 2017 ~ present NAVER Corp. * billboard.js project is licensed under the MIT license */import {select as d3Select} 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, parseDate} from "../../module/util";
export default { /** * Initialize the brush. * @private */ initBrush(): void { const $$ = this; const {config, scale, $el: {subchart}} = $$; const isRotated = config.axis_rotated; let lastDomain; let timeout;
// set the brush $$.brush = ( isRotated ? d3BrushY() : d3BrushX() ).handleSize(5);
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"]; };
// bind brush event $$.brush.on("start brush end", event => { const {selection, target, type} = event;
if (type === "start") { $$.state.inputType === "touch" && $$.hideTooltip(); }
if (/(start|brush)/.test(type)) { $$.redrawForBrush(); }
if (type === "end") { lastDomain = scale.x.orgDomain(); }
// handle brush's handle position & visibility if (target?.handle) { if (selection === null) { $$.brush.handle.attr("display", "none"); } else { $$.brush.handle.attr("display", null) .attr("transform", (d, i) => { const pos = isRotated ? [33, selection[i] - (i === 0 ? 30 : 24)] : [selection[i], 3];
return `translate(${pos})`; }); } } });
$$.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?.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 ? null : "hidden"; const clipId = `${clip.id}-subchart`; const clipPath = $$.getClipPath(clipId);
clip.idSubchart = clipId; $$.appendClip(defs, clipId); $$.initBrush();
subchart.main = svg.append("g") .classed(CLASS.subchart, true) .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 chart types area ["bar", "line", "bubble", "candlestick", "scatter"].forEach(v => { const type = capitalize(/^(bubble|scatter)$/.test(v) ? "circle" : v);
if ($$.hasType(v) || $$.hasTypeOf(type)) { const chart = main.select(`.${CLASS.chart}`); const chartClassName = CLASS[`chart${type}s`];
if (chart.select(`.${chartClassName}`).empty()) { chart .append("g") .attr("class", chartClassName); } } });
// Add extent rect for Brush const brush = main.append("g") .attr("clip-path", clipPath) .attr("class", CLASS.brush) .call($$.brush);
config.subchart_showHandle && $$.addBrushHandle(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"); },
/** * Add brush handle * Enabled when: subchart.showHandle=true * @param {d3Selection} brush Brush selection * @private */ addBrushHandle(brush): void { const $$ = this; const {config} = $$; const isRotated = config.axis_rotated; const initRange = config.subchart_init_range; const customHandleClass = "handle--custom";
// brush handle shape's path const path = isRotated ? [ "M 5.2491724,29.749209 a 6,6 0 0 0 -5.50000003,-6.5 H -5.7508276 a 6,6 0 0 0 -6.0000004,6.5 z m -5.00000003,-2 H -6.7508276 m 6.99999997,-2 H -6.7508276Z", "M 5.2491724,23.249172 a 6,-6 0 0 1 -5.50000003,6.5 H -5.7508276 a 6,-6 0 0 1 -6.0000004,-6.5 z m -5.00000003,2 H -6.7508276 m 6.99999997,2 H -6.7508276Z" ] : [ "M 0 18 A 6 6 0 0 0 -6.5 23.5 V 29 A 6 6 0 0 0 0 35 Z M -2 23 V 30 M -4 23 V 30Z", "M 0 18 A 6 6 0 0 1 6.5 23.5 V 29 A 6 6 0 0 1 0 35 Z M 2 23 V 30 M 4 23 V 30Z" ];

$$.brush.handle = brush.selectAll(`.${customHandleClass}`) .data(isRotated ? [{type: "n"}, {type: "s"}] : [{type: "w"}, {type: "e"}] ) .enter() .append("path") .attr("class", customHandleClass) .attr("cursor", `${isRotated ? "ns" : "ew"}-resize`) .attr("d", d => path[+/[se]/.test(d.type)]) .attr("display", initRange ? null : "none"); },
/** * Update sub chart * @param {object} targets $$.data.targets * @private */ updateTargetsForSubchart(targets): void { const $$ = this; const {config, state, $el: {subchart: {main}}} = $$;
if (config.subchart_show) { ["bar", "line", "bubble", "candlestick", "scatter"] .filter(v => $$.hasType(v) || $$.hasTypeOf(capitalize(v))) .forEach(v => { const isPointType = /^(bubble|scatter)$/.test(v); const name = capitalize(isPointType ? "circle" : v); const chartClass = $$.getChartClass(name, true); const shapeClass = $$.getClass(isPointType ? "circles" : `${v}s`, true);
const shapeChart = main.select(`.${CLASS[`chart${`${name}s`}`]}`);
if (isPointType) { const circle = shapeChart .selectAll(`.${CLASS.circles}`) .data(targets.filter($$[`is${capitalize(v)}Type`].bind($$))) .attr("class", shapeClass);
circle.exit().remove(); circle.enter().append("g") .attr("class", shapeClass); } else { const shapeUpdate = shapeChart .selectAll(`.${CLASS[`chart${name}`]}`) .attr("class", chartClass) .data(targets.filter($$[`is${name}Type`].bind($$)));
const shapeEnter = shapeUpdate.enter() .append("g") .style("opacity", "0") .attr("class", chartClass) .append("g") .attr("class", shapeClass);
shapeUpdate.exit().remove();
// Area v === "line" && $$.hasTypeOf("Area") && shapeEnter.append("g").attr("class", $$.getClass("areas", true)); } });
// -- Brush --// main.selectAll(`.${CLASS.brush} rect`) .attr(config.axis_rotated ? "width" : "height", config.axis_rotated ? state.width2 : state.height2); } },
/** * 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}}, state} = $$; const withTransition = !!duration;
main.style("visibility", config.subchart_show ? null : "hidden");
// subchart if (config.subchart_show) { // reflect main chart to extent on subchart if zoomed if (state.event?.type === "zoom") { $$.brush.update(); }
// update subchart elements if needed if (withSubchart) { const initRange = config.subchart_init_range;
// extent rect !brushEmpty($$) && $$.brush.update();
Object.keys(shape.type).forEach(v => { const name = capitalize(v); const drawFn = $$[`generateDraw${name}`](shape.indices[v], true);
// call shape's update & redraw method $$[`update${name}`](withTransition, true); $$[`redraw${name}`](drawFn, withTransition, true); });
if ($$.hasType("bubble") || $$.hasType("scatter")) { const {cx} = shape.pos; const cy = $$.updateCircleY(true);
$$.updateCircle(true); $$.redrawCircle(cx, cy, withTransition, undefined, true); }
!state.rendered && initRange && $$.brush.move( $$.brush.getSelection(), initRange.map($$.scale.x) ); } } },
/** * 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 {$el: {subchart}, $T} = $$;
const subXAxis = transitions?.axisSubX ? transitions.axisSubX : $T(subchart.main.select(`.${CLASS.axisX}`), withTransition);
subchart.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; }};