Allow axes to be centered on the chart area or at a dynamic position based on another axis
| Name | Type | Default | Description
| ---- | ---- | ------- | -----------
| `type` | `string` | | Type of scale being employed. Custom scales can be created and registered with a string key. This allows changing the type of an axis for a chart.
-| `position` | `string` | | Position of the axis in the chart. Possible values are: `'top'`, `'left'`, `'bottom'`, `'right'`
+| `position` | `string` | | Position of the axis. [more...](#axis-position)
+| `axis` | `string` | | Which type of axis this is. Possible values are: `'x'`, `'y'`. If not set, this is inferred from the first character of the ID which should be `'x'` or `'y'`.
| `offset` | `boolean` | `false` | If true, extra space is added to the both edges and the axis is scaled to fit into the chart area. This is set to `true` for a bar chart by default.
| `id` | `string` | | The ID is used to link datasets and scale axes together. [more...](#axis-id)
| `gridLines` | `object` | | Grid line configuration. [more...](../styling.md#grid-line-configuration)
| `scaleLabel` | `object` | | Scale title configuration. [more...](../labelling.md#scale-title-configuration)
| `ticks` | `object` | | Tick configuration. [more...](#tick-configuration)
+### Axis Position
+
+An axis can either be positioned at the edge of the chart, at the center of the chart area, or dynamically with respect to a data value.
+
+To position the axis at the edge of the chart, set the `position` option to one of: `'top'`, `'left'`, `'bottom'`, `'right'`.
+To position the axis at the center of the chart area, set the `position` option to `'center'`. In this mode, either the `axis` option is specified or the axis ID starts with the letter 'x' or 'y'.
+To position the axis with respect to a data value, set the `position` option to an object such as:
+
+```javascript
+{
+ x: -20
+}
+```
+
+This will position the axis at a value of -20 on the axis with ID "x". For cartesian axes, only 1 axis may be specified.
+
### Tick Configuration
The following options are common to all cartesian axes but do not apply to other axes.
}, {
title: 'Axes Labels',
path: 'scales/axes-labels.html'
+ }, {
+ title: 'Center Positioning',
+ path: 'scales/axis-center-position.html'
}]
}, {
title: 'Legend',
--- /dev/null
+<!doctype html>
+<html>
+
+<head>
+ <title>Scatter Chart</title>
+ <script src="../../dist/Chart.js"></script>
+ <script src="../utils.js"></script>
+ <style>
+ canvas {
+ -moz-user-select: none;
+ -webkit-user-select: none;
+ -ms-user-select: none;
+ }
+ .chart-container {
+ width: 500px;
+ margin-left: 40px;
+ margin-right: 40px;
+ margin-bottom: 40px;
+ }
+ .container {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ justify-content: center;
+ }
+ </style>
+</head>
+
+<body>
+ <div class="container"></div>
+ <script>
+ var color = Chart.helpers.color;
+ function generateData() {
+ var data = [];
+ for (var i = 0; i < 7; i++) {
+ data.push({
+ x: randomScalingFactor(),
+ y: randomScalingFactor()
+ });
+ }
+ return data;
+ }
+
+ function createConfig(xPosition, yPosition, title) {
+ var scatterChartData = {
+ datasets: [{
+ label: 'My First dataset',
+ borderColor: window.chartColors.red,
+ backgroundColor: color(window.chartColors.red).alpha(0.2).rgbString(),
+ data: generateData()
+ }, {
+ label: 'My Second dataset',
+ borderColor: window.chartColors.blue,
+ backgroundColor: color(window.chartColors.blue).alpha(0.2).rgbString(),
+ data: generateData()
+ }]
+ };
+
+ return {
+ type: 'scatter',
+ data: scatterChartData,
+ options: {
+ responsive: true,
+ title: {
+ display: true,
+ text: title
+ },
+ scales: {
+ x: {
+ position: xPosition,
+ axis: 'x',
+ min: -100,
+ max: 100,
+ },
+ y: {
+ position: yPosition,
+ axis: 'y',
+ min: -100,
+ max: 100,
+ }
+ }
+ }
+ };
+ }
+
+ window.onload = function() {
+ var container = document.querySelector('.container');
+
+ [{
+ title: 'Position: Vertical: left, Horizontal: bottom',
+ xPosition: 'bottom',
+ yPosition: 'left'
+ }, {
+ title: 'Position: Vertical: center, Horizontal: center',
+ xPosition: 'center',
+ yPosition: 'center'
+ }, {
+ title: 'Position: Vertical: x=-60, Horizontal: y=30',
+ xPosition: {y: 30},
+ yPosition: {x: -60}
+ }].forEach(function(details) {
+ var div = document.createElement('div');
+ div.classList.add('chart-container');
+
+ var canvas = document.createElement('canvas');
+ div.appendChild(canvas);
+ container.appendChild(div);
+
+ var ctx = canvas.getContext('2d');
+ var config = createConfig(details.xPosition, details.yPosition, details.title);
+ new Chart(ctx, config);
+ });
+ };
+ </script>
+</body>
+
+</html>
chart.tooltip.initialize();
}
-function positionIsHorizontal(position) {
- return position === 'top' || position === 'bottom';
+const KNOWN_POSITIONS = ['top', 'bottom', 'left', 'right', 'chartArea'];
+function positionIsHorizontal(position, axis) {
+ return position === 'top' || position === 'bottom' || (!KNOWN_POSITIONS.includes(position) && axis === 'x');
}
function compare2Level(l1, l2) {
var id = scaleOptions.id;
var scaleType = valueOrDefault(scaleOptions.type, item.dtype);
- if (positionIsHorizontal(scaleOptions.position) !== positionIsHorizontal(item.dposition)) {
+ if (scaleOptions.position === undefined || positionIsHorizontal(scaleOptions.position, scaleOptions.axis || id[0]) !== positionIsHorizontal(item.dposition)) {
scaleOptions.position = item.dposition;
}
var extend = helpers.extend;
+const STATIC_POSITIONS = ['left', 'top', 'right', 'bottom'];
+
function filterByPosition(array, position) {
- return helpers.where(array, function(v) {
- return v.pos === position;
- });
+ return helpers.where(array, v => v.pos === position);
+}
+
+function filterDynamicPositionByAxis(array, axis) {
+ return helpers.where(array, v => STATIC_POSITIONS.indexOf(v.pos) === -1 && v.box.axis === axis);
}
function sortByWeight(array, reverse) {
}
function buildLayoutBoxes(boxes) {
- var layoutBoxes = wrapBoxes(boxes);
- var left = sortByWeight(filterByPosition(layoutBoxes, 'left'), true);
- var right = sortByWeight(filterByPosition(layoutBoxes, 'right'));
- var top = sortByWeight(filterByPosition(layoutBoxes, 'top'), true);
- var bottom = sortByWeight(filterByPosition(layoutBoxes, 'bottom'));
+ const layoutBoxes = wrapBoxes(boxes);
+ const left = sortByWeight(filterByPosition(layoutBoxes, 'left'), true);
+ const right = sortByWeight(filterByPosition(layoutBoxes, 'right'));
+ const top = sortByWeight(filterByPosition(layoutBoxes, 'top'), true);
+ const bottom = sortByWeight(filterByPosition(layoutBoxes, 'bottom'));
+ const centerHorizontal = filterDynamicPositionByAxis(layoutBoxes, 'x');
+ const centerVertical = filterDynamicPositionByAxis(layoutBoxes, 'y');
return {
leftAndTop: left.concat(top),
- rightAndBottom: right.concat(bottom),
+ rightAndBottom: right.concat(centerVertical).concat(bottom).concat(centerHorizontal),
chartArea: filterByPosition(layoutBoxes, 'chartArea'),
- vertical: left.concat(right),
- horizontal: top.concat(bottom)
+ vertical: left.concat(right).concat(centerVertical),
+ horizontal: top.concat(bottom).concat(centerHorizontal)
};
}
left: chartArea.left,
top: chartArea.top,
right: chartArea.left + chartArea.w,
- bottom: chartArea.top + chartArea.h
+ bottom: chartArea.top + chartArea.h,
+ height: chartArea.h,
+ width: chartArea.w,
};
// Finally update boxes in chartArea (radial scale for example)
defaults._set('scale', {
display: true,
- position: 'left',
offset: false,
reverse: false,
beginAtZero: false,
var scaleLabelOpts = opts.scaleLabel;
var gridLineOpts = opts.gridLines;
var display = me._isVisible();
- var isBottom = opts.position === 'bottom';
+ var labelsBelowTicks = opts.position !== 'top' && me.axis === 'x';
var isHorizontal = me.isHorizontal();
// Width
// Ensure that our ticks are always inside the canvas. When rotated, ticks are right aligned
// which means that the right padding is dominated by the font height
if (isRotated) {
- paddingLeft = isBottom ?
+ paddingLeft = labelsBelowTicks ?
cosRotation * firstLabelSize.width + sinRotation * firstLabelSize.offset :
sinRotation * (firstLabelSize.height - firstLabelSize.offset);
- paddingRight = isBottom ?
+ paddingRight = labelsBelowTicks ?
sinRotation * (lastLabelSize.height - lastLabelSize.offset) :
cosRotation * lastLabelSize.width + sinRotation * lastLabelSize.offset;
} else {
// Shared Methods
isHorizontal() {
- var pos = this.options.position;
- return pos === 'top' || pos === 'bottom';
+ const {axis, position} = this.options;
+ return position === 'top' || position === 'bottom' || axis === 'x';
}
isFullWidth() {
return this.options.fullWidth;
*/
_computeGridLineItems(chartArea) {
var me = this;
+ const axis = me.axis;
var chart = me.chart;
var options = me.options;
- var gridLines = options.gridLines;
- var position = options.position;
+ const {gridLines, position} = options;
var offsetGridLines = gridLines.offsetGridLines;
var isHorizontal = me.isHorizontal();
var ticks = me.ticks;
tx2 = borderValue - axisHalfWidth;
x1 = alignBorderValue(chartArea.left) + axisHalfWidth;
x2 = chartArea.right;
- } else {
+ } else if (position === 'right') {
borderValue = alignBorderValue(me.left);
x1 = chartArea.left;
x2 = alignBorderValue(chartArea.right) - axisHalfWidth;
tx1 = borderValue + axisHalfWidth;
tx2 = me.left + tl;
+ } else if (axis === 'x') {
+ if (position === 'center') {
+ borderValue = alignBorderValue((chartArea.top + chartArea.bottom) / 2);
+ } else if (helpers.isObject(position)) {
+ const positionAxisID = Object.keys(position)[0];
+ const value = position[positionAxisID];
+ borderValue = alignBorderValue(me.chart.scales[positionAxisID].getPixelForValue(value));
+ }
+
+ y1 = chartArea.top;
+ y2 = chartArea.bottom;
+ ty1 = borderValue + axisHalfWidth;
+ ty2 = ty1 + tl;
+ } else if (axis === 'y') {
+ if (position === 'center') {
+ borderValue = alignBorderValue((chartArea.left + chartArea.right) / 2);
+ } else if (helpers.isObject(position)) {
+ const positionAxisID = Object.keys(position)[0];
+ const value = position[positionAxisID];
+ borderValue = alignBorderValue(me.chart.scales[positionAxisID].getPixelForValue(value));
+ }
+
+ tx1 = borderValue - axisHalfWidth;
+ tx2 = tx1 - tl;
+ x1 = chartArea.left;
+ x2 = chartArea.right;
}
for (i = 0; i < ticksLength; ++i) {
/**
* @private
*/
- _computeLabelItems() {
- var me = this;
- var options = me.options;
- var optionTicks = options.ticks;
- var position = options.position;
- var isMirrored = optionTicks.mirror;
- var isHorizontal = me.isHorizontal();
- var ticks = me.ticks;
- var fonts = parseTickFontOptions(optionTicks);
- var tickPadding = optionTicks.padding;
- var tl = getTickMarkLength(options.gridLines);
- var rotation = -helpers.toRadians(me.labelRotation);
- var items = [];
- var i, ilen, tick, label, x, y, textAlign, pixel, font, lineHeight, lineCount, textOffset;
+ _computeLabelItems(chartArea) {
+ const me = this;
+ const axis = me.axis;
+ const options = me.options;
+ const {position, ticks: optionTicks} = options;
+ const isMirrored = optionTicks.mirror;
+ const isHorizontal = me.isHorizontal();
+ const ticks = me.ticks;
+ const fonts = parseTickFontOptions(optionTicks);
+ const tickPadding = optionTicks.padding;
+ const tl = getTickMarkLength(options.gridLines);
+ const rotation = -helpers.toRadians(me.labelRotation);
+ const items = [];
+ let i, ilen, tick, label, x, y, textAlign, pixel, font, lineHeight, lineCount, textOffset;
if (position === 'top') {
y = me.bottom - tl - tickPadding;
} else if (position === 'left') {
x = me.right - (isMirrored ? 0 : tl) - tickPadding;
textAlign = isMirrored ? 'left' : 'right';
- } else {
+ } else if (position === 'right') {
x = me.left + (isMirrored ? 0 : tl) + tickPadding;
textAlign = isMirrored ? 'right' : 'left';
+ } else if (axis === 'x') {
+ if (position === 'center') {
+ y = ((chartArea.top + chartArea.bottom) / 2) + tl + tickPadding;
+ } else if (helpers.isObject(position)) {
+ const positionAxisID = Object.keys(position)[0];
+ const value = position[positionAxisID];
+ y = me.chart.scales[positionAxisID].getPixelForValue(value) + tl + tickPadding;
+ }
+ textAlign = !rotation ? 'center' : 'right';
+ } else if (axis === 'y') {
+ if (position === 'center') {
+ x = ((chartArea.left + chartArea.right) / 2) - tl - tickPadding;
+ } else if (helpers.isObject(position)) {
+ const positionAxisID = Object.keys(position)[0];
+ const value = position[positionAxisID];
+ x = me.chart.scales[positionAxisID].getPixelForValue(value);
+ }
+ textAlign = 'right';
}
for (i = 0, ilen = ticks.length; i < ilen; ++i) {
/**
* @private
*/
- _drawLabels() {
+ _drawLabels(chartArea) {
var me = this;
var optionTicks = me.options.ticks;
}
var ctx = me.ctx;
- var items = me._labelItems || (me._labelItems = me._computeLabelItems());
+ var items = me._labelItems || (me._labelItems = me._computeLabelItems(chartArea));
var i, j, ilen, jlen, item, tickFont, label, y;
for (i = 0, ilen = items.length; i < ilen; ++i) {
me._drawGrid(chartArea);
me._drawTitle();
- me._drawLabels();
+ me._drawLabels(chartArea);
}
/**
import Scale from '../core/core.scale';
const defaultConfig = {
- position: 'bottom'
};
class CategoryScale extends Scale {
import Ticks from '../core/core.ticks';
const defaultConfig = {
- position: 'left',
ticks: {
callback: Ticks.formatters.linear
}
}
const defaultConfig = {
- position: 'left',
-
// label settings
ticks: {
callback: Ticks.formatters.logarithmic
}
const defaultConfig = {
- position: 'bottom',
-
/**
* Data distribution along the scale:
* - 'linear': data are spread according to their time (distances can vary),
--- /dev/null
+{
+ "config": {
+ "type": "scatter",
+ "data": {
+ "datasets": [{
+ "data": [{
+ "x": -20,
+ "y": -30
+ }, {
+ "x": 0,
+ "y": 0
+ }, {
+ "x": 20,
+ "y": 15
+ }]
+ }]
+ },
+ "options": {
+ "legend": false,
+ "title": false,
+ "scales": {
+ "x": {
+ "position": "center",
+ "axis": "x",
+ "min": -100,
+ "max": 100,
+ "gridLines": {
+ "color": "red",
+ "drawOnChartArea": false
+ },
+ "ticks": {
+ "display": false
+ }
+ },
+ "y": {
+ "position": "left",
+ "axis": "y",
+ "min": -100,
+ "max": 100,
+ "gridLines": {
+ "color": "red",
+ "drawOnChartArea": false
+ },
+ "ticks": {
+ "display": false
+ }
+ }
+ }
+ }
+ },
+ "options": {
+ "canvas": {
+ "height": 256,
+ "width": 512
+ }
+ }
+}
--- /dev/null
+{
+ "config": {
+ "type": "scatter",
+ "data": {
+ "datasets": [{
+ "data": [{
+ "x": -20,
+ "y": -30
+ }, {
+ "x": 0,
+ "y": 0
+ }, {
+ "x": 20,
+ "y": 15
+ }]
+ }]
+ },
+ "options": {
+ "legend": false,
+ "title": false,
+ "scales": {
+ "x": {
+ "position": {
+ "y": 30
+ },
+ "axis": "x",
+ "min": -100,
+ "max": 100,
+ "gridLines": {
+ "color": "red",
+ "drawOnChartArea": false
+ },
+ "ticks": {
+ "display": false
+ }
+ },
+ "y": {
+ "position": "left",
+ "axis": "y",
+ "min": -100,
+ "max": 100,
+ "gridLines": {
+ "color": "red",
+ "drawOnChartArea": false
+ },
+ "ticks": {
+ "display": false
+ }
+ }
+ }
+ }
+ },
+ "options": {
+ "canvas": {
+ "height": 256,
+ "width": 512
+ }
+ }
+}
--- /dev/null
+{
+ "config": {
+ "type": "scatter",
+ "data": {
+ "datasets": [{
+ "data": [{
+ "x": -20,
+ "y": -30
+ }, {
+ "x": 0,
+ "y": 0
+ }, {
+ "x": 20,
+ "y": 15
+ }]
+ }]
+ },
+ "options": {
+ "legend": false,
+ "title": false,
+ "scales": {
+ "x": {
+ "position": "bottom",
+ "axis": "x",
+ "min": -100,
+ "max": 100,
+ "gridLines": {
+ "color": "red",
+ "drawOnChartArea": false
+ },
+ "ticks": {
+ "display": false
+ }
+ },
+ "y": {
+ "position": "center",
+ "axis": "y",
+ "min": -100,
+ "max": 100,
+ "gridLines": {
+ "color": "red",
+ "drawOnChartArea": false
+ },
+ "ticks": {
+ "display": false
+ }
+ }
+ }
+ }
+ },
+ "options": {
+ "canvas": {
+ "height": 256,
+ "width": 512
+ }
+ }
+}
--- /dev/null
+{
+ "config": {
+ "type": "scatter",
+ "data": {
+ "datasets": [{
+ "data": [{
+ "x": -20,
+ "y": -30
+ }, {
+ "x": 0,
+ "y": 0
+ }, {
+ "x": 20,
+ "y": 15
+ }]
+ }]
+ },
+ "options": {
+ "legend": false,
+ "title": false,
+ "scales": {
+ "x": {
+ "position": "bottom",
+ "axis": "x",
+ "min": -100,
+ "max": 100,
+ "gridLines": {
+ "color": "red",
+ "drawOnChartArea": false
+ },
+ "ticks": {
+ "display": false
+ }
+ },
+ "y": {
+ "position": {
+ "x": -50
+ },
+ "axis": "y",
+ "min": -100,
+ "max": 100,
+ "gridLines": {
+ "color": "red",
+ "drawOnChartArea": false
+ },
+ "ticks": {
+ "display": false
+ }
+ }
+ }
+ }
+ },
+ "options": {
+ "canvas": {
+ "height": 256,
+ "width": 512
+ }
+ }
+}
borderDash: [],
borderDashOffset: 0.0
},
- position: 'bottom',
offset: false,
scaleLabel: Chart.defaults.scale.scaleLabel,
ticks: {
};
var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('category'));
+ config.position = 'bottom';
var Constructor = Chart.scaleService.getScaleConstructor('category');
var scale = new Constructor({
ctx: {},
};
var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('category'));
+ config.position = 'bottom';
var Constructor = Chart.scaleService.getScaleConstructor('category');
var scale = new Constructor({
ctx: {},
borderDash: [],
borderDashOffset: 0.0
},
- position: 'left',
offset: false,
reverse: false,
beginAtZero: false,
borderDash: [],
borderDashOffset: 0.0
},
- position: 'left',
offset: false,
reverse: false,
beginAtZero: false,
borderDash: [],
borderDashOffset: 0.0
},
- position: 'bottom',
offset: false,
reverse: false,
beginAtZero: false,