From: Jukka Kurkela Date: Thu, 18 Jun 2020 21:36:53 +0000 (+0300) Subject: Parse from custom properties in data (#7489) X-Git-Tag: v3.0.0-beta.2~81 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=40871b0062637adf2245ba0d6afcbb81f1d4c55b;p=thirdparty%2FChart.js.git Parse from custom properties in data (#7489) * Parse from custom properties in data * Resolve CC issues * Review update --- 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 000000000..f0a63fd4d Binary files /dev/null and b/test/fixtures/controller.bar/data/parsing.png differ diff --git a/test/fixtures/scale.time/data-ty.js b/test/fixtures/scale.time/data-ty.js index be1fa404d..6fff2a486 100644 --- a/test/fixtures/scale.time/data-ty.js +++ b/test/fixtures/scale.time/data-ty.js @@ -30,7 +30,10 @@ module.exports = { t: newDateFromRef(9), y: 5 }], - fill: false + fill: false, + parsing: { + xAxisKey: 't' + } }], }, options: { diff --git a/test/specs/core.datasetController.tests.js b/test/specs/core.datasetController.tests.js index c78b96a05..67aacaa3d 100644 --- a/test/specs/core.datasetController.tests.js +++ b/test/specs/core.datasetController.tests.js @@ -167,6 +167,43 @@ describe('Chart.DatasetController', function() { expect(parsedYValues2).toEqual([0, 1, 2, 3, 0]); // label indices }); + it('should parse using provided keys', function() { + const chart = acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [ + {x: 1, data: {key: 'one', value: 20}}, + {data: {key: 'two', value: 30}} + ] + }] + }, + options: { + parsing: { + xAxisKey: 'data.key', + yAxisKey: 'data.value' + }, + scales: { + x: { + type: 'category', + labels: ['one', 'two'] + }, + y: { + type: 'linear' + }, + } + } + }); + + const meta = chart.getDatasetMeta(0); + const parsedXValues = meta._parsed.map(p => 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} ]} ] },