UNPKG

billboard.js

Version:

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

387 lines (315 loc) 9.27 kB
/** * Copyright (c) 2017 ~ present NAVER Corp. * billboard.js project is licensed under the MIT license */ import { csvParse as d3CsvParse, tsvParse as d3TsvParse, csvParseRows as d3CsvParseRows, tsvParseRows as d3TsvParseRows, } from "d3-dsv"; import {isUndefined, isDefined, isObject, isValue, notEmpty, isArray, capitalize} from "../../module/util"; /** * 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 * @returns {object} * @private */ convertData(args, callback: Function): any[] | false { let data; 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]; } }); } else { data = args; } if (data.url && callback) { this.convertUrlToData(data.url, data.mimeType, data.headers, data.keys, callback); } else if (data.json) { data = this.convertJsonToData(data.json, data.keys); } else if (data.rows) { data = this.convertRowsToData(data.rows); } else if (data.columns) { data = this.convertColumnsToData(data.columns); } else if (args.bindto) { throw Error("url or json or rows or columns is required."); } return isArray(data) && data; }, /** * Convert URL data * @param {string} url Remote URL * @param {string} mimeType MIME type string: json | csv | tsv * @param {object} headers Header object * @param {object} keys Key object * @param {Function} done Callback function * @private */ convertUrlToData(url: string, mimeType = "csv", headers: object, keys: object, done: Function): void { const req = new XMLHttpRequest(); req.open("GET", url); if (headers) { Object.keys(headers).forEach(key => { req.setRequestHeader(key, headers[key]); }); } req.onreadystatechange = () => { if (req.readyState === 4) { if (req.status === 200) { const response = req.responseText; response && done.call(this, this[`convert${capitalize(mimeType)}ToData`]( mimeType === "json" ? JSON.parse(response) : response, keys )); } else { throw new Error(`${url}: Something went wrong loading!`); } } }; req.send(); }, /** * Convert CSV/TSV data * @param {object} parser Parser object * @param {object} xsv Data * @private * @returns {object} */ convertCsvTsvToData(parser, xsv) { const rows = parser.rows(xsv); let d; if (rows.length === 1) { d = [{}]; rows[0].forEach(id => { d[0][id] = null; }); } else { d = parser.parse(xsv); } return d; }, convertCsvToData(xsv) { return this.convertCsvTsvToData({ rows: d3CsvParseRows, parse: d3CsvParse }, xsv); }, convertTsvToData(tsv) { return this.convertCsvTsvToData({ rows: d3TsvParseRows, parse: d3TsvParse }, tsv); }, convertJsonToData(json, keysParam) { const {config} = this; const newRows: string[][] = []; let targetKeys: string[]; let data; if (isArray(json)) { const keys = keysParam || config.data_keys; if (keys.x) { targetKeys = keys.value.concat(keys.x); config.data_x = keys.x; } else { targetKeys = keys.value; } newRows.push(targetKeys); json.forEach(o => { const newRow = targetKeys.map(key => { // convert undefined to null because undefined data will be removed in convertDataToTargets() let v = this.findValueInJson(o, key); if (isUndefined(v)) { v = null; } return v; }); newRows.push(newRow); }); data = this.convertRowsToData(newRows); } else { Object.keys(json).forEach(key => { const tmp = json[key].concat(); tmp.unshift(key); newRows.push(tmp); }); data = this.convertColumnsToData(newRows); } return data; }, findValueInJson(object, path) { if (object[path] !== undefined) { return object[path]; } const convertedPath = path.replace(/\[(\w+)\]/g, ".$1"); // convert indexes to properties (replace [] with .) const pathArray = convertedPath.replace(/^\./, "").split("."); // strip a leading dot let target = object; pathArray.some(k => !( target = target && k in target ? target[k] : undefined )); return target; }, convertRowsToData(rows) { const keys = rows[0]; const newRows: any[] = []; rows.forEach((row, i) => { if (i > 0) { const newRow = {}; row.forEach((v, j) => { if (isUndefined(v)) { throw new Error(`Source data is missing a component at (${i}, ${j})!`); } newRow[keys[j]] = v; }); newRows.push(newRow); } }); return newRows; }, convertColumnsToData(columns) { const newRows: any[] = []; columns.forEach((col, i) => { const key = col[0]; col.forEach((v, j) => { if (j > 0) { if (isUndefined(newRows[j - 1])) { newRows[j - 1] = {}; } if (isUndefined(v)) { throw new Error(`Source data is missing a component at (${i}, ${j})!`); } newRows[j - 1][key] = v; } }); }); return newRows; }, convertDataToTargets(data, appendXs) { const $$ = this; const {axis, config, state} = $$; let isCategorized = false; let isTimeSeries = false; let isCustomX = false; if (axis) { isCategorized = axis.isCategorized(); isTimeSeries = axis.isTimeSeries(); isCustomX = axis.isCustomX(); } const dataKeys = Object.keys(data[0] || {}); const ids = dataKeys.length ? dataKeys.filter($$.isNotX, $$) : []; const xs = dataKeys.length ? dataKeys.filter($$.isX, $$) : []; let xsData; // save x for update data by load when custom x and bb.x API ids.forEach(id => { const xKey = this.getXKey(id); if (isCustomX || isTimeSeries) { // if included in input data if (xs.indexOf(xKey) >= 0) { xsData = ((appendXs && $$.data.xs[id]) || []) .concat( data.map(d => d[xKey]) .filter(isValue) .map((rawX, i) => $$.generateTargetX(rawX, id, i)) ); } 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); } xsData && (this.data.xs[id] = xsData); }); // check x is defined ids.forEach(id => { if (!this.data.xs[id]) { throw new Error(`x is not defined for id = "${id}".`); } }); // convert to target const targets = ids.map((id, index) => { const convertedId = config.data_idConverter.bind($$.api)(id); const xKey = $$.getXKey(id); const isCategory = isCustomX && isCategorized; const hasCategory = isCategory && data.map(v => v.x) .every(v => config.axis_x_categories.indexOf(v) > -1); // when .load() with 'append' option is used for indexed axis 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}; }).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 (config.data_type) { $$.setTargetType($$.mapToIds(targets) .filter(id => !(id in config.data_types)), config.data_type); } // cache as original id keyed targets.forEach(d => $$.cache.add(d.id_org, d, true)); return targets; } };