From 68eb2eb353194aa6db25be90f0298806531bccbc Mon Sep 17 00:00:00 2001
From: Guille
Date: Fri, 4 Jun 2021 17:30:40 +0300
Subject: [PATCH] feat: SVGGenerator for exporting charts in the server side.
(#1756)
Generate an SVG string of a chart using a Configuration instance with the chart's data.
Related: #1111
Example usage:
```java
Configuration chartConfiguration = createChartConfiguration();
String svg;
// using a try-with-resources block is recommended
try (SVGGenerator generator = new SVGGenerator()) {
svg = generator.generate(chartConfiguration);
}
// svg now holds the chart as an SVG string
```
---
.gitignore | 2 +-
vaadin-charts-flow-parent/pom.xml | 1 +
.../pom.xml | 5 +
.../vaadin-charts-flow-svg-generator/.npmrc | 1 +
.../LICENSE.txt | 262 +++++++++++++
.../jsdom-exporter.js | 260 +++++++++++++
.../package.json | 23 ++
.../vaadin-charts-flow-svg-generator/pom.xml | 90 +++++
.../charts/export/ExportOptions.java | 166 ++++++++
.../component/charts/export/NodeRunner.java | 26 ++
.../component/charts/export/SVGGenerator.java | 179 +++++++++
.../charts/export/SVGGeneratorTest.java | 295 ++++++++++++++
.../component/charts/export/StockPrices.java | 139 +++++++
.../src/test/resources/custom-height.svg | 2 +
.../src/test/resources/custom-lang.svg | 2 +
.../src/test/resources/custom-width.svg | 2 +
.../src/test/resources/daily-temp-ranges.json | 367 ++++++++++++++++++
.../src/test/resources/empty.svg | 2 +
.../src/test/resources/enabled-functions.svg | 1 +
.../src/test/resources/lumo-dark.svg | 2 +
.../src/test/resources/pie.svg | 2 +
.../src/test/resources/timeline.svg | 2 +
.../test/index.test.js | 145 +++++++
.../webpack.config.js | 30 ++
24 files changed, 2005 insertions(+), 1 deletion(-)
create mode 100644 vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/.npmrc
create mode 100644 vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/LICENSE.txt
create mode 100644 vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/jsdom-exporter.js
create mode 100644 vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/package.json
create mode 100644 vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/pom.xml
create mode 100644 vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/main/java/com/vaadin/flow/component/charts/export/ExportOptions.java
create mode 100644 vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/main/java/com/vaadin/flow/component/charts/export/NodeRunner.java
create mode 100644 vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/main/java/com/vaadin/flow/component/charts/export/SVGGenerator.java
create mode 100644 vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/java/com/vaadin/flow/component/charts/export/SVGGeneratorTest.java
create mode 100644 vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/java/com/vaadin/flow/component/charts/export/StockPrices.java
create mode 100644 vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/custom-height.svg
create mode 100644 vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/custom-lang.svg
create mode 100644 vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/custom-width.svg
create mode 100644 vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/daily-temp-ranges.json
create mode 100644 vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/empty.svg
create mode 100644 vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/enabled-functions.svg
create mode 100644 vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/lumo-dark.svg
create mode 100644 vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/pie.svg
create mode 100644 vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/timeline.svg
create mode 100644 vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/test/index.test.js
create mode 100644 vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/webpack.config.js
diff --git a/.gitignore b/.gitignore
index 8284990eb18..49c84ce9e4c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,4 +23,4 @@ package*json
pnpm*
error-screenshots
-*/*/frontend/generated
+**/frontend/generated
diff --git a/vaadin-charts-flow-parent/pom.xml b/vaadin-charts-flow-parent/pom.xml
index 8192612f5c0..81ff0e83919 100644
--- a/vaadin-charts-flow-parent/pom.xml
+++ b/vaadin-charts-flow-parent/pom.xml
@@ -19,6 +19,7 @@
vaadin-charts-flowvaadin-charts-testbenchvaadin-charts-flow-demo
+ vaadin-charts-flow-svg-generator
diff --git a/vaadin-charts-flow-parent/vaadin-charts-flow-integration-tests/pom.xml b/vaadin-charts-flow-parent/vaadin-charts-flow-integration-tests/pom.xml
index f8d9b408bdc..39e6b06796e 100644
--- a/vaadin-charts-flow-parent/vaadin-charts-flow-integration-tests/pom.xml
+++ b/vaadin-charts-flow-parent/vaadin-charts-flow-integration-tests/pom.xml
@@ -27,6 +27,11 @@
vaadin-charts-flow-demo${project.version}
+
+ com.vaadin
+ vaadin-charts-flow-svg-generator
+ ${project.version}
+ com.vaadinvaadin-lumo-theme
diff --git a/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/.npmrc b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/.npmrc
new file mode 100644
index 00000000000..37cfe26a972
--- /dev/null
+++ b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/.npmrc
@@ -0,0 +1 @@
+unsafe-perm = true
\ No newline at end of file
diff --git a/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/LICENSE.txt b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/LICENSE.txt
new file mode 100644
index 00000000000..0c5f03d2363
--- /dev/null
+++ b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/LICENSE.txt
@@ -0,0 +1,262 @@
+Commercial Vaadin Developer License version 4
+
+Terms and Conditions for Use, Reproduction and Distribution
+
+NOTICE TO USER: PLEASE READ THIS LICENSE AGREEMENT CAREFULLY.
+
+BY USING ALL OR ANY PART OF THE LICENSED SOFTWARE YOU ACCEPT ALL THE TERMS AND
+CONDITIONS OF THIS AGREEMENT, INCLUDING, IN PARTICULAR THE RESTRICTIONS ON: USE
+AND TRANSFERABILITY CONTAINED IN CLAUSE 2; WARRANTY IN CLAUSE 6; LIABILITY IN
+CLAUSE 7. YOU ACCEPT THAT THIS AGREEMENT IS ENFORCEABLE LIKE ANY WRITTEN
+NEGOTIATED AGREEMENT DULY SIGNED BY YOU. IF YOU DO NOT AGREE ON ALL THE TERMS
+AND CONDITIONS OF THIS AGREEMENT, STOP THE USE OF THE LICENSED SOFTWARE
+IMMEDIATELY.
+
+1. Definitions
+In this Agreement, unless the context requires otherwise, the following words
+and phrases shall have the following meanings:
+
+ * "Developer" shall mean a software developer, tester, designer or other
+ person developing a software application.
+ * "Vaadin Platform" shall mean the Vaadin web framework, components, themes,
+ tools and libraries that help Developers in building software applications.
+ * "Licensed Software" shall mean an add-on software component, extended
+ support version of Vaadin Platform, library, theme, tool or other software
+ or resource that is part of or adds functionality to Vaadin Platform or
+ helps Developers in developing applications. Licensed Software include, but
+ are not limited to, user interface components, integration components,
+ themes, libraries and development tools.
+ * "Use Licensed Software" shall mean either directly interacting with,
+ including without limitations using the user interface of, running or
+ installing, the Licensed Software during Project or editing Project source
+ code file that refers to or depends on Licensed Software either directly or
+ indirectly. Developer who edits source code that can not be compiled and/or
+ run without a copy of Licensed Software is considered to Use Licensed
+ Software. Interacting with a server that runs Licensed Software as a part
+ of an automated test suite or a design system is not considered as Use of
+ Licensed Software, but the maintainers of the mentioned test suite or
+ design system are considered to Use Licensed Software.
+ * "Agreement" shall mean this Commercial Vaadin Developer License version 4
+ agreement. Previous versions of the agreement were called Commercial Vaadin
+ Add-on License.
+ * "License" shall mean the right to Use Licensed Software according to
+ Agreement by one Developer.
+ * "Intellectual Property Rights" shall mean any and all patent, copyright,
+ trademark, design right, petty patent, service mark, domain name or any
+ other right or trade secret whether registered or not.
+ * "Licensee" shall mean the entity that has subscribed to a Subscription that
+ includes the right to Use Licensed Software.
+ * "Licensor" shall mean Vaadin Ltd. or a third party licensing Licensed
+ Software under the Agreement.
+ * "Parties/Party" shall mean Licensee and Licensor, or either of them.
+ * "Project" shall mean Licensee's software development project during which
+ the participating Developers Use Licensed Software and which aims to
+ produce Project Result.
+ * "Project Result" shall mean the outcome of the Project.
+ * “End User” shall mean a person using Project Result that does not involve
+ changing any source code.
+ * "Subscription" shall mean a subscription offered by Licensor in which
+ Licensor grants rights to Use Licensed Software according to the terms of
+ the subscription agreement and this Agreement.
+
+2. Grant of License
+2.1 Licensor grants to Licensee, against full payment of the Subscription fee, a
+worldwide, royalty-free, non-exclusive limited License to Use Licensed Software
+in Project(s) by a Developer.
+
+2.2 Licensee shall not, unless expressly provided in Agreement or in the
+applicable legislation
+
+2.2.1 rent, lease or loan Licensed Software or any copy of it;
+
+2.2.2 remove, obliterate, deface or in any way alter the notice of Licensor’s or
+a third party’s proprietary rights related to Licensed Software;
+
+2.2.3 grant sub-licenses to Licensed Software or assign its rights or
+obligations under this Agreement to a third party.
+
+2.3 Licensee may grant licenses, for free or against a payment, to the Project
+Result including Licensed Software whether regarded as derivative works or not.
+End Users are not required to have a valid License. If the Project Result is
+further developed or modified by changing its source code or the Project Results
+is used as a software component or framework in a software development project,
+all Developers who Use Licensed Software in such a context need to have a valid
+License. If the Project Result is a software development tool, component or
+environment that provides the functionality of the Licensed Software for use in
+software development projects, all Developers who Use Licensed Software need to
+have a valid License.
+
+3. Intellectual Property Rights
+3.1 All Intellectual Property Rights in and to Licensed Software are and shall
+at all times remain the sole and exclusive property of Licensor and its third
+party licensors, if any.
+
+3.2 Licensee will not at any time do or cause to be done any such act or thing
+which in any way impairs, or intends to impair, any right, title, interest or
+any Intellectual Property Right of Licensor or its third party licensors.
+Licensee shall not in any manner represent that it has any ownership of any kind
+in any of the above mentioned Intellectual Property Rights.
+
+4. Subscription fee and Subscription term
+4.1 In consideration for the Subscription based license granted herein, Licensor
+shall charge a recurring Subscription fee from the Licensee.
+
+4.2 Licensee must have a valid License for all Developers who Use Licensed
+Software in the Project. During the Subscription term, the License may be Used
+in many Projects simultaneously without additional payments. The Project Result
+may be copied an unlimited number of times and deployed to an unlimited number
+of computers without additional payments.
+
+4.3 Licensee's License will be valid starting from the beginning of the
+Subscription term and remain in force until the end of the Subscription term.
+The start date of the Subscription term and its possible renewal mechanism are
+set out in the Subscription agreement or the Licensor’s invoice to the Licensee.
+If no Subscription start date is specified in the Subscription agreement or the
+invoice, the start date shall be the date when the Licensor provides the
+Licensee access to the Licensed Software.
+
+4.4 If License is given without a fee, the License is valid for the time defined
+by the Licensor.
+
+5. Term and termination
+5.1 This Agreement is effective as of the effective date of the Subscription and
+expires on the day that the Subscription term for the Licensed Software has
+expired. Notwithstanding the aforesaid, in the event that the Licensee fails to
+comply with the terms set in this Agreement, the License granted herein shall
+not be valid and Licensee must at once cease the use of Licensed Software and
+any of its rights under Clause 2, and delete all copies of Licensed Software.
+
+5.2 Licensor has the right, in addition and without prejudice to any other
+rights or remedies, to terminate this Agreement immediately as follows:
+
+5.2.1 for any material breach of Agreement, that is not cured within seven (7)
+days of receipt by Licensee in default of a written notice specifying the breach
+and requiring its cure;
+
+5.2.2 upon receiving a written notice, if (a) all or a substantial portion of
+the assets of Licensee are transferred to an assignee for the benefit of
+creditors, or to a receiver or a trustee in bankruptcy, (b) a proceeding is
+commenced by or against Licensee for relief under bankruptcy or similar laws and
+such proceeding is not dismissed within sixty (60) days, or (c) Licensee is
+adjudged bankrupt.
+
+5.3 If and when this Agreement is terminated due to any reason or cause
+whatsoever, the Licensee shall cease to Use Licensed Software and any of its
+rights under Clause 2, and delete all copies of Licensed Software. For clarity,
+the termination of this Agreement shall not affect the validity of any licenses
+granted by the Licensee to End Users with respect to Project Results prior to
+the termination of this Agreement, but the Licensee shall not be entitled to
+Use Licensed Software after the termination of the Agreement.
+
+5.4 Provisions of the Agreement which, by their nature, are intended to survive
+its termination or expiration, shall survive its termination or expiration.
+
+6. Warranties
+THE LICENSED SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND EXPRESS
+OR IMPLIED, AND TO THE MAXIMUM EXTENT PERMITTED BY THE APPLICABLE LAW. EXCEPT AS
+EXPRESSLY PROVIDED IN THIS CLAUSE, NO WARRANTY, CONDITION, UNDERTAKING,
+LIABILITY OR TERM, EXPRESS OR IMPLIED, STATUTORY OR OTHERWISE, AS TO CONDITION,
+QUALITY, PERFORMANCE, FUNCTIONALITY, INFRINGEMENT, MERCHANTABILITY, DURABILITY
+OR FITNESS FOR PURPOSE, IS GIVEN OR ASSUMED BY VAADIN LTD., LICENSOR OR ITS
+LICENSORS AND ALL SUCH WARRANTIES, CONDITIONS, UNDERTAKINGS AND TERMS ARE HEREBY
+EXCLUDED.
+
+7. Limitation of Liability
+VAADIN LTD. AND/OR LICENSOR WILL NOT BE LIABLE FOR ANY INDIRECT, INCIDENTAL,
+PUNITIVE OR CONSEQUENTIAL LOSS, DAMAGE, COST OR EXPENSE OF ANY KIND WHATSOEVER
+AND HOWSOEVER CAUSED (INCLUDING BUSINESS INTERRUPTION, OR ANY LOSS OF BUSINESS,
+ANTICIPATED SAVINGS, REVENUE, GOODWILL, MANAGEMENT TIME, LOSS OF PROFITS OR OF
+CONTRACTS, LOSS OF OPERATION TIME, LOSS OF REPUTATION OR OF DATA) INCLUDING
+NEGLIGENCE, WHETHER IN CONTRACT OR TORT, EVEN IF THEY HAVE BEEN ADVISED OF THE
+POSSIBILITY. DEVELOPER AND/OR LICENSEE WILL USE REASONABLE EFFORTS TO MITIGATE
+THEIR LOSS SUFFERED. VAADIN LTD'S AND/OR LICENSOR'S AGGREGATE LIABILITY AND THAT
+OF ITS AFFILIATES AND SUPPLIERS UNDER OR IN CONNECTION WITH THIS LICENSE
+AGREEMENT WILL BE LIMITED TO THE AMOUNT ACTUALLY PAID FOR THE LICENSED SOFTWARE
+DURING THE MOST RECENT SUBSCRIPTION PERIOD.
+
+8. Updates, new versions maintenance and support
+Licensor may, at its sole discretion, during the Subscription term provide
+maintenance releases, updates and upgrades as new versions of the Licensed
+Software.
+
+Licensee is not entitled to receive support for the Licensed Software, but
+Licensor may, at its sole discretion during the Subscription term, provide
+support either for free or for a fee.
+
+9. Miscellaneous
+9.1 No Waiver
+
+The failure of Licensor to exercise any of its rights under this Agreement or to
+require the performance of any term or provision of this Agreement, or any
+waiver by the Licensor of any term or provision or breach of this Agreement,
+shall not prevent a subsequent exercise or enforcement of such right or be
+deemed a waiver of any subsequent breach of the same or any other term or
+provision of this Agreement. Any waiver of the performance of any of the terms
+or conditions of this Agreement shall be effective only if in writing and signed
+by the Party against which such waiver is to be enforced.
+
+9.2 Headings
+
+The headings in this Agreement are for the convenience of the Parties only and
+are not intended to define or limit the scope or interpretation of the Agreement
+or any provision hereof.
+
+9.3 Severability
+
+If any term of this Agreement is invalid or unenforceable, such terms or
+provisions shall not invalidate the rest of the Agreement which shall remain in
+full force and effect as if such invalidated or unenforceable terms or
+conditions had not been made a part of this Agreement. In the event this Clause
+(Severability) becomes operative, Parties agree to attempt to negotiate
+settlement that carries out the economic intent of the terms or provisions found
+invalid or unenforceable.
+
+9.4 Export Control
+
+The Licensed Software may be subject to import and export controls in other
+countries. Licensee agrees to strictly comply with all applicable import and
+export regulations and acknowledge that Licensee has the responsibility to
+obtain licenses to export, re-export, transfer or import Licensed Software.
+
+9.5 Entire Agreement and Assignment
+
+Agreement sets forth the entire agreement between the Parties with respect to
+the subject matter hereof and supersedes any prior proposals and
+representations, whether written or oral. Neither Party shall have the right to
+assign this Agreement to a third party without the prior written consent of the
+other party. However, Licensor shall have the right to assign this Agreement and
+the rights and obligations contained therein to a company belonging to the same
+group of companies as Licensor, and to a third party to which the business of
+Licensor is transferred.
+
+9.6 Governing Law and Jurisdiction
+
+9.6.1 For customers domiciled in the United States
+
+If the Customer’s domicile is in the United States, the Agreement shall be
+governed by and construed in accordance with the substantive laws of the State
+of California. The Agreement shall be construed and enforced without regard to
+the United Nations Convention on the International Sale of Goods (CISG). Any
+dispute or controversy or claim arising out of or relating to this Agreement, or
+the breach, termination or validity thereof, shall be resolved by final and
+binding arbitration in accordance with the International Chamber of Commerce
+Rules of Arbitration, by one (1) arbitrator appointed according to the
+aforementioned rules. The arbitration shall be conducted in the English
+language in San Francisco, California, United States.
+
+9.6.2 For customers domiciled outside the United States
+
+If the Customer’s domicile is outside the United States, the Agreement shall be
+governed by and construed in accordance with the substantive laws of Finland,
+excluding its choice of law provisions and the United Nations Convention on
+Contracts for the International Sale of Goods (CISG). Any dispute, controversy
+or claim arising out of or relating to the Agreement, or the breach, termination
+or validity thereof, shall be finally settled by arbitration in accordance with
+the Arbitration Rules of the Finland Chamber of Commerce. The number of
+arbitrators shall be one. The seat of arbitration shall be Turku, Finland. The
+language of the arbitration shall be English.
+
+9.7 Language
+
+The official text of the Agreement or any notices given or accounts or
+statements required hereby shall be in English.
\ 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
new file mode 100644
index 00000000000..3f0139620e9
--- /dev/null
+++ b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/jsdom-exporter.js
@@ -0,0 +1,260 @@
+/**
+ * Sample of serverside generation of Highcharts using an extension to jsdom in
+ * node.js.
+ */
+
+/* eslint-env node, es6 */
+/* eslint no-console: 0 */
+const jsdom = require('jsdom');
+const fs = require('fs');
+
+const { JSDOM } = jsdom;
+
+// Get the document and window
+const dom = new JSDOM(
+ `
+
+
+
+
+ `);
+const win = dom.window;
+const doc = win.document;
+
+// Require Highcharts with the window shim
+const Highcharts = require('highcharts/highstock')(win);
+require("highcharts/modules/accessibility")(Highcharts);
+require("highcharts/highcharts-more")(Highcharts);
+require("highcharts/highcharts-3d")(Highcharts);
+require("highcharts/modules/data")(Highcharts);
+require("highcharts/modules/drilldown")(Highcharts);
+require("highcharts/modules/exporting")(Highcharts);
+require("highcharts/modules/funnel")(Highcharts);
+require("highcharts/modules/heatmap")(Highcharts);
+require("highcharts/modules/solid-gauge")(Highcharts);
+require("highcharts/modules/treemap")(Highcharts);
+require("highcharts/modules/no-data-to-display")(Highcharts);
+require("highcharts/modules/sankey")(Highcharts);
+require("highcharts/modules/timeline")(Highcharts);
+require("highcharts/modules/organization")(Highcharts);
+require("highcharts/modules/xrange")(Highcharts);
+require("highcharts/modules/bullet")(Highcharts);
+
+win.Date = Date;
+
+// 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.
+ * @returns {Object} The bounding box
+ */
+ elem.getBBox = () => {
+ let lineWidth = 0,
+ width = 0,
+ height = 0;
+
+ let children = [].slice.call(
+ elem.children.length ? elem.children : [elem]
+ );
+
+ 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;
+ }
+
+ // New line
+ if (child.getAttribute('dy') !== null) {
+ lineWidth = 0;
+ }
+
+ lineWidth += textLength;
+ width = Math.max(width, lineWidth);
+
+ }
+ );
+
+ return {
+ x: 0,
+ y: 0,
+ width: width,
+ height: height
+ };
+ };
+ return elem;
+};
+
+const inflateFunctions = (jsonConfiguration) => {
+ Object.entries(jsonConfiguration).forEach(([attr, targetProperty]) => {
+ if (attr.indexOf('_fn_') === 0 && (typeof targetProperty === 'string' || targetProperty instanceof String)) {
+ const property = attr.replace('_fn_','');
+ const jsFunction = Function(`'use strict'; return ${targetProperty}`);
+ if (targetProperty.trim().startsWith('function')) {
+ jsonConfiguration[property] = jsFunction();
+ } else {
+ jsonConfiguration[property] = jsFunction;
+ }
+ delete jsonConfiguration[attr];
+ } else if (targetProperty instanceof Object) {
+ inflateFunctions(targetProperty);
+ }
+ });
+ }
+
+/**
+ * ExportOptions
+ *
+ * @typedef ExportOptions
+ *
+ * @property {object} theme
+ * @property {object} lang
+ * @property {string} width
+ * @property {string} height
+ * @property {boolean} timeline
+ * @property {boolean} executeFunctions
+ */
+
+/**
+ * ExportConfiguration
+ *
+ * @typedef ExportConfiguration
+ *
+ * @property {object} chartConfiguration
+ * @property {string} outFile
+ * @property {ExportOptions} exportOptions
+ */
+
+/**
+ * SVGResult
+ *
+ * @typedef SVGResult
+ *
+ * @property {string} svgString
+ * @property {string} outFile
+ */
+
+/**
+ * Function to export SVG a string containing a chart based
+ * on the configuration provided
+ *
+ * @param {ExportConfiguration} configuration
+ *
+ * @returns {Promise} Object with the result of the export
+ */
+const jsdomExporter = ({ chartConfiguration, outFile = 'chart.svg', exportOptions }) => {
+ return new Promise((resolve, reject) => {
+
+ // Disable all animation
+ Highcharts.setOptions({
+ plotOptions: {
+ series: {
+ animation: false,
+ dataLabels: {
+ defer: false
+ }
+ }
+ }
+ });
+
+ let isTimeline = false;
+ if (exportOptions) {
+ if (exportOptions.theme) {
+ Highcharts.setOptions(exportOptions.theme);
+ }
+
+ if (exportOptions.lang) {
+ Highcharts.setOptions({ lang: exportOptions.lang })
+ }
+
+ if (exportOptions.height || exportOptions.width) {
+ const chartOptions = {
+ ...exportOptions.height && { height: exportOptions.height },
+ ...exportOptions.width && { width: exportOptions.width },
+ };
+ chartConfiguration.chart = { ...chartConfiguration.chart, ...chartOptions };
+ }
+
+ isTimeline = exportOptions.timeline;
+
+ if(exportOptions.executeFunctions) {
+ inflateFunctions(chartConfiguration);
+ }
+ }
+
+ let chart;
+
+ // Generate the chart into the container
+ try {
+ const constr = isTimeline ? 'stockChart' : 'chart';
+ chart = Highcharts[constr](
+ 'container',
+ { ...chartConfiguration, exporting: { enabled: false } }
+ );
+ } catch (e) {
+ reject(e);
+ }
+
+ let svg = chart.sanitizeSVG(
+ chart.container.innerHTML
+ );
+ fs.writeFile(__dirname + '/' + outFile, svg, function (err) {
+ if (err) {
+ reject(err);
+ }
+
+ resolve({
+ svgString: svg,
+ outFile: __dirname + '/' + outFile
+ });
+ });
+ });
+};
+
+module.exports = jsdomExporter;
\ No newline at end of file
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
new file mode 100644
index 00000000000..4d4047026c7
--- /dev/null
+++ b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "vaadin-charts-flow-svg-generator",
+ "version": "1.0.0",
+ "description": "",
+ "scripts": {
+ "test": "mocha",
+ "test:watch": "mocha --watch",
+ "postinstall": "webpack --config webpack.config.js"
+ },
+ "dependencies": {
+ "highcharts": "^8.2.2",
+ "jsdom": "^16.5.3"
+ },
+ "devDependencies": {
+ "chai": "^4.3.4",
+ "mocha": "^8.4.0",
+ "mock-fs": "^5.0.0",
+ "webpack": "^5.37.1",
+ "webpack-cli": "^4.7.0"
+ },
+ "author": "Vaadin Ltd",
+ "license": "SEE LICENSE IN LICENSE.txt"
+}
diff --git a/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/pom.xml b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/pom.xml
new file mode 100644
index 00000000000..2361e4fcff6
--- /dev/null
+++ b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/pom.xml
@@ -0,0 +1,90 @@
+
+
+ 4.0.0
+
+ vaadin-charts-flow-parent
+ com.vaadin
+ 21.0-SNAPSHOT
+
+ vaadin-charts-flow-svg-generator
+ jar
+ Vaadin Charts SVG Generator API
+
+
+
+ com.vaadin
+ flow-server
+
+
+ com.vaadin
+ vaadin-charts-flow
+ ${project.version}
+
+
+ com.vaadin
+ flow-test-generic
+ test
+
+
+
+
+
+ attach-docs
+
+
+ with-docs
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+
+
+
+
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 1.6.0
+
+
+ npm-install
+ validate
+
+ exec
+
+
+ npm
+
+ install
+
+
+
+
+ run-tests
+ test
+
+ exec
+
+
+ npm
+
+ test
+
+ ${skipTests}
+
+
+
+
+
+
+
diff --git a/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/main/java/com/vaadin/flow/component/charts/export/ExportOptions.java b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/main/java/com/vaadin/flow/component/charts/export/ExportOptions.java
new file mode 100644
index 00000000000..0319a8f0c49
--- /dev/null
+++ b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/main/java/com/vaadin/flow/component/charts/export/ExportOptions.java
@@ -0,0 +1,166 @@
+package com.vaadin.flow.component.charts.export;
+
+import com.vaadin.flow.component.charts.model.AbstractConfigurationObject;
+import com.vaadin.flow.component.charts.model.Lang;
+import com.vaadin.flow.component.charts.model.style.Theme;
+
+/**
+ *
+ * Optional settings for exporting charts in the server.
+ *
+ *
+ *
+ * Options include:
+ *
+ *
width: Width of the exported image.
+ *
height: Height of the exported image.
+ *
theme: Theme used to style the chart. For example:
+ * {@link com.vaadin.flow.component.charts.themes.LumoDarkTheme}
+ *
lang: Lang specifications for internationalization purposes.
+ *
isTimeline: Determines if the generated chart is in timeline mode.
+ *
+ * @see SVGGenerator
+ *
+ * @since 21.0
+ */
+public class ExportOptions extends AbstractConfigurationObject {
+
+ private Theme theme;
+ private Lang lang;
+ private Number width;
+ private Number height;
+ private Boolean timeline;
+ private Boolean executeFunctions;
+
+ public ExportOptions() {
+ super();
+ }
+
+ //
+
+ /**
+ * Get the {@link Theme} used to style the chart.
+ *
+ * @return the {@link Theme} used to style the chart.
+ */
+ public Theme getTheme() {
+ return theme;
+ }
+
+ /**
+ * Set the {@link Theme} used to style the chart.
+ *
+ * @param theme
+ * the theme to use when styling the chart.
+ * @see com.vaadin.flow.component.charts.themes.LumoLightTheme
+ * @see com.vaadin.flow.component.charts.themes.LumoDarkTheme
+ */
+ public void setTheme(Theme theme) {
+ this.theme = theme;
+ }
+
+ /**
+ * Get the {@link Lang} object used to add i18n features to the chart.
+ *
+ * @return the lang object used to provide i18n to the chart.
+ */
+ public Lang getLang() {
+ return lang;
+ }
+
+ /**
+ * Set the {@link Lang} object used to add i18n features to the chart.
+ *
+ * @param lang
+ * the {@link Lang} object used to provide the i18n features of
+ * the chart.
+ */
+ public void setLang(Lang lang) {
+ this.lang = lang;
+ }
+
+ /**
+ * Get the width the resulting chart will have.
+ *
+ * @return the width the resulting chart will have.
+ */
+ public Number getWidth() {
+ return width;
+ }
+
+ /**
+ * Set the width the resulting chart will have.
+ *
+ * @param width
+ * the width the resulting chart will have.
+ */
+ public void setWidth(Number width) {
+ this.width = width;
+ }
+
+ /**
+ * Get the height the resulting chart will have.
+ *
+ * @return the height the resulting chart will have.
+ */
+ public Number getHeight() {
+ return height;
+ }
+
+ /**
+ * Get the height the resulting chart will have.
+ *
+ * @param height
+ * the height the resulting chart will have.
+ */
+ public void setHeight(Number height) {
+ this.height = height;
+ }
+
+ /**
+ * Check flag to see if the generated chart will be in timeline mode.
+ *
+ * @return true if generated chart will be in timeline mode,
+ * false otherwise.
+ */
+ public Boolean getTimeline() {
+ return timeline;
+ }
+
+ /**
+ * Set flag to mark the generated chart in timeline mode.
+ *
+ * @param timeline
+ * if the generated chart should be in timeline mode.
+ */
+ public void setTimeline(Boolean timeline) {
+ this.timeline = timeline;
+ }
+
+ /**
+ * Check flag to see if generators will execute JavaScript functions when
+ * using a {@link com.vaadin.flow.component.charts.model.Configuration} with
+ * JS functions.
+ *
+ * @return true if the generator will execute JavaScript
+ * functions, false otherwise.
+ */
+ public boolean getExecuteFunctions() {
+ return executeFunctions;
+ }
+
+ /**
+ * Set flag to execute JS functions when exporting charts.
+ *
+ * @param executeFunctions
+ * if the generator should execute JS functions.
+ */
+ public void setExecuteFunctions(Boolean executeFunctions) {
+ this.executeFunctions = executeFunctions;
+ }
+ //
+}
diff --git a/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/main/java/com/vaadin/flow/component/charts/export/NodeRunner.java b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/main/java/com/vaadin/flow/component/charts/export/NodeRunner.java
new file mode 100644
index 00000000000..9880230dc0d
--- /dev/null
+++ b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/main/java/com/vaadin/flow/component/charts/export/NodeRunner.java
@@ -0,0 +1,26 @@
+package com.vaadin.flow.component.charts.export;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.vaadin.flow.server.frontend.FrontendTools;
+import com.vaadin.flow.server.frontend.FrontendUtils;
+
+public class NodeRunner {
+
+ public int runJavascript(String script)
+ throws InterruptedException, IOException {
+ FrontendTools tools = new FrontendTools("",
+ () -> FrontendUtils.getVaadinHomeDirectory().getAbsolutePath());
+ String node = tools.getNodeExecutable();
+ List command = new ArrayList<>();
+ command.add(node);
+ command.add("-e");
+ command.add(script);
+ ProcessBuilder builder = FrontendUtils.createProcessBuilder(command);
+ builder.inheritIO();
+ Process process = builder.start();
+ return process.waitFor();
+ }
+}
diff --git a/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/main/java/com/vaadin/flow/component/charts/export/SVGGenerator.java b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/main/java/com/vaadin/flow/component/charts/export/SVGGenerator.java
new file mode 100644
index 00000000000..04c3a8d7e98
--- /dev/null
+++ b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/main/java/com/vaadin/flow/component/charts/export/SVGGenerator.java
@@ -0,0 +1,179 @@
+/*-
+ * #%L
+ * Vaadin Charts for Flow
+ * %%
+ * Copyright (C) 2021 Vaadin Ltd
+ * %%
+ * This program is available under Commercial Vaadin Developer License
+ * 4.0 (CVDLv4).
+ *
+ * For the full License, see .
+ * #L%
+ */
+
+package com.vaadin.flow.component.charts.export;
+
+import java.io.IOException;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Objects;
+
+import com.vaadin.flow.component.charts.model.Configuration;
+import com.vaadin.flow.component.charts.util.ChartSerialization;
+
+/**
+ *
+ * Use instances of this class to generate SVG strings from chart
+ * {@link Configuration} instances. You must close the generator when
+ * you're done with it. You can use a try-with-resources block to close it
+ * automatically. You must have NodeJS installed for this to work.
+ *
+ *
+ *
+ * @since 21.0
+ */
+public class SVGGenerator implements AutoCloseable {
+
+ /**
+ * Pathname to the internal exporter bundle file. We use it to copy its
+ * contents to a temporary file that can be then accessed by a NodeJS
+ * process.
+ */
+ private static final String INTERNAL_BUNDLE_PATH = "/META-INF/frontend/generated/jsdom-exporter-bundle.js"
+ .replace("/", FileSystems.getDefault().getSeparator());
+ /**
+ * String template for the script to be run with NodeJS to generate an svg
+ * file which contents can be then read by this class.
+ */
+ private static final String SCRIPT_TEMPLATE = "const exporter = require('%s');\n"
+ + "exporter({\n" + "chartConfiguration: %s,\n" + "outFile: '%s',\n"
+ + "exportOptions: %s,\n" + "})";
+
+ /**
+ * Path to the temporary directory used to hold the temporary bundle file
+ * and the temporary chart svg file.
+ */
+ private final Path tempDirPath;
+ /**
+ * Path to the temporary Javascript bundle file which contents are a copy of
+ * the internal bundle file. This file can then be accessed by a NodeJS
+ * process.
+ */
+ private final Path bundleTempPath;
+
+ /**
+ * Creates a new instance of {@link SVGGenerator} which allocates resources
+ * used to transform a {@link Configuration} object to an SVG string.
+ *
+ * @throws IOException
+ * if there's any issue allocating resources needed.
+ */
+ public SVGGenerator() throws IOException {
+ tempDirPath = Files.createTempDirectory("svg-export");
+ bundleTempPath = tempDirPath.resolve("export-svg-bundle.js");
+ Files.copy(getClass().getResourceAsStream(INTERNAL_BUNDLE_PATH),
+ bundleTempPath);
+ }
+
+ @Override
+ public void close() throws IOException {
+ // cleanup by deleting all temp files
+ Files.deleteIfExists(bundleTempPath);
+ Files.deleteIfExists(tempDirPath);
+ }
+
+ /**
+ * Generate an SVG string that can be used to render a chart with data from
+ * a {@link Configuration} instance.
+ *
+ * @param chartConfiguration
+ * the {@link Configuration} with the chart's data.
+ * @return an SVG string resulting from the {@link Configuration}.
+ * @throws NullPointerException
+ * when passing a null configuration.
+ * @throws IllegalStateException
+ * when called on a closed generator.
+ * @throws IOException
+ * if anything happens using or allocating resources to
+ * virtually render the chart.
+ * @throws InterruptedException
+ * if the rendering process gets interrupted.
+ */
+ public String generate(Configuration chartConfiguration)
+ throws IOException, InterruptedException {
+ return generate(chartConfiguration, null);
+ }
+
+ /**
+ * Generate an SVG string that can be used to render a chart with data from
+ * a {@link Configuration} instance.
+ *
+ * @param chartConfiguration
+ * the {@link Configuration} with the chart's data.
+ * @param exportOptions
+ * optional exporting options to customize the result.
+ * @return an SVG string resulting from the {@link Configuration},
+ * customized as per the {@link ExportOptions}.
+ * @throws NullPointerException
+ * when passing a null configuration.
+ * @throws IllegalStateException
+ * when called on a closed generator.
+ * @throws IOException
+ * if anything happens using or allocating resources to
+ * virtually render the chart.
+ * @throws InterruptedException
+ * if the rendering process gets interrupted.
+ */
+ public String generate(Configuration chartConfiguration,
+ ExportOptions exportOptions)
+ throws IOException, InterruptedException {
+ if (isClosed()) {
+ throw new IllegalStateException(
+ "This generator is already closed.");
+ }
+ Configuration config = Objects.requireNonNull(chartConfiguration,
+ "Chart configuration must not be null.");
+ String jsonConfig = ChartSerialization.toJSON(config);
+ String jsonExportOptions = ChartSerialization.toJSON(exportOptions);
+ Path chartFilePath = Files.createTempFile(tempDirPath, "chart", ".svg");
+ String chartFileName = chartFilePath.toFile().getName();
+ String command = String.format(SCRIPT_TEMPLATE,
+ bundleTempPath.toAbsolutePath(), jsonConfig, chartFileName,
+ jsonExportOptions);
+
+ NodeRunner nodeRunner = new NodeRunner();
+ nodeRunner.runJavascript(command);
+ // when script completes, the chart svg file should exist
+ try {
+ return new String(Files.readAllBytes(chartFilePath));
+ } finally {
+ Files.delete(chartFilePath);
+ }
+ }
+
+ /**
+ *