diff --git a/docs/configuration/legend.md b/docs/configuration/legend.md index 5caef61759d..e6ab285e780 100644 --- a/docs/configuration/legend.md +++ b/docs/configuration/legend.md @@ -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: diff --git a/docs/configuration/tooltip.md b/docs/configuration/tooltip.md index 2ae32ac91fb..213f5c4b891 100644 --- a/docs/configuration/tooltip.md +++ b/docs/configuration/tooltip.md @@ -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 diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index 00e97ca3810..97f7f16df75 100644 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -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: { @@ -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), @@ -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; @@ -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) { @@ -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; @@ -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; } @@ -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; @@ -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; } } @@ -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); @@ -955,6 +971,8 @@ var exports = Element.extend({ // Footer this.drawFooter(pt, vm, ctx); + helpers.rtl.restoreTextDirection(ctx, vm.textDirection); + ctx.restore(); } }, diff --git a/src/helpers/helpers.rtl.js b/src/helpers/helpers.rtl.js new file mode 100644 index 00000000000..392b0534be4 --- /dev/null +++ b/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) { + 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, +}; diff --git a/src/helpers/index.js b/src/helpers/index.js index ef510076090..167c550b6c2 100644 --- a/src/helpers/index.js +++ b/src/helpers/index.js @@ -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'); diff --git a/src/plugins/plugin.legend.js b/src/plugins/plugin.legend.js index b98f2655dc1..78a5efe7076 100644 --- a/src/plugins/plugin.legend.js +++ b/src/plugins/plugin.legend.js @@ -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; @@ -355,6 +356,7 @@ 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); @@ -362,7 +364,7 @@ var Legend = Element.extend({ 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 @@ -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); @@ -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(); } }; @@ -457,6 +460,8 @@ 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; @@ -464,6 +469,8 @@ var Legend = Element.extend({ 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 @@ -479,13 +486,15 @@ 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; @@ -493,6 +502,8 @@ var Legend = Element.extend({ cursor.y += itemHeight; } }); + + helpers.rtl.restoreTextDirection(me.ctx, opts.textDirection); }, /**