From: Jukka Kurkela Date: Fri, 7 May 2021 06:03:37 +0000 (+0300) Subject: Add includeBounds option for cartesian ticks (#9020) X-Git-Tag: v3.3.0~28 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=7c3a412887b83a76e0acf784937ed31db1bc6749;p=thirdparty%2FChart.js.git Add includeBounds option for cartesian ticks (#9020) * Add includeBounds option for cartesian ticks * Types, test and fix * lint * Improve linear tick min/max collision detection * Update comments --- diff --git a/docs/axes/cartesian/_common_ticks.md b/docs/axes/cartesian/_common_ticks.md index 12c911467..9f2575477 100644 --- a/docs/axes/cartesian/_common_ticks.md +++ b/docs/axes/cartesian/_common_ticks.md @@ -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.* diff --git a/src/scales/scale.linearbase.js b/src/scales/scale.linearbase.js index de1dbe967..8e4440aea 100644 --- a/src/scales/scale.linearbase.js +++ b/src/scales/scale.linearbase.js @@ -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-edge-case-1.js b/test/fixtures/scale.linear/min-max-skip/edge-case-1.js similarity index 100% rename from test/fixtures/scale.linear/min-max-skip-edge-case-1.js rename to test/fixtures/scale.linear/min-max-skip/edge-case-1.js diff --git a/test/fixtures/scale.linear/min-max-skip-edge-case-1.png b/test/fixtures/scale.linear/min-max-skip/edge-case-1.png similarity index 100% rename from test/fixtures/scale.linear/min-max-skip-edge-case-1.png rename to test/fixtures/scale.linear/min-max-skip/edge-case-1.png diff --git a/test/fixtures/scale.linear/min-max-skip-edge-case-2.js b/test/fixtures/scale.linear/min-max-skip/edge-case-2.js similarity index 100% rename from test/fixtures/scale.linear/min-max-skip-edge-case-2.js rename to test/fixtures/scale.linear/min-max-skip/edge-case-2.js diff --git a/test/fixtures/scale.linear/min-max-skip-edge-case-2.png b/test/fixtures/scale.linear/min-max-skip/edge-case-2.png similarity index 100% rename from test/fixtures/scale.linear/min-max-skip-edge-case-2.png rename to test/fixtures/scale.linear/min-max-skip/edge-case-2.png diff --git a/test/fixtures/scale.linear/min-max-skip-edge-case-3.js b/test/fixtures/scale.linear/min-max-skip/edge-case-3.js similarity index 100% rename from test/fixtures/scale.linear/min-max-skip-edge-case-3.js rename to test/fixtures/scale.linear/min-max-skip/edge-case-3.js diff --git a/test/fixtures/scale.linear/min-max-skip-edge-case-3.png b/test/fixtures/scale.linear/min-max-skip/edge-case-3.png similarity index 100% rename from test/fixtures/scale.linear/min-max-skip-edge-case-3.png rename to test/fixtures/scale.linear/min-max-skip/edge-case-3.png diff --git a/test/fixtures/scale.linear/min-max-skip-edge-case-4.js b/test/fixtures/scale.linear/min-max-skip/edge-case-4.js similarity index 100% rename from test/fixtures/scale.linear/min-max-skip-edge-case-4.js rename to test/fixtures/scale.linear/min-max-skip/edge-case-4.js diff --git a/test/fixtures/scale.linear/min-max-skip-edge-case-4.png b/test/fixtures/scale.linear/min-max-skip/edge-case-4.png similarity index 100% rename from test/fixtures/scale.linear/min-max-skip-edge-case-4.png rename to test/fixtures/scale.linear/min-max-skip/edge-case-4.png 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 index 000000000..c1a180856 --- /dev/null +++ b/test/fixtures/scale.linear/min-max-skip/includeBounds.js @@ -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 index 000000000..afb6fe407 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.js b/test/fixtures/scale.linear/min-max-skip/min-max-skip.js similarity index 100% rename from test/fixtures/scale.linear/min-max-skip.js rename to test/fixtures/scale.linear/min-max-skip/min-max-skip.js diff --git a/test/fixtures/scale.linear/min-max-skip.png b/test/fixtures/scale.linear/min-max-skip/min-max-skip.png similarity index 100% rename from test/fixtures/scale.linear/min-max-skip.png rename to test/fixtures/scale.linear/min-max-skip/min-max-skip.png 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 index 000000000..7d2b3a3e5 --- /dev/null +++ b/test/fixtures/scale.linear/min-max-skip/no-collision.js @@ -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 index 000000000..dfd659613 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 index 000000000..bc9d26e98 --- /dev/null +++ b/test/fixtures/scale.linear/min-max-skip/rotated-case-1.js @@ -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 index 000000000..c23af5493 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 index 000000000..7179518a7 --- /dev/null +++ b/test/fixtures/scale.linear/min-max-skip/rotated-case-2.js @@ -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 index 000000000..335a03b4d 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 index 000000000..22d8426e3 --- /dev/null +++ b/test/fixtures/scale.linear/min-max-skip/rotated-case-3.js @@ -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 index 000000000..790f5525e 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 index 000000000..6da47127a --- /dev/null +++ b/test/fixtures/scale.linear/min-max-skip/rotated-case-4.js @@ -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 index 000000000..df57d4f91 Binary files /dev/null and b/test/fixtures/scale.linear/min-max-skip/rotated-case-4.png differ diff --git a/types/index.esm.d.ts b/types/index.esm.d.ts index 3066472c0..85c317a4c 100644 --- a/types/index.esm.d.ts +++ b/types/index.esm.d.ts @@ -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