billboard.js
Version:
Re-usable easy interface JavaScript chart library, based on D3 v4+
444 lines (443 loc) • 12.4 kB
JavaScript
/*!
* Copyright (c) 2017 ~ present NAVER Corp.
* billboard.js project is licensed under the MIT license
*
* billboard.js, JavaScript chart library
* https://naver.github.io/billboard.js/
*
* @version 4.1.1
* @requires billboard.js
* @summary billboard.js plugin
*/
//#region src/config/classes.ts
/**
* Copyright (c) 2017 ~ present NAVER Corp.
* billboard.js project is licensed under the MIT license
*/
/**
* CSS class names definition
* @private
*/
const $COMMON = {
button: "bb-button",
chart: "bb-chart",
empty: "bb-empty",
main: "bb-main",
target: "bb-target",
EXPANDED: "_expanded_",
dummy: "_dummy_"
};
//#endregion
//#region src/module/util/type-checks.ts
const isDefined = (v) => typeof v !== "undefined";
const isObjectType = (v) => typeof v === "object";
//#endregion
//#region src/module/browser.ts
/**
* Copyright (c) 2017 ~ present NAVER Corp.
* billboard.js project is licensed under the MIT license
*/
/**
* Window object
* @private
*/
/**
* Get global object
* @returns {object} window object
* @private
*/
function getGlobal() {
return typeof globalThis === "object" && globalThis !== null && globalThis.Object === Object && globalThis || typeof self === "object" && self !== null && self.Object === Object && self || Function("return this")();
}
/**
* Get fallback object
* @param {object} w global object
* @returns {Array} fallback object array
* @private
*/
function getFallback(w) {
const hasRAF = typeof w?.requestAnimationFrame === "function" && typeof w?.cancelAnimationFrame === "function";
const hasRIC = typeof w?.requestIdleCallback === "function" && typeof w?.cancelIdleCallback === "function";
const request = (cb) => setTimeout(cb, 1);
const cancel = (id) => clearTimeout(id);
return [
hasRAF ? w.requestAnimationFrame : request,
hasRAF ? w.cancelAnimationFrame : cancel,
hasRIC ? w.requestIdleCallback : request,
hasRIC ? w.cancelIdleCallback : cancel
];
}
const win = getGlobal();
const doc = win?.document;
const [requestAnimationFrame, cancelAnimationFrame, requestIdleCallback, cancelIdleCallback] = getFallback(win);
//#endregion
//#region src/module/util/dom.ts
/**
* Get boundingClientRect or BBox with caching.
* Internal helper for getBoundingRect() and getBBox()
* @param {boolean} relativeViewport Relative to viewport - true: will use .getBoundingClientRect(), false: will use .getBBox()
* @param {SVGElement} node Target element
* @param {boolean} forceEval Force evaluation
* @returns {object}
* @private
*/
function _getRect(relativeViewport, node, forceEval = false) {
const _ = (n) => n[relativeViewport ? "getBoundingClientRect" : "getBBox"]();
const cacheKey = relativeViewport ? "rectClient" : "rectBBox";
if (forceEval) return _(node);
else return !(cacheKey in node) || node.hasAttribute("width") && node[cacheKey].width !== +(node.getAttribute("width") || 0) ? node[cacheKey] = _(node) : node[cacheKey];
}
/**
* Get boundingClientRect.
* @param {SVGElement} node Target element
* @param {boolean} forceEval Force evaluation
* @returns {object}
* @private
*/
function getBoundingRect(node, forceEval = false) {
return _getRect(true, node, forceEval);
}
(() => {
const getParams = () => ({
bubbles: false,
cancelable: false,
screenX: 0,
screenY: 0,
clientX: 0,
clientY: 0
});
try {
new MouseEvent("t");
return (el, eventType, params = getParams()) => {
el.dispatchEvent(new MouseEvent(eventType, params));
};
} catch {
return (el, eventType, params = getParams()) => {
const mouseEvent = doc.createEvent("MouseEvent");
mouseEvent.initMouseEvent(eventType, params.bubbles, params.cancelable, win, 0, params.screenX, params.screenY, params.clientX, params.clientY, false, false, false, false, 0, null);
el.dispatchEvent(mouseEvent);
};
}
})();
//#endregion
//#region src/config/config.ts
/**
* Load configuration option
* @param {object} config User's generation config value
* @private
*/
function loadConfig(config) {
const thisConfig = this.config;
let target;
let keys;
let read;
const find = () => {
const key = keys.shift();
if (key && target && isObjectType(target) && key in target) {
target = target[key];
return find();
} else if (!key) return target;
};
Object.keys(thisConfig).forEach((key) => {
target = config;
keys = key.split("_");
read = find();
if (isDefined(read)) thisConfig[key] = read;
});
if (this.api) this.state.orgConfig = config;
}
//#endregion
//#region src/Plugin/Plugin.ts
/**
* Copyright (c) 2017 ~ present NAVER Corp.
* billboard.js project is licensed under the MIT license
*/
/**
* Base class to generate billboard.js plugin
* @class Plugin
*/
/**
* Version info string for plugin
* @name version
* @static
* @memberof Plugin
* @type {string}
* @example
* bb.plugin.stanford.version; // ex) 1.9.0
*/
var Plugin = class {
$$;
options;
config;
static version = "4.1.1";
/**
* Constructor
* @param {Any} options config option object
* @private
*/
constructor(options = {}) {
this.options = options;
}
/**
* Load plugin config from options
* @private
*/
loadConfig() {
loadConfig.call(this, this.options);
}
/**
* Lifecycle hook for 'beforeInit' phase.
* @private
*/
$beforeInit() {}
/**
* Lifecycle hook for 'init' phase.
* @private
*/
$init() {}
/**
* Lifecycle hook for 'afterInit' phase.
* @private
*/
$afterInit() {}
/**
* Lifecycle hook for 'redraw' phase.
* @private
*/
$redraw() {}
/**
* Lifecycle hook for 'willDestroy' phase.
* @private
*/
$willDestroy() {
Object.keys(this).forEach((key) => {
this[key] = null;
delete this[key];
});
}
};
//#endregion
//#region src/Plugin/sparkline/Options.ts
/**
* Copyright (c) 2021 ~ present NAVER Corp.
* billboard.js project is licensed under the MIT license
*/
/**
* Sparkline plugin option class
* @class SparklineOptions
* @param {Options} options Sparkline plugin options
* @augments Plugin
* @returns {TableviewOptions}
* @private
*/
var Options = class {
constructor() {
return {
/**
* Specify sparkline charts holder selector.
* - **NOTE:** The amount of holder should match with the amount of data. If has less, will append necessaray amount nodes as sibling of main chart.
* @name selector
* @memberof plugin-sparkline
* @type {string}
* @default undefined
* @example
* selector: ".sparkline"
*/
selector: void 0 };
}
};
//#endregion
//#region src/Plugin/sparkline/index.ts
/**
* Sparkline plugin.<br>
* Generates sparkline charts
* - **NOTE:**
* - Plugins aren't built-in. Need to be loaded or imported to be used.
* - Non required modules from billboard.js core, need to be installed separately.
*
* - **Bear in mind:**
* - Use this plugin to visualize multiple tiny chart only and chart APIs won't work properly.
* - Sparkline chart size will be based on the main chart element size. To control spakrline charts, is highly recommended to set `size` option.
* - Bubble, scatter and Arc(pie, donut, ratdar) types aren't supported.
* - Some options will be stricted to be:
* - `resize.auto = false`
* - `axis.x.show = false`
* - `axis.y.show = false`
* - `axis.y.padding = 10`
* - `legend.show = false`
*
* @class plugin-sparkline
* @param {object} options sparkline plugin options
* @augments Plugin
* @returns {Sparkline}
* @example
* // Plugin must be loaded before the use.
* <script src="$YOUR_PATH/plugin/billboardjs-plugin-sparkline.js"><\/script>
*
* var chart = bb.generate({
* ...
* plugins: [
* new bb.plugin.sparkline({
* selector: ".sparkline"
* }),
* ]
* });
* @example
* import {bb} from "billboard.js";
* import Sparkline from "billboard.js/dist/billboardjs-plugin-sparkline";
*
* bb.generate({
* ...
* plugins: [
* new Sparkline({ ... })
* ]
* })
*/
var Sparkline = class extends Plugin {
static {
this.version = `0.0.1`;
}
constructor(options) {
super(options);
this.config = new Options();
return this;
}
$beforeInit() {
this.loadConfig();
this.validate();
this.element = [].slice.call(document.querySelectorAll(this.config.selector));
this.overrideInternals();
this.overrideOptions();
this.overHandler = this.overHandler.bind(this);
this.moveHandler = this.moveHandler.bind(this);
this.outHandler = this.outHandler.bind(this);
}
validate() {
const { $$, config } = this;
let msg = "";
if (!config.selector || !document.querySelector(config.selector)) msg = "No holder elements found from given selector option.";
if ($$.hasType("bubble") || $$.hasType("scatter") || $$.hasArcType($$.data.targets)) msg = "Contains non supported chart types.";
if (msg) throw new Error(`[Sparkline plugin] ${msg}`);
}
overrideInternals() {
const { $$ } = this;
const { getBarW, getIndices } = $$;
$$.getIndices = function(indices, d, caller) {
return caller === "getShapeX" ? {} : getIndices.call(this, indices, d);
};
$$.getBarW = function(type, axis) {
return getBarW.call(this, type, axis, 1);
};
}
overrideOptions() {
const { config } = this.$$;
config.legend_show = false;
config.resize_auto = false;
config.axis_x_show = false;
if (config.padding !== false) {
const hasOption = (o) => Object.keys(o || {}).length > 0;
if (hasOption(config.axis_x_padding)) config.axis_x_padding = {
left: 15,
right: 15,
unit: "px"
};
if (hasOption(config.axis_y_padding)) config.axis_y_padding = 5;
}
config.axis_y_show = false;
if (!config.tooltip_position) config.tooltip_position = function(data, width, height) {
const { internal: { state: { event } } } = this;
let top = event.pageY - height * 1.35;
let left = event.pageX - width / 2;
if (top < 0) top = 0;
if (left < 0) left = 0;
return {
top,
left
};
};
}
$init() {
const { $$: { $el } } = this;
$el.chart.style("width", "0").style("height", "0").style("pointer-events", "none");
$el.tooltip?.node() && document.body.appendChild($el.tooltip.node());
}
$afterInit() {
const { $$ } = this;
$$.$el.svg.attr("style", null).style("width", "0").style("height", "0");
this.bindEvents(true);
}
/**
* Bind tooltip event handlers for each sparkline elements.
* @param {boolean} bind or unbind
* @private
*/
bindEvents(bind = true) {
const { $$: { config } } = this;
if (config.interaction_enabled && config.tooltip_show) {
const method = `${bind ? "add" : "remove"}EventListener`;
this.element.forEach((el) => {
const svg = el.querySelector("svg");
svg[method]("mouseover", this.overHandler);
svg[method]("mousemove", this.moveHandler);
svg[method]("mouseout", this.outHandler);
});
}
}
overHandler(e) {
const { $$ } = this;
const { state: { eventReceiver } } = $$;
eventReceiver.rect = getBoundingRect(e.target, true);
}
moveHandler(e) {
const { $$ } = this;
const index = $$.getDataIndexFromEvent(e);
const data = $$.api.data(e.target.__id)?.[0];
const d = data?.values?.[index];
if (d && !d.name) d.name = d.id;
$$.state.event = e;
if ($$.isPointFocusOnly?.() && d) $$.showCircleFocus?.([d]);
$$.setExpand(index, data.id, true);
$$.showTooltip([d], e.target);
}
outHandler(e) {
const { $$ } = this;
$$.state.event = e;
$$.isPointFocusOnly() ? $$.hideCircleFocus() : $$.unexpandCircles();
$$.hideTooltip();
}
$redraw() {
const { $$ } = this;
const { $el } = $$;
let el = this.element;
const data = $$.api.data();
const svgWrapper = $el.chart.html().match(/<svg[^>]*>/)?.[0];
if (el.length < data.length) {
const chart = $el.chart.node();
for (let i = data.length - el.length; i > 0; i--) chart.parentNode.insertBefore(el[0].cloneNode(), chart.nextSibling);
this.element = document.querySelectorAll(this.config.selector);
el = this.element;
}
data.map((v) => v.id).forEach((id, i) => {
const selector = `.${$COMMON.target}-${id}`;
const shape = $el.main.selectAll(selector);
let svg = el[i].querySelector("svg");
if (!svg) {
el[i].innerHTML = `${svgWrapper}</svg>`;
svg = el[i].querySelector("svg");
svg.__id = id;
}
if (!svg.querySelector(selector)) shape.style("opacity", null);
shape.style("fill", "none").style("opacity", null);
svg.innerHTML = "";
svg.appendChild(shape.node());
});
}
$willDestroy() {
this.bindEvents(false);
this.element.forEach((el) => {
el.innerHTML = "";
});
}
};
//#endregion
export { Sparkline as default };