From 50aab059611231b7fb2d4b47b8e8a9cdeb1ef4f1 Mon Sep 17 00:00:00 2001 From: Giampaolo Fois Date: Fri, 10 Jan 2025 14:32:33 +0100 Subject: [PATCH 1/2] Add possibility to style text --- docs/guide/formatting.md | 10 +++++++ src/label.js | 29 ++++++++++++++------ src/utils.js | 58 ++++++++++++++++++++++++++++++++++++++++ test/specs/utils.spec.js | 54 +++++++++++++++++++++++++++++++++++++ 4 files changed, 143 insertions(+), 8 deletions(-) diff --git a/docs/guide/formatting.md b/docs/guide/formatting.md index 2bb6528..be16d83 100644 --- a/docs/guide/formatting.md +++ b/docs/guide/formatting.md @@ -104,3 +104,13 @@ Supported values for `textAlign`: - `'end'`: the text is right-aligned - `'left'`: alias of `'start'` - `'right'`: alias of `'end'` + +## Text style + +Labels style can be modified to be bold or italic using Markdown syntax **bold** or *italic*. + +```javascript +formatter: function(value) { + return 'normal text **bold text** *italic text*'; +} +``` diff --git a/src/label.js b/src/label.js index 640d8e4..096f5f0 100644 --- a/src/label.js +++ b/src/label.js @@ -154,16 +154,14 @@ function textGeometry(rect, align, font) { y: y }; } - -function drawTextLine(ctx, text, cfg) { +function drawTextChunk(ctx, chunk, cfg, baseFont, x, y, w) { var shadow = ctx.shadowBlur; var stroked = cfg.stroked; - var x = rasterize(cfg.x); - var y = rasterize(cfg.y); - var w = rasterize(cfg.w); + + ctx.font = `${chunk.style} ${baseFont}`; if (stroked) { - ctx.strokeText(text, x, y, w); + ctx.strokeText(chunk.text, x, y, w); } if (cfg.filled) { @@ -172,8 +170,7 @@ function drawTextLine(ctx, text, cfg) { // if the text is stroked, remove the shadow for the text fill. ctx.shadowBlur = 0; } - - ctx.fillText(text, x, y, w); + ctx.fillText(chunk.text, x, y, w); if (shadow && stroked) { ctx.shadowBlur = shadow; @@ -181,6 +178,22 @@ function drawTextLine(ctx, text, cfg) { } } +function drawTextLine(ctx, text, cfg) { + var x = rasterize(cfg.x); + var y = rasterize(cfg.y); + var w = rasterize(cfg.w); + + var chunks = utils.toTextChunks(text); + var baseFont = ctx.font; + for (var chunk of chunks) { + drawTextChunk(ctx, chunk, cfg, baseFont, x, y, w); + // move text position for each chunk + x = x + Math.floor(ctx.measureText(chunk.text).width); + } + // reset font for next line + ctx.font = baseFont; +} + function drawText(ctx, lines, rect, model) { var align = model.textAlign; var color = model.color; diff --git a/src/utils.js b/src/utils.js index 84f2ae3..8637ada 100644 --- a/src/utils.js +++ b/src/utils.js @@ -39,6 +39,64 @@ var utils = { return lines; }, + toTextChunks(text) { + if (typeof text !== 'string') { + throw new TypeError('Text must be a string'); + } + + var chunks = []; + var preIndex = 0; + + // Looks for **bold text** or *italic text* + var regex = /\*{2}(?.+?)\*{2}|\*(?.+?)\*/gm; + + // Text is split into chunks, for every match found + text.matchAll(regex).forEach(elem => { + var styledText = elem.groups['bold'] ?? elem.groups['italic'] ?? ""; + // Every match two chunks are created. + // The first chuck contains the unstyled text preceding the styled text. + // If the text starts styled the first chunk is empty. + chunks.push( + { + text: text.substring(preIndex, elem.index), + start: preIndex, + end: elem.index, + style: 'normal', + }, + { + text: styledText, + start: elem.index, + end: elem.index + elem[0].length, + style: elem.groups['bold'] ? 'bold' : 'italic', + } + ); + // Move index to end of the styled word to start with the next chunk. + preIndex = elem.index + elem[0].length; + }); + + // Add a chuck for any non styled text remaing after last chuck is added. + if(chunks.length && chunks.at(-1).end !== text.length) { + chunks.push({ + text: text.substring(chunks.at(-1).end,), + start: chunks.at(-1).end, + end: text.length, + style: 'normal', + }); + } + + // If no style is detected just created a chuck containing the whole text + if(!chunks.length && text) { + chunks.push({ + text: text, + start: 0, + end: text.length, + style: 'normal', + }); + } + + return chunks; + }, + // @todo move this in Chart.helpers.canvas.textSize // @todo cache calls of measureText if font doesn't change?! textSize: function(ctx, lines, font) { diff --git a/test/specs/utils.spec.js b/test/specs/utils.spec.js index a2c67c0..54083d9 100644 --- a/test/specs/utils.spec.js +++ b/test/specs/utils.spec.js @@ -36,6 +36,60 @@ describe('utils.js', function() { }); }); + describe('toTextChunks', function() { + var toTextChunks = utils.toTextChunks; + + it ('text should be a string', function() { + expect(toTextChunks(123)).toThrow(new TypeError('Text must be a string')); + }); + + it('should create one chunk with no format', function() { + expect(toTextChunks('test')).toEqual([{ + text: 'test', + start: 0, + end: 'test'.length, + style: 'normal', + }]); + }); + + it('should create chunks with format', function() { + expect(toTextChunks('test **bold** *italics* normal')).toEqual( + [ + { + text: 'test ', + start: 0, + end: 5, + style: 'normal' + }, + { + text: 'bold', + start: 5, + end: 13, + style: 'bold' + }, + { + text: ' ', + start: 13, + end: 14, + style: 'normal' + }, + { + text: 'italics', + start: 14, + end: 23, + style: 'italic' + }, + { + text: ' normal', + start: 23, + end: 30, + style: 'normal' + } + ] + ); + }); + }); + describe('arrayDiff', function() { var arrayDiff = utils.arrayDiff; From 4856a4622e31e51562c16b631a814b1f5f0df413 Mon Sep 17 00:00:00 2001 From: Giampaolo Fois Date: Fri, 7 Mar 2025 10:01:04 +0100 Subject: [PATCH 2/2] Fix style --- .eslintrc.yml | 2 +- src/label.js | 18 ++++++++++-------- src/utils.js | 24 ++++++++++++------------ 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/.eslintrc.yml b/.eslintrc.yml index 4ca7fc7..ee5b042 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -1,7 +1,7 @@ extends: chartjs parserOptions: - ecmaVersion: 8 + ecmaVersion: 2022 sourceType: module env: diff --git a/src/label.js b/src/label.js index 096f5f0..73fd8c3 100644 --- a/src/label.js +++ b/src/label.js @@ -154,14 +154,14 @@ function textGeometry(rect, align, font) { y: y }; } -function drawTextChunk(ctx, chunk, cfg, baseFont, x, y, w) { +function drawTextChunk(ctx, chunk, cfg, baseFont, axis) { var shadow = ctx.shadowBlur; var stroked = cfg.stroked; ctx.font = `${chunk.style} ${baseFont}`; if (stroked) { - ctx.strokeText(chunk.text, x, y, w); + ctx.strokeText(chunk.text, axis.x, axis.y, axis.w); } if (cfg.filled) { @@ -170,7 +170,7 @@ function drawTextChunk(ctx, chunk, cfg, baseFont, x, y, w) { // if the text is stroked, remove the shadow for the text fill. ctx.shadowBlur = 0; } - ctx.fillText(chunk.text, x, y, w); + ctx.fillText(chunk.text, axis.x, axis.y, axis.w); if (shadow && stroked) { ctx.shadowBlur = shadow; @@ -179,16 +179,18 @@ function drawTextChunk(ctx, chunk, cfg, baseFont, x, y, w) { } function drawTextLine(ctx, text, cfg) { - var x = rasterize(cfg.x); - var y = rasterize(cfg.y); - var w = rasterize(cfg.w); + var axis = { + x: rasterize(cfg.x), + y: rasterize(cfg.y), + w: rasterize(cfg.w), + }; var chunks = utils.toTextChunks(text); var baseFont = ctx.font; for (var chunk of chunks) { - drawTextChunk(ctx, chunk, cfg, baseFont, x, y, w); + drawTextChunk(ctx, chunk, cfg, baseFont, axis); // move text position for each chunk - x = x + Math.floor(ctx.measureText(chunk.text).width); + axis.x = axis.x + Math.floor(ctx.measureText(chunk.text).width); } // reset font for next line ctx.font = baseFont; diff --git a/src/utils.js b/src/utils.js index 8637ada..6151266 100644 --- a/src/utils.js +++ b/src/utils.js @@ -43,16 +43,16 @@ var utils = { if (typeof text !== 'string') { throw new TypeError('Text must be a string'); } - + var chunks = []; var preIndex = 0; // Looks for **bold text** or *italic text* var regex = /\*{2}(?.+?)\*{2}|\*(?.+?)\*/gm; - + // Text is split into chunks, for every match found text.matchAll(regex).forEach(elem => { - var styledText = elem.groups['bold'] ?? elem.groups['italic'] ?? ""; + var styledText = elem.groups.bold ?? elem.groups.italic ?? ''; // Every match two chunks are created. // The first chuck contains the unstyled text preceding the styled text. // If the text starts styled the first chunk is empty. @@ -67,25 +67,25 @@ var utils = { text: styledText, start: elem.index, end: elem.index + elem[0].length, - style: elem.groups['bold'] ? 'bold' : 'italic', + style: elem.groups.bold ? 'bold' : 'italic', } ); // Move index to end of the styled word to start with the next chunk. preIndex = elem.index + elem[0].length; }); - + // Add a chuck for any non styled text remaing after last chuck is added. - if(chunks.length && chunks.at(-1).end !== text.length) { + if (chunks.length && chunks.at(-1).end !== text.length) { chunks.push({ - text: text.substring(chunks.at(-1).end,), - start: chunks.at(-1).end, - end: text.length, - style: 'normal', - }); + text: text.substring(chunks.at(-1).end), + start: chunks.at(-1).end, + end: text.length, + style: 'normal', + }); } // If no style is detected just created a chuck containing the whole text - if(!chunks.length && text) { + if (!chunks.length && text) { chunks.push({ text: text, start: 0,