]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Use null for skipped values instead of NaN (#8510)
authorBen McCann <322311+benmccann@users.noreply.github.com>
Wed, 24 Feb 2021 23:20:11 +0000 (15:20 -0800)
committerGitHub <noreply@github.com>
Wed, 24 Feb 2021 23:20:11 +0000 (18:20 -0500)
* 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 <jukka.kurkela@gmail.com>
* Update src/core/core.datasetController.js

Co-authored-by: Jukka Kurkela <jukka.kurkela@gmail.com>
* 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 <jukka.kurkela@gmail.com>
13 files changed:
docs/docs/charts/line.mdx
docs/docs/charts/radar.mdx
docs/docs/configuration/tooltip.md
docs/docs/developers/axes.md
docs/docs/general/data-structures.md
src/controllers/controller.bar.js
src/controllers/controller.doughnut.js
src/core/core.datasetController.js
src/scales/scale.linear.js
src/scales/scale.linearbase.js
src/scales/scale.logarithmic.js
src/scales/scale.time.js
test/fixtures/element.line/skip/middle-span.js

index e8b6b6822943c38c2ed3e285d3054dd4c7a33840..0536a1cead77d76206d0686e3b80d2addf6dcfdc 100644 (file)
@@ -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
 
index 689a66b40b96932fa4d8d9c446e46f8cae56ab0c..414f49daf700a94aa095acb11100642ef98d8230 100644 (file)
@@ -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
 
index a4b1a67da653c07c47c058d10295004bbd4df652..07dc6717a0fdcbb8bbf17a2cf60e47fa7d78fd39 100644 (file)
@@ -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;
index 8a60ecb1052203ed6a5283eebfdfd8103dfcc1a0..2762c38dded7a1eb3c631fc0490a5c3d7c4be2f5 100644 (file)
@@ -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() {}
 }
index 8bc65bf18017951031e1ff8027483edde1929be1..49c863b6fd0ee519b25fd79c7c7ebb6ee02de652 100644 (file)
@@ -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
 
index 1782d91279869153788026096c7a295d5b14a287..94d7a5313eecf247a1ccaa234963466866be32ba 100644 (file)
@@ -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);
       }
     }
index 6c5753153906f92426391d1a0453160b9ff0da79..ef38f02250edf6ea5e43f22e42e3b7d1696b1fe3 100644 (file)
@@ -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);
       }
     }
index 07b1795ba95704a7fe9b09ad418a60b01694040b..7413ec954feda4906685b758d20b9a35676e0cfb 100644 (file)
@@ -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);
       }
     }
index ece759902fb82582eff2babc18c5db4bc9f2d57d..e9617fb192242dc2ec2e0beaa101cd7e99fa9782 100644 (file)
@@ -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) {
index 9c669aa621fe4fc31e56c8ab1b42943d685d43bf..fe439156850655ae2836cd2442d81a9df00e23ee 100644 (file)
@@ -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;
index 5dccaf7926c7b5a0f840c696eb921ce6e4c960d3..f88b02c2d554777e121875dc2ce29b6e0de8b7ec 100644 (file)
@@ -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() {
index 52ead35238e55bec28e3b7a7157e766fe688c981..04df0ae1d20e92198791fe2f42b610fb26b949d3 100644 (file)
@@ -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);
   }
 
   /**
index 8abfa4c34d892de6548673597d871829ff697555..0df72aca97b88cc033fd15f6c8f0919a2c9f001d 100644 (file)
@@ -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,