Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented RTL support for legends and tooltips #6460

Merged
merged 5 commits into from Sep 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/configuration/legend.md
Expand Up @@ -16,6 +16,8 @@ The legend configuration is passed into the `options.legend` namespace. The glob
| `onLeave` | `function` | | A callback that is called when a 'mousemove' event is registered outside of a previously hovered label item.
| `reverse` | `boolean` | `false` | Legend will show datasets in reverse order.
| `labels` | `object` | | See the [Legend Label Configuration](#legend-label-configuration) section below.
| `rtl` | `boolean` | | `true` for rendering the legends from right to left.
| `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

## Position
Position of the legend. Options are:
Expand Down
2 changes: 2 additions & 0 deletions docs/configuration/tooltip.md
Expand Up @@ -44,6 +44,8 @@ The tooltip configuration is passed into the `options.tooltips` namespace. The g
| `displayColors` | `boolean` | `true` | If true, color boxes are shown in the tooltip.
| `borderColor` | `Color` | `'rgba(0, 0, 0, 0)'` | Color of the border.
| `borderWidth` | `number` | `0` | Size of the border.
| `rtl` | `boolean` | | `true` for rendering the legends from right to left.
| `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

### Position Modes

Expand Down
38 changes: 28 additions & 10 deletions src/core/core.tooltip.js
Expand Up @@ -5,6 +5,7 @@ var Element = require('./core.element');
var helpers = require('../helpers/index');

var valueOrDefault = helpers.valueOrDefault;
var getRtlHelper = helpers.rtl.getRtlAdapter;

defaults._set('global', {
tooltips: {
Expand Down Expand Up @@ -242,6 +243,10 @@ function getBaseModel(tooltipOpts) {
xAlign: tooltipOpts.xAlign,
yAlign: tooltipOpts.yAlign,

// Drawing direction and text direction
rtl: tooltipOpts.rtl,
textDirection: tooltipOpts.textDirection,

// Body
bodyFontColor: tooltipOpts.bodyFontColor,
_bodyFontFamily: valueOrDefault(tooltipOpts.bodyFontFamily, globalDefaults.defaultFontFamily),
Expand Down Expand Up @@ -752,9 +757,11 @@ var exports = Element.extend({
var titleFontSize, titleSpacing, i;

if (length) {
var rtlHelper = getRtlHelper(vm.rtl, vm.x, vm.width);

pt.x = getAlignedX(vm, vm._titleAlign);

ctx.textAlign = vm._titleAlign;
ctx.textAlign = rtlHelper.textAlign(vm._titleAlign);
ctx.textBaseline = 'middle';

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

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

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

var rtlHelper = getRtlHelper(vm.rtl, vm.x, vm.width);

var fillLineOfText = function(line) {
ctx.fillText(line, pt.x + xLinePadding, pt.y + bodyFontSize / 2);
ctx.fillText(line, rtlHelper.x(pt.x + xLinePadding), pt.y + bodyFontSize / 2);
pt.y += bodyFontSize + bodySpacing;
};

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

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

pt.x = getAlignedX(vm, bodyAlign);
pt.x = getAlignedX(vm, bodyAlignForCalculation);

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

xLinePadding = drawColorBoxes && bodyAlign !== 'right'
xLinePadding = drawColorBoxes && bodyAlignForCalculation !== 'right'
? bodyAlign === 'center' ? (bodyFontSize / 2 + 1) : (bodyFontSize + 2)
: 0;

Expand All @@ -817,18 +827,20 @@ var exports = Element.extend({
for (j = 0, jlen = lines.length; j < jlen; ++j) {
// Draw Legend-like boxes if needed
if (drawColorBoxes) {
var rtlColorX = rtlHelper.x(colorX);

// Fill a white rect so that colours merge nicely if the opacity is < 1
ctx.fillStyle = vm.legendColorBackground;
ctx.fillRect(colorX, pt.y, bodyFontSize, bodyFontSize);
ctx.fillRect(rtlHelper.leftForLtr(rtlColorX, bodyFontSize), pt.y, bodyFontSize, bodyFontSize);

// Border
ctx.lineWidth = 1;
ctx.strokeStyle = labelColors.borderColor;
ctx.strokeRect(colorX, pt.y, bodyFontSize, bodyFontSize);
ctx.strokeRect(rtlHelper.leftForLtr(rtlColorX, bodyFontSize), pt.y, bodyFontSize, bodyFontSize);

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

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

if (length) {
var rtlHelper = getRtlHelper(vm.rtl, vm.x, vm.width);

pt.x = getAlignedX(vm, vm._footerAlign);
pt.y += vm.footerMarginTop;

ctx.textAlign = vm._footerAlign;
ctx.textAlign = rtlHelper.textAlign(vm._footerAlign);
ctx.textBaseline = 'middle';

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

for (i = 0; i < length; ++i) {
ctx.fillText(footer[i], pt.x, pt.y + footerFontSize / 2);
ctx.fillText(footer[i], rtlHelper.x(pt.x), pt.y + footerFontSize / 2);
pt.y += footerFontSize + vm.footerSpacing;
}
}
Expand Down Expand Up @@ -946,6 +960,8 @@ var exports = Element.extend({
// Draw Title, Body, and Footer
pt.y += vm.yPadding;

helpers.rtl.overrideTextDirection(ctx, vm.textDirection);

// Titles
this.drawTitle(pt, vm, ctx);

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

helpers.rtl.restoreTextDirection(ctx, vm.textDirection);

ctx.restore();
}
},
Expand Down
75 changes: 75 additions & 0 deletions src/helpers/helpers.rtl.js
@@ -0,0 +1,75 @@
'use strict';

var getRtlAdapter = function(rectX, width) {
return {
x: function(x) {
return rectX + rectX + width - x;
},
setWidth: function(w) {
width = w;
},
textAlign: function(align) {
if (align === 'center') {
return align;
}
return align === 'right' ? 'left' : 'right';
},
xPlus: function(x, value) {
return x - value;
},
leftForLtr: function(x, itemWidth) {
return x - itemWidth;
},
};
};

var getLtrAdapter = function() {
return {
x: function(x) {
return x;
},
setWidth: function(w) { // eslint-disable-line no-unused-vars
},
textAlign: function(align) {
return align;
},
xPlus: function(x, value) {
return x + value;
},
leftForLtr: function(x, _itemWidth) { // eslint-disable-line no-unused-vars
return x;
},
};
};

var getAdapter = function(rtl, rectX, width) {
return rtl ? getRtlAdapter(rectX, width) : getLtrAdapter();
};

var overrideTextDirection = function(ctx, direction) {
var style, original;
if (direction === 'ltr' || direction === 'rtl') {
style = ctx.canvas.style;
original = [
style.getPropertyValue('direction'),
style.getPropertyPriority('direction'),
];

style.setProperty('direction', direction, 'important');
ctx.prevTextDirection = original;
}
};

var restoreTextDirection = function(ctx) {
danielgindi marked this conversation as resolved.
Show resolved Hide resolved
var original = ctx.prevTextDirection;
if (original !== undefined) {
delete ctx.prevTextDirection;
ctx.canvas.style.setProperty('direction', original[0], original[1]);
}
};

module.exports = {
getRtlAdapter: getAdapter,
overrideTextDirection: overrideTextDirection,
restoreTextDirection: restoreTextDirection,
};
1 change: 1 addition & 0 deletions src/helpers/index.js
Expand Up @@ -5,3 +5,4 @@ module.exports.easing = require('./helpers.easing');
module.exports.canvas = require('./helpers.canvas');
module.exports.options = require('./helpers.options');
module.exports.math = require('./helpers.math');
module.exports.rtl = require('./helpers.rtl');
29 changes: 20 additions & 9 deletions src/plugins/plugin.legend.js
Expand Up @@ -5,6 +5,7 @@ var Element = require('../core/core.element');
var helpers = require('../helpers/index');
var layouts = require('../core/core.layouts');

var getRtlHelper = helpers.rtl.getRtlAdapter;
var noop = helpers.noop;
var valueOrDefault = helpers.valueOrDefault;

Expand Down Expand Up @@ -355,14 +356,15 @@ var Legend = Element.extend({
return;
}

var rtlHelper = getRtlHelper(opts.rtl, me.left, me.minSize.width);
var ctx = me.ctx;
var fontColor = valueOrDefault(labelOpts.fontColor, globalDefaults.defaultFontColor);
var labelFont = helpers.options._parseFont(labelOpts);
var fontSize = labelFont.size;
var cursor;

// Canvas setup
ctx.textAlign = 'left';
ctx.textAlign = rtlHelper.textAlign('left');
ctx.textBaseline = 'middle';
ctx.lineWidth = 0.5;
ctx.strokeStyle = fontColor; // for strikethrough effect
Expand Down Expand Up @@ -398,24 +400,25 @@ var Legend = Element.extend({
// Recalculate x and y for drawPoint() because its expecting
// x and y to be center of figure (instead of top left)
var radius = boxWidth * Math.SQRT2 / 2;
var centerX = x + boxWidth / 2;
var centerX = rtlHelper.xPlus(x, boxWidth / 2);
var centerY = y + fontSize / 2;

// Draw pointStyle as legend symbol
helpers.canvas.drawPoint(ctx, legendItem.pointStyle, radius, centerX, centerY, legendItem.rotation);
} else {
// Draw box as legend symbol
ctx.fillRect(x, y, boxWidth, fontSize);
ctx.fillRect(rtlHelper.leftForLtr(x, boxWidth), y, boxWidth, fontSize);
if (lineWidth !== 0) {
ctx.strokeRect(x, y, boxWidth, fontSize);
ctx.strokeRect(rtlHelper.leftForLtr(x, boxWidth), y, boxWidth, fontSize);
}
}

ctx.restore();
};

var fillText = function(x, y, legendItem, textWidth) {
var halfFontSize = fontSize / 2;
var xLeft = boxWidth + halfFontSize + x;
var xLeft = rtlHelper.xPlus(x, boxWidth + halfFontSize);
var yMiddle = y + halfFontSize;

ctx.fillText(legendItem.text, xLeft, yMiddle);
Expand All @@ -425,7 +428,7 @@ var Legend = Element.extend({
ctx.beginPath();
ctx.lineWidth = 2;
ctx.moveTo(xLeft, yMiddle);
ctx.lineTo(xLeft + textWidth, yMiddle);
ctx.lineTo(rtlHelper.xPlus(xLeft, textWidth), yMiddle);
ctx.stroke();
}
};
Expand Down Expand Up @@ -457,13 +460,17 @@ var Legend = Element.extend({
};
}

helpers.rtl.overrideTextDirection(me.ctx, opts.textDirection);

var itemHeight = fontSize + labelOpts.padding;
helpers.each(me.legendItems, function(legendItem, i) {
var textWidth = ctx.measureText(legendItem.text).width;
var width = boxWidth + (fontSize / 2) + textWidth;
var x = cursor.x;
var y = cursor.y;

rtlHelper.setWidth(me.minSize.width);

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

drawLegendBox(x, y, legendItem);
var realX = rtlHelper.x(x);

hitboxes[i].left = x;
drawLegendBox(realX, y, legendItem);

hitboxes[i].left = rtlHelper.leftForLtr(realX, hitboxes[i].width);
hitboxes[i].top = y;

// Fill the actual label
fillText(x, y, legendItem, textWidth);
fillText(realX, y, legendItem, textWidth);

if (isHorizontal) {
cursor.x += width + labelOpts.padding;
} else {
cursor.y += itemHeight;
}
});

helpers.rtl.restoreTextDirection(me.ctx, opts.textDirection);
},

/**
Expand Down