Skip to content

Commit 376da21

Browse files
danielgindietimberg
authored andcommitted
Implemented RTL support for legends and tooltips (#6460)
Implemented RTL support for legends and tooltips
1 parent 995efa5 commit 376da21

File tree

6 files changed

+128
-19
lines changed

6 files changed

+128
-19
lines changed

docs/configuration/legend.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ The legend configuration is passed into the `options.legend` namespace. The glob
1616
| `onLeave` | `function` | | A callback that is called when a 'mousemove' event is registered outside of a previously hovered label item.
1717
| `reverse` | `boolean` | `false` | Legend will show datasets in reverse order.
1818
| `labels` | `object` | | See the [Legend Label Configuration](#legend-label-configuration) section below.
19+
| `rtl` | `boolean` | | `true` for rendering the legends from right to left.
20+
| `textDirection` | `string` | canvas' default | This will force the text direction `'rtl'|'ltr` on the canvas for rendering the legend, regardless of the css specified on the canvas
1921

2022
## Position
2123
Position of the legend. Options are:

docs/configuration/tooltip.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ The tooltip configuration is passed into the `options.tooltips` namespace. The g
4444
| `displayColors` | `boolean` | `true` | If true, color boxes are shown in the tooltip.
4545
| `borderColor` | `Color` | `'rgba(0, 0, 0, 0)'` | Color of the border.
4646
| `borderWidth` | `number` | `0` | Size of the border.
47+
| `rtl` | `boolean` | | `true` for rendering the legends from right to left.
48+
| `textDirection` | `string` | canvas' default | This will force the text direction `'rtl'|'ltr` on the canvas for rendering the tooltips, regardless of the css specified on the canvas
4749

4850
### Position Modes
4951

src/core/core.tooltip.js

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ var Element = require('./core.element');
55
var helpers = require('../helpers/index');
66

77
var valueOrDefault = helpers.valueOrDefault;
8+
var getRtlHelper = helpers.rtl.getRtlAdapter;
89

910
defaults._set('global', {
1011
tooltips: {
@@ -242,6 +243,10 @@ function getBaseModel(tooltipOpts) {
242243
xAlign: tooltipOpts.xAlign,
243244
yAlign: tooltipOpts.yAlign,
244245

246+
// Drawing direction and text direction
247+
rtl: tooltipOpts.rtl,
248+
textDirection: tooltipOpts.textDirection,
249+
245250
// Body
246251
bodyFontColor: tooltipOpts.bodyFontColor,
247252
_bodyFontFamily: valueOrDefault(tooltipOpts.bodyFontFamily, globalDefaults.defaultFontFamily),
@@ -752,9 +757,11 @@ var exports = Element.extend({
752757
var titleFontSize, titleSpacing, i;
753758

754759
if (length) {
760+
var rtlHelper = getRtlHelper(vm.rtl, vm.x, vm.width);
761+
755762
pt.x = getAlignedX(vm, vm._titleAlign);
756763

757-
ctx.textAlign = vm._titleAlign;
764+
ctx.textAlign = rtlHelper.textAlign(vm._titleAlign);
758765
ctx.textBaseline = 'middle';
759766

760767
titleFontSize = vm.titleFontSize;
@@ -764,7 +771,7 @@ var exports = Element.extend({
764771
ctx.font = helpers.fontString(titleFontSize, vm._titleFontStyle, vm._titleFontFamily);
765772

766773
for (i = 0; i < length; ++i) {
767-
ctx.fillText(title[i], pt.x, pt.y + titleFontSize / 2);
774+
ctx.fillText(title[i], rtlHelper.x(pt.x), pt.y + titleFontSize / 2);
768775
pt.y += titleFontSize + titleSpacing; // Line Height and spacing
769776

770777
if (i + 1 === length) {
@@ -783,24 +790,27 @@ var exports = Element.extend({
783790
var xLinePadding = 0;
784791
var colorX = drawColorBoxes ? getAlignedX(vm, 'left') : 0;
785792

793+
var rtlHelper = getRtlHelper(vm.rtl, vm.x, vm.width);
794+
786795
var fillLineOfText = function(line) {
787-
ctx.fillText(line, pt.x + xLinePadding, pt.y + bodyFontSize / 2);
796+
ctx.fillText(line, rtlHelper.x(pt.x + xLinePadding), pt.y + bodyFontSize / 2);
788797
pt.y += bodyFontSize + bodySpacing;
789798
};
790799

791800
var bodyItem, textColor, labelColors, lines, i, j, ilen, jlen;
801+
var bodyAlignForCalculation = rtlHelper.textAlign(bodyAlign);
792802

793803
ctx.textAlign = bodyAlign;
794804
ctx.textBaseline = 'middle';
795805
ctx.font = helpers.fontString(bodyFontSize, vm._bodyFontStyle, vm._bodyFontFamily);
796806

797-
pt.x = getAlignedX(vm, bodyAlign);
807+
pt.x = getAlignedX(vm, bodyAlignForCalculation);
798808

799809
// Before body lines
800810
ctx.fillStyle = vm.bodyFontColor;
801811
helpers.each(vm.beforeBody, fillLineOfText);
802812

803-
xLinePadding = drawColorBoxes && bodyAlign !== 'right'
813+
xLinePadding = drawColorBoxes && bodyAlignForCalculation !== 'right'
804814
? bodyAlign === 'center' ? (bodyFontSize / 2 + 1) : (bodyFontSize + 2)
805815
: 0;
806816

@@ -817,18 +827,20 @@ var exports = Element.extend({
817827
for (j = 0, jlen = lines.length; j < jlen; ++j) {
818828
// Draw Legend-like boxes if needed
819829
if (drawColorBoxes) {
830+
var rtlColorX = rtlHelper.x(colorX);
831+
820832
// Fill a white rect so that colours merge nicely if the opacity is < 1
821833
ctx.fillStyle = vm.legendColorBackground;
822-
ctx.fillRect(colorX, pt.y, bodyFontSize, bodyFontSize);
834+
ctx.fillRect(rtlHelper.leftForLtr(rtlColorX, bodyFontSize), pt.y, bodyFontSize, bodyFontSize);
823835

824836
// Border
825837
ctx.lineWidth = 1;
826838
ctx.strokeStyle = labelColors.borderColor;
827-
ctx.strokeRect(colorX, pt.y, bodyFontSize, bodyFontSize);
839+
ctx.strokeRect(rtlHelper.leftForLtr(rtlColorX, bodyFontSize), pt.y, bodyFontSize, bodyFontSize);
828840

829841
// Inner square
830842
ctx.fillStyle = labelColors.backgroundColor;
831-
ctx.fillRect(colorX + 1, pt.y + 1, bodyFontSize - 2, bodyFontSize - 2);
843+
ctx.fillRect(rtlHelper.leftForLtr(rtlHelper.xPlus(rtlColorX, 1), bodyFontSize - 2), pt.y + 1, bodyFontSize - 2, bodyFontSize - 2);
832844
ctx.fillStyle = textColor;
833845
}
834846

@@ -852,10 +864,12 @@ var exports = Element.extend({
852864
var footerFontSize, i;
853865

854866
if (length) {
867+
var rtlHelper = getRtlHelper(vm.rtl, vm.x, vm.width);
868+
855869
pt.x = getAlignedX(vm, vm._footerAlign);
856870
pt.y += vm.footerMarginTop;
857871

858-
ctx.textAlign = vm._footerAlign;
872+
ctx.textAlign = rtlHelper.textAlign(vm._footerAlign);
859873
ctx.textBaseline = 'middle';
860874

861875
footerFontSize = vm.footerFontSize;
@@ -864,7 +878,7 @@ var exports = Element.extend({
864878
ctx.font = helpers.fontString(footerFontSize, vm._footerFontStyle, vm._footerFontFamily);
865879

866880
for (i = 0; i < length; ++i) {
867-
ctx.fillText(footer[i], pt.x, pt.y + footerFontSize / 2);
881+
ctx.fillText(footer[i], rtlHelper.x(pt.x), pt.y + footerFontSize / 2);
868882
pt.y += footerFontSize + vm.footerSpacing;
869883
}
870884
}
@@ -946,6 +960,8 @@ var exports = Element.extend({
946960
// Draw Title, Body, and Footer
947961
pt.y += vm.yPadding;
948962

963+
helpers.rtl.overrideTextDirection(ctx, vm.textDirection);
964+
949965
// Titles
950966
this.drawTitle(pt, vm, ctx);
951967

@@ -955,6 +971,8 @@ var exports = Element.extend({
955971
// Footer
956972
this.drawFooter(pt, vm, ctx);
957973

974+
helpers.rtl.restoreTextDirection(ctx, vm.textDirection);
975+
958976
ctx.restore();
959977
}
960978
},

src/helpers/helpers.rtl.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
'use strict';
2+
3+
var getRtlAdapter = function(rectX, width) {
4+
return {
5+
x: function(x) {
6+
return rectX + rectX + width - x;
7+
},
8+
setWidth: function(w) {
9+
width = w;
10+
},
11+
textAlign: function(align) {
12+
if (align === 'center') {
13+
return align;
14+
}
15+
return align === 'right' ? 'left' : 'right';
16+
},
17+
xPlus: function(x, value) {
18+
return x - value;
19+
},
20+
leftForLtr: function(x, itemWidth) {
21+
return x - itemWidth;
22+
},
23+
};
24+
};
25+
26+
var getLtrAdapter = function() {
27+
return {
28+
x: function(x) {
29+
return x;
30+
},
31+
setWidth: function(w) { // eslint-disable-line no-unused-vars
32+
},
33+
textAlign: function(align) {
34+
return align;
35+
},
36+
xPlus: function(x, value) {
37+
return x + value;
38+
},
39+
leftForLtr: function(x, _itemWidth) { // eslint-disable-line no-unused-vars
40+
return x;
41+
},
42+
};
43+
};
44+
45+
var getAdapter = function(rtl, rectX, width) {
46+
return rtl ? getRtlAdapter(rectX, width) : getLtrAdapter();
47+
};
48+
49+
var overrideTextDirection = function(ctx, direction) {
50+
var style, original;
51+
if (direction === 'ltr' || direction === 'rtl') {
52+
style = ctx.canvas.style;
53+
original = [
54+
style.getPropertyValue('direction'),
55+
style.getPropertyPriority('direction'),
56+
];
57+
58+
style.setProperty('direction', direction, 'important');
59+
ctx.prevTextDirection = original;
60+
}
61+
};
62+
63+
var restoreTextDirection = function(ctx) {
64+
var original = ctx.prevTextDirection;
65+
if (original !== undefined) {
66+
delete ctx.prevTextDirection;
67+
ctx.canvas.style.setProperty('direction', original[0], original[1]);
68+
}
69+
};
70+
71+
module.exports = {
72+
getRtlAdapter: getAdapter,
73+
overrideTextDirection: overrideTextDirection,
74+
restoreTextDirection: restoreTextDirection,
75+
};

src/helpers/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ module.exports.easing = require('./helpers.easing');
55
module.exports.canvas = require('./helpers.canvas');
66
module.exports.options = require('./helpers.options');
77
module.exports.math = require('./helpers.math');
8+
module.exports.rtl = require('./helpers.rtl');

src/plugins/plugin.legend.js

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ var Element = require('../core/core.element');
55
var helpers = require('../helpers/index');
66
var layouts = require('../core/core.layouts');
77

8+
var getRtlHelper = helpers.rtl.getRtlAdapter;
89
var noop = helpers.noop;
910
var valueOrDefault = helpers.valueOrDefault;
1011

@@ -355,14 +356,15 @@ var Legend = Element.extend({
355356
return;
356357
}
357358

359+
var rtlHelper = getRtlHelper(opts.rtl, me.left, me.minSize.width);
358360
var ctx = me.ctx;
359361
var fontColor = valueOrDefault(labelOpts.fontColor, globalDefaults.defaultFontColor);
360362
var labelFont = helpers.options._parseFont(labelOpts);
361363
var fontSize = labelFont.size;
362364
var cursor;
363365

364366
// Canvas setup
365-
ctx.textAlign = 'left';
367+
ctx.textAlign = rtlHelper.textAlign('left');
366368
ctx.textBaseline = 'middle';
367369
ctx.lineWidth = 0.5;
368370
ctx.strokeStyle = fontColor; // for strikethrough effect
@@ -398,24 +400,25 @@ var Legend = Element.extend({
398400
// Recalculate x and y for drawPoint() because its expecting
399401
// x and y to be center of figure (instead of top left)
400402
var radius = boxWidth * Math.SQRT2 / 2;
401-
var centerX = x + boxWidth / 2;
403+
var centerX = rtlHelper.xPlus(x, boxWidth / 2);
402404
var centerY = y + fontSize / 2;
403405

404406
// Draw pointStyle as legend symbol
405407
helpers.canvas.drawPoint(ctx, legendItem.pointStyle, radius, centerX, centerY, legendItem.rotation);
406408
} else {
407409
// Draw box as legend symbol
408-
ctx.fillRect(x, y, boxWidth, fontSize);
410+
ctx.fillRect(rtlHelper.leftForLtr(x, boxWidth), y, boxWidth, fontSize);
409411
if (lineWidth !== 0) {
410-
ctx.strokeRect(x, y, boxWidth, fontSize);
412+
ctx.strokeRect(rtlHelper.leftForLtr(x, boxWidth), y, boxWidth, fontSize);
411413
}
412414
}
413415

414416
ctx.restore();
415417
};
418+
416419
var fillText = function(x, y, legendItem, textWidth) {
417420
var halfFontSize = fontSize / 2;
418-
var xLeft = boxWidth + halfFontSize + x;
421+
var xLeft = rtlHelper.xPlus(x, boxWidth + halfFontSize);
419422
var yMiddle = y + halfFontSize;
420423

421424
ctx.fillText(legendItem.text, xLeft, yMiddle);
@@ -425,7 +428,7 @@ var Legend = Element.extend({
425428
ctx.beginPath();
426429
ctx.lineWidth = 2;
427430
ctx.moveTo(xLeft, yMiddle);
428-
ctx.lineTo(xLeft + textWidth, yMiddle);
431+
ctx.lineTo(rtlHelper.xPlus(xLeft, textWidth), yMiddle);
429432
ctx.stroke();
430433
}
431434
};
@@ -457,13 +460,17 @@ var Legend = Element.extend({
457460
};
458461
}
459462

463+
helpers.rtl.overrideTextDirection(me.ctx, opts.textDirection);
464+
460465
var itemHeight = fontSize + labelOpts.padding;
461466
helpers.each(me.legendItems, function(legendItem, i) {
462467
var textWidth = ctx.measureText(legendItem.text).width;
463468
var width = boxWidth + (fontSize / 2) + textWidth;
464469
var x = cursor.x;
465470
var y = cursor.y;
466471

472+
rtlHelper.setWidth(me.minSize.width);
473+
467474
// Use (me.left + me.minSize.width) and (me.top + me.minSize.height)
468475
// instead of me.right and me.bottom because me.width and me.height
469476
// may have been changed since me.minSize was calculated
@@ -479,20 +486,24 @@ var Legend = Element.extend({
479486
y = cursor.y = me.top + alignmentOffset(legendHeight, columnHeights[cursor.line]);
480487
}
481488

482-
drawLegendBox(x, y, legendItem);
489+
var realX = rtlHelper.x(x);
483490

484-
hitboxes[i].left = x;
491+
drawLegendBox(realX, y, legendItem);
492+
493+
hitboxes[i].left = rtlHelper.leftForLtr(realX, hitboxes[i].width);
485494
hitboxes[i].top = y;
486495

487496
// Fill the actual label
488-
fillText(x, y, legendItem, textWidth);
497+
fillText(realX, y, legendItem, textWidth);
489498

490499
if (isHorizontal) {
491500
cursor.x += width + labelOpts.padding;
492501
} else {
493502
cursor.y += itemHeight;
494503
}
495504
});
505+
506+
helpers.rtl.restoreTextDirection(me.ctx, opts.textDirection);
496507
},
497508

498509
/**

0 commit comments

Comments
 (0)