UNPKG

9.04 kBPlain TextView Raw
1/**
2 * Copyright (c) 2017 ~ present NAVER Corp.
3 * billboard.js project is licensed under the MIT license
4 */
5import {
6 csvParse as d3CsvParse,
7 tsvParse as d3TsvParse,
8 csvParseRows as d3CsvParseRows,
9 tsvParseRows as d3TsvParseRows,
10} from "d3-dsv";
11import {isUndefined, isDefined, isObject, isValue, notEmpty, isArray, capitalize} from "../../module/util";
12
13/**
14 * Data convert
15 * @memberof ChartInternal
16 * @private
17 */
18export default {
19 /**
20 * Convert data according its type
21 * @param {object} args data object
22 * @param {Function} [callback] callback for url(XHR) type loading
23 * @returns {object}
24 * @private
25 */
26 convertData(args, callback: Function): object {
27 let data;
28
29 if (args.bindto) {
30 data = {};
31
32 ["url", "mimeType", "headers", "keys", "json", "keys", "rows", "columns"]
33 .forEach(v => {
34 const key = `data_${v}`;
35
36 if (key in args) {
37 data[v] = args[key];
38 }
39 });
40 } else {
41 data = args;
42 }
43
44 if (data.url && callback) {
45 this.convertUrlToData(data.url, data.mimeType, data.headers, data.keys, callback);
46 } else if (data.json) {
47 data = this.convertJsonToData(data.json, data.keys);
48 } else if (data.rows) {
49 data = this.convertRowsToData(data.rows);
50 } else if (data.columns) {
51 data = this.convertColumnsToData(data.columns);
52 } else if (args.bindto) {
53 throw Error("url or json or rows or columns is required.");
54 }
55
56 return isArray(data) && data;
57 },
58
59 /**
60 * Convert URL data
61 * @param {string} url Remote URL
62 * @param {string} mimeType MIME type string: json | csv | tsv
63 * @param {object} headers Header object
64 * @param {object} keys Key object
65 * @param {Function} done Callback function
66 * @private
67 */
68 convertUrlToData(url: string, mimeType = "csv", headers: object, keys: object, done: Function): void {
69 const req = new XMLHttpRequest();
70
71 req.open("GET", url);
72
73 if (headers) {
74 Object.keys(headers).forEach(key => {
75 req.setRequestHeader(key, headers[key]);
76 });
77 }
78
79 req.onreadystatechange = () => {
80 if (req.readyState === 4) {
81 if (req.status === 200) {
82 const response = req.responseText;
83
84 response && done.call(this,
85 this[`convert${capitalize(mimeType)}ToData`](
86 mimeType === "json" ? JSON.parse(response) : response,
87 keys
88 ));
89 } else {
90 throw new Error(`${url}: Something went wrong loading!`);
91 }
92 }
93 };
94
95 req.send();
96 },
97
98 /**
99 * Convert CSV/TSV data
100 * @param {object} parser Parser object
101 * @param {object} xsv Data
102 * @private
103 * @returns {object}
104 */
105 convertCsvTsvToData(parser, xsv) {
106 const rows = parser.rows(xsv);
107 let d;
108
109 if (rows.length === 1) {
110 d = [{}];
111
112 rows[0].forEach(id => {
113 d[0][id] = null;
114 });
115 } else {
116 d = parser.parse(xsv);
117 }
118
119 return d;
120 },
121
122 convertCsvToData(xsv) {
123 return this.convertCsvTsvToData({
124 rows: d3CsvParseRows,
125 parse: d3CsvParse
126 }, xsv);
127 },
128
129 convertTsvToData(tsv) {
130 return this.convertCsvTsvToData({
131 rows: d3TsvParseRows,
132 parse: d3TsvParse
133 }, tsv);
134 },
135
136 convertJsonToData(json, keysParam) {
137 const {config} = this;
138 const newRows: string[][] = [];
139 let targetKeys: string[];
140 let data;
141
142 if (isArray(json)) {
143 const keys = keysParam || config.data_keys;
144
145 if (keys.x) {
146 targetKeys = keys.value.concat(keys.x);
147 config.data_x = keys.x;
148 } else {
149 targetKeys = keys.value;
150 }
151
152 newRows.push(targetKeys);
153
154 json.forEach(o => {
155 const newRow = targetKeys.map(key => {
156 // convert undefined to null because undefined data will be removed in convertDataToTargets()
157 let v = this.findValueInJson(o, key);
158
159 if (isUndefined(v)) {
160 v = null;
161 }
162
163 return v;
164 });
165
166 newRows.push(newRow);
167 });
168
169 data = this.convertRowsToData(newRows);
170 } else {
171 Object.keys(json).forEach(key => {
172 const tmp = json[key].concat();
173
174 tmp.unshift(key);
175 newRows.push(tmp);
176 });
177
178 data = this.convertColumnsToData(newRows);
179 }
180
181 return data;
182 },
183
184 findValueInJson(object, path) {
185 if (object[path] !== undefined) {
186 return object[path];
187 }
188
189 const convertedPath = path.replace(/\[(\w+)\]/g, ".$1"); // convert indexes to properties (replace [] with .)
190 const pathArray = convertedPath.replace(/^\./, "").split("."); // strip a leading dot
191 let target = object;
192
193 pathArray.some(k => !(
194 target = target && k in target ?
195 target[k] : undefined
196 ));
197
198 return target;
199 },
200
201 convertRowsToData(rows) {
202 const keys = rows[0];
203 const newRows: any[] = [];
204
205 rows.forEach((row, i) => {
206 if (i > 0) {
207 const newRow = {};
208
209 row.forEach((v, j) => {
210 if (isUndefined(v)) {
211 throw new Error(`Source data is missing a component at (${i}, ${j})!`);
212 }
213
214 newRow[keys[j]] = v;
215 });
216
217 newRows.push(newRow);
218 }
219 });
220
221 return newRows;
222 },
223
224 convertColumnsToData(columns) {
225 const newRows: any[] = [];
226
227 columns.forEach((col, i) => {
228 const key = col[0];
229
230 col.forEach((v, j) => {
231 if (j > 0) {
232 if (isUndefined(newRows[j - 1])) {
233 newRows[j - 1] = {};
234 }
235
236 if (isUndefined(v)) {
237 throw new Error(`Source data is missing a component at (${i}, ${j})!`);
238 }
239
240 newRows[j - 1][key] = v;
241 }
242 });
243 });
244
245 return newRows;
246 },
247
248 convertDataToTargets(data, appendXs) {
249 const $$ = this;
250 const {axis, config, state} = $$;
251 let isCategorized = false;
252 let isTimeSeries = false;
253 let isCustomX = false;
254
255 if (axis) {
256 isCategorized = axis.isCategorized();
257 isTimeSeries = axis.isTimeSeries();
258 isCustomX = axis.isCustomX();
259 }
260
261 const dataKeys = Object.keys(data[0] || {});
262 const ids = dataKeys.length ? dataKeys.filter($$.isNotX, $$) : [];
263 const xs = dataKeys.length ? dataKeys.filter($$.isX, $$) : [];
264
265 let xsData;
266
267 // save x for update data by load when custom x and bb.x API
268 ids.forEach(id => {
269 const xKey = this.getXKey(id);
270
271 if (isCustomX || isTimeSeries) {
272 // if included in input data
273 if (xs.indexOf(xKey) >= 0) {
274 xsData = ((appendXs && $$.data.xs[id]) || [])
275 .concat(
276 data.map(d => d[xKey])
277 .filter(isValue)
278 .map((rawX, i) => $$.generateTargetX(rawX, id, i))
279 );
280 } else if (config.data_x) {
281 // if not included in input data, find from preloaded data of other id's x
282 xsData = this.getOtherTargetXs();
283 } else if (notEmpty(config.data_xs)) {
284 // if not included in input data, find from preloaded data
285 xsData = $$.getXValuesOfXKey(xKey, $$.data.targets);
286 }
287 // MEMO: if no x included, use same x of current will be used
288 } else {
289 xsData = data.map((d, i) => i);
290 }
291
292 xsData && (this.data.xs[id] = xsData);
293 });
294
295 // check x is defined
296 ids.forEach(id => {
297 if (!this.data.xs[id]) {
298 throw new Error(`x is not defined for id = "${id}".`);
299 }
300 });
301
302 // convert to target
303 const targets = ids.map((id, index) => {
304 const convertedId = config.data_idConverter.bind($$.api)(id);
305 const xKey = $$.getXKey(id);
306 const isCategory = isCustomX && isCategorized;
307 const hasCategory = isCategory && data.map(v => v.x)
308 .every(v => config.axis_x_categories.indexOf(v) > -1);
309
310 return {
311 id: convertedId,
312 id_org: id,
313 values: data.map((d, i) => {
314 const rawX = d[xKey];
315 let value = d[id];
316 let x;
317
318 value = value !== null && !isNaN(value) && !isObject(value) ?
319 +value : (isArray(value) || isObject(value) ? value : null);
320
321 // use x as categories if custom x and categorized
322 if ((isCategory || state.hasRadar) && index === 0 && !isUndefined(rawX)) {
323 if (!hasCategory && index === 0 && i === 0) {
324 config.axis_x_categories = [];
325 }
326
327 x = config.axis_x_categories.indexOf(rawX);
328
329 if (x === -1) {
330 x = config.axis_x_categories.length;
331 config.axis_x_categories.push(rawX);
332 }
333 } else {
334 x = $$.generateTargetX(rawX, id, i);
335 }
336
337 // mark as x = undefined if value is undefined and filter to remove after mapped
338 if (isUndefined(value) || $$.data.xs[id].length <= i) {
339 x = undefined;
340 }
341
342 return {x, value, id: convertedId};
343 }).filter(v => isDefined(v.x))
344 };
345 });
346
347 // finish targets
348 targets.forEach(t => {
349 // sort values by its x
350 if (config.data_xSort) {
351 t.values = t.values.sort((v1, v2) => {
352 const x1 = v1.x || v1.x === 0 ? v1.x : Infinity;
353 const x2 = v2.x || v2.x === 0 ? v2.x : Infinity;
354
355 return x1 - x2;
356 });
357 }
358
359 // indexing each value
360 t.values.forEach((v, i) => (v.index = i));
361
362 // this needs to be sorted because its index and value.index is identical
363 $$.data.xs[t.id].sort((v1, v2) => v1 - v2);
364 });
365
366 // cache information about values
367 state.hasNegativeValue = $$.hasNegativeValueInTargets(targets);
368 state.hasPositiveValue = $$.hasPositiveValueInTargets(targets);
369
370 // set target types
371 if (config.data_type) {
372 $$.setTargetType($$.mapToIds(targets)
373 .filter(id => !(id in config.data_types)), config.data_type);
374 }
375
376 // cache as original id keyed
377 targets.forEach(d => $$.cache.add(d.id_org, d, true));
378
379 return targets;
380 }
381};