]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Linear Scale: Ability to specify a fixed number of ticks (#8643)
authorEvert Timberg <evert.timberg+github@gmail.com>
Mon, 15 Mar 2021 12:49:25 +0000 (08:49 -0400)
committerGitHub <noreply@github.com>
Mon, 15 Mar 2021 12:49:25 +0000 (08:49 -0400)
* Option to specify exact number of ticks on linear scale
* Fix issue description in tests
* Add tests for conditions where min/max do not align with niceMin/niceMax
* Refactor linear tick generation algorithm
* Add TS definitions
* Update docs
* Code review feedback + lint fixes

12 files changed:
docs/docs/axes/cartesian/linear.mdx
docs/docs/axes/radial/linear.mdx
src/scales/scale.linearbase.js
test/fixtures/scale.linear/tick-count-data-limits.js [new file with mode: 0644]
test/fixtures/scale.linear/tick-count-data-limits.png [new file with mode: 0644]
test/fixtures/scale.linear/tick-count-min-max-not-aligned.js [new file with mode: 0644]
test/fixtures/scale.linear/tick-count-min-max-not-aligned.png [new file with mode: 0644]
test/fixtures/scale.linear/tick-count-min-max.js [new file with mode: 0644]
test/fixtures/scale.linear/tick-count-min-max.png [new file with mode: 0644]
test/fixtures/scale.linear/tick-step-min-max.js [new file with mode: 0644]
test/fixtures/scale.linear/tick-step-min-max.png [new file with mode: 0644]
types/index.esm.d.ts

index bd6670297cfaa2f74df0301226eb47183eae4bbc..5a5f8588abc4ce23819f65ace510f54d139079af 100644 (file)
@@ -31,6 +31,7 @@ Namespace: `options.scales[scaleId].ticks`
 
 | Name | Type | Default | Description
 | ---- | ---- | ------- | -----------
+| `count` | `number` | `undefined` | The number of ticks to generate. If specified, this overrides the automatic generation.
 | `format` | `object` | | The [`Intl.NumberFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat) options used by the default label formatter
 | `maxTicksLimit` | `number` | `11` | Maximum number of ticks and gridlines to show.
 | `precision` | `number` | | if defined and `stepSize` is not specified, the step size will be rounded to this many decimal places.
index e66f8a414b78b38e9a6f9207fe53f94fad438312..663e949b439250a3e11ae4f81af8273b9671f4df 100644 (file)
@@ -35,6 +35,7 @@ Namespace: `options.scales[scaleId].ticks`
 | ---- | ---- | ------- | ------- | -----------
 | `backdropColor` | [`Color`](../../general/colors.md) | Yes | `'rgba(255, 255, 255, 0.75)'` | Color of label backdrops.
 | `backdropPadding` | [`Padding`](../../general/padding.md) | Yes | `2` | Padding of label backdrop.
+| `count` | `number` | Yes | `undefined` | The number of ticks to generate. If specified, this overrides the automatic generation.
 | `format` | `object` | Yes | | The [`Intl.NumberFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat) options used by the default label formatter
 | `maxTicksLimit` | `number` | Yes | `11` | Maximum number of ticks and gridlines to show.
 | `precision` | `number` | Yes | | if defined and `stepSize` is not specified, the step size will be rounded to this many decimal places.
index 1dbaf285aa0c3d944beee95cbfe59a5dbcefec34..be5b0508cefa0b42f579e2b2747fb315423296ab 100644 (file)
@@ -5,7 +5,20 @@ import {formatNumber} from '../core/core.intl';
 import {_addGrace} from '../helpers/helpers.options';
 
 /**
- * Generate a set of linear ticks
+ * Generate a set of linear ticks for an axis
+ * 1. If generationOptions.min, generationOptions.max, and generationOptions.step are defined:
+ *    if (max - min) / step is an integer, ticks are generated as [min, min + step, ..., max]
+ *    Note that the generationOptions.maxCount setting is respected in this scenario
+ *
+ * 2. If generationOptions.min, generationOptions.max, and generationOptions.count is defined
+ *    spacing = (max - min) / count
+ *    Ticks are generated as [min, min + spacing, ..., max]
+ *
+ * 3. If generationOptions.count is defined
+ *    spacing = (niceMax - niceMin) / count
+ *
+ * 4. Compute optimal spacing of ticks using niceNum algorithm
+ *
  * @param generationOptions the options used to generate the ticks
  * @param dataRange the range of the data
  * @returns {object[]} array of tick objects
@@ -17,13 +30,14 @@ function generateTicks(generationOptions, dataRange) {
   // for details.
 
   const MIN_SPACING = 1e-14;
-  const {stepSize, min, max, precision} = generationOptions;
-  const unit = stepSize || 1;
-  const maxNumSpaces = generationOptions.maxTicks - 1;
+  const {step, min, max, precision, count, maxTicks} = generationOptions;
+  const unit = step || 1;
+  const maxSpaces = maxTicks - 1;
   const {min: rmin, max: rmax} = dataRange;
   const minDefined = !isNullOrUndef(min);
   const maxDefined = !isNullOrUndef(max);
-  let spacing = niceNum((rmax - rmin) / maxNumSpaces / unit) * unit;
+  const countDefined = !isNullOrUndef(count);
+  let spacing = niceNum((rmax - rmin) / maxSpaces / unit) * unit;
   let factor, niceMin, niceMax, numSpaces;
 
   // Beyond MIN_SPACING floating point numbers being to lose precision
@@ -33,15 +47,12 @@ function generateTicks(generationOptions, dataRange) {
   }
 
   numSpaces = Math.ceil(rmax / spacing) - Math.floor(rmin / spacing);
-  if (numSpaces > maxNumSpaces) {
+  if (numSpaces > maxSpaces) {
     // If the calculated num of spaces exceeds maxNumSpaces, recalculate it
-    spacing = niceNum(numSpaces * spacing / maxNumSpaces / unit) * unit;
+    spacing = niceNum(numSpaces * spacing / maxSpaces / unit) * unit;
   }
 
-  if (stepSize || isNullOrUndef(precision)) {
-    // If a precision is not specified, calculate factor based on spacing
-    factor = Math.pow(10, _decimalPlaces(spacing));
-  } else {
+  if (!isNullOrUndef(precision)) {
     // If the user specified a precision, round to that number of decimal places
     factor = Math.pow(10, precision);
     spacing = Math.ceil(spacing * factor) / factor;
@@ -50,23 +61,37 @@ function generateTicks(generationOptions, dataRange) {
   niceMin = Math.floor(rmin / spacing) * spacing;
   niceMax = Math.ceil(rmax / spacing) * spacing;
 
-  // If min, max and stepSize is set and they make an evenly spaced scale use it.
-  if (stepSize && minDefined && maxDefined) {
-    // If very close to our whole number, use it.
-    if (almostWhole((max - min) / stepSize, spacing / 1000)) {
-      niceMin = min;
-      niceMax = max;
-    }
-  }
-
-  numSpaces = (niceMax - niceMin) / spacing;
-  // If very close to our rounded value, use it.
-  if (almostEquals(numSpaces, Math.round(numSpaces), spacing / 1000)) {
-    numSpaces = Math.round(numSpaces);
+  if (minDefined && maxDefined && step && almostWhole((max - min) / step, spacing / 1000)) {
+    // Case 1: If min, max and stepSize are set and they make an evenly spaced scale use it.
+    // spacing = step;
+    // numSpaces = (max - min) / spacing;
+    numSpaces = Math.min((max - min) / spacing, maxTicks);
+    spacing = (max - min) / numSpaces;
+    niceMin = min;
+    niceMax = max;
+  } else if (countDefined) {
+    // Cases 2 & 3, we have a count specified. Handle optional user defined edges to the range.
+    // Sometimes these are no-ops, but it makes the code a lot clearer
+    // and when a user defined range is specified, we want the correct ticks
+    niceMin = minDefined ? min : niceMin;
+    niceMax = maxDefined ? max : niceMax;
+    numSpaces = count - 1;
+    spacing = (niceMax - niceMin) / numSpaces;
   } else {
-    numSpaces = Math.ceil(numSpaces);
+    // Case 4
+    numSpaces = (niceMax - niceMin) / spacing;
+
+    // If very close to our rounded value, use it.
+    if (almostEquals(numSpaces, Math.round(numSpaces), spacing / 1000)) {
+      numSpaces = Math.round(numSpaces);
+    } else {
+      numSpaces = Math.ceil(numSpaces);
+    }
   }
 
+  // The spacing will have changed in cases 1, 2, and 3 so the factor cannot be computed
+  // until this point
+  factor = Math.pow(10, isNullOrUndef(precision) ? _decimalPlaces(spacing) : precision);
   niceMin = Math.round(niceMin * factor) / factor;
   niceMax = Math.round(niceMax * factor) / factor;
 
@@ -204,7 +229,8 @@ export default class LinearScaleBase extends Scale {
       min: opts.min,
       max: opts.max,
       precision: tickOpts.precision,
-      stepSize: tickOpts.stepSize
+      step: tickOpts.stepSize,
+      count: tickOpts.count,
     };
     const ticks = generateTicks(numericGeneratorOptions, _addGrace(me, opts.grace));
 
diff --git a/test/fixtures/scale.linear/tick-count-data-limits.js b/test/fixtures/scale.linear/tick-count-data-limits.js
new file mode 100644 (file)
index 0000000..497ed11
--- /dev/null
@@ -0,0 +1,28 @@
+module.exports = {
+  description: 'https://github.com/chartjs/Chart.js/issues/4234',
+  config: {
+    type: 'line',
+    data: {
+      datasets: [{
+        data: [0, 2, 45, 30]
+      }],
+      labels: ['A', 'B', 'C', 'D']
+    },
+    options: {
+      scales: {
+        y: {
+          ticks: {
+            count: 21,
+            callback: (v) => v.toString(),
+          }
+        },
+        x: {
+          display: false
+        }
+      }
+    }
+  },
+  options: {
+    spriteText: true
+  }
+};
diff --git a/test/fixtures/scale.linear/tick-count-data-limits.png b/test/fixtures/scale.linear/tick-count-data-limits.png
new file mode 100644 (file)
index 0000000..bb365bc
Binary files /dev/null and b/test/fixtures/scale.linear/tick-count-data-limits.png differ
diff --git a/test/fixtures/scale.linear/tick-count-min-max-not-aligned.js b/test/fixtures/scale.linear/tick-count-min-max-not-aligned.js
new file mode 100644 (file)
index 0000000..1747f9f
--- /dev/null
@@ -0,0 +1,23 @@
+module.exports = {
+  description: 'https://github.com/chartjs/Chart.js/issues/4234',
+  config: {
+    type: 'line',
+    options: {
+      scales: {
+        y: {
+          max: 27,
+          min: -3,
+          ticks: {
+            count: 11,
+          }
+        },
+        x: {
+          display: false
+        }
+      }
+    }
+  },
+  options: {
+    spriteText: true
+  }
+};
diff --git a/test/fixtures/scale.linear/tick-count-min-max-not-aligned.png b/test/fixtures/scale.linear/tick-count-min-max-not-aligned.png
new file mode 100644 (file)
index 0000000..dea42ec
Binary files /dev/null and b/test/fixtures/scale.linear/tick-count-min-max-not-aligned.png differ
diff --git a/test/fixtures/scale.linear/tick-count-min-max.js b/test/fixtures/scale.linear/tick-count-min-max.js
new file mode 100644 (file)
index 0000000..49ec3fb
--- /dev/null
@@ -0,0 +1,23 @@
+module.exports = {
+  description: 'https://github.com/chartjs/Chart.js/issues/4234',
+  config: {
+    type: 'line',
+    options: {
+      scales: {
+        y: {
+          max: 50,
+          min: 0,
+          ticks: {
+            count: 21,
+          }
+        },
+        x: {
+          display: false
+        }
+      }
+    }
+  },
+  options: {
+    spriteText: true
+  }
+};
diff --git a/test/fixtures/scale.linear/tick-count-min-max.png b/test/fixtures/scale.linear/tick-count-min-max.png
new file mode 100644 (file)
index 0000000..dcba5ad
Binary files /dev/null and b/test/fixtures/scale.linear/tick-count-min-max.png differ
diff --git a/test/fixtures/scale.linear/tick-step-min-max.js b/test/fixtures/scale.linear/tick-step-min-max.js
new file mode 100644 (file)
index 0000000..115549b
--- /dev/null
@@ -0,0 +1,23 @@
+module.exports = {
+  description: 'https://github.com/chartjs/Chart.js/issues/4234',
+  config: {
+    type: 'line',
+    options: {
+      scales: {
+        y: {
+          max: 27,
+          min: -3,
+          ticks: {
+            stepSize: 3,
+          }
+        },
+        x: {
+          display: false
+        }
+      }
+    }
+  },
+  options: {
+    spriteText: true
+  }
+};
diff --git a/test/fixtures/scale.linear/tick-step-min-max.png b/test/fixtures/scale.linear/tick-step-min-max.png
new file mode 100644 (file)
index 0000000..dea42ec
Binary files /dev/null and b/test/fixtures/scale.linear/tick-step-min-max.png differ
index d12b52500a0e6075963c7bdd5d6fd391da011e54..936c9f938fed88b79ccec9b039579ee017acf0db 100644 (file)
@@ -2825,6 +2825,11 @@ export type LinearScaleOptions = CartesianScaleOptions & {
      * @see https://www.chartjs.org/docs/next/axes/cartesian/linear#step-size
      */
     stepSize: number;
+
+    /**
+     * User defined count of ticks
+     */
+    count: number;
   };
 };
 
@@ -3080,6 +3085,11 @@ export type RadialLinearScaleOptions = CoreScaleOptions & {
      */
     stepSize: number;
 
+    /**
+     * User defined number of ticks
+     */
+    count: number;
+
     /**
      * If true, draw a background behind the tick labels.
      * @default true