From 9c678b0c9ce4d5cc7ccbd7e8401bd0eca09a27bd Mon Sep 17 00:00:00 2001 From: Vaadin Bot Date: Thu, 26 Sep 2024 16:53:02 +0200 Subject: [PATCH] fix!: shim an implementation of `getSubStringLength` (#6663) (CP: 24.4) (#6670) * Shim an implementation of `getSubStringLength` (#6663) * Shim an implementation of getSubStringLength Estimate the rendered length of a substring of text in an SVG element. Supports wrapping/truncation of long labels in charts. * Improve shimmed measurement routines Improve shims of getBBox and getSubStringLength: The prior implementations were relying on specific DOM structures that Highcharts used to create: if the measured element had children, all text was expected to be within those children, and none in the top-level element. Also, it did not handle arbitrary element nesting. The current version of Highcharts does not always follow these rules, and there were cases of structures like: Some text hereand some in a nested element In that example, 'Some text here' would be omitted from measurement. Replace that strategy with one that processes each text node separately, providing the context of the containing element, and recursing into all nested elements. Fix some bugs around new line management in getBBox that became apparent after that change. The previous method of computing string width was a rough approximation based on average character width. This method produced suboptimal results in many situtations, generating extra space or overlapping text. Instead, use the string-pixel-width library to provide a better estimate. This library has per-character widths for a number of font families and variants, and results in widths much closer to the actual rendered style. As Lucida is used heavily in at least the test charts, and it is not natively supported by the library, add a custom mapping file including metrics for it. Also, improve detection of font family and font size, walking up the element parent chain (even potentially outside of the measured element) to find settings of these attributes. * Review cleanup * Test getSubStringLength Add test for getSubStringLength to ensure proper handling of sub-strings crossing text nodes (and other cases). To support this, change the way methods are added to jsdom SVG elements: rather than adding them only for elements instantiated via createElementNS, add them to the SVGElement prototype. This makes them available for elements created by any method of instantiation (including by setting innerHTML, as used in the test). Use rewire to access private elements of the exporter during tests. * spotless * chore: pin npm dependencies --------- Co-authored-by: Aron Nopanen Co-authored-by: Diego Cardoso --- .../customWidthsMap.js | 583 ++++++++++++++++++ .../jsdom-exporter.js | 284 ++++++--- .../package.json | 6 +- .../charts/export/SVGGeneratorTest.java | 5 +- .../test/resources/column-without-title.svg | 2 +- .../src/test/resources/custom-height.svg | 2 +- .../src/test/resources/custom-lang.svg | 2 +- .../src/test/resources/custom-width.svg | 2 +- .../src/test/resources/empty.svg | 2 +- .../src/test/resources/enabled-functions.svg | 2 +- .../src/test/resources/lumo-dark.svg | 2 +- .../src/test/resources/pie.svg | 2 +- .../src/test/resources/timeline.svg | 2 +- .../test/index.test.js | 26 +- 14 files changed, 822 insertions(+), 100 deletions(-) create mode 100644 vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/customWidthsMap.js diff --git a/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/customWidthsMap.js b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/customWidthsMap.js new file mode 100644 index 00000000000..78883b7e639 --- /dev/null +++ b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/customWidthsMap.js @@ -0,0 +1,583 @@ +const widthsMap = { + "lucida sans unicode": { + "0": [ + 63, + 63, + 63, + 63 + ], + "1": [ + 63, + 63, + 63, + 63 + ], + "2": [ + 63, + 63, + 63, + 63 + ], + "3": [ + 63, + 63, + 63, + 63 + ], + "4": [ + 63, + 63, + 63, + 63 + ], + "5": [ + 63, + 63, + 63, + 63 + ], + "6": [ + 63, + 63, + 63, + 63 + ], + "7": [ + 63, + 63, + 63, + 63 + ], + "8": [ + 63, + 63, + 63, + 63 + ], + "9": [ + 63, + 63, + 63, + 63 + ], + " ": [ + 32, + 32, + 32, + 32 + ], + "!": [ + 32, + 32, + 32, + 32 + ], + "\"": [ + 37, + 37, + 37, + 37 + ], + "#": [ + 63, + 63, + 63, + 63 + ], + "$": [ + 63, + 63, + 63, + 63 + ], + "%": [ + 67, + 67, + 67, + 67 + ], + "&": [ + 70, + 70, + 70, + 70 + ], + "'": [ + 23, + 23, + 23, + 23 + ], + "(": [ + 33, + 33, + 33, + 33 + ], + ")": [ + 33, + 33, + 33, + 33 + ], + "*": [ + 48, + 48, + 48, + 48 + ], + "+": [ + 80, + 80, + 80, + 80 + ], + ",": [ + 32, + 32, + 32, + 32 + ], + "-": [ + 58, + 58, + 58, + 58 + ], + ".": [ + 32, + 32, + 32, + 32 + ], + "/": [ + 52, + 52, + 52, + 52 + ], + ":": [ + 32, + 32, + 32, + 32 + ], + ";": [ + 32, + 32, + 32, + 32 + ], + "<": [ + 80, + 80, + 80, + 80 + ], + "=": [ + 80, + 80, + 80, + 80 + ], + ">": [ + 80, + 80, + 80, + 80 + ], + "?": [ + 42, + 42, + 42, + 42 + ], + "@": [ + 86, + 86, + 86, + 86 + ], + "A": [ + 69, + 69, + 69, + 69 + ], + "B": [ + 58, + 58, + 58, + 58 + ], + "C": [ + 69, + 69, + 69, + 69 + ], + "D": [ + 75, + 75, + 75, + 75 + ], + "E": [ + 54, + 54, + 54, + 54 + ], + "F": [ + 54, + 54, + 54, + 54 + ], + "G": [ + 72, + 72, + 72, + 72 + ], + "H": [ + 74, + 74, + 74, + 74 + ], + "I": [ + 29, + 29, + 29, + 29 + ], + "J": [ + 31, + 31, + 31, + 31 + ], + "K": [ + 65, + 65, + 65, + 65 + ], + "L": [ + 53, + 53, + 53, + 53 + ], + "M": [ + 86, + 86, + 86, + 86 + ], + "N": [ + 74, + 74, + 74, + 74 + ], + "O": [ + 78, + 78, + 78, + 78 + ], + "P": [ + 55, + 55, + 55, + 55 + ], + "Q": [ + 78, + 78, + 78, + 78 + ], + "R": [ + 63, + 63, + 63, + 63 + ], + "S": [ + 54, + 54, + 54, + 54 + ], + "T": [ + 63, + 63, + 63, + 63 + ], + "U": [ + 69, + 69, + 69, + 69 + ], + "V": [ + 65, + 65, + 65, + 65 + ], + "W": [ + 86, + 86, + 86, + 86 + ], + "X": [ + 63, + 63, + 63, + 63 + ], + "Y": [ + 62, + 62, + 62, + 62 + ], + "Z": [ + 60, + 60, + 60, + 60 + ], + "[": [ + 33, + 33, + 33, + 33 + ], + "\\": [ + 52, + 52, + 52, + 52 + ], + "]": [ + 33, + 33, + 33, + 33 + ], + "^": [ + 63, + 63, + 63, + 63 + ], + "_": [ + 50, + 50, + 50, + 50 + ], + "`": [ + 61, + 61, + 61, + 61 + ], + "a": [ + 55, + 55, + 55, + 55 + ], + "b": [ + 63, + 63, + 63, + 63 + ], + "c": [ + 51, + 51, + 51, + 51 + ], + "d": [ + 63, + 63, + 63, + 63 + ], + "e": [ + 56, + 56, + 56, + 56 + ], + "f": [ + 37, + 37, + 37, + 37 + ], + "g": [ + 62, + 62, + 62, + 62 + ], + "h": [ + 62, + 62, + 62, + 62 + ], + "i": [ + 29, + 29, + 29, + 29 + ], + "j": [ + 30, + 30, + 30, + 30 + ], + "k": [ + 58, + 58, + 58, + 58 + ], + "l": [ + 29, + 29, + 29, + 29 + ], + "m": [ + 93, + 93, + 93, + 93 + ], + "n": [ + 62, + 62, + 62, + 62 + ], + "o": [ + 61, + 61, + 61, + 61 + ], + "p": [ + 63, + 63, + 63, + 63 + ], + "q": [ + 63, + 63, + 63, + 63 + ], + "r": [ + 41, + 41, + 41, + 41 + ], + "s": [ + 51, + 51, + 51, + 51 + ], + "t": [ + 37, + 37, + 37, + 37 + ], + "u": [ + 62, + 62, + 62, + 62 + ], + "v": [ + 52, + 52, + 52, + 52 + ], + "w": [ + 77, + 77, + 77, + 77 + ], + "x": [ + 61, + 61, + 61, + 61 + ], + "y": [ + 52, + 52, + 52, + 52 + ], + "z": [ + 57, + 57, + 57, + 57 + ], + "{": [ + 33, + 33, + 33, + 33 + ], + "|": [ + 37, + 37, + 37, + 37 + ], + "}": [ + 33, + 33, + 33, + 33 + ], + "~": [ + 63, + 63, + 63, + 63 + ], + "→": [ + 94, + 94, + 94, + 94 + ] + } +}; + +exports.default = widthsMap; +module.exports = exports["default"]; \ No newline at end of file diff --git a/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/jsdom-exporter.js b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/jsdom-exporter.js index e1dcacbfe2b..9d1088f2584 100644 --- a/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/jsdom-exporter.js +++ b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/jsdom-exporter.js @@ -8,6 +8,12 @@ const jsdom = require('jsdom'); const fs = require('fs'); const path = require('path'); +const customWidthsMap = require('./customWidthsMap.js'); +const defaultWidthsMap = require('string-pixel-width/lib/widthsMap.js'); +const pixelWidth = require('string-pixel-width'); + +// Combine the default font widths map from string-pixel-width with our additions +const widthsMap = { ...defaultWidthsMap, ...customWidthsMap }; const { JSDOM } = jsdom; @@ -25,7 +31,7 @@ const doc = win.document; global.Node = win.Node // Require Highcharts with the window shim -const Highcharts = require('highcharts/highstock')(win); +const Highcharts = require('highcharts/highstock.src')(win); require("highcharts/modules/accessibility")(Highcharts); require("highcharts/highcharts-more")(Highcharts); require("highcharts/highcharts-3d")(Highcharts); @@ -45,101 +51,207 @@ require("highcharts/modules/bullet")(Highcharts); win.Date = Date; +function processTextNodes(element, cb) { + for (var childNode of element.childNodes) { + if (childNode.nodeType === Node.ELEMENT_NODE) { + processTextNodes(childNode, cb); + } else if (childNode.nodeType === Node.TEXT_NODE) { + cb(childNode, element); + } + } +} + +/** + * Search up parent chain for an element with the requested attribute set + * + * @param {*} ele + * @param {*} attr + * @returns Attribute, possibly inherited, or undefined if not found + */ +function findStyleAttr(ele, attr) { + while (ele) { + if (ele.style[attr]) { + return ele.style[attr]; + } + ele = ele.parentElement; + } +} + +const sizableFonts = Object.keys(widthsMap); + +/** + * The string-pixel-width library supports a limited set of fonts. Search a font-family + * list for a usable font, or find the next-best fallback + */ +function findSizableFont(fontFamily = "") { + let fonts = fontFamily.split(",") + .map(s => s.trim()) + .map(s => s.replace(/^"(.*)"$/, '$1')) // Un-quote any quoted entries + .map(s => s.toLowerCase()); + + // Search the font list for one in our list of sizable fonts + let usableFont = fonts.find(f => sizableFonts.includes(f)); + + if (!usableFont) { + // None of those are sizable. Go with Highcharts default font + usableFont = 'times new roman'; + } + + return usableFont; +} + +function removeHighchartsTextOutlines(elem) { + let children = [].slice.call( + elem.children.length ? elem.children : [elem] + ); + children.forEach(child => { + if (child.getAttribute('class') === 'highcharts-text-outline') { + child.parentNode.removeChild(child); + } + }); + +} + // Do some modifications to the jsdom document in order to get the SVG bounding // boxes right. -let oldCreateElementNS = doc.createElementNS; -doc.createElementNS = (ns, tagName) => { - let elem = oldCreateElementNS.call(doc, ns, tagName); - if (ns !== 'http://www.w3.org/2000/svg') { - return elem; - } - /** - * Pass Highcharts' test for SVG capabilities - * @returns {undefined} - */ - elem.createSVGRect = () => { }; - /** - * jsdom doesn't compute layout (see - * https://github.com/tmpvar/jsdom/issues/135). This getBBox implementation - * provides just enough information to get Highcharts to render text boxes - * correctly, and is not intended to work like a general getBBox - * implementation. The height of the boxes are computed from the sum of - * tspans and their font sizes. The width is based on an average width for - * each glyph. It could easily be improved to take font-weight into account. - * For a more exact result we could to create a map over glyph widths for - * several fonts and sizes, but it may not be necessary for the purpose. - * If the width for the element is zero, then the height is also - * set to zero, in order to not reserve any vertical space for elements - * without content. - * @returns {Object} The bounding box - */ - elem.getBBox = () => { - let lineWidth = 0, - width = 0, - height = 0; - - let children = [].slice.call( - elem.children.length ? elem.children : [elem] - ); +/** + * Pass Highcharts' test for SVG capabilities + * @returns {undefined} + */ +win.SVGElement.prototype.createSVGRect = function() { }; +/** + * jsdom doesn't compute layout (see + * https://github.com/tmpvar/jsdom/issues/135). This getBBox implementation + * provides just enough information to get Highcharts to render text boxes + * correctly, and is not intended to work like a general getBBox + * implementation. The height of the boxes are computed from the sum of + * tspans and their font sizes. The width is based on an average width for + * each glyph. It could easily be improved to take font-weight into account. + * For a more exact result we could to create a map over glyph widths for + * several fonts and sizes, but it may not be necessary for the purpose. + * If the width for the element is zero, then the height is also + * set to zero, in order to not reserve any vertical space for elements + * without content. + * @returns {Object} The bounding box + */ +win.SVGElement.prototype.getBBox = function() { + let lineWidth = 0, + lineHeight = 0, + width = 0, + height = 0, + newLine = false; - children - .filter(child => { - if (child.getAttribute('class') === 'highcharts-text-outline') { - child.parentNode.removeChild(child); - return false; - } - return true; - }) - .forEach(child => { - let fontSize = child.style.fontSize || elem.style.fontSize, - lineHeight, - textLength; - - // The font size and lineHeight is based on empirical values, - // copied from the SVGRenderer.fontMetrics function in - // Highcharts. - if (/px/.test(fontSize)) { - fontSize = parseInt(fontSize, 10); - } else { - fontSize = /em/.test(fontSize) ? - parseFloat(fontSize) * 12 : - 12; - } - lineHeight = fontSize < 24 ? - fontSize + 3 : - Math.round(fontSize * 1.2); - textLength = child.textContent.length * fontSize * 0.55; - - // Tspans on the same line - if (child.getAttribute('dx') !== '0') { - height += lineHeight; - } + removeHighchartsTextOutlines(this); - // New line - if (child.getAttribute('dy') !== null) { - lineWidth = 0; - } + processTextNodes(this, (textNode, child) => { + if (child.tagName === 'title') { + return; + } + let fontSize = findStyleAttr(child, 'fontSize'), + fontFamily = findStyleAttr(child, 'fontFamily'), + textLength; - lineWidth += textLength; - width = Math.max(width, lineWidth); + // The font size and lineHeight is based on empirical values, + // copied from the SVGRenderer.fontMetrics function in + // Highcharts. + if (/px/.test(fontSize)) { + fontSize = parseInt(fontSize, 10); + } else { + fontSize = /em/.test(fontSize) ? + parseFloat(fontSize) * 12 : + 12; + } + let nodeHeight = fontSize < 24 ? + fontSize + 3 : + Math.round(fontSize * 1.2); + lineHeight = Math.max(lineHeight, nodeHeight); + let fontToUse = findSizableFont(fontFamily); + textLength = pixelWidth(textNode.data, { size: fontSize, font: fontToUse, map: widthsMap }); - } - ); + // In practice, dy is used to trigger a new line + if (child.getAttribute('dy') !== null) { + lineWidth = 0; + newLine = true; + } + + lineWidth += textLength; + width = Math.max(width, lineWidth); + + if (newLine) { + height += lineHeight; + newLine = false; + lineHeight = 0; + } + }); + + // Add the height of the ongoing line + height += lineHeight; - // If the width of the text box is 0, always return a 0 height (since the element indeed consumes no space) - // Returning a non-zero height causes Highcharts to allocate vertical space in the chart for text that doesn't - // exist - let retHeight = width == 0 ? 0 : height; - return { - x: 0, - y: 0, - width: width, - height: retHeight - }; + // If the width of the text box is 0, always return a 0 height (since the element indeed consumes no space) + // Returning a non-zero height causes Highcharts to allocate vertical space in the chart for text that doesn't + // exist + let retHeight = width == 0 ? 0 : height; + return { + x: 0, + y: 0, + width: width, + height: retHeight }; - return elem; }; +/** + * Estimate the rendered length of a substring of text. Uses a similar strategy to getBBox, + * above. + * + * @param {integer} charnum Starting character position + * @param {integer} numchars Number of characters to count + * @returns Rendered length of the substring (estimated) + */ +win.SVGElement.prototype.getSubStringLength = function(charnum, numchars) { + let offset = charnum, + remaining = numchars, + textLength = 0; + + removeHighchartsTextOutlines(this); + + processTextNodes(this, (textNode, child) => { + if (child.tagName === 'title') { + return; + } + + if (remaining <= 0) { + return; + } + let childLength = textNode.length; + + if (childLength <= offset) { + offset -= childLength; + } else { + let usedLength = Math.min(childLength - offset, remaining); + remaining -= usedLength; + + let fontSize = findStyleAttr(child, 'fontSize'), + fontFamily = findStyleAttr(child, 'fontFamily'); + + // The font size is based on empirical values, + // copied from the SVGRenderer.fontMetrics function in + // Highcharts. + if (/px/.test(fontSize)) { + fontSize = parseInt(fontSize, 10); + } else { + fontSize = /em/.test(fontSize) ? + parseFloat(fontSize) * 12 : + 12; + } + let textToSize = textNode.data.substring(offset, offset + usedLength); + let fontToUse = findSizableFont(fontFamily); + let measuredWidth = pixelWidth(textToSize, { size: fontSize, font: fontToUse, map: widthsMap }); + textLength += measuredWidth; + } + }); + + return textLength; +} const inflateFunctions = (jsonConfiguration) => { Object.entries(jsonConfiguration).forEach(([attr, targetProperty]) => { diff --git a/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/package.json b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/package.json index c534f44234e..b1e89fd433f 100644 --- a/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/package.json +++ b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/package.json @@ -5,16 +5,18 @@ "scripts": { "test": "mocha", "test:watch": "mocha --watch", - "build": "webpack --config webpack.config.js" + "build": "webpack --config webpack.config.js" }, "dependencies": { "highcharts": "9.2.2", - "jsdom": "16.5.3" + "jsdom": "16.5.3", + "string-pixel-width": "1.11.0" }, "devDependencies": { "chai": "4.3.4", "mocha": "8.4.0", "mock-fs": "5.0.0", + "rewire": "7.0.0", "webpack": "5.76.0", "webpack-cli": "4.9.2" }, diff --git a/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/java/com/vaadin/flow/component/charts/export/SVGGeneratorTest.java b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/java/com/vaadin/flow/component/charts/export/SVGGeneratorTest.java index 727f075cfe1..dae4e4f09a3 100644 --- a/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/java/com/vaadin/flow/component/charts/export/SVGGeneratorTest.java +++ b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/java/com/vaadin/flow/component/charts/export/SVGGeneratorTest.java @@ -261,8 +261,9 @@ private Configuration createColumnWithoutTitle() { XAxis x = new XAxis(); x.setCategories("January is a long month", "February is rather boring", - "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", - "Dec"); + "Mar", "Apr", "May", "Jun", + "Jul is a month to enjoy really nice weather", "Aug", "Sep", + "Oct", "Nov", "Dec"); configuration.addxAxis(x); YAxis y = new YAxis(); diff --git a/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/column-without-title.svg b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/column-without-title.svg index 02a0c50e992..4d9ec97de60 100644 --- a/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/column-without-title.svg +++ b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/column-without-title.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/custom-height.svg b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/custom-height.svg index 9ba44bd101d..8cc9a23abca 100644 --- a/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/custom-height.svg +++ b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/custom-height.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/custom-lang.svg b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/custom-lang.svg index 0f0d1c27947..013f52a3ee2 100644 --- a/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/custom-lang.svg +++ b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/custom-lang.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/custom-width.svg b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/custom-width.svg index ee9f080c048..a4999078ca2 100644 --- a/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/custom-width.svg +++ b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/custom-width.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/empty.svg b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/empty.svg index 20ada559d65..2eb2dbcd1f9 100644 --- a/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/empty.svg +++ b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/empty.svg @@ -1 +1 @@ -Created with Highcharts 9.2.2No data to display \ No newline at end of file +Created with Highcharts 9.2.2No data to display \ No newline at end of file diff --git a/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/enabled-functions.svg b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/enabled-functions.svg index 2a0f9494181..0bc1cdece79 100644 --- a/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/enabled-functions.svg +++ b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/enabled-functions.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/lumo-dark.svg b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/lumo-dark.svg index 32b3f9b865e..298b8b31f65 100644 --- a/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/lumo-dark.svg +++ b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/lumo-dark.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/pie.svg b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/pie.svg index fd29933e4f3..1f2d416cd1c 100644 --- a/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/pie.svg +++ b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/pie.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/timeline.svg b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/timeline.svg index d88b9e06019..0df3e1b1f6d 100644 --- a/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/timeline.svg +++ b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/timeline.svg @@ -1 +1 @@ -View 1 month1mView 3 months3mView 6 months6mView year to dateYTDView 1 year1yView allAll \ No newline at end of file +View 1 month1mView 3 months3mView 6 months6mView year to dateYTDView 1 year1yView allAll \ No newline at end of file diff --git a/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/test/index.test.js b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/test/index.test.js index e7654e06b58..3c414fd2ce2 100644 --- a/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/test/index.test.js +++ b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/test/index.test.js @@ -1,8 +1,13 @@ const { expect } = require('chai') +const rewire = require('rewire'); const { JSDOM } = require('jsdom'); const mock = require('mock-fs') +const pixelWidth = require('string-pixel-width'); -const jsdomExporter = require('../jsdom-exporter.js') +const jsdomExporter = rewire('../jsdom-exporter.js') + +const exporterDom = jsdomExporter.__get__("dom"); +const widthsMap = jsdomExporter.__get__("widthsMap"); /** * @@ -157,3 +162,22 @@ describe('timeline', () => { expect(document.querySelector('.highcharts-navigator')).to.be.not.null; }); }); + +describe('getSubStringLength', () => { + it('should measure strings split across multiple elements', () => { + let window = exporterDom.window; + let document = window.document; + let container = document.getElementById('container'); + container.innerHTML = ` + + 0123456789 + + `; + let text = container.querySelector('text'); + expect(text).to.be.not.null; + expect(text.getSubStringLength(6, 3)).to.equal(pixelWidth("678", { size: 12, font: 'arial', map: widthsMap })) + expect(text.getSubStringLength(1, 2)).to.equal(pixelWidth("12", { size: 12, font: 'arial', map: widthsMap })) + expect(text.getSubStringLength(2, 6)).to.equal(pixelWidth("234567", { size: 12, font: 'arial', map: widthsMap })) + expect(text.getSubStringLength(0, 20)).to.equal(pixelWidth("0123456789", { size: 12, font: 'arial', map: widthsMap })) + }); +}); \ No newline at end of file