Skip to main content
Module

x/billboardjs/ChartInternal/ChartInternal.ts

πŸ“Š Re-usable, easy interface JavaScript chart library based on D3.js
Go to Latest
File
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812
/** * Copyright (c) 2017 ~ present NAVER Corp. * billboard.js project is licensed under the MIT license * @ignore */import { timeParse as d3TimeParse, timeFormat as d3TimeFormat, utcParse as d3UtcParse, utcFormat as d3UtcFormat} from "d3-time-format";import {select as d3Select} from "d3-selection";import type {d3Selection} from "../../types/types";import {checkModuleImport} from "../module/error";import {$COMMON, $CIRCLE, $TEXT} from "../config/classes";import Store from "../config/Store/Store";import Options from "../config/Options/Options";import {document, window} from "../module/browser";import Cache from "../module/Cache";import {generateResize} from "../module/generator";import {capitalize, extend, notEmpty, convertInputType, getOption, getRandom, isFunction, isObject, isString, callFn, sortValue} from "../module/util";
// dataimport dataConvert from "./data/convert";import data from "./data/data";import dataLoad from "./data/load";
// interactionsimport interaction from "./interactions/interaction";
// internalsimport classModule from "./internals/class";import category from "./internals/category"; // used to retrieve radar Axis nameimport color from "./internals/color";import domain from "./internals/domain";import format from "./internals/format";import legend from "./internals/legend";import redraw from "./internals/redraw";import scale from "./internals/scale";import shape from "./shape/shape";import size from "./internals/size";import style from "./internals/style";import text from "./internals/text";import title from "./internals/title";import tooltip from "./internals/tooltip";import transform from "./internals/transform";import type from "./internals/type";
/** * Internal chart class. * - Note: Instantiated internally, not exposed for public. * @class ChartInternal * @ignore * @private */export default class ChartInternal { public api; // API interface public config; // config object public cache; // cache instance public $el; // elements public state; // state variables public charts; // all Chart instances array within page (equivalent of 'bb.instances')
// data object public data = { xs: {}, targets: [] };
// Axis public axis; // Axis
// scales public scale = { x: null, y: null, y2: null, subX: null, subY: null, subY2: null, zoom: null };
// original values public org = { xScale: null, xDomain: null };
// formatter function public color; public patterns; public levelColor; public point; public brush;
// format function public format = { extraLineClasses: null, xAxisTick: null, dataTime: null, // dataTimeFormat defaultAxisTime: null, // defaultAxisTimeFormat axisTime: null // axisTimeFormat };
constructor(api) { const $$ = this;
$$.api = api; // Chart class instance alias $$.config = new Options(); $$.cache = new Cache();
const store = new Store();
$$.$el = store.getStore("element"); $$.state = store.getStore("state");
$$.$T = $$.$T.bind($$); }
/** * Get the selection based on transition config * @param {SVGElement|d3Selection} selection Target selection * @param {boolean} force Force transition * @param {string} name Transition name * @returns {d3Selection} * @private */ $T(selection: SVGElement | d3Selection, force?: boolean, name?: string): d3Selection { const {config, state} = this; const duration = config.transition_duration; const subchart = config.subchart_show; let t = selection;
if (t) { // in case of non d3 selection, wrap with d3 selection if ("tagName" in t) { t = d3Select(t); }
// do not transit on: // - wheel zoom (state.zooming = true) // - when has no subchart // - initialization // - resizing const transit = ((force !== false && duration) || force) && (!state.zooming || state.dragging) && !state.resizing && state.rendered && !subchart;
t = (transit ? t.transition(name).duration(duration) : t) as d3Selection; }
return t; }
beforeInit(): void { const $$ = this;
$$.callPluginHook("$beforeInit");
// can do something callFn($$.config.onbeforeinit, $$.api); }
afterInit(): void { const $$ = this;
$$.callPluginHook("$afterInit");
// can do something callFn($$.config.onafterinit, $$.api); }
init(): void { const $$ = <any> this; const {config, state, $el} = $$; const useCssRule = config.boost_useCssRule;
checkModuleImport($$);
state.hasAxis = !$$.hasArcType(); state.hasRadar = !state.hasAxis && $$.hasType("radar");
// datetime to be used for uniqueness state.datetimeId = `bb-${+new Date() * (getRandom() as number)}`;
if (useCssRule) { // append style element const styleEl = document.createElement("style");
// styleEl.id = styleId; styleEl.type = "text/css"; document.head.appendChild(styleEl);
state.style = { rootSelctor: `.${state.datetimeId}`, sheet: styleEl.sheet };
// used on .destroy() $el.style = styleEl; }
const bindto = { element: config.bindto, classname: "bb" };
if (isObject(config.bindto)) { bindto.element = config.bindto.element || "#chart"; bindto.classname = config.bindto.classname || bindto.classname; }
// select bind element $el.chart = isFunction(bindto.element.node) ? config.bindto.element : d3Select(bindto.element || []);
if ($el.chart.empty()) { $el.chart = d3Select(document.body.appendChild(document.createElement("div"))); }
$el.chart.html("") .classed(bindto.classname, true) .classed(state.datetimeId, useCssRule) .style("position", "relative");
$$.initParams(); $$.initToRender(); }
/** * Initialize the rendering process * @param {boolean} forced Force to render process * @private */ initToRender(forced?: boolean): void { const $$ = <any> this; const {config, state, $el: {chart}} = $$; const isHidden = () => chart.style("display") === "none" || chart.style("visibility") === "hidden";
const isLazy = config.render.lazy || isHidden(); const MutationObserver = window.MutationObserver;
if (isLazy && MutationObserver && config.render.observe !== false && !forced) { new MutationObserver((mutation, observer) => { if (!isHidden()) { observer.disconnect(); !state.rendered && $$.initToRender(true); } }).observe(chart.node(), { attributes: true, attributeFilter: ["class", "style"] }); }
if (!isLazy || forced) { $$.convertData(config, res => { $$.initWithData(res); $$.afterInit(); }); } }
initParams(): void { const $$ = <any> this; const {config, format, state} = $$; const isRotated = config.axis_rotated;
// color settings $$.color = $$.generateColor(); $$.levelColor = $$.generateLevelColor();
// when 'padding=false' is set, disable axes and subchart. Because they are useless. if (config.padding === false) { config.axis_x_show = false; config.axis_y_show = false; config.axis_y2_show = false; config.subchart_show = false; }
if ($$.hasPointType()) { $$.point = $$.generatePoint(); }
if (state.hasAxis) { $$.initClip();
format.extraLineClasses = $$.generateExtraLineClass(); format.dataTime = config.data_xLocaltime ? d3TimeParse : d3UtcParse; format.axisTime = config.axis_x_localtime ? d3TimeFormat : d3UtcFormat;
const isDragZoom = $$.config.zoom_enabled && $$.config.zoom_type === "drag";
format.defaultAxisTime = d => { const {x, zoom} = $$.scale; const isZoomed = isDragZoom ? zoom : zoom && x.orgDomain().toString() !== zoom.domain().toString();
const specifier: string = (d.getMilliseconds() && ".%L") || (d.getSeconds() && ".:%S") || (d.getMinutes() && "%I:%M") || (d.getHours() && "%I %p") || (d.getDate() !== 1 && "%b %d") || (isZoomed && d.getDate() === 1 && "%b\'%y") || (d.getMonth() && "%-m/%-d") || "%Y";
return format.axisTime(specifier)(d); }; }
state.isLegendRight = config.legend_position === "right"; state.isLegendInset = config.legend_position === "inset";
state.isLegendTop = config.legend_inset_anchor === "top-left" || config.legend_inset_anchor === "top-right";
state.isLegendLeft = config.legend_inset_anchor === "top-left" || config.legend_inset_anchor === "bottom-left";
state.rotatedPadding.top = $$.getResettedPadding(state.rotatedPadding.top); state.rotatedPadding.right = isRotated && !config.axis_x_show ? 0 : 30;
state.inputType = convertInputType( config.interaction_inputType_mouse, config.interaction_inputType_touch ); }
initWithData(data): void { const $$ = <any> this; const {config, scale, state, $el, org} = $$; const {hasAxis} = state; const hasInteraction = config.interaction_enabled; const hasPolar = $$.hasType("polar");
// for arc type, set axes to not be shown // $$.hasArcType() && ["x", "y", "y2"].forEach(id => (config[`axis_${id}_show`] = false));
if (hasAxis) { $$.axis = $$.getAxisInstance(); config.zoom_enabled && $$.initZoom(); }
// Init data as targets $$.data.xs = {}; $$.data.targets = $$.convertDataToTargets(data);
if (config.data_filter) { $$.data.targets = $$.data.targets.filter(config.data_filter.bind($$.api)); }
// Set targets to hide if needed if (config.data_hide) { $$.addHiddenTargetIds( config.data_hide === true ? $$.mapToIds($$.data.targets) : config.data_hide ); }
if (config.legend_hide) { $$.addHiddenLegendIds( config.legend_hide === true ? $$.mapToIds($$.data.targets) : config.legend_hide ); }
// Init sizes and scales $$.updateSizes(); $$.updateScales(true);
// retrieve scale after the 'updateScales()' is called const {x, y, y2, subX, subY, subY2} = scale;
// Set domains for each scale if (x) { x.domain(sortValue($$.getXDomain($$.data.targets))); subX.domain(x.domain());
// Save original x domain for zoom update org.xDomain = x.domain(); }
if (y) { y.domain($$.getYDomain($$.data.targets, "y")); subY.domain(y.domain()); }
if (y2) { y2.domain($$.getYDomain($$.data.targets, "y2")); subY2 && subY2.domain(y2.domain()); }
// -- Basic Elements -- $el.svg = $el.chart.append("svg") .style("overflow", "hidden") .style("display", "block");
if (hasInteraction && state.inputType) { const isTouch = state.inputType === "touch"; const {onclick, onover, onout} = config;
$el.svg .on("click", onclick?.bind($$.api) || null) .on(isTouch ? "touchstart" : "mouseenter", onover?.bind($$.api) || null) .on(isTouch ? "touchend" : "mouseleave", onout?.bind($$.api) || null); }
config.svg_classname && $el.svg.attr("class", config.svg_classname);
// Define defs const hasColorPatterns = (isFunction(config.color_tiles) && $$.patterns);
if (hasAxis || hasColorPatterns || config.data_labels_backgroundColors || hasPolar) { $el.defs = $el.svg.append("defs");
if (hasAxis) { ["id", "idXAxis", "idYAxis", "idGrid"].forEach(v => { $$.appendClip($el.defs, state.clip[v]); }); }
// Append data background color filter definition $$.generateDataLabelBackgroundColorFilter();
// set color patterns if (hasColorPatterns) { $$.patterns.forEach(p => $el.defs.append(() => p.node)); } }
$$.updateSvgSize();
// Bind resize event $$.bindResize();
// Define regions const main = $el.svg.append("g") .classed($COMMON.main, true) .attr("transform", $$.getTranslate("main"));
$el.main = main;
// initialize subchart when subchart show option is set config.subchart_show && $$.initSubchart();
config.tooltip_show && $$.initTooltip(); config.title_text && $$.initTitle(); config.legend_show && $$.initLegend();
// -- Main Region --
// text when empty if (config.data_empty_label_text) { main.append("text") .attr("class", `${$TEXT.text} ${$COMMON.empty}`) .attr("text-anchor", "middle") // horizontal centering of text at x position in all browsers. .attr("dominant-baseline", "middle"); // vertical centering of text at y position in all browsers, except IE. }
if (hasAxis) { // Regions config.regions.length && $$.initRegion();
// Add Axis here, when clipPath is 'false' !config.clipPath && $$.axis.init(); }
// Define g for chart area main.append("g").attr("class", $COMMON.chart) .attr("clip-path", state.clip.path);
$$.callPluginHook("$init");
if (hasAxis) { // Cover whole with rects for events hasInteraction && $$.initEventRect?.();
// Grids $$.initGrid();
// Add Axis here, when clipPath is 'true' config.clipPath && $$.axis?.init(); }
$$.initChartElements();
// Set targets $$.updateTargets($$.data.targets);
// Draw with targets $$.updateDimension();
// oninit callback callFn(config.oninit, $$.api);
// Set background $$.setBackground();
$$.redraw({ withTransition: false, withTransform: true, withUpdateXDomain: true, withUpdateOrgXDomain: true, withTransitionForAxis: false, initializing: true });
// data.onmin/max callback if (config.data_onmin || config.data_onmax) { const minMax = $$.getMinMaxData();
callFn(config.data_onmin, $$.api, minMax.min); callFn(config.data_onmax, $$.api, minMax.max); }
config.tooltip_show && $$.initShowTooltip(); state.rendered = true; }
/** * Initialize chart elements * @private */ initChartElements(): void { const $$ = <any> this; const {hasAxis, hasRadar} = $$.state; const types: string[] = [];
if (hasAxis) { ["bar", "bubble", "candlestick", "line"].forEach(v => { const name = capitalize(v);
if ((v === "line" && $$.hasTypeOf(name)) || $$.hasType(v)) { types.push(name); } }); } else { const hasPolar = $$.hasType("polar");
if (!hasRadar) { types.push("Arc", "Pie"); }
if ($$.hasType("gauge")) { types.push("Gauge"); } else if (hasRadar) { types.push("Radar"); } else if (hasPolar) { types.push("Polar"); } }
types.forEach(v => { $$[`init${v}`](); });
notEmpty($$.config.data_labels) && !$$.hasArcType(null, ["radar"]) && $$.initText(); }
/** * Set chart elements * @private */ setChartElements(): void { const $$ = this; const {$el: { chart, svg, defs, main, tooltip, legend, title, grid, arcs: arc, circle: circles, bar: bars, candlestick, line: lines, area: areas, text: texts }} = $$;
$$.api.$ = { chart, svg, defs, main, tooltip, legend, title, grid, arc, circles, bar: {bars}, candlestick, line: {lines, areas}, text: {texts} }; }
/** * Set background element/image * @private */ setBackground(): void { const $$ = this; const {config: {background: bg}, state, $el: {svg}} = $$;
if (notEmpty(bg)) { const element = svg.select("g") .insert(bg.imgUrl ? "image" : "rect", ":first-child");
if (bg.imgUrl) { element.attr("href", bg.imgUrl); } else if (bg.color) { element .style("fill", bg.color) .attr("clip-path", state.clip.path); }
element .attr("class", bg.class || null) .attr("width", "100%") .attr("height", "100%"); } }
/** * Update targeted element with given data * @param {object} targets Data object formatted as 'target' * @private */ updateTargets(targets): void { const $$ = <any> this; const {hasAxis, hasRadar} = $$.state; const helper = type => $$[`updateTargetsFor${type}`]( targets.filter($$[`is${type}Type`].bind($$)) );
// Text $$.updateTargetsForText(targets);
if (hasAxis) { ["bar", "candlestick", "line"].forEach(v => { const name = capitalize(v);
if ((v === "line" && $$.hasTypeOf(name)) || $$.hasType(v)) { helper(name); } });
// Sub Chart $$.updateTargetsForSubchart && $$.updateTargetsForSubchart(targets);
// Arc, Polar, Radar } else if ($$.hasArcType(targets)) { let type = "Arc";
if (hasRadar) { type = "Radar"; } else if ($$.hasType("polar")) { type = "Polar"; }
helper(type); }
// Point types const hasPointType = $$.hasType("bubble") || $$.hasType("scatter");
if (hasPointType) { $$.updateTargetForCircle?.(); }
// Fade-in each chart $$.filterTargetsToShowAtInit(hasPointType); }
/** * Display targeted elements at initialization * @param {boolean} hasPointType whether has point type(bubble, scatter) or not * @private */ filterTargetsToShowAtInit(hasPointType: boolean = false): void { const $$ = <any> this; const {$el: {svg}, $T} = $$; let selector = `.${$COMMON.target}`;
if (hasPointType) { selector += `, .${$CIRCLE.chartCircles} > .${$CIRCLE.circles}`; }
$T(svg.selectAll(selector) .filter(d => $$.isTargetToShow(d.id)) ).style("opacity", null); }
getWithOption(options) { const withOptions = { Dimension: true, EventRect: true, Legend: false, Subchart: true, Transform: false, Transition: true, TrimXDomain: true, UpdateXAxis: "UpdateXDomain", UpdateXDomain: false, UpdateOrgXDomain: false, TransitionForExit: "Transition", TransitionForAxis: "Transition", Y: true };
Object.keys(withOptions).forEach(key => { let defVal = withOptions[key];
if (isString(defVal)) { defVal = withOptions[defVal]; }
withOptions[key] = getOption(options, `with${key}`, defVal); });
return withOptions; }
initialOpacity(d): null | "0" { const $$ = <any> this; const {withoutFadeIn} = $$.state;
const r = $$.getBaseValue(d) !== null && withoutFadeIn[d.id] ? null : "0";
return r; }
bindResize(): void { const $$ = <any> this; const {config, state} = $$; const resizeFunction = generateResize(config.resize_timer); const list: Function[] = [];
list.push(() => callFn(config.onresize, $$.api));
if (config.resize_auto) { list.push(() => { state.resizing = true;
// https://github.com/naver/billboard.js/issues/2650 if (config.legend_show) { $$.updateSizes(); $$.updateLegend(); }
$$.api.flush(false); }); }
list.push(() => { callFn(config.onresized, $$.api); state.resizing = false; });
// add resize functions list.forEach(v => resizeFunction.add(v));
$$.resizeFunction = resizeFunction;
// attach resize event window.addEventListener("resize", $$.resizeFunction = resizeFunction); }
/** * Call plugin hook * @param {string} phase The lifecycle phase * @param {Array} args Arguments * @private */ callPluginHook(phase, ...args): void { this.config.plugins.forEach(v => { if (phase === "$beforeInit") { v.$$ = this; this.api.plugins.push(v); }
v[phase](...args); }); }}
extend(ChartInternal.prototype, [ // common dataConvert, data, dataLoad, category, classModule, color, domain, interaction, format, legend, redraw, scale, shape, size, style, text, title, tooltip, transform, type]);