diff --git a/docs/docs/axes/cartesian/linear.mdx b/docs/docs/axes/cartesian/linear.mdx
index ac1418389..da7a3b4dc 100644
--- a/docs/docs/axes/cartesian/linear.mdx
+++ b/docs/docs/axes/cartesian/linear.mdx
@@ -18,6 +18,7 @@ Namespace: `options.scales[scaleId]`
| Name | Type | Description
| ---- | ---- | -----------
| `beginAtZero` | `boolean` | if true, scale will include 0 if it is not already included.
+| `grace` | `number`\|`string` | Percentage (string ending with `%`) or amount (number) for added room in the scale range above and below data. [more...](#grace)
@@ -58,6 +59,45 @@ let options = {
};
```
+## Grace
+
+If the value is string ending with `%`, its treat as percentage. If number, its treat as value.
+The value is added to the maximum data value and subtracted from the minumum data. This extends the scale range as if the data values were that much greater.
+
+import { useEffect, useRef } from 'react';
+
+```jsx live
+function example() {
+ const canvas = useRef(null);
+ useEffect(() => {
+ const cfg = {
+ type: 'bar',
+ data: {
+ labels: ['Positive', 'Negative'],
+ datasets: [{
+ data: [100, -50],
+ backgroundColor: 'rgb(255, 99, 132)'
+ }],
+ },
+ options: {
+ scales: {
+ y: {
+ type: 'linear',
+ grace: '5%'
+ }
+ },
+ plugins: {
+ legend: false
+ }
+ }
+ };
+ const chart = new Chart(canvas.current.getContext('2d'), cfg);
+ return () => chart.destroy();
+ });
+ return
;
+}
+```
+
## Internal data format
Internally, the linear scale uses numeric data
diff --git a/src/controllers/controller.doughnut.js b/src/controllers/controller.doughnut.js
index a8a3fbcbd..427316c32 100644
--- a/src/controllers/controller.doughnut.js
+++ b/src/controllers/controller.doughnut.js
@@ -1,6 +1,6 @@
import DatasetController from '../core/core.datasetController';
import {formatNumber} from '../core/core.intl';
-import {isArray, toPercentage, toPixels, valueOrDefault} from '../helpers/helpers.core';
+import {isArray, toPercentage, toDimension, valueOrDefault} from '../helpers/helpers.core';
import {toRadians, PI, TAU, HALF_PI, _angleBetween} from '../helpers/helpers.math';
/**
@@ -123,7 +123,7 @@ export default class DoughnutController extends DatasetController {
const maxWidth = (chartArea.width - spacing) / ratioX;
const maxHeight = (chartArea.height - spacing) / ratioY;
const maxRadius = Math.max(Math.min(maxWidth, maxHeight) / 2, 0);
- const outerRadius = toPixels(me.options.radius, maxRadius);
+ const outerRadius = toDimension(me.options.radius, maxRadius);
const innerRadius = Math.max(outerRadius * cutout, 0);
const radiusLength = (outerRadius - innerRadius) / me._getVisibleDatasetWeightTotal();
me.offsetX = offsetX * outerRadius;
diff --git a/src/core/core.scale.js b/src/core/core.scale.js
index ec8a42ae8..ca7b278dd 100644
--- a/src/core/core.scale.js
+++ b/src/core/core.scale.js
@@ -26,6 +26,12 @@ defaults.set('scale', {
*/
bounds: 'ticks',
+ /**
+ * Addition grace added to max and reduced from min data value.
+ * @since 3.0.0
+ */
+ grace: 0,
+
// grid line settings
gridLines: {
display: true,
diff --git a/src/helpers/helpers.core.js b/src/helpers/helpers.core.js
index 78ce47409..cf3170ea9 100644
--- a/src/helpers/helpers.core.js
+++ b/src/helpers/helpers.core.js
@@ -90,7 +90,7 @@ export const toPercentage = (value, dimension) =>
parseFloat(value) / 100
: value / dimension;
-export const toPixels = (value, dimension) =>
+export const toDimension = (value, dimension) =>
typeof value === 'string' && value.endsWith('%') ?
parseFloat(value) / 100 * dimension
: +value;
diff --git a/src/helpers/helpers.options.js b/src/helpers/helpers.options.js
index 604c09603..25302133a 100644
--- a/src/helpers/helpers.options.js
+++ b/src/helpers/helpers.options.js
@@ -1,5 +1,5 @@
import defaults from '../core/core.defaults';
-import {isArray, isObject, valueOrDefault} from './helpers.core';
+import {isArray, isObject, toDimension, valueOrDefault} from './helpers.core';
import {toFontString} from './helpers.canvas';
const LINE_HEIGHT = new RegExp(/^(normal|(\d+(?:\.\d+)?)(px|em|%)?)$/);
@@ -175,3 +175,16 @@ export function resolve(inputs, context, index, info) {
}
}
}
+
+/**
+ * @param {{min: number, max: number}} minmax
+ * @param {number|string} grace
+ * @private
+ */
+export function _addGrace(minmax, grace) {
+ const {min, max} = minmax;
+ return {
+ min: min - Math.abs(toDimension(grace, min)),
+ max: max + toDimension(grace, max)
+ };
+}
diff --git a/src/scales/scale.linearbase.js b/src/scales/scale.linearbase.js
index fe954a43f..1dbaf285a 100644
--- a/src/scales/scale.linearbase.js
+++ b/src/scales/scale.linearbase.js
@@ -2,6 +2,7 @@ import {isNullOrUndef} from '../helpers/helpers.core';
import {almostEquals, almostWhole, niceNum, _decimalPlaces, _setMinAndMaxByKey, sign} from '../helpers/helpers.math';
import Scale from '../core/core.scale';
import {formatNumber} from '../core/core.intl';
+import {_addGrace} from '../helpers/helpers.options';
/**
* Generate a set of linear ticks
@@ -205,7 +206,7 @@ export default class LinearScaleBase extends Scale {
precision: tickOpts.precision,
stepSize: tickOpts.stepSize
};
- const ticks = generateTicks(numericGeneratorOptions, me);
+ const ticks = generateTicks(numericGeneratorOptions, _addGrace(me, opts.grace));
// At this point, we need to update our max and min given the tick values,
// since we probably have expanded the range of the scale
diff --git a/test/fixtures/scale.linear/grace-neg.js b/test/fixtures/scale.linear/grace-neg.js
new file mode 100644
index 000000000..f606202ae
--- /dev/null
+++ b/test/fixtures/scale.linear/grace-neg.js
@@ -0,0 +1,30 @@
+module.exports = {
+ description: 'https://github.com/chartjs/Chart.js/issues/7734',
+ config: {
+ type: 'bar',
+ data: {
+ labels: ['a'],
+ datasets: [{
+ data: [-0.18],
+ }],
+ },
+ options: {
+ indexAxis: 'y',
+ scales: {
+ y: {
+ display: false
+ },
+ x: {
+ grace: '5%'
+ }
+ }
+ }
+ },
+ options: {
+ spriteText: true,
+ canvas: {
+ width: 512,
+ height: 128
+ }
+ }
+};
diff --git a/test/fixtures/scale.linear/grace-neg.png b/test/fixtures/scale.linear/grace-neg.png
new file mode 100644
index 000000000..fbe444b38
Binary files /dev/null and b/test/fixtures/scale.linear/grace-neg.png differ
diff --git a/test/fixtures/scale.linear/grace-pos.js b/test/fixtures/scale.linear/grace-pos.js
new file mode 100644
index 000000000..76d6c693d
--- /dev/null
+++ b/test/fixtures/scale.linear/grace-pos.js
@@ -0,0 +1,30 @@
+module.exports = {
+ description: 'https://github.com/chartjs/Chart.js/issues/7734',
+ config: {
+ type: 'bar',
+ data: {
+ labels: ['a'],
+ datasets: [{
+ data: [0.18],
+ }],
+ },
+ options: {
+ indexAxis: 'y',
+ scales: {
+ y: {
+ display: false
+ },
+ x: {
+ grace: '5%'
+ }
+ }
+ }
+ },
+ options: {
+ spriteText: true,
+ canvas: {
+ width: 512,
+ height: 128
+ }
+ }
+};
diff --git a/test/fixtures/scale.linear/grace-pos.png b/test/fixtures/scale.linear/grace-pos.png
new file mode 100644
index 000000000..e6b654505
Binary files /dev/null and b/test/fixtures/scale.linear/grace-pos.png differ
diff --git a/test/fixtures/scale.linear/grace.js b/test/fixtures/scale.linear/grace.js
new file mode 100644
index 000000000..8f3f09f96
--- /dev/null
+++ b/test/fixtures/scale.linear/grace.js
@@ -0,0 +1,30 @@
+module.exports = {
+ description: 'https://github.com/chartjs/Chart.js/issues/7734',
+ config: {
+ type: 'bar',
+ data: {
+ labels: ['a', 'b'],
+ datasets: [{
+ data: [1.2, -0.2],
+ }],
+ },
+ options: {
+ indexAxis: 'y',
+ scales: {
+ y: {
+ display: false
+ },
+ x: {
+ grace: 0.3
+ }
+ }
+ }
+ },
+ options: {
+ spriteText: true,
+ canvas: {
+ width: 512,
+ height: 128
+ }
+ }
+};
diff --git a/test/fixtures/scale.linear/grace.png b/test/fixtures/scale.linear/grace.png
new file mode 100644
index 000000000..b7556e412
Binary files /dev/null and b/test/fixtures/scale.linear/grace.png differ