]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Add includeBounds option for cartesian ticks (#9020)
authorJukka Kurkela <jukka.kurkela@gmail.com>
Fri, 7 May 2021 06:03:37 +0000 (09:03 +0300)
committerGitHub <noreply@github.com>
Fri, 7 May 2021 06:03:37 +0000 (09:03 +0300)
* Add includeBounds option for cartesian ticks

* Types, test and fix

* lint

* Improve linear tick min/max collision detection

* Update comments

25 files changed:
docs/axes/cartesian/_common_ticks.md
src/scales/scale.linearbase.js
test/fixtures/scale.linear/min-max-skip/edge-case-1.js [moved from test/fixtures/scale.linear/min-max-skip-edge-case-1.js with 100% similarity]
test/fixtures/scale.linear/min-max-skip/edge-case-1.png [moved from test/fixtures/scale.linear/min-max-skip-edge-case-1.png with 100% similarity]
test/fixtures/scale.linear/min-max-skip/edge-case-2.js [moved from test/fixtures/scale.linear/min-max-skip-edge-case-2.js with 100% similarity]
test/fixtures/scale.linear/min-max-skip/edge-case-2.png [moved from test/fixtures/scale.linear/min-max-skip-edge-case-2.png with 100% similarity]
test/fixtures/scale.linear/min-max-skip/edge-case-3.js [moved from test/fixtures/scale.linear/min-max-skip-edge-case-3.js with 100% similarity]
test/fixtures/scale.linear/min-max-skip/edge-case-3.png [moved from test/fixtures/scale.linear/min-max-skip-edge-case-3.png with 100% similarity]
test/fixtures/scale.linear/min-max-skip/edge-case-4.js [moved from test/fixtures/scale.linear/min-max-skip-edge-case-4.js with 100% similarity]
test/fixtures/scale.linear/min-max-skip/edge-case-4.png [moved from test/fixtures/scale.linear/min-max-skip-edge-case-4.png with 100% similarity]
test/fixtures/scale.linear/min-max-skip/includeBounds.js [new file with mode: 0644]
test/fixtures/scale.linear/min-max-skip/includeBounds.png [new file with mode: 0644]
test/fixtures/scale.linear/min-max-skip/min-max-skip.js [moved from test/fixtures/scale.linear/min-max-skip.js with 100% similarity]
test/fixtures/scale.linear/min-max-skip/min-max-skip.png [moved from test/fixtures/scale.linear/min-max-skip.png with 100% similarity]
test/fixtures/scale.linear/min-max-skip/no-collision.js [new file with mode: 0644]
test/fixtures/scale.linear/min-max-skip/no-collision.png [new file with mode: 0644]
test/fixtures/scale.linear/min-max-skip/rotated-case-1.js [new file with mode: 0644]
test/fixtures/scale.linear/min-max-skip/rotated-case-1.png [new file with mode: 0644]
test/fixtures/scale.linear/min-max-skip/rotated-case-2.js [new file with mode: 0644]
test/fixtures/scale.linear/min-max-skip/rotated-case-2.png [new file with mode: 0644]
test/fixtures/scale.linear/min-max-skip/rotated-case-3.js [new file with mode: 0644]
test/fixtures/scale.linear/min-max-skip/rotated-case-3.png [new file with mode: 0644]
test/fixtures/scale.linear/min-max-skip/rotated-case-4.js [new file with mode: 0644]
test/fixtures/scale.linear/min-max-skip/rotated-case-4.png [new file with mode: 0644]
types/index.esm.d.ts

index 12c9114670c9908ab2bbc5120b05487d4fd478b7..9f2575477cb92198ce186d03a9c7472f33f99687 100644 (file)
@@ -9,6 +9,7 @@ Namespace: `options.scales[scaleId].ticks`
 | `sampleSize` | `number` | `ticks.length` | The number of ticks to examine when deciding how many labels will fit. Setting a smaller value will be faster, but may be less accurate when there is large variability in label length.
 | `autoSkip` | `boolean` | `true` | If true, automatically calculates how many labels can be shown and hides labels accordingly. Labels will be rotated up to `maxRotation` before skipping any. Turn `autoSkip` off to show all labels no matter what.
 | `autoSkipPadding` | `number` | `3` | Padding between the ticks on the horizontal axis when `autoSkip` is enabled.
+| `includeBounds` | `boolean` | `true` | Should the defined `min` and `max` values be presented as ticks even if they are not "nice".
 | `labelOffset` | `number` | `0` | Distance in pixels to offset the label from the centre point of the tick (in the x-direction for the x-axis, and the y-direction for the y-axis). *Note: this can cause labels at the edges to be cropped by the edge of the canvas*
 | `maxRotation` | `number` | `50` | Maximum rotation for tick labels when rotating to condense labels. Note: Rotation doesn't occur until necessary. *Note: Only applicable to horizontal scales.*
 | `minRotation` | `number` | `0` | Minimum rotation for tick labels. *Note: Only applicable to horizontal scales.*
index de1dbe967e57415283673dc2c04aef6e6e8cc7a6..8e4440aea6f277909c4459cddc4ffa7e80a540c3 100644 (file)
@@ -1,5 +1,5 @@
 import {isNullOrUndef} from '../helpers/helpers.core';
-import {almostEquals, almostWhole, niceNum, _decimalPlaces, _setMinAndMaxByKey, sign} from '../helpers/helpers.math';
+import {almostEquals, almostWhole, niceNum, _decimalPlaces, _setMinAndMaxByKey, sign, toRadians} from '../helpers/helpers.math';
 import Scale from '../core/core.scale';
 import {formatNumber} from '../helpers/helpers.intl';
 
@@ -29,7 +29,7 @@ function generateTicks(generationOptions, dataRange) {
   // for details.
 
   const MIN_SPACING = 1e-14;
-  const {step, min, max, precision, count, maxTicks, maxDigits, horizontal} = generationOptions;
+  const {step, min, max, precision, count, maxTicks, maxDigits, includeBounds} = generationOptions;
   const unit = step || 1;
   const maxSpaces = maxTicks - 1;
   const {min: rmin, max: rmax} = dataRange;
@@ -97,13 +97,17 @@ function generateTicks(generationOptions, dataRange) {
 
   let j = 0;
   if (minDefined) {
-    ticks.push({value: min});
-    // If the niceMin is smaller or equal to min, skip it
-    if (niceMin <= min) {
-      j++;
-    }
-    // If the next nice tick is close to min, skip that too
-    if (almostEquals(Math.round((niceMin + j * spacing) * factor) / factor, min, minSpacing * (horizontal ? ('' + min).length : 1))) {
+    if (includeBounds && niceMin !== min) {
+      ticks.push({value: min});
+
+      if (niceMin < min) {
+        j++; // Skip niceMin
+      }
+      // If the next nice tick is close to min, skip it
+      if (almostEquals(Math.round((niceMin + j * spacing) * factor) / factor, min, relativeLabelSize(min, minSpacing, generationOptions))) {
+        j++;
+      }
+    } else if (niceMin < min) {
       j++;
     }
   }
@@ -112,20 +116,29 @@ function generateTicks(generationOptions, dataRange) {
     ticks.push({value: Math.round((niceMin + j * spacing) * factor) / factor});
   }
 
-  if (maxDefined) {
-    // If the previous tick is close to max, replace it with max, else add max
-    if (almostEquals(ticks[ticks.length - 1].value, max, minSpacing * (horizontal ? ('' + max).length : 1))) {
+  if (maxDefined && includeBounds && niceMax !== max) {
+    // If the previous tick is too close to max, replace it with max, else add max
+    if (almostEquals(ticks[ticks.length - 1].value, max, relativeLabelSize(max, minSpacing, generationOptions))) {
       ticks[ticks.length - 1].value = max;
     } else {
       ticks.push({value: max});
     }
-  } else {
+  } else if (!maxDefined || niceMax === max) {
     ticks.push({value: niceMax});
   }
 
   return ticks;
 }
 
+function relativeLabelSize(value, minSpacing, {horizontal, minRotation}) {
+  const rot = toRadians(minRotation);
+  const useLength = (horizontal && minRotation <= 45) || (!horizontal && minRotation >= 45);
+  const l = useLength ? minSpacing * ('' + value).length : 0;
+  const sin = Math.sin(rot);
+  const cos = Math.cos(rot);
+  return horizontal ? cos * l + sin * minSpacing : sin * l + cos * minSpacing;
+}
+
 export default class LinearScaleBase extends Scale {
 
   constructor(cfg) {
@@ -232,7 +245,9 @@ export default class LinearScaleBase extends Scale {
       step: tickOpts.stepSize,
       count: tickOpts.count,
       maxDigits: me._maxDigits(),
-      horizontal: me.isHorizontal()
+      horizontal: me.isHorizontal(),
+      minRotation: tickOpts.minRotation || 0,
+      includeBounds: tickOpts.includeBounds !== false
     };
     const dataRange = me._range || me;
     const ticks = generateTicks(numericGeneratorOptions, dataRange);
diff --git a/test/fixtures/scale.linear/min-max-skip/includeBounds.js b/test/fixtures/scale.linear/min-max-skip/includeBounds.js
new file mode 100644 (file)
index 0000000..c1a1808
--- /dev/null
@@ -0,0 +1,26 @@
+module.exports = {
+  config: {
+    type: 'scatter',
+    options: {
+      scales: {
+        y: {
+          max: 1225.2,
+          min: 369.5,
+          ticks: {
+            includeBounds: false
+          }
+        },
+        x: {
+          min: 20,
+          max: 100,
+          ticks: {
+            includeBounds: false
+          }
+        }
+      }
+    }
+  },
+  options: {
+    spriteText: true
+  }
+};
diff --git a/test/fixtures/scale.linear/min-max-skip/includeBounds.png b/test/fixtures/scale.linear/min-max-skip/includeBounds.png
new file mode 100644 (file)
index 0000000..afb6fe4
Binary files /dev/null and b/test/fixtures/scale.linear/min-max-skip/includeBounds.png differ
diff --git a/test/fixtures/scale.linear/min-max-skip/no-collision.js b/test/fixtures/scale.linear/min-max-skip/no-collision.js
new file mode 100644 (file)
index 0000000..7d2b3a3
--- /dev/null
@@ -0,0 +1,41 @@
+module.exports = {
+  description: 'https://github.com/chartjs/Chart.js/issues/9025',
+  threshold: 0.2,
+  config: {
+    type: 'line',
+    data: {
+      datasets: [{
+        data: [
+          {x: 10000000, y: 65},
+          {x: 20000000, y: 12},
+          {x: 30000000, y: 23},
+          {x: 40000000, y: 51},
+          {x: 50000000, y: 17},
+          {x: 60000000, y: 23}
+        ]
+      }]
+    },
+    options: {
+      scales: {
+        x: {
+          type: 'linear',
+          min: 10000000,
+          max: 60000000,
+          ticks: {
+            minRotation: 45,
+            maxRotation: 45,
+            count: 6
+          }
+        }
+      }
+    }
+  },
+  options: {
+    canvas: {
+      width: 200,
+      height: 200
+    },
+    spriteText: true
+  }
+};
+
diff --git a/test/fixtures/scale.linear/min-max-skip/no-collision.png b/test/fixtures/scale.linear/min-max-skip/no-collision.png
new file mode 100644 (file)
index 0000000..dfd6596
Binary files /dev/null and b/test/fixtures/scale.linear/min-max-skip/no-collision.png differ
diff --git a/test/fixtures/scale.linear/min-max-skip/rotated-case-1.js b/test/fixtures/scale.linear/min-max-skip/rotated-case-1.js
new file mode 100644 (file)
index 0000000..bc9d26e
--- /dev/null
@@ -0,0 +1,34 @@
+module.exports = {
+  description: 'https://github.com/chartjs/Chart.js/issues/9025',
+  threshold: 0.15,
+  config: {
+    type: 'scatter',
+    options: {
+      scales: {
+        y: {
+          max: 1069,
+          min: 230,
+          ticks: {
+            autoSkip: false,
+            minRotation: 22.5
+          }
+        },
+        x: {
+          max: 1069,
+          min: 230,
+          ticks: {
+            autoSkip: false,
+            minRotation: 35
+          }
+        }
+      }
+    }
+  },
+  options: {
+    spriteText: true,
+    canvas: {
+      height: 211,
+      width: 415
+    }
+  }
+};
diff --git a/test/fixtures/scale.linear/min-max-skip/rotated-case-1.png b/test/fixtures/scale.linear/min-max-skip/rotated-case-1.png
new file mode 100644 (file)
index 0000000..c23af54
Binary files /dev/null and b/test/fixtures/scale.linear/min-max-skip/rotated-case-1.png differ
diff --git a/test/fixtures/scale.linear/min-max-skip/rotated-case-2.js b/test/fixtures/scale.linear/min-max-skip/rotated-case-2.js
new file mode 100644 (file)
index 0000000..7179518
--- /dev/null
@@ -0,0 +1,34 @@
+module.exports = {
+  description: 'https://github.com/chartjs/Chart.js/issues/9025',
+  threshold: 0.15,
+  config: {
+    type: 'scatter',
+    options: {
+      scales: {
+        y: {
+          max: 1069,
+          min: 230,
+          ticks: {
+            autoSkip: false,
+            minRotation: 22.5
+          }
+        },
+        x: {
+          max: 1069,
+          min: 230,
+          ticks: {
+            autoSkip: false,
+            minRotation: 35
+          }
+        }
+      }
+    }
+  },
+  options: {
+    spriteText: true,
+    canvas: {
+      height: 214,
+      width: 416
+    }
+  }
+};
diff --git a/test/fixtures/scale.linear/min-max-skip/rotated-case-2.png b/test/fixtures/scale.linear/min-max-skip/rotated-case-2.png
new file mode 100644 (file)
index 0000000..335a03b
Binary files /dev/null and b/test/fixtures/scale.linear/min-max-skip/rotated-case-2.png differ
diff --git a/test/fixtures/scale.linear/min-max-skip/rotated-case-3.js b/test/fixtures/scale.linear/min-max-skip/rotated-case-3.js
new file mode 100644 (file)
index 0000000..22d8426
--- /dev/null
@@ -0,0 +1,34 @@
+module.exports = {
+  description: 'https://github.com/chartjs/Chart.js/issues/9025',
+  threshold: 0.15,
+  config: {
+    type: 'scatter',
+    options: {
+      scales: {
+        y: {
+          max: 1069,
+          min: 230,
+          ticks: {
+            autoSkip: false,
+            minRotation: 22.5
+          }
+        },
+        x: {
+          max: 1069,
+          min: 230,
+          ticks: {
+            autoSkip: false,
+            minRotation: 35
+          }
+        }
+      }
+    }
+  },
+  options: {
+    spriteText: true,
+    canvas: {
+      height: 216,
+      width: 520
+    }
+  }
+};
diff --git a/test/fixtures/scale.linear/min-max-skip/rotated-case-3.png b/test/fixtures/scale.linear/min-max-skip/rotated-case-3.png
new file mode 100644 (file)
index 0000000..790f552
Binary files /dev/null and b/test/fixtures/scale.linear/min-max-skip/rotated-case-3.png differ
diff --git a/test/fixtures/scale.linear/min-max-skip/rotated-case-4.js b/test/fixtures/scale.linear/min-max-skip/rotated-case-4.js
new file mode 100644 (file)
index 0000000..6da4712
--- /dev/null
@@ -0,0 +1,34 @@
+module.exports = {
+  description: 'https://github.com/chartjs/Chart.js/issues/9025',
+  threshold: 0.15,
+  config: {
+    type: 'scatter',
+    options: {
+      scales: {
+        y: {
+          max: 1069,
+          min: 230,
+          ticks: {
+            autoSkip: false,
+            minRotation: 22.5
+          }
+        },
+        x: {
+          max: 1069,
+          min: 230,
+          ticks: {
+            autoSkip: false,
+            minRotation: 35
+          }
+        }
+      }
+    }
+  },
+  options: {
+    spriteText: true,
+    canvas: {
+      height: 217,
+      width: 521
+    }
+  }
+};
diff --git a/test/fixtures/scale.linear/min-max-skip/rotated-case-4.png b/test/fixtures/scale.linear/min-max-skip/rotated-case-4.png
new file mode 100644 (file)
index 0000000..df57d4f
Binary files /dev/null and b/test/fixtures/scale.linear/min-max-skip/rotated-case-4.png differ
index 3066472c0b87d83d26dd3eedc52b0d991b966cf9..85c317a4c455922f205dcb4acee5e058abc3cea1 100644 (file)
@@ -2831,6 +2831,12 @@ export interface CartesianScaleOptions extends CoreScaleOptions {
      */
     crossAlign: 'near' | 'center' | 'far';
 
+    /**
+     * Should the defined `min` and `max` values be presented as ticks even if they are not "nice".
+     * @default: true
+     */
+    includeBounds: boolean;
+
     /**
      * Distance in pixels to offset the label from the centre point of the tick (in the x direction for the x axis, and the y direction for the y axis). Note: this can cause labels at the edges to be cropped by the edge of the canvas
      * @default 0