UNPKG

billboard.js

Version:

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

263 lines (227 loc) 7.33 kB
/** * Copyright (c) 2017 ~ present NAVER Corp. * billboard.js project is licensed under the MIT license */ import {isArray, isDefined, isObject, isUndefined, isValue, notEmpty} from "../../module/util"; import {runWorker} from "../../module/worker"; import type {IData} from "../data/IData"; import {columns, json, rows, url} from "./convert.helper"; /** * Get data key for JSON * @param {string|object} keysParam Key params * @param {object} config Config object * @returns {string} Data key * @private */ function getDataKeyForJson(keysParam, config) { const keys = keysParam || config?.data_keys; if (keys?.x) { config.data_x = keys.x; } return keys; } /** * Set `xs` for each id * @param {string[]} ids Ids to set xs * @param {object[]} data Data to set xs from * @param {object} params Parameters for setting xs * @param {boolean} params.appendXs Whether to append xs * @param {string[]} params.xs X keys to set xs from * @param {boolean} params.categorized Whether the axis is categorized * @param {boolean} params.timeSeries Whether the axis is time series * @param {boolean} params.customX Whether the x is custom * @private */ function setXS( ids: string[], data: {[key: string]: number | null}[], params: {appendXs, xs, categorized: boolean, timeSeries: boolean, customX: boolean} ): void { const $$ = this; const {config} = $$; let xsData; ids.forEach(id => { const xKey = $$.getXKey(id); if (params.customX || params.timeSeries) { // if included in input data if (params.xs.indexOf(xKey) >= 0) { xsData = ((params.appendXs && $$.data.xs[id]) || []) .concat( data.map((d, i) => { const rawX = isValue(d[xKey]); return rawX ? $$.generateTargetX(rawX, id, i) : false; }).filter(v => v !== false) ); } else if (config.data_x) { // if not included in input data, find from preloaded data of other id's x xsData = this.getOtherTargetXs(); } else if (notEmpty(config.data_xs)) { // if not included in input data, find from preloaded data xsData = $$.getXValuesOfXKey(xKey, $$.data.targets); } // MEMO: if no x included, use same x of current will be used } else { xsData = data.map((d, i) => i); } if (xsData) { $$.data.xs[id] = xsData; } else { throw new Error(`x is not defined for id = "${id}".`); } }); } /** * Data convert * @memberof ChartInternal * @private */ export default { /** * Convert data according its type * @param {object} args data object * @param {Function} [callback] callback for url(XHR) type loading * @private */ convertData(args, callback: Function): void { const {config} = this; const useWorker = config.boost_useWorker; let data = args; if (args.bindto) { data = {}; ["url", "mimeType", "headers", "keys", "json", "keys", "rows", "columns"] .forEach(v => { const key = `data_${v}`; if (key in args) { data[v] = args[key]; } }); } if (data.url && callback) { url(data.url, data.mimeType, data.headers, getDataKeyForJson(data.keys, config), callback); } else if (data.json) { runWorker(useWorker, json, callback, [columns, rows])( data.json, getDataKeyForJson(data.keys, config) ); } else if (data.rows) { runWorker(useWorker, rows, callback)(data.rows); } else if (data.columns) { runWorker(useWorker, columns, callback)(data.columns); } else if (args.bindto) { throw Error("url or json or rows or columns is required."); } }, /** * Convert data to targets * @param {object[]} data Data to convert * @param {boolean} appendXs Whether to append xs * @returns {IData[]} Converted targets * @private */ convertDataToTargets(data: {[key: string]: number | null}[], appendXs: boolean): IData[] { const $$ = this; const {axis, config, state} = $$; const chartType = config.data_type; const dataKeys = Object.keys(data[0] || {}); // Extract ids and xs from data keys to handle x and non-x values const {ids, xs} = dataKeys.length ? dataKeys.reduce((acc, key) => { if ($$.isX.call($$, key)) { acc.xs.push(key); } else if ($$.isNotX.call($$, key)) { acc.ids.push(key); } return acc; }, {ids: [] as string[], xs: [] as string[]}) : {ids: [], xs: []}; const params = { appendXs, xs, idConverter: config.data_idConverter.bind($$.api), categorized: axis?.isCategorized(), timeSeries: axis?.isTimeSeries(), customX: axis?.isCustomX() }; // save x for update data by load when custom x and bb.x API setXS.bind($$)(ids, data, params); // convert to target const targets = ids.map((id, index) => { const convertedId = config.data_idConverter.bind($$.api)(id); const xKey = $$.getXKey(id); const isCategory = params.customX && params.categorized; const hasCategory = isCategory && (() => { const categorySet = new Set(config.axis_x_categories); return data.every(v => categorySet.has(v.x)); })(); // when .load() with 'append' option is used for indexed axis // @ts-ignore const isDataAppend = data.__append__; const xIndex = xKey === null && isDataAppend ? $$.api.data.values(id).length : 0; return { id: convertedId, id_org: id, values: data.map((d, i) => { const rawX = d[xKey]; let value = d[id]; let x; value = value !== null && !isNaN(value) && !isObject(value) ? +value : (isArray(value) || isObject(value) ? value : null); // use x as categories if custom x and categorized if ((isCategory || state.hasRadar) && index === 0 && !isUndefined(rawX)) { if (!hasCategory && index === 0 && i === 0 && !isDataAppend) { config.axis_x_categories = []; } x = config.axis_x_categories.indexOf(rawX); if (x === -1) { x = config.axis_x_categories.length; config.axis_x_categories.push(rawX); } } else { x = $$.generateTargetX(rawX, id, xIndex + i); } // mark as x = undefined if value is undefined and filter to remove after mapped if (isUndefined(value) || $$.data.xs[id].length <= i) { x = undefined; } return { x, value, id: convertedId, index: -1 }; }).filter(v => isDefined(v.x)) }; }); // finish targets targets.forEach(t => { // sort values by its x if (config.data_xSort) { t.values = t.values.sort((v1, v2) => { const x1 = v1.x || v1.x === 0 ? v1.x : Infinity; const x2 = v2.x || v2.x === 0 ? v2.x : Infinity; return x1 - x2; }); } // indexing each value t.values.forEach((v, i) => (v.index = i)); // this needs to be sorted because its index and value.index is identical $$.data.xs[t.id]?.sort((v1, v2) => v1 - v2); }); // cache information about values state.hasNegativeValue = $$.hasNegativeValueInTargets(targets); state.hasPositiveValue = $$.hasPositiveValueInTargets(targets); // set target types if (chartType && $$.isValidChartType(chartType)) { const targetIds = $$.mapToIds(targets) .filter(id => !(id in config.data_types) || !$$.isValidChartType(config.data_types[id]) ); $$.setTargetType(targetIds, chartType); } // cache as original id keyed targets.forEach(d => $$.cache.add(d.id_org, d, true)); return targets as IData[]; } };