Cartesian axis text alignment (#7846)

* Generate textBaseline per tick label
* Enable configuration of tick alignment
* Add image based tests of text alignment options
This commit is contained in:
Evert Timberg 2020-10-10 11:38:55 -04:00 committed by GitHub
parent ef6a0e176c
commit dc4eac6323
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 330 additions and 8 deletions

View File

@ -42,6 +42,7 @@ The tick configuration is nested under the scale configuration in the `ticks` ke
| Name | Type | Scriptable | Default | Description
| ---- | ---- | :-------------------------------: | ------- | -----------
| `alignment` | `string` | | `'center'` | The tick alignment along the axis. Can be `'start'`, `'center'`, or `'end'`.
| `callback` | `function` | | | Returns the string representation of the tick value as it should be displayed on the chart. See [callback](../axes/labelling.md#creating-custom-tick-formats).
| `display` | `boolean` | | `true` | If true, show tick labels.
| `font` | `Font` | Yes | `defaults.font` | See [Fonts](../general/fonts.md)

View File

@ -145,6 +145,9 @@
}, {
title: 'Filtering Labels',
path: 'scales/filtering-labels.html'
}, {
title: 'Label Text Alignment',
path: 'scales/label-text-alignment.html'
}, {
title: 'Non numeric Y Axis',
path: 'scales/non-numeric-y.html'

View File

@ -0,0 +1,163 @@
<!doctype html>
<html>
<head>
<title>Label Text Alignment</title>
<script src="../../dist/chart.min.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;
}
.container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
}
</style>
</head>
<body>
<div class="container">
<div class="chart-container">
<canvas id="chart-start-start"></canvas>
</div>
<div class="chart-container">
<canvas id="chart-start-center"></canvas>
</div>
<div class="chart-container">
<canvas id="chart-start-end"></canvas>
</div>
<div class="chart-container">
<canvas id="chart-center-start"></canvas>
</div>
<div class="chart-container">
<canvas id="chart-center-center"></canvas>
</div>
<div class="chart-container">
<canvas id="chart-center-end"></canvas>
</div>
<div class="chart-container">
<canvas id="chart-end-start"></canvas>
</div>
<div class="chart-container">
<canvas id="chart-end-center"></canvas>
</div>
<div class="chart-container">
<canvas id="chart-end-end"></canvas>
</div>
</div>
<script>
var color = Chart.helpers.color;
function createConfig(xAlign, yAlign, colorName) {
return {
type: 'line',
data: {
labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],
datasets: [{
label: 'My First dataset',
data: [
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor()
],
backgroundColor: color(window.chartColors[colorName]).alpha(0.5).rgbString(),
borderColor: window.chartColors[colorName],
borderWidth: 1
}]
},
options: {
responsive: true,
legend: {
display: false,
},
scales: {
x: {
display: true,
ticks: {
alignment: xAlign,
}
},
y: {
display: true,
ticks: {
alignment: yAlign
}
}
},
title: {
display: true,
text: 'X Tick Alignment: ' + xAlign + ', Y Tick Alignment ' + yAlign
}
}
};
}
window.onload = function() {
[{
id: 'chart-start-start',
xAlign: 'start',
yAlign: 'start',
color: 'red'
}, {
id: 'chart-start-center',
xAlign: 'start',
yAlign: 'center',
color: 'orange'
}, {
id: 'chart-start-end',
xAlign: 'start',
yAlign: 'end',
color: 'yellow'
}, {
id: 'chart-center-start',
xAlign: 'center',
yAlign: 'start',
color: 'green'
}, {
id: 'chart-center-center',
xAlign: 'center',
yAlign: 'center',
color: 'blue'
}, {
id: 'chart-center-end',
xAlign: 'center',
yAlign: 'end',
color: 'purple'
}, {
id: 'chart-end-start',
xAlign: 'end',
yAlign: 'start',
color: 'grey'
}, {
id: 'chart-end-center',
xAlign: 'end',
yAlign: 'center',
color: 'red'
}, {
id: 'chart-end-end',
xAlign: 'end',
yAlign: 'end',
color: 'orange'
}].forEach(function(details) {
var ctx = document.getElementById(details.id).getContext('2d');
var config = createConfig(details.xAlign, details.yAlign, details.color);
new Chart(ctx, config);
});
};
</script>
</body>
</html>

View File

@ -61,7 +61,8 @@ defaults.set('scale', {
// We pass through arrays to be rendered as multiline labels, we convert Others to strings here.
callback: Ticks.formatters.values,
minor: {},
major: {}
major: {},
alignment: 'center',
}
});
@ -761,6 +762,12 @@ export default class Scale extends Element {
paddingRight = labelsBelowTicks ?
sinRotation * (lastLabelSize.height - lastLabelSize.offset) :
cosRotation * lastLabelSize.width + sinRotation * lastLabelSize.offset;
} else if (tickOpts.alignment === 'start') {
paddingLeft = 0;
paddingRight = lastLabelSize.width;
} else if (tickOpts.alignment === 'end') {
paddingLeft = firstLabelSize.width;
paddingRight = 0;
} else {
paddingLeft = firstLabelSize.width / 2;
paddingRight = lastLabelSize.width / 2;
@ -780,8 +787,19 @@ export default class Scale extends Element {
minSize.width = Math.min(me.maxWidth, minSize.width + labelWidth);
me.paddingTop = lastLabelSize.height / 2;
me.paddingBottom = firstLabelSize.height / 2;
let paddingTop = lastLabelSize.height / 2;
let paddingBottom = firstLabelSize.height / 2;
if (tickOpts.alignment === 'start') {
paddingTop = 0;
paddingBottom = firstLabelSize.height;
} else if (tickOpts.alignment === 'end') {
paddingTop = lastLabelSize.height;
paddingBottom = 0;
}
me.paddingTop = paddingTop;
me.paddingBottom = paddingBottom;
}
}
@ -1240,13 +1258,14 @@ export default class Scale extends Element {
const rotation = -toRadians(me.labelRotation);
const items = [];
let i, ilen, tick, label, x, y, textAlign, pixel, font, lineHeight, lineCount, textOffset;
let textBaseline = 'middle';
if (position === 'top') {
y = me.bottom - tl - tickPadding;
textAlign = !rotation ? 'center' : 'left';
textAlign = me._getXAxisLabelAlignment();
} else if (position === 'bottom') {
y = me.top + tl + tickPadding;
textAlign = !rotation ? 'center' : 'right';
textAlign = me._getXAxisLabelAlignment();
} else if (position === 'left') {
x = me.right - (isMirrored ? 0 : tl) - tickPadding;
textAlign = isMirrored ? 'left' : 'right';
@ -1261,7 +1280,7 @@ export default class Scale extends Element {
const value = position[positionAxisID];
y = me.chart.scales[positionAxisID].getPixelForValue(value) + tl + tickPadding;
}
textAlign = !rotation ? 'center' : 'right';
textAlign = me._getXAxisLabelAlignment();
} else if (axis === 'y') {
if (position === 'center') {
x = ((chartArea.left + chartArea.right) / 2) - tl - tickPadding;
@ -1273,6 +1292,14 @@ export default class Scale extends Element {
textAlign = 'right';
}
if (axis === 'y') {
if (optionTicks.alignment === 'start') {
textBaseline = 'top';
} else if (optionTicks.alignment === 'end') {
textBaseline = 'bottom';
}
}
for (i = 0, ilen = ticks.length; i < ilen; ++i) {
tick = ticks[i];
label = tick.label;
@ -1303,13 +1330,34 @@ export default class Scale extends Element {
label,
font,
textOffset,
textAlign
textAlign,
textBaseline,
});
}
return items;
}
_getXAxisLabelAlignment() {
const me = this;
const {position, ticks} = me.options;
const rotation = -toRadians(me.labelRotation);
if (rotation) {
return position === 'top' ? 'left' : 'right';
}
let align = 'center';
if (ticks.alignment === 'start') {
align = 'left';
} else if (ticks.alignment === 'end') {
align = 'right';
}
return align;
}
/**
* @protected
*/
@ -1409,8 +1457,8 @@ export default class Scale extends Element {
ctx.rotate(item.rotation);
ctx.font = tickFont.string;
ctx.fillStyle = tickFont.color;
ctx.textBaseline = 'middle';
ctx.textAlign = item.textAlign;
ctx.textBaseline = item.textBaseline;
if (useStroke) {
ctx.strokeStyle = tickFont.strokeStyle;

View File

@ -0,0 +1,34 @@
module.exports = {
config: {
type: 'line',
data: {
datasets: [{
data: [1, 2, 3],
}],
labels: ['Label1', 'Label2', 'Label3']
},
options: {
legend: false,
title: false,
scales: {
x: {
ticks: {
alignment: 'center',
},
},
y: {
ticks: {
alignment: 'center',
}
}
}
}
},
options: {
spriteText: true,
canvas: {
height: 256,
width: 512
}
}
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,34 @@
module.exports = {
config: {
type: 'line',
data: {
datasets: [{
data: [1, 2, 3],
}],
labels: ['Label1', 'Label2', 'Label3']
},
options: {
legend: false,
title: false,
scales: {
x: {
ticks: {
alignment: 'end',
},
},
y: {
ticks: {
alignment: 'end',
}
}
}
}
},
options: {
spriteText: true,
canvas: {
height: 256,
width: 512
}
}
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,34 @@
module.exports = {
config: {
type: 'line',
data: {
datasets: [{
data: [1, 2, 3],
}],
labels: ['Label1', 'Label2', 'Label3']
},
options: {
legend: false,
title: false,
scales: {
x: {
ticks: {
alignment: 'start',
},
},
y: {
ticks: {
alignment: 'start',
}
}
}
}
},
options: {
spriteText: true,
canvas: {
height: 256,
width: 512
}
}
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -128,6 +128,11 @@ export interface ICartesianScaleOptions extends ICoreScaleOptions {
* @default ticks.length
*/
sampleSize: number;
/**
* The label alignment
* @default 'center'
*/
alignment: 'start' | 'center' | 'end';
/**
* 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.
* @default true