From d867f455b52482dfbea0f0a4b260460b8e9072b7 Mon Sep 17 00:00:00 2001 From: Anthony Pessy Date: Thu, 12 Apr 2018 03:15:27 +0200 Subject: [PATCH] Keep ordered keys as separate property in pivot format returned by convert methods. (#2314) This fixes the issue where targets order would not be kept when some of them have numerical values (ex: "1", "2", ..). --- spec/data.convert.js | 465 +++++++++++++++++++++++++++++++++++-------- src/data.convert.js | 54 +++-- 2 files changed, 415 insertions(+), 104 deletions(-) diff --git a/spec/data.convert.js b/spec/data.convert.js index d5d38bb..9182798 100644 --- a/spec/data.convert.js +++ b/spec/data.convert.js @@ -1,97 +1,392 @@ import c3 from '../src'; const $$ = c3.chart.internal.fn; +$$.d3 = require("d3"); -describe('$$.convertColumnsToData', () => { - it('converts column data to normalized data', () => { - const data = $$.convertColumnsToData([ - ["cat1", "a", "b", "c", "d"], - ["data1", 30, 200, 100, 400], - ["cat2", "b", "a", "c", "d", "e", "f"], - ["data2", 400, 60, 200, 800, 10, 10] - ]); - - expect(data).toEqual([{ - cat1: 'a', - data1: 30, - cat2: 'b', - data2: 400 - }, { - cat1: 'b', - data1: 200, - cat2: 'a', - data2: 60 - }, { - cat1: 'c', - data1: 100, - cat2: 'c', - data2: 200 - }, { - cat1: 'd', - data1: 400, - cat2: 'd', - data2: 800 - }, { - cat2: 'e', - data2: 10 - }, { - cat2: 'f', - data2: 10 - }]); +describe('data.convert', () => { + + describe('$$.convertColumnsToData', () => { + it('converts column data to normalized data', () => { + const data = $$.convertColumnsToData([ + ["cat1", "a", "b", "c", "d"], + ["data1", 30, 200, 100, 400], + ["cat2", "b", "a", "c", "d", "e", "f"], + ["data2", 400, 60, 200, 800, 10, 10] + ]); + + expect(data).toEqual({ + keys: [ 'cat1', 'data1', 'cat2', 'data2' ], + rows: [{ + cat1: 'a', + data1: 30, + cat2: 'b', + data2: 400 + }, { + cat1: 'b', + data1: 200, + cat2: 'a', + data2: 60 + }, { + cat1: 'c', + data1: 100, + cat2: 'c', + data2: 200 + }, { + cat1: 'd', + data1: 400, + cat2: 'd', + data2: 800 + }, { + cat2: 'e', + data2: 10 + }, { + cat2: 'f', + data2: 10 + }] + }); + }); + + it('throws when the column data contains undefined', () => { + expect(() => $$.convertColumnsToData([ + ["cat1", "a", "b", "c", "d"], + ["data1", undefined] + ])).toThrowError(Error, /Source data is missing a component/); + }); }); - it('throws when the column data contains undefined', () => { - expect(() => $$.convertColumnsToData([ - ["cat1", "a", "b", "c", "d"], - ["data1", undefined] - ])).toThrowError(Error, /Source data is missing a component/); + describe('$$.convertRowsToData', () => { + it('converts the row data to normalized data', () => { + const data = $$.convertRowsToData([ + ['data1', 'data2', 'data3'], + [90, 120, 300], + [40, 160, 240], + [50, 200, 290], + [120, 160, 230], + [80, 130, 300], + [90, 220, 320] + ]); + + expect(data).toEqual({ + keys: ['data1', 'data2', 'data3'], + rows: [{ + data1: 90, + data2: 120, + data3: 300 + }, { + data1: 40, + data2: 160, + data3: 240 + }, { + data1: 50, + data2: 200, + data3: 290 + }, { + data1: 120, + data2: 160, + data3: 230 + }, { + data1: 80, + data2: 130, + data3: 300 + }, { + data1: 90, + data2: 220, + data3: 320 + }] + }); + }); + + it('throws when the row data contains undefined', () => { + expect(() => $$.convertRowsToData([ + ['data1', 'data2', 'data3'], + [40, 160, 240], + [90, 120, undefined] + ])).toThrowError(Error, /Source data is missing a component/); + }); }); -}); -describe('$$.convertRowsToData', () => { - it('converts the row data to normalized data', () => { - const data = $$.convertRowsToData([ - ['data1', 'data2', 'data3'], - [90, 120, 300], - [40, 160, 240], - [50, 200, 290], - [120, 160, 230], - [80, 130, 300], - [90, 220, 320] - ]); - - expect(data).toEqual([{ - data1: 90, - data2: 120, - data3: 300 - }, { - data1: 40, - data2: 160, - data3: 240 - }, { - data1: 50, - data2: 200, - data3: 290 - }, { - data1: 120, - data2: 160, - data3: 230 - }, { - data1: 80, - data2: 130, - data3: 300 - }, { - data1: 90, - data2: 220, - data3: 320 - }]); + describe('$$.convertCsvToData', () => { + it('converts the csv data to normalized data', () => { + const data = $$.convertCsvToData(`data1,data2,data3 +90,120,300 +40,160,240 +50,200,290 +120,160,230 +80,130,300 +90,220,320`); + + expect(data).toEqual({ + keys: ['data1', 'data2', 'data3'], + rows: [{ + data1: '90', + data2: '120', + data3: '300' + }, { + data1: '40', + data2: '160', + data3: '240' + }, { + data1: '50', + data2: '200', + data3: '290' + }, { + data1: '120', + data2: '160', + data3: '230' + }, { + data1: '80', + data2: '130', + data3: '300' + }, { + data1: '90', + data2: '220', + data3: '320' + }] + }); + }); + + it('converts one lined CSV data', () => { + const data = $$.convertCsvToData(`data1,data2,data3`); + + expect(data).toEqual({ + keys: ['data1', 'data2', 'data3'], + rows: [{ + data1: null, + data2: null, + data3: null + }] + }); + }); }); - it('throws when the row data contains undefined', () => { - expect(() => $$.convertRowsToData([ - ['data1', 'data2', 'data3'], - [40, 160, 240], - [90, 120, undefined] - ])).toThrowError(Error, /Source data is missing a component/); + describe('$$.convertTsvToData', () => { + it('converts the tsv data to normalized data', () => { + const data = $$.convertTsvToData(`data1\tdata2\tdata3 +90\t120\t300 +40\t160\t240 +50\t200\t290 +120\t160\t230 +80\t130\t300 +90\t220\t320`); + + expect(data).toEqual({ + keys: ['data1', 'data2', 'data3'], + rows: [{ + data1: '90', + data2: '120', + data3: '300' + }, { + data1: '40', + data2: '160', + data3: '240' + }, { + data1: '50', + data2: '200', + data3: '290' + }, { + data1: '120', + data2: '160', + data3: '230' + }, { + data1: '80', + data2: '130', + data3: '300' + }, { + data1: '90', + data2: '220', + data3: '320' + }] + }); + }); + + it('converts one lined TSV data', () => { + const data = $$.convertTsvToData(`data1\tdata2\tdata3`); + + expect(data).toEqual({ + keys: ['data1', 'data2', 'data3'], + rows: [{ + data1: null, + data2: null, + data3: null + }] + }); + }); + }); + + describe('$$.convertDataToTargets', () => { + + beforeEach(() => { + $$.cache = {}; + + $$.data = { + xs: [] + }; + + $$.config = { + data_idConverter: (v) => v + }; + }); + + it('converts the legacy data format into targets', () => { + const targets = $$.convertDataToTargets([ { + data1: 90, + data2: 120, + data3: 300 + }, { + data1: 40, + data2: 160, + data3: 240 + } ]); + + expect(targets).toEqual([{ + id: 'data1', + id_org: 'data1', + values: [ { x: 0, value: 90, id: 'data1', index: 0 }, { x: 1, value: 40, id: 'data1', index: 1 } ] + }, { + id: 'data2', + id_org: 'data2', + values: [ { x: 0, value: 120, id: 'data2', index: 0 }, { x: 1, value: 160, id: 'data2', index: 1 } ] + }, { + id: 'data3', + id_org: 'data3', + values: [ { x: 0, value: 300, id: 'data3', index: 0 }, { x: 1, value: 240, id: 'data3', index: 1 } ] + }]); + }); + + it('converts the data into targets', () => { + const targets = $$.convertDataToTargets({ + keys: [ 'data1', 'data2', 'data3' ], + rows: [ { + data1: 90, + data2: 120, + data3: 300 + }, { + data1: 40, + data2: 160, + data3: 240 + } ] + }); + + expect(targets).toEqual([{ + id: 'data1', + id_org: 'data1', + values: [ { x: 0, value: 90, id: 'data1', index: 0 }, { x: 1, value: 40, id: 'data1', index: 1 } ] + }, { + id: 'data2', + id_org: 'data2', + values: [ { x: 0, value: 120, id: 'data2', index: 0 }, { x: 1, value: 160, id: 'data2', index: 1 } ] + }, { + id: 'data3', + id_org: 'data3', + values: [ { x: 0, value: 300, id: 'data3', index: 0 }, { x: 1, value: 240, id: 'data3', index: 1 } ] + }]); + }); + + }); + + describe('$$.convertJsonToData', () => { + + it('converts JSON as object (no keys provided)', () => { + const data = $$.convertJsonToData({ + data1: [ 90, 40, 50, 120, 80, 90 ], + data2: [ 120, 160, 200, 160, 130, 220 ], + data3: [ 300, 240, 290, 230, 300, 320 ] + }); + + expect(data).toEqual({ + keys: ['data1', 'data2', 'data3'], + rows: [{ + data1: 90, + data2: 120, + data3: 300 + }, { + data1: 40, + data2: 160, + data3: 240 + }, { + data1: 50, + data2: 200, + data3: 290 + }, { + data1: 120, + data2: 160, + data3: 230 + }, { + data1: 80, + data2: 130, + data3: 300 + }, { + data1: 90, + data2: 220, + data3: 320 + }] + }); + }); + + it('converts JSON as rows (keys provided)', () => { + const data = $$.convertJsonToData([{ + data1: 90, + data2: 120, + data3: 300, + unused: 42 + }, { + data1: 40, + data2: 160, + data3: 240, + unused: 42 + }, { + data1: 50, + data2: 200, + data3: 290, + unused: 42 + }, { + data1: 120, + data2: 160, + data3: 230, + unused: 42 + }, { + data1: 80, + data2: 130, + data3: 300, + unused: 42 + }, { + data1: 90, + data2: 220, + data3: 320, + unused: 42 + }], { + value: [ 'data1', 'data2', 'data3' ] + }); + + expect(data).toEqual({ + keys: ['data1', 'data2', 'data3'], + rows: [{ + data1: 90, + data2: 120, + data3: 300 + }, { + data1: 40, + data2: 160, + data3: 240 + }, { + data1: 50, + data2: 200, + data3: 290 + }, { + data1: 120, + data2: 160, + data3: 230 + }, { + data1: 80, + data2: 130, + data3: 300 + }, { + data1: 90, + data2: 220, + data3: 320 + }] + }); + }); + }); }); diff --git a/src/data.convert.js b/src/data.convert.js index f3e435e..9ef9fc9 100644 --- a/src/data.convert.js +++ b/src/data.convert.js @@ -1,5 +1,5 @@ import { c3_chart_internal_fn } from './core'; -import { isValue, isUndefined, isDefined, notEmpty } from './util'; +import { isValue, isUndefined, isDefined, notEmpty, isArray } from './util'; c3_chart_internal_fn.convertUrlToData = function (url, mimeType, headers, keys, done) { var $$ = this, type = mimeType ? mimeType : 'csv'; @@ -26,22 +26,20 @@ c3_chart_internal_fn.convertUrlToData = function (url, mimeType, headers, keys, }); }; c3_chart_internal_fn.convertXsvToData = function (xsv, parser) { - var rows = parser(xsv), d; - if (rows.length === 1) { - d = [{}]; - rows[0].forEach(function (id) { - d[0][id] = null; - }); + var [ keys, ...rows ] = parser.parseRows(xsv); + if (rows.length === 0) { + return { keys, rows: [ keys.reduce((row, key) => Object.assign(row, { [key]: null }), {}) ] }; } else { - d = parser(xsv); + // [].concat() is to convert result into a plain array otherwise + // test is not happy because rows have properties. + return { keys, rows: [].concat(parser.parse(xsv)) }; } - return d; }; c3_chart_internal_fn.convertCsvToData = function (csv) { - return this.convertXsvToData(csv, this.d3.csvParse); + return this.convertXsvToData(csv, { parse: this.d3.csvParse, parseRows: this.d3.csvParseRows }); }; c3_chart_internal_fn.convertTsvToData = function (tsv) { - return this.convertXsvToData(tsv, this.d3.tsvParse); + return this.convertXsvToData(tsv, { parse: this.d3.tsvParse, parseRows: this.d3.tsvParseRows }); }; c3_chart_internal_fn.convertJsonToData = function (json, keys) { var $$ = this, @@ -93,7 +91,7 @@ c3_chart_internal_fn.findValueInJson = function (object, path) { /** * Converts the rows to normalized data. * @param {any[][]} rows The row data - * @return {Object[]} + * @return {Object} */ c3_chart_internal_fn.convertRowsToData = (rows) => { const newRows = []; @@ -109,16 +107,17 @@ c3_chart_internal_fn.convertRowsToData = (rows) => { } newRows.push(newRow); } - return newRows; + return { keys, rows: newRows }; }; /** * Converts the columns to normalized data. * @param {any[][]} columns The column data - * @return {Object[]} + * @return {Object} */ c3_chart_internal_fn.convertColumnsToData = (columns) => { const newRows = []; + const keys = []; for (let i = 0; i < columns.length; i++) { const key = columns[i][0]; @@ -131,16 +130,33 @@ c3_chart_internal_fn.convertColumnsToData = (columns) => { } newRows[j - 1][key] = columns[i][j]; } + keys.push(key); } - return newRows; + return { keys, rows: newRows }; }; +/** + * Converts the data format into the target format. + * @param {!Object} data + * @param {!Array} data.keys Ordered list of target IDs. + * @param {!Array} data.rows Rows of data to convert. + * @param {boolean} appendXs True to append to $$.data.xs, False to replace. + * @return {!Array} + */ c3_chart_internal_fn.convertDataToTargets = function (data, appendXs) { - var $$ = this, config = $$.config, - ids = $$.d3.keys(data[0]).filter($$.isNotX, $$), - xs = $$.d3.keys(data[0]).filter($$.isX, $$), - targets; + var $$ = this, config = $$.config, targets, ids, xs, keys; + + // handles format where keys are not orderly provided + if (isArray(data)) { + keys = Object.keys(data[ 0 ]); + } else { + keys = data.keys; + data = data.rows; + } + + ids = keys.filter($$.isNotX, $$); + xs = keys.filter($$.isX, $$); // save x for update data by load when custom x and c3.x API ids.forEach(function (id) {