From: Evert Timberg Date: Sat, 26 Dec 2020 16:23:02 +0000 (-0500) Subject: Create standardized text render method (#8227) X-Git-Tag: v3.0.0-beta.8~33 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=988b3c5d2b6f91d839e2f7c9329ade42c5ba8f32;p=thirdparty%2FChart.js.git Create standardized text render method (#8227) * Create standardized text render method * Document renderText options and enable configurable decoration width * Add tests for font rendering * Split color definition to it's own file * renderText supports setting styles * Mock context needs to track textBaseline * renderText can set textAlign and textBaseline * renderText does not mutate the context + translate/rotate * Explicitly set the text decoration style * Move useStroke logic into renderText * Cartesian scale: Update computeLabelItems to avoid duplicate allocations --- diff --git a/docs/docs/axes/_common_ticks.md b/docs/docs/axes/_common_ticks.md index 365286e54..658ce49bf 100644 --- a/docs/docs/axes/_common_ticks.md +++ b/docs/docs/axes/_common_ticks.md @@ -8,6 +8,6 @@ | `font` | `Font` | Yes | `Chart.defaults.font` | See [Fonts](../general/fonts.md) | `major` | `object` | | `{}` | [Major ticks configuration](./styling.mdx#major-tick-configuration). | `padding` | `number` | | `0` | Sets the offset of the tick labels from the axis -| `textStrokeColor` | `string` | | `` | The color of the stroke around the text. -| `textStrokeWidth` | `number` | | `0` | Stroke width around the text. +| `textStrokeColor` | [`Color`](../general/colors.md) | Yes | `` | The color of the stroke around the text. +| `textStrokeWidth` | `number` | Yes | `0` | Stroke width around the text. | `z` | `number` | | `0` | z-index of tick layer. Useful when ticks are drawn on chart area. Values <= 0 are drawn under datasets, > 0 on top. diff --git a/src/core/core.scale.js b/src/core/core.scale.js index e844544f1..66c890b4c 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -1,6 +1,6 @@ import defaults from './core.defaults'; import Element from './core.element'; -import {_alignPixel, _measureText} from '../helpers/helpers.canvas'; +import {_alignPixel, _measureText, renderText} from '../helpers/helpers.canvas'; import {callback as call, each, finiteOrDefault, isArray, isFinite, isNullOrUndef, isObject, valueOrDefault} from '../helpers/helpers.core'; import {_factorize, toDegrees, toRadians, _int16Range, HALF_PI} from '../helpers/helpers.math'; import {toFont, resolve, toPadding} from '../helpers/helpers.options'; @@ -1386,6 +1386,8 @@ export default class Scale extends Element { lineCount = isArray(label) ? label.length : 1; const halfCount = lineCount / 2; const color = resolve([optionTicks.color], me.getContext(i), i); + const strokeColor = resolve([optionTicks.textStrokeColor], me.getContext(i), i); + const strokeWidth = resolve([optionTicks.textStrokeWidth], me.getContext(i), i); if (isHorizontal) { x = pixel; @@ -1417,15 +1419,16 @@ export default class Scale extends Element { } items.push({ - x, - y, rotation, label, font, color, + strokeColor, + strokeWidth, textOffset, textAlign, textBaseline, + translation: [x, y] }); } @@ -1596,45 +1599,14 @@ export default class Scale extends Element { const ctx = me.ctx; const items = me._labelItems || (me._labelItems = me._computeLabelItems(chartArea)); - let i, j, ilen, jlen; + let i, ilen; for (i = 0, ilen = items.length; i < ilen; ++i) { const item = items[i]; const tickFont = item.font; - const useStroke = optionTicks.textStrokeWidth > 0 && optionTicks.textStrokeColor !== ''; - - // Make sure we draw text in the correct color and font - ctx.save(); - ctx.translate(item.x, item.y); - ctx.rotate(item.rotation); - ctx.font = tickFont.string; - ctx.fillStyle = item.color; - ctx.textAlign = item.textAlign; - ctx.textBaseline = item.textBaseline; - - if (useStroke) { - ctx.strokeStyle = optionTicks.textStrokeColor; - ctx.lineWidth = optionTicks.textStrokeWidth; - } - const label = item.label; let y = item.textOffset; - if (isArray(label)) { - for (j = 0, jlen = label.length; j < jlen; ++j) { - // We just make sure the multiline element is a string here.. - if (useStroke) { - ctx.strokeText('' + label[j], 0, y); - } - ctx.fillText('' + label[j], 0, y); - y += tickFont.lineHeight; - } - } else { - if (useStroke) { - ctx.strokeText(label, 0, y); - } - ctx.fillText(label, 0, y); - } - ctx.restore(); + renderText(ctx, label, 0, y, tickFont, item); } } @@ -1700,15 +1672,13 @@ export default class Scale extends Element { rotation = isLeft ? -HALF_PI : HALF_PI; } - ctx.save(); - ctx.translate(scaleLabelX, scaleLabelY); - ctx.rotate(rotation); - ctx.textAlign = textAlign; - ctx.textBaseline = 'middle'; - ctx.fillStyle = scaleLabel.color; - ctx.font = scaleLabelFont.string; - ctx.fillText(scaleLabel.labelString, 0, 0); - ctx.restore(); + renderText(ctx, scaleLabel.labelString, 0, 0, scaleLabelFont, { + color: scaleLabel.color, + rotation, + textAlign, + textBaseline: 'middle', + translation: [scaleLabelX, scaleLabelY], + }); } draw(chartArea) { diff --git a/src/helpers/helpers.canvas.js b/src/helpers/helpers.canvas.js index 7f8c8356c..5d463de28 100644 --- a/src/helpers/helpers.canvas.js +++ b/src/helpers/helpers.canvas.js @@ -291,3 +291,80 @@ export function _bezierCurveTo(ctx, previous, target, flip) { target.x, target.y); } + +/** + * Render text onto the canvas + */ +export function renderText(ctx, text, x, y, font, opts = {}) { + const lines = isArray(text) ? text : [text]; + const stroke = opts.strokeWidth > 0 && opts.strokeColor !== ''; + let i, line; + + ctx.save(); + + if (opts.translation) { + ctx.translate(opts.translation[0], opts.translation[1]); + } + + if (!isNullOrUndef(opts.rotation)) { + ctx.rotate(opts.rotation); + } + + ctx.font = font.fontString; + + if (opts.color) { + ctx.fillStyle = opts.color; + } + + if (opts.textAlign) { + ctx.textAlign = opts.textAlign; + } + + if (opts.textBaseline) { + ctx.textBaseline = opts.textBaseline; + } + + for (i = 0; i < lines.length; ++i) { + line = lines[i]; + + if (stroke) { + if (opts.strokeColor) { + ctx.strokeStyle = opts.strokeColor; + } + + if (!isNullOrUndef(opts.strokeWidth)) { + ctx.lineWidth = opts.strokeWidth; + } + + ctx.strokeText(line, x, y, opts.maxWidth); + } + + ctx.fillText(line, x, y, opts.maxWidth); + + if (opts.strikethrough || opts.underline) { + /** + * Now that IE11 support has been dropped, we can use more + * of the TextMetrics object. The actual bounding boxes + * are unflagged in Chrome, Firefox, Edge, and Safari so they + * can be safely used. + * See https://developer.mozilla.org/en-US/docs/Web/API/TextMetrics#Browser_compatibility + */ + const metrics = ctx.measureText(line); + const left = x - metrics.actualBoundingBoxLeft; + const right = x + metrics.actualBoundingBoxRight; + const top = y - metrics.actualBoundingBoxAscent; + const bottom = y + metrics.actualBoundingBoxDescent; + const yDecoration = opts.strikethrough ? (top + bottom) / 2 : bottom; + + ctx.strokeStyle = ctx.fillStyle; + ctx.beginPath(); + ctx.lineWidth = opts.decorationWidth || 2; + ctx.moveTo(left, yDecoration); + ctx.lineTo(right, yDecoration); + ctx.stroke(); + } + y += font.lineHeight; + } + + ctx.restore(); +} diff --git a/src/plugins/plugin.legend.js b/src/plugins/plugin.legend.js index 467aa1fbe..f0fa067b7 100644 --- a/src/plugins/plugin.legend.js +++ b/src/plugins/plugin.legend.js @@ -1,7 +1,7 @@ import defaults from '../core/core.defaults'; import Element from '../core/core.element'; import layouts from '../core/core.layouts'; -import {drawPoint} from '../helpers/helpers.canvas'; +import {drawPoint, renderText} from '../helpers/helpers.canvas'; import { callback as call, valueOrDefault, toFont, isObject, toPadding, getRtlAdapter, overrideTextDirection, restoreTextDirection, @@ -305,20 +305,10 @@ export class Legend extends Element { ctx.restore(); }; - const fillText = function(x, y, legendItem, textWidth) { + const fillText = function(x, y, legendItem) { const halfFontSize = fontSize / 2; const xLeft = rtlHelper.xPlus(x, boxWidth + halfFontSize); - const yMiddle = y + (itemHeight / 2); - ctx.fillText(legendItem.text, xLeft, yMiddle); - - if (legendItem.hidden) { - // Strikethrough the text if hidden - ctx.beginPath(); - ctx.lineWidth = 2; - ctx.moveTo(xLeft, yMiddle); - ctx.lineTo(rtlHelper.xPlus(xLeft, textWidth), yMiddle); - ctx.stroke(); - } + renderText(ctx, legendItem.text, xLeft, y + (itemHeight / 2), labelFont, {strikethrough: legendItem.hidden}); }; // Horizontal @@ -369,7 +359,7 @@ export class Legend extends Element { legendHitBoxes[i].top = y; // Fill the actual label - fillText(realX, y, legendItem, textWidth); + fillText(realX, y, legendItem); if (isHorizontal) { cursor.x += width + padding; @@ -429,7 +419,7 @@ export class Legend extends Element { ctx.fillStyle = titleOpts.color; ctx.font = titleFont.string; - ctx.fillText(titleOpts.text, x, y); + renderText(ctx, titleOpts.text, x, y, titleFont); } /** diff --git a/src/plugins/plugin.title.js b/src/plugins/plugin.title.js index eebb70c7f..8a2e6085e 100644 --- a/src/plugins/plugin.title.js +++ b/src/plugins/plugin.title.js @@ -2,6 +2,7 @@ import Element from '../core/core.element'; import layouts from '../core/core.layouts'; import {PI, isArray, toPadding, toFont} from '../helpers'; import {_toLeftRightCenter, _alignStartEnd} from '../helpers/helpers.extras'; +import {renderText} from '../helpers/helpers.canvas'; export class Title extends Element { /** @@ -95,28 +96,14 @@ export class Title extends Element { const offset = lineHeight / 2 + me._padding.top; const {titleX, titleY, maxWidth, rotation} = me._drawArgs(offset); - ctx.save(); - - ctx.fillStyle = opts.color; - ctx.font = fontOpts.string; - - ctx.translate(titleX, titleY); - ctx.rotate(rotation); - ctx.textAlign = _toLeftRightCenter(opts.align); - ctx.textBaseline = 'middle'; - - const text = opts.text; - if (isArray(text)) { - let y = 0; - for (let i = 0; i < text.length; ++i) { - ctx.fillText(text[i], 0, y, maxWidth); - y += lineHeight; - } - } else { - ctx.fillText(text, 0, 0, maxWidth); - } - - ctx.restore(); + renderText(ctx, opts.text, 0, 0, fontOpts, { + color: opts.color, + maxWidth, + rotation, + textAlign: _toLeftRightCenter(opts.align), + textBaseline: 'middle', + translation: [titleX, titleY], + }); } } diff --git a/src/scales/scale.radialLinear.js b/src/scales/scale.radialLinear.js index ceb661e71..92411cfbf 100644 --- a/src/scales/scale.radialLinear.js +++ b/src/scales/scale.radialLinear.js @@ -1,5 +1,5 @@ import defaults from '../core/core.defaults'; -import {_longestText} from '../helpers/helpers.canvas'; +import {_longestText, renderText} from '../helpers/helpers.canvas'; import {HALF_PI, isNumber, TAU, toDegrees, toRadians, _normalizeAngle} from '../helpers/helpers.math'; import LinearScaleBase from './scale.linearbase'; import Ticks from '../core/core.ticks'; @@ -142,20 +142,6 @@ function getTextAlignForAngle(angle) { return 'right'; } -function fillText(ctx, text, position, lineHeight) { - let y = position.y + lineHeight / 2; - let i, ilen; - - if (isArray(text)) { - for (i = 0, ilen = text.length; i < ilen; ++i) { - ctx.fillText(text[i], position.x, y); - y += lineHeight; - } - } else { - ctx.fillText(text, position.x, y); - } -} - function adjustPointPositionForLabelHeight(angle, textSize, position) { if (angle === 90 || angle === 270) { position.y -= (textSize.h / 2); @@ -182,13 +168,19 @@ function drawPointLabels(scale) { const context = scale.getContext(i); const plFont = toFont(resolve([pointLabelOpts.font], context, i), scale.chart.options.font); - ctx.font = plFont.string; - ctx.fillStyle = resolve([pointLabelOpts.color], context, i); - const angle = toDegrees(scale.getIndexAngle(i)); - ctx.textAlign = getTextAlignForAngle(angle); adjustPointPositionForLabelHeight(angle, scale._pointLabelSizes[i], pointLabelPosition); - fillText(ctx, scale.pointLabels[i], pointLabelPosition, plFont.lineHeight); + renderText( + ctx, + scale.pointLabels[i], + pointLabelPosition.x, + pointLabelPosition.y + (plFont.lineHeight / 2), + plFont, + { + color: resolve([pointLabelOpts.color], context, i), + textAlign: getTextAlignForAngle(angle), + } + ); } ctx.restore(); } @@ -482,7 +474,6 @@ export default class RadialLinearScale extends LinearScaleBase { const context = me.getContext(index); const tickFont = me._resolveTickFontOptions(index); - ctx.font = tickFont.string; offset = me.getDistanceFromCenterForValue(me.ticks[index].value); const showLabelBackdrop = resolve([tickOpts.showLabelBackdrop], context, index); @@ -499,8 +490,9 @@ export default class RadialLinearScale extends LinearScaleBase { ); } - ctx.fillStyle = tickOpts.color; - ctx.fillText(tick.label, 0, -offset); + renderText(ctx, tick.label, 0, -offset, tickFont, { + color: tickOpts.color, + }); }); ctx.restore(); diff --git a/test/context.js b/test/context.js index d37324f28..63c872c98 100644 --- a/test/context.js +++ b/test/context.js @@ -10,6 +10,7 @@ const Context = function() { this._lineWidth = null; this._strokeStyle = null; this._textAlign = null; + this._textBaseline = null; // Define properties here so that we can record each time they are set Object.defineProperties(this, { @@ -75,6 +76,15 @@ const Context = function() { this._textAlign = align; this.record('setTextAlign', [align]); } + }, + textBaseline: { + get: function() { + return this._textBaseline; + }, + set: function(baseline) { + this._textBaseline = baseline; + this.record('setTextBaseline', [baseline]); + } } }); }; @@ -98,7 +108,20 @@ Context.prototype._initMethods = function() { lineTo: function() {}, measureText: function(text) { // return the number of characters * fixed size - return text ? {width: text.length * 10} : {width: 0}; + // Uses fake numbers for the bounding box + return text ? { + actualBoundingBoxAscent: 4, + actualBoundingBoxDescent: 8, + actualBoundingBoxLeft: 15, + actualBoundingBoxRight: 25, + width: text.length * 10 + } : { + actualBoundingBoxAscent: 0, + actualBoundingBoxDescent: 0, + actualBoundingBoxLeft: 0, + actualBoundingBoxRight: 0, + width: 0 + }; }, moveTo: function() {}, quadraticCurveTo: function() {}, diff --git a/test/specs/helpers.canvas.tests.js b/test/specs/helpers.canvas.tests.js index 031491b0d..35410fea1 100644 --- a/test/specs/helpers.canvas.tests.js +++ b/test/specs/helpers.canvas.tests.js @@ -99,4 +99,209 @@ describe('Chart.helpers.canvas', function() { args: ['foobar_1'] }]); }); + + describe('renderText', function() { + it('should render multiple lines of text', function() { + var context = window.createMockContext(); + var font = {fontString: '', lineHeight: 20}; + helpers.renderText(context, ['foo', 'foo2'], 0, 0, font); + + expect(context.getCalls()).toEqual([{ + name: 'save', + args: [], + }, { + name: 'fillText', + args: ['foo', 0, 0, undefined], + }, { + name: 'fillText', + args: ['foo2', 0, 20, undefined], + }, { + name: 'restore', + args: [], + }]); + }); + + it('should accept the text maxWidth', function() { + var context = window.createMockContext(); + var font = {fontString: '', lineHeight: 20}; + helpers.renderText(context, 'foo', 0, 0, font, {maxWidth: 30}); + expect(context.getCalls()).toEqual([{ + name: 'save', + args: [], + }, { + name: 'fillText', + args: ['foo', 0, 0, 30], + }, { + name: 'restore', + args: [], + }]); + }); + + it('should underline the text', function() { + var context = window.createMockContext(); + var font = {fontString: '', lineHeight: 20}; + helpers.renderText(context, 'foo', 0, 0, font, {decorationWidth: 3, underline: true}); + + expect(context.getCalls()).toEqual([{ + name: 'save', + args: [], + }, { + name: 'fillText', + args: ['foo', 0, 0, undefined], + }, { + name: 'measureText', + args: ['foo'], + }, { + name: 'setStrokeStyle', + args: [null], + }, { + name: 'beginPath', + args: [], + }, { + name: 'setLineWidth', + args: [3], + }, { + name: 'moveTo', + args: [-15, 8], + }, { + name: 'lineTo', + args: [25, 8], + }, { + name: 'stroke', + args: [], + }, { + name: 'restore', + args: [], + }]); + }); + + it('should strikethrough the text', function() { + var context = window.createMockContext(); + var font = {fontString: '', lineHeight: 20}; + helpers.renderText(context, 'foo', 0, 0, font, {strikethrough: true}); + + expect(context.getCalls()).toEqual([{ + name: 'save', + args: [], + }, { + name: 'fillText', + args: ['foo', 0, 0, undefined], + }, { + name: 'measureText', + args: ['foo'], + }, { + name: 'setStrokeStyle', + args: [null], + }, { + name: 'beginPath', + args: [], + }, { + name: 'setLineWidth', + args: [2], + }, { + name: 'moveTo', + args: [-15, 2], + }, { + name: 'lineTo', + args: [25, 2], + }, { + name: 'stroke', + args: [], + }, { + name: 'restore', + args: [], + }]); + }); + + it('should set the fill style if supplied', function() { + var context = window.createMockContext(); + var font = {fontString: '', lineHeight: 20}; + helpers.renderText(context, 'foo', 0, 0, font, {color: 'red'}); + + expect(context.getCalls()).toEqual([{ + name: 'save', + args: [], + }, { + name: 'setFillStyle', + args: ['red'], + }, { + name: 'fillText', + args: ['foo', 0, 0, undefined], + }, { + name: 'restore', + args: [], + }]); + }); + + it('should set the stroke style if supplied', function() { + var context = window.createMockContext(); + var font = {fontString: '', lineHeight: 20}; + helpers.renderText(context, 'foo', 0, 0, font, {strokeColor: 'red', strokeWidth: 2}); + expect(context.getCalls()).toEqual([{ + name: 'save', + args: [], + }, { + name: 'setStrokeStyle', + args: ['red'], + }, { + name: 'setLineWidth', + args: [2], + }, { + name: 'strokeText', + args: ['foo', 0, 0, undefined], + }, { + name: 'fillText', + args: ['foo', 0, 0, undefined], + }, { + name: 'restore', + args: [], + }]); + }); + + it('should set the text alignment', function() { + var context = window.createMockContext(); + var font = {fontString: '', lineHeight: 20}; + helpers.renderText(context, 'foo', 0, 0, font, {textAlign: 'left', textBaseline: 'middle'}); + + expect(context.getCalls()).toEqual([{ + name: 'save', + args: [], + }, { + name: 'setTextAlign', + args: ['left'], + }, { + name: 'setTextBaseline', + args: ['middle'], + }, { + name: 'fillText', + args: ['foo', 0, 0, undefined], + }, { + name: 'restore', + args: [], + }]); + }); + + it('should translate and rotate text', function() { + var context = window.createMockContext(); + var font = {fontString: '', lineHeight: 20}; + helpers.renderText(context, 'foo', 0, 0, font, {rotation: 90, translation: [10, 20]}); + + expect(context.getCalls()).toEqual([{ + name: 'save', + args: [], + }, { + name: 'translate', + args: [10, 20], + }, { + name: 'rotate', + args: [90], + }, { + name: 'fillText', + args: ['foo', 0, 0, undefined], + }, { + name: 'restore', + args: [], + }]); + }); + }); }); diff --git a/test/specs/plugin.title.tests.js b/test/specs/plugin.title.tests.js index 9a794f65a..fcfe104b0 100644 --- a/test/specs/plugin.title.tests.js +++ b/test/specs/plugin.title.tests.js @@ -129,18 +129,21 @@ describe('Title block tests', function() { expect(context.getCalls()).toEqual([{ name: 'save', args: [] - }, { - name: 'setFillStyle', - args: ['#666'] }, { name: 'translate', args: [300, 67.2] }, { name: 'rotate', args: [0] + }, { + name: 'setFillStyle', + args: ['#666'] }, { name: 'setTextAlign', args: ['center'], + }, { + name: 'setTextBaseline', + args: ['middle'], }, { name: 'fillText', args: ['My title', 0, 0, 400] @@ -184,18 +187,21 @@ describe('Title block tests', function() { expect(context.getCalls()).toEqual([{ name: 'save', args: [] - }, { - name: 'setFillStyle', - args: ['#666'] }, { name: 'translate', args: [117.2, 250] }, { name: 'rotate', args: [-0.5 * Math.PI] + }, { + name: 'setFillStyle', + args: ['#666'] }, { name: 'setTextAlign', args: ['center'], + }, { + name: 'setTextBaseline', + args: ['middle'], }, { name: 'fillText', args: ['My title', 0, 0, 400] @@ -220,18 +226,21 @@ describe('Title block tests', function() { expect(context.getCalls()).toEqual([{ name: 'save', args: [] - }, { - name: 'setFillStyle', - args: ['#666'] }, { name: 'translate', args: [117.2, 250] }, { name: 'rotate', args: [0.5 * Math.PI] + }, { + name: 'setFillStyle', + args: ['#666'] }, { name: 'setTextAlign', args: ['center'], + }, { + name: 'setTextBaseline', + args: ['middle'], }, { name: 'fillText', args: ['My title', 0, 0, 400] diff --git a/test/specs/plugin.tooltip.tests.js b/test/specs/plugin.tooltip.tests.js index d6e370167..c353374fc 100644 --- a/test/specs/plugin.tooltip.tests.js +++ b/test/specs/plugin.tooltip.tests.js @@ -1455,13 +1455,16 @@ describe('Plugin.Tooltip', function() { expect(mockContext.getCalls()).toEqual(Array.prototype.concat(drawBody, [ {name: 'setTextAlign', args: ['left']}, + {name: 'setTextBaseline', args: ['middle']}, {name: 'setFillStyle', args: ['#fff']}, {name: 'fillText', args: ['title', 105, 111]}, {name: 'setTextAlign', args: ['left']}, + {name: 'setTextBaseline', args: ['middle']}, {name: 'setFillStyle', args: ['#fff']}, {name: 'setFillStyle', args: ['#fff']}, {name: 'fillText', args: ['label', 105, 129]}, {name: 'setTextAlign', args: ['left']}, + {name: 'setTextBaseline', args: ['middle']}, {name: 'setFillStyle', args: ['#fff']}, {name: 'fillText', args: ['footer', 105, 147]}, {name: 'restore', args: []} @@ -1475,13 +1478,16 @@ describe('Plugin.Tooltip', function() { expect(mockContext.getCalls()).toEqual(Array.prototype.concat(drawBody, [ {name: 'setTextAlign', args: ['right']}, + {name: 'setTextBaseline', args: ['middle']}, {name: 'setFillStyle', args: ['#fff']}, {name: 'fillText', args: ['title', 195, 111]}, {name: 'setTextAlign', args: ['right']}, + {name: 'setTextBaseline', args: ['middle']}, {name: 'setFillStyle', args: ['#fff']}, {name: 'setFillStyle', args: ['#fff']}, {name: 'fillText', args: ['label', 195, 129]}, {name: 'setTextAlign', args: ['right']}, + {name: 'setTextBaseline', args: ['middle']}, {name: 'setFillStyle', args: ['#fff']}, {name: 'fillText', args: ['footer', 195, 147]}, {name: 'restore', args: []} @@ -1495,13 +1501,16 @@ describe('Plugin.Tooltip', function() { expect(mockContext.getCalls()).toEqual(Array.prototype.concat(drawBody, [ {name: 'setTextAlign', args: ['center']}, + {name: 'setTextBaseline', args: ['middle']}, {name: 'setFillStyle', args: ['#fff']}, {name: 'fillText', args: ['title', 150, 111]}, {name: 'setTextAlign', args: ['center']}, + {name: 'setTextBaseline', args: ['middle']}, {name: 'setFillStyle', args: ['#fff']}, {name: 'setFillStyle', args: ['#fff']}, {name: 'fillText', args: ['label', 150, 129]}, {name: 'setTextAlign', args: ['center']}, + {name: 'setTextBaseline', args: ['middle']}, {name: 'setFillStyle', args: ['#fff']}, {name: 'fillText', args: ['footer', 150, 147]}, {name: 'restore', args: []} @@ -1515,13 +1524,16 @@ describe('Plugin.Tooltip', function() { expect(mockContext.getCalls()).toEqual(Array.prototype.concat(drawBody, [ {name: 'setTextAlign', args: ['right']}, + {name: 'setTextBaseline', args: ['middle']}, {name: 'setFillStyle', args: ['#fff']}, {name: 'fillText', args: ['title', 195, 111]}, {name: 'setTextAlign', args: ['center']}, + {name: 'setTextBaseline', args: ['middle']}, {name: 'setFillStyle', args: ['#fff']}, {name: 'setFillStyle', args: ['#fff']}, {name: 'fillText', args: ['label', 150, 129]}, {name: 'setTextAlign', args: ['left']}, + {name: 'setTextBaseline', args: ['middle']}, {name: 'setFillStyle', args: ['#fff']}, {name: 'fillText', args: ['footer', 105, 147]}, {name: 'restore', args: []} diff --git a/types/color.d.ts b/types/color.d.ts new file mode 100644 index 000000000..4a68f98bb --- /dev/null +++ b/types/color.d.ts @@ -0,0 +1 @@ +export type Color = string | CanvasGradient | CanvasPattern; diff --git a/types/helpers/helpers.canvas.d.ts b/types/helpers/helpers.canvas.d.ts index 0152fee68..4dcaa022c 100644 --- a/types/helpers/helpers.canvas.d.ts +++ b/types/helpers/helpers.canvas.d.ts @@ -1,5 +1,7 @@ import { PointStyle } from '../index.esm'; +import { Color } from '../color'; import { ChartArea } from '../geometric'; +import { CanvasFontSpec } from './helpers.options'; /** * Clears the entire canvas associated to the given `chart`. @@ -26,3 +28,76 @@ export function drawPoint(ctx: CanvasRenderingContext2D, options: DrawPointOptio * @return The CSS font string. See https://developer.mozilla.org/en-US/docs/Web/CSS/font */ export function toFontString(font: { size: number; family: string; style?: string; weight?: string }): string | null; + +export interface RenderTextOpts { + /** + * The fill color of the text. If unset, the existing + * fillStyle property of the canvas is unchanged. + */ + color?: Color; + + /** + * The width of the strikethrough / underline + * @default 2 + */ + decorationWidth?: number; + + /** + * The max width of the text in pixels + */ + maxWidth?: number; + + /** + * A rotation to be applied to the canvas + * This is applied after the translation is applied + */ + rotation?: number; + + /** + * Apply a strikethrough effect to the text + */ + strikethrough?: boolean; + + /** + * The color of the text stroke. If unset, the existing + * strokeStyle property of the context is unchanged + */ + strokeColor?: Color; + + /** + * The text stroke width. If unset, the existing + * lineWidth property of the context is unchanged + */ + strokeWidth?: number; + + /** + * The text alignment to use. If unset, the existing + * textAlign property of the context is unchanged + */ + textAlign: CanvasTextAlign; + + /** + * The text baseline to use. If unset, the existing + * textBaseline property of the context is unchanged + */ + textBaseline: CanvasTextBaseline; + + /** + * If specified, a translation to apply to the context + */ + translation?: [number, number]; + + /** + * Underline the text + */ + underline?: boolean; +} + +export function renderText( + ctx: CanvasRenderingContext2D, + text: string | string[], + x: number, + y: number, + font: CanvasFontSpec, + opts?: RenderTextOpts +): void; diff --git a/types/index.esm.d.ts b/types/index.esm.d.ts index 8745bee01..b6df557a7 100644 --- a/types/index.esm.d.ts +++ b/types/index.esm.d.ts @@ -14,6 +14,7 @@ import { TimeUnit } from "./adapters"; import { AnimationEvent } from './animation'; +import { Color } from './color'; import { Element }from './element'; import { ChartArea, Point } from './geometric'; import { @@ -24,6 +25,7 @@ import { export { DateAdapterBase, DateAdapter, TimeUnit, _adapters } from './adapters'; export { Animation, Animations, Animator, AnimationEvent } from './animation'; +export { Color } from './color'; export { Element } from './element'; export { ChartArea, Point } from './geometric'; export { @@ -1332,8 +1334,6 @@ export interface TypedRegistry { unregister(item: ChartComponent): void; } -export type Color = string | CanvasGradient | CanvasPattern; - export interface ChartEvent { type: | 'contextmenu' @@ -2637,12 +2637,12 @@ export interface TickOptions { * The color of the stroke around the text. * @default undefined */ - textStrokeColor: Color; + textStrokeColor: Scriptable; /** * Stroke width around the text. * @default 0 */ - textStrokeWidth: number; + textStrokeWidth: Scriptable; /** * z-index of tick layer. Useful when ticks are drawn on chart area. Values <= 0 are drawn under datasets, > 0 on top. * @default 0