UNPKG

billboard.js

Version:

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

510 lines (443 loc) 12.2 kB
/** * Copyright (c) 2017 ~ present NAVER Corp. * billboard.js project is licensed under the MIT license * @ignore */ import {requestAnimationFrame} from "../browser"; import {sanitize} from "../sanitize"; import { isArray, isDefined, isFunction, isNumber, isObject, isString, notEmpty } from "./type-checks"; /** * Internal helper to iterate over array items and invoke a callback for each valid item * @param {Array} items Array to iterate * @param {function} callback Callback function (item, index) => void * @private */ function _forEachValidItem<T>(items: T[], callback: (item: T, index: number) => void): void { for (let i = 0; i < items.length; i++) { const item = items[i]; if (item !== null && isDefined(item)) { callback(item, i); } } } /** * Get specified key value from object * If default value is given, will return if given key value not found * @param {object} options Source object * @param {string} key Key value * @param {string|number|boolean|object|Array|function|null|undefined} defaultValue Default value * @returns {string|number|boolean|object|Array|function|null|undefined} Option value or default value * @private */ function getOption(options: object, key: string, defaultValue): any { return isDefined(options[key]) ? options[key] : defaultValue; } /** * Check if value exist in the given object * @param {object} dict Target object to be checked * @param {string|number|boolean|object|Array|function|null|undefined} value Value to be checked * @returns {boolean} * @private */ function hasValue(dict: object, value: any): boolean { for (const key in dict) { if (dict[key] === value) return true; } return false; } /** * Call function with arguments * @param {function} fn Function to be called * @param {object|null|undefined} thisArg "this" value for fn * @param {...(string|number|boolean|object|Array|function|null|undefined)} args Arguments for fn * @returns {boolean} true: fn is function, false: fn is not function * @private */ function callFn(fn: unknown, thisArg: any, ...args: any[]): boolean { const isFn = isFunction(fn); isFn && fn.call(thisArg, ...args); return isFn; } /** * Call function after all transitions ends * @param {d3.transition} transition Transition * @param {Fucntion} cb Callback function * @private */ function endall(transition, cb: Function): void { let n = 0; const end = function(...args) { !--n && cb.apply(this, args); }; // if is transition selection if ("duration" in transition) { transition .each(() => ++n) .on("end", end); } else { ++n; transition.call(end); } } /** * Return first letter capitalized * @param {string} str Target string * @returns {string} capitalized string * @private */ const capitalize = (str: string): string => str.charAt(0).toUpperCase() + str.slice(1); /** * Camelize from kebob style string * @param {string} str Target string * @param {string} separator Separator string * @returns {string} camelized string * @private */ function camelize(str: string, separator = "-"): string { return str.split(separator) .map((v, i) => ( i ? v.charAt(0).toUpperCase() + v.slice(1).toLowerCase() : v.toLowerCase() )) .join(""); } /** * Convert to array * @param {object} v Target to be converted * @returns {Array} * @private */ const toArray = (v: CSSStyleDeclaration | any): any => [].slice.call(v); /** * Deep copy object * @param {object} objectN Source object * @returns {object} Cloned object * @private */ function deepClone(...objectN) { const clone = v => { if (isArray(v)) { return v.map(clone); } else if (isObject(v) && v.constructor) { const r = new v.constructor(); for (const k in v) { r[k] = clone(v[k]); } return r; } return v; }; return objectN.map(v => clone(v)) .reduce((a, c) => ( {...a, ...c} )); } /** * Extend target from source object * @param {object} target Target object * @param {object|Array} source Source object * @returns {object} * @private */ function extend(target = {}, source): object { if (isArray(source)) { source.forEach(v => extend(target, v)); } // exclude name with only numbers for (const p in source) { if (/^\d+$/.test(p) || p in target) { continue; } target[p] = source[p]; } return target; } /** * Get unique value from array * @param {Array} data Source data * @returns {Array} Unique array value * @private */ function getUnique(data: any[]): any[] { const isDate = data[0] instanceof Date; const d = Array.from(new Set(isDate ? data.map(Number) : data)); return isDate ? d.map(v => new Date(v)) : d; } /** * Merge array * @param {Array} arr Source array * @returns {Array} * @private */ function mergeArray(arr: any[]): any[] { return arr && arr.length ? arr.reduce((p, c) => p.concat(c)) : []; } /** * Merge object returning new object * @param {object} target Target object * @param {object} objectN Source object * @returns {object} merged target object * @private */ function mergeObj(target: object, ...objectN): any { if (!objectN.length || (objectN.length === 1 && !objectN[0])) { return target; } const source = objectN.shift(); if (isObject(target) && isObject(source)) { Object.keys(source).forEach(key => { if (!/^(__proto__|constructor|prototype)$/i.test(key)) { const value = source[key]; if (value instanceof Date) { target[key] = new Date(value.getTime()); } else if (isObject(value)) { !target[key] && (target[key] = {}); target[key] = mergeObj(target[key], value); } else { target[key] = isArray(value) ? value.concat() : value; } } }); } return mergeObj(target, ...objectN); } /** * Sort value * @param {Array} data value to be sorted * @param {boolean} isAsc true: asc, false: desc * @returns {number|string|Date} sorted date * @private */ function sortValue(data: any[], isAsc = true): any[] { let fn; if (data[0] instanceof Date) { fn = isAsc ? (a, b) => a - b : (a, b) => b - a; } else { if (isAsc && !data.every(isNaN)) { fn = (a, b) => a - b; } else if (!isAsc) { fn = (a, b) => (a > b && -1) || (a < b && 1) || (a === b && 0); } } return data.concat().sort(fn); } /** * Get min/max value * @param {string} type 'min' or 'max' * @param {Array} data Array data value * @returns {number|Date|undefined} * @private */ function getMinMax(type: "min" | "max", data: number[] | Date[] | any): number | Date | undefined | any { let res = data.filter(v => notEmpty(v)); if (res.length) { if (isNumber(res[0])) { let result = type === "min" ? Infinity : -Infinity; for (const v of res) { if (type === "min" ? v < result : v > result) { result = v; } } res = result; } else if (res[0] instanceof Date) { res = sortValue(res, type === "min")[0]; } } else { res = undefined; } return res; } /** * Get range * @param {number} start Start number * @param {number} end End number * @param {number} step Step number * @returns {Array} * @private */ const getRange = (start: number, end: number, step = 1): number[] => { const res: number[] = []; const n = Math.max(0, Math.ceil((end - start) / step)) | 0; for (let i = 0; i < n; i++) { res.push(start + i * step); } return res; }; let _transitionCounter = 0; /** * Return auto-incrementing counter value. * Transition names only need uniqueness, not cryptographic randomness. * @param {boolean} asStr Convert returned value as string * @returns {number|string} * @private */ function getRandom(asStr = true) { const id = ++_transitionCounter; return asStr ? String(id) : id; } /** * Find index based on binary search * @param {Array} arr Data array * @param {number} v Target number to find * @param {number} start Start index of data array * @param {number} end End index of data arr * @param {boolean} isRotated Weather is roted axis * @returns {number} Index number * @private */ function findIndex(arr, v: number, start: number, end: number, isRotated: boolean): number { if (start > end) { return -1; } const mid = Math.floor((start + end) / 2); let {x, w = 0} = arr[mid]; if (isRotated) { x = arr[mid].y; w = arr[mid].h; } if (v >= x && v <= x + w) { return mid; } return v < x ? findIndex(arr, v, start, mid - 1, isRotated) : findIndex(arr, v, mid + 1, end, isRotated); } /** * Process the template & return bound string * @param {string} tpl Template string * @param {object} data Data value to be replaced * @returns {string} * @private */ function tplProcess(tpl: string, data: object): string { return sanitize(tpl.replace(/\{=([^}]+)\}/g, (_, key) => data[key] ?? "")); } /** * Get parsed date value * (It must be called in 'ChartInternal' context) * @param {Date|string|number} date Value of date to be parsed * @returns {Date} * @private */ function parseDate(date: Date | string | number | any): Date { let parsedDate; if (date instanceof Date) { parsedDate = date; } else if (isString(date)) { const {config, format} = this; // if fails to parse, try by new Date() // https://github.com/naver/billboard.js/issues/1714 parsedDate = format.dataTime(config.data_xFormat)(date) ?? new Date(date); } else if (isNumber(date) && !isNaN(date)) { parsedDate = new Date(+date); } if (!parsedDate || isNaN(+parsedDate)) { console && console.error && console.error(`Failed to parse x '${date}' to Date object`); } return parsedDate; } /** * Parse CSS shorthand values (padding, margin, border-radius, etc.) * @param {number|string|object} value Shorthand value(s) * @returns {object} Parsed object with top, right, bottom, left properties * @private */ function parseShorthand( value: number | string | object ): {top: number, right: number, bottom: number, left: number} { if (isObject(value) && !isString(value)) { const obj = value as {top?: number, right?: number, bottom?: number, left?: number}; return { top: obj.top || 0, right: obj.right || 0, bottom: obj.bottom || 0, left: obj.left || 0 }; } const values = (isString(value) ? value.trim().split(/\s+/) : [value]).map(v => +v || 0); const [a, b = a, c = a, d = b] = values; return {top: a, right: b, bottom: c, left: d}; } /** * Run function until given condition function return true * @param {function} fn Function to be executed when condition is true * @param {function(): boolean} conditionFn Condition function to check if condition is true * @private */ function runUntil(fn: Function, conditionFn: Function): void { if (conditionFn() === false) { requestAnimationFrame(() => runUntil(fn, conditionFn)); } else { fn(); } } /** * Convert an array to a Set by applying a key extractor * @param {Array} items Array of items to convert to Set * @param {function} keyFn Function to extract key from each item (item, index) => key. Defaults to identity function * @returns {Set} Set with extracted keys * @private */ function toSet<T, K = T>( items: T[], keyFn: (item: T, index: number) => K = (item => item as unknown as K) ): Set<K> { const set = new Set<K>(); _forEachValidItem(items, (item, i) => { set.add(keyFn(item, i)); }); return set; } /** * Convert an array to a Map by applying key and value extractors * @param {Array} items Array of items to convert to Map * @param {function} keyFn Function to extract key from each item (item, index) => key * @param {function} valueFn Function to extract value from each item (item, index) => value. Defaults to identity function * @returns {Map} Map with extracted keys and values * @private */ function toMap<T, K, V = T>( items: T[], keyFn: (item: T, index: number) => K, valueFn: (item: T, index: number) => V = (item => item as unknown as V) ): Map<K, V> { const map = new Map<K, V>(); _forEachValidItem(items, (item, i) => { map.set(keyFn(item, i), valueFn(item, i)); }); return map; } export { callFn, camelize, capitalize, deepClone, endall, extend, findIndex, getMinMax, getOption, getRandom, getRange, getUnique, hasValue, mergeArray, mergeObj, parseDate, parseShorthand, runUntil, sortValue, toArray, toMap, toSet, tplProcess };