From 40871b0062637adf2245ba0d6afcbb81f1d4c55b Mon Sep 17 00:00:00 2001 From: Jukka Kurkela Date: Fri, 19 Jun 2020 00:36:53 +0300 Subject: [PATCH] Parse from custom properties in data (#7489) * Parse from custom properties in data * Resolve CC issues * Review update --- docs/docs/axes/cartesian/time.md | 14 +---- docs/docs/general/data-structures.md | 48 ++++++++++++++++++ docs/docs/getting-started/v3-migration.md | 2 + src/controllers/controller.bar.js | 42 ++++++++------- src/controllers/controller.bubble.js | 6 ++- src/core/core.datasetController.js | 8 +-- src/core/core.scale.js | 15 ------ src/helpers/helpers.core.js | 16 ++++++ src/scales/scale.time.js | 16 ------ .../bar-thickness-no-overlap.json | 10 ++-- test/fixtures/controller.bar/data/parsing.js | 46 +++++++++++++++++ test/fixtures/controller.bar/data/parsing.png | Bin 0 -> 5450 bytes test/fixtures/scale.time/data-ty.js | 5 +- test/specs/core.datasetController.tests.js | 37 ++++++++++++++ test/specs/scale.time.tests.js | 9 ++-- 15 files changed, 194 insertions(+), 80 deletions(-) create mode 100644 test/fixtures/controller.bar/data/parsing.js create mode 100644 test/fixtures/controller.bar/data/parsing.png diff --git a/docs/docs/axes/cartesian/time.md b/docs/docs/axes/cartesian/time.md index 557b1fb41..61b1b2cf2 100644 --- a/docs/docs/axes/cartesian/time.md +++ b/docs/docs/axes/cartesian/time.md @@ -12,17 +12,7 @@ The time scale **requires** both a date library and corresponding adapter to be ### Input Data -The x-axis data points may additionally be specified via the `t` or `x` attribute when using the time scale. - -```javascript -data: [{ - x: new Date(), - y: 1 -}, { - t: new Date(), - y: 10 -}] -``` +See [data structures](../../general/data-structures.md). ### Date Formats @@ -153,7 +143,7 @@ The `bounds` property controls the scale boundary strategy (bypassed by `min`/`m The `ticks.source` property controls the ticks generation. * `'auto'`: generates "optimal" ticks based on scale size and time options -* `'data'`: generates ticks from data (including labels from data `{t|x|y}` objects) +* `'data'`: generates ticks from data (including labels from data `{x|y}` objects) * `'labels'`: generates ticks from user given `labels` ONLY ### Parser diff --git a/docs/docs/general/data-structures.md b/docs/docs/general/data-structures.md index 7e6c7230c..1d53bbe7e 100644 --- a/docs/docs/general/data-structures.md +++ b/docs/docs/general/data-structures.md @@ -29,6 +29,54 @@ data: [{x:'Sales', y:20}, {x:'Revenue', y:10}] This is also the internal format used for parsed data. In this mode, parsing can be disabled by specifying `parsing: false` at chart options or dataset. If parsing is disabled, data must be sorted and in the formats the associated chart type and scales use internally. +## Object[] using custom properties + +```javascript +type: 'bar', +data: { + datasets: [{ + data: [{id: 'Sales', nested: {value: 1500}}, {id: 'Purchases', nested: {value: 500}}] + }] +}, +options: { + parsing: { + xAxisKey: 'id', + yAxisKey: 'nested.value' + } +} +``` + +### `parsing` can also be specified per dataset + +```javascript +const data = [{x: 'Jan', net: 100, cogs: 50, gm: 50}, {x: 'Feb', net: 120, cogs: 55, gm: 75}]; +const cfg = { + type: 'bar', + data: { + labels: ['Jan', 'Feb'], + datasets: [{ + label: 'Net sales', + data: data, + parsing: { + yAxisKey: 'net' + } + }, { + label: 'Cost of goods sold', + data: data, + parsing: { + yAxisKey: 'cogs' + } + }, { + label: 'Gross margin', + data: data, + parsing: { + yAxisKey: 'gm' + } + }] + }, +}; +``` + ## Object ```javascript diff --git a/docs/docs/getting-started/v3-migration.md b/docs/docs/getting-started/v3-migration.md index b5190a50e..c93c0fb01 100644 --- a/docs/docs/getting-started/v3-migration.md +++ b/docs/docs/getting-started/v3-migration.md @@ -32,6 +32,7 @@ A number of changes were made to the configuration options passed to the `Chart` #### Generic changes * Indexable options are now looping. `backgroundColor: ['red', 'green']` will result in alternating `'red'` / `'green'` if there are more than 2 data points. +* The input properties of object data can now be freely specified, see [data structures](../general/data-structures.md) for details. #### Specific changes @@ -61,6 +62,7 @@ A number of changes were made to the configuration options passed to the `Chart` * Dataset options are now configured as `options[type].datasets` rather than `options.datasets[type]` * To override the platform class used in a chart instance, pass `platform: PlatformClass` in the config object. Note that the class should be passed, not an instance of the class. * `aspectRatio` defaults to 1 for doughnut, pie, polarArea, and radar charts +* `TimeScale` does not read `t` from object data by default anymore. The default property is `x` or `y`, depending on the orientation. See [data structures](../general/data-structures.md) for details on how to change the default. #### Defaults diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js index e39d748a7..5afb5b4ed 100644 --- a/src/controllers/controller.bar.js +++ b/src/controllers/controller.bar.js @@ -2,7 +2,7 @@ import DatasetController from '../core/core.datasetController'; import defaults from '../core/core.defaults'; import {Rectangle} from '../elements/index'; import {clipArea, unclipArea} from '../helpers/helpers.canvas'; -import {isArray, isNullOrUndef, valueOrDefault} from '../helpers/helpers.core'; +import {isArray, isNullOrUndef, valueOrDefault, resolveObjectKey} from '../helpers/helpers.core'; import {_limitValue, sign} from '../helpers/helpers.math'; defaults.set('bar', { @@ -120,9 +120,9 @@ function computeFlexCategoryTraits(index, ruler, options) { }; } -function parseFloatBar(arr, item, vScale, i) { - const startValue = vScale.parse(arr[0], i); - const endValue = vScale.parse(arr[1], i); +function parseFloatBar(entry, item, vScale, i) { + const startValue = vScale.parse(entry[0], i); + const endValue = vScale.parse(entry[1], i); const min = Math.min(startValue, endValue); const max = Math.max(startValue, endValue); let barStart = min; @@ -147,6 +147,15 @@ function parseFloatBar(arr, item, vScale, i) { }; } +function parseValue(entry, item, vScale, i) { + if (isArray(entry)) { + parseFloatBar(entry, item, vScale, i); + } else { + item[vScale.axis] = vScale.parse(entry, i); + } + return item; +} + function parseArrayOrPrimitive(meta, data, start, count) { const iScale = meta.iScale; const vScale = meta.vScale; @@ -159,14 +168,7 @@ function parseArrayOrPrimitive(meta, data, start, count) { entry = data[i]; item = {}; item[iScale.axis] = singleScale || iScale.parse(labels[i], i); - - if (isArray(entry)) { - parseFloatBar(entry, item, vScale, i); - } else { - item[vScale.axis] = vScale.parse(entry, i); - } - - parsed.push(item); + parsed.push(parseValue(entry, item, vScale, i)); } return parsed; } @@ -202,20 +204,16 @@ export default class BarController extends DatasetController { */ parseObjectData(meta, data, start, count) { const {iScale, vScale} = meta; - const vProp = vScale.axis; + const {xAxisKey = 'x', yAxisKey = 'y'} = this._parsing; + const iAxisKey = iScale.axis === 'x' ? xAxisKey : yAxisKey; + const vAxisKey = vScale.axis === 'x' ? xAxisKey : yAxisKey; const parsed = []; - let i, ilen, item, obj, value; + let i, ilen, item, obj; for (i = start, ilen = start + count; i < ilen; ++i) { obj = data[i]; item = {}; - item[iScale.axis] = iScale.parseObject(obj, iScale.axis, i); - value = obj[vProp]; - if (isArray(value)) { - parseFloatBar(value, item, vScale, i); - } else { - item[vScale.axis] = vScale.parseObject(obj, vProp, i); - } - parsed.push(item); + item[iScale.axis] = iScale.parse(resolveObjectKey(obj, iAxisKey), i); + parsed.push(parseValue(resolveObjectKey(obj, vAxisKey), item, vScale, i)); } return parsed; } diff --git a/src/controllers/controller.bubble.js b/src/controllers/controller.bubble.js index fdbe5b90f..0fd32fec3 100644 --- a/src/controllers/controller.bubble.js +++ b/src/controllers/controller.bubble.js @@ -2,6 +2,7 @@ import DatasetController from '../core/core.datasetController'; import defaults from '../core/core.defaults'; import {Point} from '../elements/index'; import {resolve} from '../helpers/helpers.options'; +import {resolveObjectKey} from '../helpers/helpers.core'; defaults.set('bubble', { animation: { @@ -36,13 +37,14 @@ export default class BubbleController extends DatasetController { */ parseObjectData(meta, data, start, count) { const {xScale, yScale} = meta; + const {xAxisKey = 'x', yAxisKey = 'y'} = this._parsing; const parsed = []; let i, ilen, item; for (i = start, ilen = start + count; i < ilen; ++i) { item = data[i]; parsed.push({ - x: xScale.parseObject(item, 'x', i), - y: yScale.parseObject(item, 'y', i), + x: xScale.parse(resolveObjectKey(item, xAxisKey), i), + y: yScale.parse(resolveObjectKey(item, yAxisKey), i), _custom: item && item.r && +item.r }); } diff --git a/src/core/core.datasetController.js b/src/core/core.datasetController.js index 76df52bc5..517187154 100644 --- a/src/core/core.datasetController.js +++ b/src/core/core.datasetController.js @@ -1,5 +1,5 @@ import Animations from './core.animations'; -import {isObject, merge, _merger, isArray, valueOrDefault, mergeIf} from '../helpers/helpers.core'; +import {isObject, merge, _merger, isArray, valueOrDefault, mergeIf, resolveObjectKey} from '../helpers/helpers.core'; import {listenArrayEvents, unlistenArrayEvents} from '../helpers/helpers.collection'; import {resolve} from '../helpers/helpers.options'; import {getHoverColor} from '../helpers/helpers.color'; @@ -162,6 +162,7 @@ export default class DatasetController { this._cachedMeta = this.getMeta(); this._type = this._cachedMeta.type; this._config = undefined; + /** @type {boolean | object} */ this._parsing = false; this._data = undefined; this._objectData = undefined; @@ -456,6 +457,7 @@ export default class DatasetController { */ parseObjectData(meta, data, start, count) { const {xScale, yScale} = meta; + const {xAxisKey = 'x', yAxisKey = 'y'} = this._parsing; const parsed = new Array(count); let i, ilen, index, item; @@ -463,8 +465,8 @@ export default class DatasetController { index = i + start; item = data[index]; parsed[i] = { - x: xScale.parseObject(item, 'x', index), - y: yScale.parseObject(item, 'y', index) + x: xScale.parse(resolveObjectKey(item, xAxisKey), index), + y: yScale.parse(resolveObjectKey(item, yAxisKey), index) }; } return parsed; diff --git a/src/core/core.scale.js b/src/core/core.scale.js index c7fc1e45d..898413a48 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -369,21 +369,6 @@ export default class Scale extends Element { return raw; } - /** - * Parse an object for axis to internal representation. - * @param {object} obj - * @param {string} axis - * @param {number} index - * @since 3.0 - * @protected - */ - parseObject(obj, axis, index) { - if (obj[axis] !== undefined) { - return this.parse(obj[axis], index); - } - return null; - } - /** * @return {{min: number, max: number, minDefined: boolean, maxDefined: boolean}} * @protected diff --git a/src/helpers/helpers.core.js b/src/helpers/helpers.core.js index a283ffba3..743e3f02d 100644 --- a/src/helpers/helpers.core.js +++ b/src/helpers/helpers.core.js @@ -260,3 +260,19 @@ export function _deprecated(scope, value, previous, current) { '" is deprecated. Please use "' + current + '" instead'); } } + +export function resolveObjectKey(obj, key) { + if (key.length < 3) { + return obj[key]; + } + const keys = key.split('.'); + for (let i = 0, n = keys.length; i < n; ++i) { + const k = keys[i]; + if (k in obj) { + obj = obj[k]; + } else { + return; + } + } + return obj; +} diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index 2dc8a4cc1..322a7c706 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -596,22 +596,6 @@ class TimeScale extends Scale { return parse(this, raw); } - /** - * @param {object} obj - * @param {string} axis - * @param {number} index - * @return {number|null} - */ - parseObject(obj, axis, index) { - if (obj && obj.t) { - return this.parse(obj.t, index); - } - if (obj[axis] !== undefined) { - return this.parse(obj[axis], index); - } - return null; - } - invalidateCaches() { this._cache = { data: [], diff --git a/test/fixtures/controller.bar/bar-thickness-no-overlap.json b/test/fixtures/controller.bar/bar-thickness-no-overlap.json index 742f9a3e8..be67964a3 100644 --- a/test/fixtures/controller.bar/bar-thickness-no-overlap.json +++ b/test/fixtures/controller.bar/bar-thickness-no-overlap.json @@ -6,11 +6,11 @@ "datasets": [{ "backgroundColor": "#FF6384", "data": [ - {"y": "1", "t": "2016"}, - {"y": "2", "t": "2017"}, - {"y": "3", "t": "2017-08"}, - {"y": "4", "t": "2024"}, - {"y": "5", "t": "2030"} + {"y": "1", "x": "2016"}, + {"y": "2", "x": "2017"}, + {"y": "3", "x": "2017-08"}, + {"y": "4", "x": "2024"}, + {"y": "5", "x": "2030"} ] }] }, diff --git a/test/fixtures/controller.bar/data/parsing.js b/test/fixtures/controller.bar/data/parsing.js new file mode 100644 index 000000000..aecab69b3 --- /dev/null +++ b/test/fixtures/controller.bar/data/parsing.js @@ -0,0 +1,46 @@ +const data = [{x: 'Jan', net: 100, cogs: 50, gm: 50}, {x: 'Feb', net: 120, cogs: 55, gm: 75}]; + +module.exports = { + config: { + type: 'bar', + data: { + labels: ['Jan', 'Feb'], + datasets: [{ + label: 'Net sales', + backgroundColor: 'blue', + data: data, + parsing: { + yAxisKey: 'net' + } + }, { + label: 'Cost of goods sold', + backgroundColor: 'red', + data: data, + parsing: { + yAxisKey: 'cogs' + } + }, { + label: 'Gross margin', + backgroundColor: 'green', + data: data, + parsing: { + yAxisKey: 'gm' + } + }] + }, + options: { + legend: false, + title: false, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/data/parsing.png b/test/fixtures/controller.bar/data/parsing.png new file mode 100644 index 0000000000000000000000000000000000000000..f0a63fd4d52456c70a489f9dbc7d7865b9bf1add GIT binary patch literal 5450 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 p.x); + const parsedYValues = meta._parsed.map(p => p.y); + + expect(meta.data.length).toBe(2); + expect(parsedXValues).toEqual([0, 1]); // label indices + expect(parsedYValues).toEqual([20, 30]); + }); + it('should synchronize metadata when data are inserted or removed and parsing is on', function() { const data = [0, 1, 2, 3, 4, 5]; const chart = acquireChart({ diff --git a/test/specs/scale.time.tests.js b/test/specs/scale.time.tests.js index 5d399eb5b..fe172fd39 100644 --- a/test/specs/scale.time.tests.js +++ b/test/specs/scale.time.tests.js @@ -466,7 +466,7 @@ describe('Time scale tests', function() { data: { datasets: [{ xAxisID: 'x', - data: [{t: '2015-01-01T20:00:00', y: 10}, {t: '2015-01-02T21:00:00', y: 3}] + data: [{x: '2015-01-01T20:00:00', y: 10}, {x: '2015-01-02T21:00:00', y: 3}] }], }, options: { @@ -530,6 +530,7 @@ describe('Time scale tests', function() { }], }, options: { + parsing: {xAxisKey: 't'}, scales: { x: { type: 'time', @@ -697,9 +698,9 @@ describe('Time scale tests', function() { datasets: [ {data: [0, 1, 2, 3, 4, 5]}, {data: [ - {t: '2018', y: 6}, - {t: '2020', y: 7}, - {t: '2043', y: 8} + {x: '2018', y: 6}, + {x: '2020', y: 7}, + {x: '2043', y: 8} ]} ] }, -- 2.47.2