]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Create standardized text render method (#8227)
authorEvert Timberg <evert.timberg+github@gmail.com>
Sat, 26 Dec 2020 16:23:02 +0000 (11:23 -0500)
committerGitHub <noreply@github.com>
Sat, 26 Dec 2020 16:23:02 +0000 (11:23 -0500)
* 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

13 files changed:
docs/docs/axes/_common_ticks.md
src/core/core.scale.js
src/helpers/helpers.canvas.js
src/plugins/plugin.legend.js
src/plugins/plugin.title.js
src/scales/scale.radialLinear.js
test/context.js
test/specs/helpers.canvas.tests.js
test/specs/plugin.title.tests.js
test/specs/plugin.tooltip.tests.js
types/color.d.ts [new file with mode: 0644]
types/helpers/helpers.canvas.d.ts
types/index.esm.d.ts

index 365286e54c58a6353001ca1697fc69d1b4f8d6c6..658ce49bf0f5ffe0fc9f65d049065dabea1566d4 100644 (file)
@@ -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 &lt;= 0 are drawn under datasets, &gt; 0 on top.
index e844544f191ee53daa0838a2cab3647a0be9a1ad..66c890b4ceb6d3540a4a79a5fbc0dd8c3fa79e2e 100644 (file)
@@ -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) {
index 7f8c8356c31fd972de13975092d8c94446e7444d..5d463de2881331bfc01d8dea36bf0284cf9edc46 100644 (file)
@@ -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();
+}
index 467aa1fbe45e41f9ebeafd29b30978df212600fc..f0fa067b71f1b20757fd7f4c591cd8dcfe143352 100644 (file)
@@ -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);
        }
 
        /**
index eebb70c7f827a65df08b566397b66c35a3d27c20..8a2e6085ee0625d8e7806f5e511a767b07fb4494 100644 (file)
@@ -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],
+               });
        }
 }
 
index ceb661e711a52af161bedc08bed0a363f04d2629..92411cfbf8ea1c023dd3dc2434fa42f5910f1ca3 100644 (file)
@@ -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();
index d37324f284902bfb4d4f6ad553f3b51b40bb1d98..63c872c98347ac677c8a87235095dc1f89eb325d 100644 (file)
@@ -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() {},
index 031491b0d2bf3eed4b5c8f3001917aa6bd963dd7..35410fea11974e35886ba72e17545fa1b9223756 100644 (file)
@@ -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: [],
+                       }]);
+               });
+       });
 });
index 9a794f65a8fdabb4e108b2da16476930aae008f7..fcfe104b05abf8f1c4910cddf0e3d4cc0c535eaf 100644 (file)
@@ -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]
index d6e37016748e1a508a7473effccdef0cb4d3bc0f..c353374fc62ea16cf87626f8b89daae90e323409 100644 (file)
@@ -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 (file)
index 0000000..4a68f98
--- /dev/null
@@ -0,0 +1 @@
+export type Color = string | CanvasGradient | CanvasPattern;
index 0152fee68bc32b92c9e9677398ef48f7ef36811f..4dcaa022c13a431c4f53b91354da9c74b36a0138 100644 (file)
@@ -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;
index 8745bee01aebae9295cecbbc90e3508a23c80ca5..b6df557a7a67c6648b95d88130266c004d7e2514 100644 (file)
@@ -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<T> {
        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<Color, ScriptableScaleContext>;
   /**
    * Stroke width around the text.
    * @default 0
    */
-  textStrokeWidth: number;
+  textStrokeWidth: Scriptable<number, ScriptableScaleContext>;
   /**
    * z-index of tick layer. Useful when ticks are drawn on chart area. Values <= 0 are drawn under datasets, > 0 on top.
    * @default 0