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/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..73fd8c3 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, axis) { 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, axis.x, axis.y, axis.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, axis.x, axis.y, axis.w); if (shadow && stroked) { ctx.shadowBlur = shadow; @@ -181,6 +178,24 @@ function drawTextLine(ctx, text, cfg) { } } +function drawTextLine(ctx, text, cfg) { + 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, axis); + // move text position for each chunk + axis.x = axis.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..6151266 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;