From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Wed, 24 Feb 2021 23:20:11 +0000 (-0800) Subject: Use null for skipped values instead of NaN (#8510) X-Git-Tag: v3.0.0-beta.12~7 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=7c75310a0c048546591745840b0ece15b7868718;p=thirdparty%2FChart.js.git Use null for skipped values instead of NaN (#8510) * Use null for skipped values instead of NaN * Document skipped values when parsing is false * Update src/core/core.datasetController.js Co-authored-by: Jukka Kurkela * Update src/core/core.datasetController.js Co-authored-by: Jukka Kurkela * fix lint issue * use isFinite * revert change checking for pixel values * ternary readability * revert accidental paren movement * test with parsing: false Co-authored-by: Jukka Kurkela --- diff --git a/docs/docs/charts/line.mdx b/docs/docs/charts/line.mdx index e8b6b6822..0536a1cea 100644 --- a/docs/docs/charts/line.mdx +++ b/docs/docs/charts/line.mdx @@ -137,7 +137,7 @@ The style of the line can be controlled with the following properties: | `fill` | How to fill the area under the line. See [area charts](area.md). | `tension` | Bezier curve tension of the line. Set to 0 to draw straightlines. This option is ignored if monotone cubic interpolation is used. | `showLine` | If false, the line is not drawn for this dataset. -| `spanGaps` | If true, lines will be drawn between points with no or null data. If false, points with `NaN` data will create a break in the line. Can also be a number specifying the maximum gap length to span. The unit of the value depends on the scale used. +| `spanGaps` | If true, lines will be drawn between points with no or null data. If false, points with `null` data will create a break in the line. Can also be a number specifying the maximum gap length to span. The unit of the value depends on the scale used. If the value is `undefined`, `showLine` and `spanGaps` fallback to the associated [chart configuration options](#configuration-options). The rest of the values fallback to the associated [`elements.line.*`](../configuration/elements.md#line-configuration) options. @@ -184,7 +184,7 @@ The line chart defines the following configuration options. These options are me | Name | Type | Default | Description | ---- | ---- | ------- | ----------- | `showLine` | `boolean` | `true` | If false, the lines between points are not drawn. -| `spanGaps` | `boolean`\|`number` | `false` | If true, lines will be drawn between points with no or null data. If false, points with `NaN` data will create a break in the line. Can also be a number specifying the maximum gap length to span. The unit of the value depends on the scale used. +| `spanGaps` | `boolean`\|`number` | `false` | If true, lines will be drawn between points with no or null data. If false, points with `null` data will create a break in the line. Can also be a number specifying the maximum gap length to span. The unit of the value depends on the scale used. ## Default Options diff --git a/docs/docs/charts/radar.mdx b/docs/docs/charts/radar.mdx index 689a66b40..414f49daf 100644 --- a/docs/docs/charts/radar.mdx +++ b/docs/docs/charts/radar.mdx @@ -149,7 +149,7 @@ The style of the line can be controlled with the following properties: | `borderWidth` | The line width (in pixels). | `fill` | How to fill the area under the line. See [area charts](area.md). | `tension` | Bezier curve tension of the line. Set to 0 to draw straight lines. -| `spanGaps` | If true, lines will be drawn between points with no or null data. If false, points with `NaN` data will create a break in the line. +| `spanGaps` | If true, lines will be drawn between points with no or null data. If false, points with `null` data will create a break in the line. If the value is `undefined`, `spanGaps` fallback to the associated [chart configuration options](#configuration-options). The rest of the values fallback to the associated [`elements.line.*`](../configuration/elements.md#line-configuration) options. @@ -170,7 +170,7 @@ The radar chart defines the following configuration options. These options are m | Name | Type | Default | Description | ---- | ---- | ------- | ----------- -| `spanGaps` | `boolean` | `false` | If false, NaN data causes a break in the line. +| `spanGaps` | `boolean` | `false` | If false, `null` data causes a break in the line. ## Scale Options diff --git a/docs/docs/configuration/tooltip.md b/docs/docs/configuration/tooltip.md index a4b1a67da..07dc6717a 100644 --- a/docs/docs/configuration/tooltip.md +++ b/docs/docs/configuration/tooltip.md @@ -139,7 +139,7 @@ var chart = new Chart(ctx, { if (label) { label += ': '; } - if (!isNaN(context.parsed.y)) { + if (context.parsed.y !== null) { label += new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(context.parsed.y); } return label; diff --git a/docs/docs/developers/axes.md b/docs/docs/developers/axes.md index 8a60ecb10..2762c38dd 100644 --- a/docs/docs/developers/axes.md +++ b/docs/docs/developers/axes.md @@ -132,13 +132,6 @@ The Core.Scale base class also has some utility functions that you may find usef // Returns true if the scale instance is horizontal isHorizontal: function() {}, - // Get the correct value from the value from this.chart.data.datasets[x].data[] - // If dataValue is an object, returns .x or .y depending on the return of isHorizontal() - // If the value is undefined, returns NaN - // Otherwise returns the value. - // Note that in all cases, the returned value is not guaranteed to be a number - getRightValue: function(dataValue) {}, - // Returns the scale tick objects ({label, major}) getTicks: function() {} } diff --git a/docs/docs/general/data-structures.md b/docs/docs/general/data-structures.md index 8bc65bf18..49c863b6f 100644 --- a/docs/docs/general/data-structures.md +++ b/docs/docs/general/data-structures.md @@ -19,7 +19,7 @@ When the `data` is an array of numbers, values from `labels` array at the same i ## Object[] ```javascript -data: [{x: 10, y: 20}, {x: 15, y: 10}] +data: [{x: 10, y: 20}, {x: 15, y: null}, {x: 20, y: 10}] ``` ```javascript @@ -32,7 +32,7 @@ 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. -The values provided must be parsable by the associated scales or in the internal format of the associated scales. A common mistake would be to provide integers for the `category` scale, which uses integers as an internal format, where each integer represents an index in the labels array. +The values provided must be parsable by the associated scales or in the internal format of the associated scales. A common mistake would be to provide integers for the `category` scale, which uses integers as an internal format, where each integer represents an index in the labels array. `null` can be used for skipped values. ## Object[] using custom properties diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js index 1782d9127..94d7a5313 100644 --- a/src/controllers/controller.bar.js +++ b/src/controllers/controller.bar.js @@ -492,7 +492,7 @@ export default class BarController extends DatasetController { clipArea(chart.ctx, chart.chartArea); for (; i < ilen; ++i) { - if (!isNaN(me.getParsed(i)[vScale.axis])) { + if (me.getParsed(i)[vScale.axis] !== null) { rects[i].draw(me._ctx); } } diff --git a/src/controllers/controller.doughnut.js b/src/controllers/controller.doughnut.js index 6c5753153..ef38f0225 100644 --- a/src/controllers/controller.doughnut.js +++ b/src/controllers/controller.doughnut.js @@ -138,14 +138,17 @@ export default class DoughnutController extends DatasetController { } /** - * @private - */ + * @private + */ _circumference(i, reset) { const me = this; const opts = me.options; const meta = me._cachedMeta; const circumference = me._getCircumference(); - return reset && opts.animation.animateRotate ? 0 : this.chart.getDataVisibility(i) ? me.calculateCircumference(meta._parsed[i] * circumference / TAU) : 0; + if ((reset && opts.animation.animateRotate) || !this.chart.getDataVisibility(i) || meta._parsed[i] === null) { + return 0; + } + return me.calculateCircumference(meta._parsed[i] * circumference / TAU); } updateElements(arcs, start, count, mode) { @@ -200,7 +203,7 @@ export default class DoughnutController extends DatasetController { for (i = 0; i < metaData.length; i++) { const value = meta._parsed[i]; - if (!isNaN(value) && this.chart.getDataVisibility(i)) { + if (value !== null && this.chart.getDataVisibility(i)) { total += Math.abs(value); } } diff --git a/src/core/core.datasetController.js b/src/core/core.datasetController.js index 07b1795ba..7413ec954 100644 --- a/src/core/core.datasetController.js +++ b/src/core/core.datasetController.js @@ -1,6 +1,6 @@ import Animations from './core.animations'; import defaults from './core.defaults'; -import {isObject, isArray, valueOrDefault, resolveObjectKey, defined} from '../helpers/helpers.core'; +import {isArray, isFinite, isObject, valueOrDefault, resolveObjectKey, defined} from '../helpers/helpers.core'; import {listenArrayEvents, unlistenArrayEvents} from '../helpers/helpers.collection'; import {sign} from '../helpers/helpers.math'; @@ -70,6 +70,10 @@ function applyStack(stack, value, dsIndex, allOther) { const keys = stack.keys; let i, ilen, datasetIndex, otherValue; + if (value === null) { + return; + } + for (i = 0, ilen = keys.length; i < ilen; ++i) { datasetIndex = +keys[i]; if (datasetIndex === dsIndex) { @@ -79,7 +83,7 @@ function applyStack(stack, value, dsIndex, allOther) { break; } otherValue = stack.values[datasetIndex]; - if (!isNaN(otherValue) && (value === 0 || sign(value) === sign(otherValue))) { + if (isFinite(otherValue) && (value === 0 || sign(value) === sign(otherValue))) { value += otherValue; } } @@ -393,7 +397,7 @@ export default class DatasetController { parsed = me.parsePrimitiveData(meta, data, start, count); } - const isNotInOrderComparedToPrev = () => isNaN(cur[iAxis]) || (prev && cur[iAxis] < prev[iAxis]); + const isNotInOrderComparedToPrev = () => cur[iAxis] === null || (prev && cur[iAxis] < prev[iAxis]); for (i = 0; i < count; ++i) { meta._parsed[i + start] = cur = parsed[i]; if (sorted) { @@ -528,7 +532,8 @@ export default class DatasetController { * @protected */ updateRangeFromParsed(range, scale, parsed, stack) { - let value = parsed[scale.axis]; + const parsedValue = parsed[scale.axis]; + let value = parsedValue === null ? NaN : parsedValue; const values = stack && parsed._stacks[scale.axis]; if (stack && values) { stack.values = values; @@ -536,7 +541,7 @@ export default class DatasetController { // in addition to the stacked value range.min = Math.min(range.min, value); range.max = Math.max(range.max, value); - value = applyStack(stack, value, this._cachedMeta.index, true); + value = applyStack(stack, parsedValue, this._cachedMeta.index, true); } range.min = Math.min(range.min, value); range.max = Math.max(range.max, value); @@ -561,7 +566,7 @@ export default class DatasetController { parsed = _parsed[i]; value = parsed[scale.axis]; otherValue = parsed[otherScale.axis]; - return (isNaN(value) || isNaN(otherValue) || otherMin > otherValue || otherMax < otherValue); + return (!isFinite(value) || !isFinite(otherValue) || otherMin > otherValue || otherMax < otherValue); } for (i = 0; i < ilen; ++i) { @@ -594,7 +599,7 @@ export default class DatasetController { for (i = 0, ilen = parsed.length; i < ilen; ++i) { value = parsed[i][scale.axis]; - if (!isNaN(value)) { + if (isFinite(value)) { values.push(value); } } diff --git a/src/scales/scale.linear.js b/src/scales/scale.linear.js index ece759902..e9617fb19 100644 --- a/src/scales/scale.linear.js +++ b/src/scales/scale.linear.js @@ -31,7 +31,7 @@ export default class LinearScale extends LinearScaleBase { // Utils getPixelForValue(value) { - return this.getPixelForDecimal((value - this._startValue) / this._valueRange); + return value === null ? NaN : this.getPixelForDecimal((value - this._startValue) / this._valueRange); } getValueForPixel(pixel) { diff --git a/src/scales/scale.linearbase.js b/src/scales/scale.linearbase.js index 9c669aa62..fe4391568 100644 --- a/src/scales/scale.linearbase.js +++ b/src/scales/scale.linearbase.js @@ -115,10 +115,10 @@ export default class LinearScaleBase extends Scale { parse(raw, index) { // eslint-disable-line no-unused-vars if (isNullOrUndef(raw)) { - return NaN; + return null; } if ((typeof raw === 'number' || raw instanceof Number) && !isFinite(+raw)) { - return NaN; + return null; } return +raw; diff --git a/src/scales/scale.logarithmic.js b/src/scales/scale.logarithmic.js index 5dccaf792..f88b02c2d 100644 --- a/src/scales/scale.logarithmic.js +++ b/src/scales/scale.logarithmic.js @@ -64,7 +64,7 @@ export default class LogarithmicScale extends Scale { this._zero = true; return undefined; } - return isFinite(value) && value > 0 ? value : NaN; + return isFinite(value) && value > 0 ? value : null; } determineDataLimits() { diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index 52ead3523..04df0ae1d 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -47,7 +47,7 @@ function sorter(a, b) { */ function parse(scale, input) { if (isNullOrUndef(input)) { - return NaN; + return null; } const adapter = scale._adapter; @@ -67,7 +67,7 @@ function parse(scale, input) { } if (value === null) { - return NaN; + return null; } if (round) { @@ -244,7 +244,7 @@ export default class TimeScale extends Scale { */ parse(raw, index) { // eslint-disable-line no-unused-vars if (raw === undefined) { - return NaN; + return null; } return parse(this, raw); } @@ -489,7 +489,7 @@ export default class TimeScale extends Scale { */ getDecimalForValue(value) { const me = this; - return (value - me.min) / (me.max - me.min); + return value === null ? NaN : (value - me.min) / (me.max - me.min); } /** diff --git a/test/fixtures/element.line/skip/middle-span.js b/test/fixtures/element.line/skip/middle-span.js index 8abfa4c34..0df72aca9 100644 --- a/test/fixtures/element.line/skip/middle-span.js +++ b/test/fixtures/element.line/skip/middle-span.js @@ -1,10 +1,11 @@ module.exports = { config: { type: 'line', + parsing: false, data: { datasets: [ { - data: [{x: 0, y: 10}, {x: 5, y: 0}, {x: NaN, y: -10}, {x: 19, y: -5}], + data: [{x: 0, y: 10}, {x: 5, y: 0}, {x: null, y: -10}, {x: 19, y: -5}], borderColor: 'red', fill: true, spanGaps: true,