Skip to content

Commit

Permalink
Add includeBounds option for cartesian ticks (#9020)
Browse files Browse the repository at this point in the history
* Add includeBounds option for cartesian ticks

* Types, test and fix

* lint

* Improve linear tick min/max collision detection

* Update comments
  • Loading branch information
kurkle committed May 7, 2021
1 parent 137b51d commit 7c3a412
Show file tree
Hide file tree
Showing 25 changed files with 239 additions and 14 deletions.
1 change: 1 addition & 0 deletions docs/axes/cartesian/_common_ticks.md
Expand Up @@ -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.*
Expand Down
43 changes: 29 additions & 14 deletions 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';

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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++;
}
}
Expand All @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down
26 changes: 26 additions & 0 deletions 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
}
};
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes.
41 changes: 41 additions & 0 deletions 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
}
};

Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 34 additions & 0 deletions 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
}
}
};
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 34 additions & 0 deletions 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
}
}
};
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 34 additions & 0 deletions 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
}
}
};
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 34 additions & 0 deletions 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
}
}
};
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions types/index.esm.d.ts
Expand Up @@ -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
Expand Down

0 comments on commit 7c3a412

Please sign in to comment.