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-flow vaadin-charts-testbench vaadin-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.vaadin vaadin-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.
  • + *
  • executeFunctions: execute JavaScript functions (for example: formatter + * functions)
  • + *
+ *

+ * + * @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. + *

+ *
+ *

+ * Example usage: + *

+ * + * + *
+ *  Configuration configuration = new Configuration();
+ *  // ...
+ *  try (SVGGenerator generator = new SVGGenerator()) {
+ *      String svg = generator.generate(configuration);
+ *  }
+ *  
+ *
+ * + * @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); + } + } + + /** + *

+ * Check if this generator is closed. + *

+ * + * @return true if the generator is closed, false + * otherwise. + */ + public boolean isClosed() { + return !tempDirPath.toFile().exists(); + } +} 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 new file mode 100644 index 00000000000..60395a5dfcd --- /dev/null +++ b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/java/com/vaadin/flow/component/charts/export/SVGGeneratorTest.java @@ -0,0 +1,295 @@ +/*- + * #%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.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.flow.component.charts.model.AxisType; +import com.vaadin.flow.component.charts.model.ChartType; +import com.vaadin.flow.component.charts.model.Configuration; +import com.vaadin.flow.component.charts.model.Cursor; +import com.vaadin.flow.component.charts.model.DataSeries; +import com.vaadin.flow.component.charts.model.DataSeriesItem; +import com.vaadin.flow.component.charts.model.IntervalUnit; +import com.vaadin.flow.component.charts.model.Lang; +import com.vaadin.flow.component.charts.model.ListSeries; +import com.vaadin.flow.component.charts.model.PlotOptionsPie; +import com.vaadin.flow.component.charts.model.PlotOptionsSeries; +import com.vaadin.flow.component.charts.model.Tooltip; +import com.vaadin.flow.component.charts.model.XAxis; +import com.vaadin.flow.component.charts.model.YAxis; +import com.vaadin.flow.component.charts.themes.LumoDarkTheme; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class SVGGeneratorTest { + + private SVGGenerator svgGenerator; + + @Before + public void setup() throws IOException { + svgGenerator = new SVGGenerator(); + } + + @After + public void cleanup() throws IOException { + if (!svgGenerator.isClosed()) { + svgGenerator.close(); + } + } + + @Test(expected = NullPointerException.class) + public void chartConfigurationMustNotBeNull() + throws IOException, InterruptedException { + svgGenerator.generate(null); + } + + @Test(expected = IllegalStateException.class) + public void throwIllegalStateExceptionOnClosedGenerator() + throws IOException, InterruptedException { + svgGenerator.close(); + // it should check to see if the generator is closed before it checks if + // the config is null + svgGenerator.generate(null); + } + + @Test + public void shouldKnowWhenItIsClosed() throws IOException { + assertFalse(svgGenerator.isClosed()); + svgGenerator.close(); + assertTrue(svgGenerator.isClosed()); + } + + @Test + public void generateSVGFromAnEmptyConfiguration() + throws IOException, InterruptedException { + Configuration configuration = new Configuration(); + String svg = svgGenerator.generate(configuration); + Path emptyConfigChart = Paths.get("src", "test", "resources", + "empty.svg"); + String emptyChartContent = new String( + Files.readAllBytes(emptyConfigChart)); + assertTrue(replaceIds(emptyChartContent).contains(replaceIds(svg))); + } + + @Test + public void generateSVGFromValidConfiguration() + throws IOException, InterruptedException { + Configuration conf = createPieChartConfiguration(); + String svg = svgGenerator.generate(conf); + Path pieChartPath = Paths.get("src", "test", "resources", "pie.svg"); + String expectedSVG = new String(Files.readAllBytes(pieChartPath)); + assertTrue(replaceIds(expectedSVG).contains(replaceIds(svg))); + } + + @Test + public void exportWithCustomWidth() + throws IOException, InterruptedException { + Configuration conf = createPieChartConfiguration(); + ExportOptions options = new ExportOptions(); + options.setWidth(999); + String svg = svgGenerator.generate(conf, options); + Path pieChartPath = Paths.get("src", "test", "resources", + "custom-width.svg"); + String expectedSVG = new String(Files.readAllBytes(pieChartPath)); + assertTrue(replaceIds(expectedSVG).contains(replaceIds(svg))); + } + + @Test + public void exportWithCustomHeight() + throws IOException, InterruptedException { + Configuration conf = createPieChartConfiguration(); + ExportOptions options = new ExportOptions(); + options.setHeight(999); + String svg = svgGenerator.generate(conf, options); + Path pieChartPath = Paths.get("src", "test", "resources", + "custom-height.svg"); + String expectedSVG = new String(Files.readAllBytes(pieChartPath)); + assertTrue(replaceIds(expectedSVG).contains(replaceIds(svg))); + } + + @Test + public void exportWithCustomTheme() + throws IOException, InterruptedException { + Configuration conf = createPieChartConfiguration(); + ExportOptions options = new ExportOptions(); + options.setTheme(new LumoDarkTheme()); + String svg = svgGenerator.generate(conf, options); + Path pieChartPath = Paths.get("src", "test", "resources", + "lumo-dark.svg"); + String expectedSVG = new String(Files.readAllBytes(pieChartPath)); + assertTrue(replaceIds(expectedSVG).contains(replaceIds(svg))); + } + + @Test + public void exportWithCustomLang() + throws IOException, InterruptedException { + Configuration conf = createAreaChartConfiguration(); + ExportOptions options = new ExportOptions(); + Lang lang = createLang(); + options.setLang(lang); + String svg = svgGenerator.generate(conf, options); + Path pieChartPath = Paths.get("src", "test", "resources", + "custom-lang.svg"); + String expectedSVG = new String(Files.readAllBytes(pieChartPath)); + assertTrue(replaceIds(expectedSVG).contains(replaceIds(svg))); + } + + @Test + public void exportWithTimeline() throws IOException, InterruptedException { + Configuration configuration = new Configuration(); + configuration.getChart().setType(ChartType.AREASPLINERANGE); + configuration.getTitle().setText("Temperature variation by day"); + Tooltip tooltip = configuration.getTooltip(); + tooltip.setValueSuffix("°C"); + DataSeries dataSeries = new DataSeries("Temperatures"); + for (StockPrices.RangeData data : StockPrices.fetchDailyTempRanges()) { + dataSeries.add(new DataSeriesItem(data.getDate(), data.getMin(), + data.getMax())); + } + configuration.setSeries(dataSeries); + ExportOptions options = new ExportOptions(); + options.setTimeline(true); + Path expectedFilePath = Paths.get("src", "test", "resources", + "timeline.svg"); + String expectedSVG = new String(Files.readAllBytes(expectedFilePath)); + String actualSVG = svgGenerator.generate(configuration, options); + assertTrue(replaceIds(expectedSVG).contains(replaceIds(actualSVG))); + } + + @Test + public void exportWithEnabledFunctions() + throws IOException, InterruptedException { + Configuration configuration = createAreaChartConfiguration(); + configuration.getyAxis().getLabels().setFormatter( + "function () { return this.value +' formatted'; }"); + ExportOptions options = new ExportOptions(); + options.setExecuteFunctions(true); + String actualSVG = svgGenerator.generate(configuration, options); + Path expectedResultPath = Paths.get("src", "test", "resources", + "enabled-functions.svg"); + String expectedSVG = new String(Files.readAllBytes(expectedResultPath)); + assertTrue(replaceIds(expectedSVG).contains(replaceIds(actualSVG))); + } + + private Configuration createPieChartConfiguration() { + Configuration conf = new Configuration(); + conf.setTitle("Browser market shares in January, 2018"); + conf.getChart().setType(ChartType.PIE); + + Tooltip tooltip = new Tooltip(); + tooltip.setValueDecimals(1); + conf.setTooltip(tooltip); + + PlotOptionsPie plotOptions = new PlotOptionsPie(); + plotOptions.setAllowPointSelect(true); + plotOptions.setCursor(Cursor.POINTER); + plotOptions.setShowInLegend(true); + conf.setPlotOptions(plotOptions); + + DataSeries series = new DataSeries(); + DataSeriesItem chrome = new DataSeriesItem("Chrome", 61.41); + chrome.setSliced(true); + chrome.setSelected(true); + series.add(chrome); + series.add(new DataSeriesItem("Internet Explorer", 11.84)); + series.add(new DataSeriesItem("Firefox", 10.85)); + series.add(new DataSeriesItem("Edge", 4.67)); + series.add(new DataSeriesItem("Safari", 4.18)); + series.add(new DataSeriesItem("Sogou Explorer", 1.64)); + series.add(new DataSeriesItem("Opera", 6.2)); + series.add(new DataSeriesItem("QQ", 1.2)); + series.add(new DataSeriesItem("Others", 2.61)); + conf.setSeries(series); + return conf; + } + + private Configuration createAreaChartConfiguration() { + Configuration configuration = new Configuration(); + configuration.setTitle("First Chart for Flow"); + configuration.getChart().setType(ChartType.AREA); + Tooltip tooltip = configuration.getTooltip(); + tooltip.setEnabled(true); + tooltip.setShared(true); + + PlotOptionsSeries options = new PlotOptionsSeries(); + options.setPointStart(0); + options.setPointIntervalUnit(IntervalUnit.DAY); + configuration.setPlotOptions(options); + configuration.addSeries(new ListSeries("Tokyo", 20, 12, 34, 23, 65, 8, + 4, 7, 76, 19, 20, 8)); + configuration.addSeries(new ListSeries("Miami", 34, 29, 23, 65, 8, 4, 7, + 7, 59, 8, 9, 19)); + + XAxis x = new XAxis(); + x.setType(AxisType.DATETIME); + x.getLabels().setFormat("{value:%a}"); + configuration.addxAxis(x); + YAxis y = new YAxis(); + y.setMin(0); + y.setTitle("Rainfall (mm)"); + configuration.addyAxis(y); + return configuration; + } + + private Lang createLang() { + Lang lang = new Lang(); + lang.setShortMonths(new String[] { "Tammi", "Helmi", "Maalis", "Huhti", + "Touko", "Kesä", "Heinä", "Elo", "Syys", "Loka", "Marras", + "Joulu" }); + lang.setMonths(new String[] { "Tammikuu", "Helmikuu", "Maaliskuu", + "Huhtikuu", "Toukokuu", "Kesäkuu", "Heinäkuu", "Elokuu", + "Syyskuu", "Lokakuu", "Marraskuu", "Joulukuu" }); + lang.setWeekdays(new String[] { "Sunnuntai", "Maanantai", "Tiistai", + "Keskiviikko", "Torstai", "Perjantai", "Lauantai" }); + lang.setShortWeekdays( + new String[] { "su", "ma", "ti", "ke", "to", "pe", "la" }); + return lang; + } + + /** + * Generated SVG documents have some elements with an "id" attribute having + * a value that is hard to predict and match to any specific expected value. + * This method replaces those Ids with simple, predictable values. First one + * will be replaced by "id-0" and all others just add 1 from the previous. + * All mentions of the same id will also be replaced. + * + * @param svg + * the string representation of the svg with Ids to replace. + * @return the same svg string but with all ids replaced with a predictable + * pattern. + */ + private String replaceIds(String svg) { + String regex = "id=\"[\\w-]+\""; + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(svg); + int index = 0; + while (matcher.find()) { + String group = matcher.group(); + svg = svg.replaceAll(group.substring(4, group.length() - 1), + "id-" + index++); + } + return svg; + } +} diff --git a/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/java/com/vaadin/flow/component/charts/export/StockPrices.java b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/java/com/vaadin/flow/component/charts/export/StockPrices.java new file mode 100644 index 00000000000..208825f3801 --- /dev/null +++ b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/java/com/vaadin/flow/component/charts/export/StockPrices.java @@ -0,0 +1,139 @@ +package com.vaadin.flow.component.charts.export; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class StockPrices { + + protected static class TimeData { + + private long date; + + private TimeData(long date) { + this.date = date; + } + + public long getDate() { + return date; + } + } + + public static class PriceData extends TimeData { + + private double price; + + private PriceData(long date, double price) { + super(date); + this.price = price; + } + + public double getPrice() { + return price; + } + } + + public static class RangeData extends TimeData { + + private double min; + private double max; + + private RangeData(long date, double min, double max) { + super(date); + this.min = min; + this.max = max; + } + + public double getMin() { + return min; + } + + public double getMax() { + return max; + } + } + + public static class OhlcData extends TimeData { + + private double open; + private double high; + private double low; + private double close; + + private OhlcData(long date, double open, double high, double low, + double close) { + super(date); + this.open = open; + this.high = high; + this.low = low; + this.close = close; + } + + public double getOpen() { + return open; + } + + public double getHigh() { + return high; + } + + public double getLow() { + return low; + } + + public double getClose() { + return close; + } + } + + public static class JsonData { + private Number[][] data; + + public Number[][] getData() { + return data; + } + + public void setData(Number[][] data) { + this.data = data; + } + } + + public static List fetchDailyTempRanges() { + List data = readRangeData("daily-temp-ranges.json"); + return Collections.unmodifiableList(data); + } + + private static List readRangeData(String filename) { + JsonData jsonData = readJsonDataFrom(filename); + + List data = new ArrayList<>(); + for (int i = 0; i < jsonData.data.length; ++i) { + Number[] row = jsonData.data[i]; + data.add(new RangeData(row[0].longValue(), row[1].doubleValue(), + row[2].doubleValue())); + } + + return data; + } + + private static JsonData readJsonDataFrom(String filename) { + try { + ObjectMapper mapper = new ObjectMapper(); + Path filePath = Paths.get("src", "test", "resources", filename); + return mapper.readValue( + new InputStreamReader(Files.newInputStream(filePath)), + JsonData.class); + + } catch (IOException e) { + throw new RuntimeException("Cannot read data from " + filename, e); + } + } + +} 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 new file mode 100644 index 00000000000..90420abf790 --- /dev/null +++ b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/custom-height.svg @@ -0,0 +1,2 @@ + +Highcharts.com 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 new file mode 100644 index 00000000000..8b8c46bbe44 --- /dev/null +++ b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/custom-lang.svg @@ -0,0 +1,2 @@ + +Highcharts.com 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 new file mode 100644 index 00000000000..9cd723701cd --- /dev/null +++ b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/custom-width.svg @@ -0,0 +1,2 @@ + +Highcharts.com \ No newline at end of file diff --git a/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/daily-temp-ranges.json b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/daily-temp-ranges.json new file mode 100644 index 00000000000..fb05f3c8ecc --- /dev/null +++ b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/daily-temp-ranges.json @@ -0,0 +1,367 @@ +{"data": [ + [1483232400000, 1.4, 4.7], + [1483318800000, -1.3, 1.9], + [1483405200000, -0.7, 4.3], + [1483491600000, -5.5, 3.2], + [1483578000000, -9.9, -6.6], + [1483664400000, -9.6, 0.1], + [1483750800000, -0.9, 4.0], + [1483837200000, -2.2, 2.9], + [1483923600000, 1.3, 2.3], + [1484010000000, -0.3, 2.9], + [1484096400000, 1.1, 3.8], + [1484182800000, 0.6, 2.1], + [1484269200000, -3.4, 2.5], + [1484355600000, -2.9, 2.0], + [1484442000000, -5.7, -2.6], + [1484528400000, -8.7, -3.3], + [1484614800000, -3.5, -0.3], + [1484701200000, -0.2, 7.0], + [1484787600000, 2.3, 8.5], + [1484874000000, 5.6, 9.5], + [1484960400000, 0.4, 5.8], + [1485046800000, 0.1, 3.1], + [1485133200000, 1.5, 4.1], + [1485219600000, -0.2, 2.8], + [1485306000000, 2.3, 10.3], + [1485392400000, -0.8, 9.4], + [1485478800000, -1.3, 4.6], + [1485565200000, -0.6, 5.3], + [1485651600000, 1.4, 5.8], + [1485738000000, -3.6, 0.9], + [1485824400000, -5.4, -2.6], + [1485910800000, -5.5, 0.8], + [1485997200000, -0.8, 2.5], + [1486083600000, 1.6, 4.1], + [1486170000000, 1.3, 4.8], + [1486256400000, 0.7, 4.6], + [1486342800000, -1.1, 4.5], + [1486429200000, -5.5, -0.9], + [1486515600000, -8.6, -4.2], + [1486602000000, -8.3, -1.6], + [1486688400000, -4.0, 0.9], + [1486774800000, -5.3, -0.5], + [1486861200000, -5.8, 0.4], + [1486947600000, -4.4, 0.4], + [1487034000000, -4.3, 0.6], + [1487120400000, -4.6, 2.7], + [1487206800000, 1.6, 3.2], + [1487293200000, 2.3, 6.2], + [1487379600000, 2.4, 4.1], + [1487466000000, 2.3, 8.5], + [1487552400000, 3.8, 6.8], + [1487638800000, 2.4, 4.9], + [1487725200000, 0.4, 4.5], + [1487811600000, -5.9, 4.0], + [1487898000000, -5.9, 1.5], + [1487984400000, -6.2, -1.7], + [1488070800000, -4.5, 2.3], + [1488157200000, 0.9, 5.6], + [1488243600000, 1.6, 4.9], + [1488330000000, -0.2, 5.5], + [1488416400000, -1.8, 4.4], + [1488502800000, -1.9, 3.3], + [1488589200000, -4.4, 2.5], + [1488675600000, 0.7, 4.1], + [1488762000000, -6.0, 2.7], + [1488848400000, -8.1, -0.1], + [1488934800000, -10.2, -0.5], + [1489021200000, -2.3, 3.7], + [1489107600000, 0.3, 4.7], + [1489194000000, -1.9, 5.2], + [1489280400000, 1.1, 3.1], + [1489366800000, 2.3, 7.0], + [1489453200000, 3.1, 7.7], + [1489539600000, 2.8, 6.3], + [1489626000000, 1.9, 8.9], + [1489712400000, 0.8, 5.5], + [1489798800000, -1.6, 4.3], + [1489885200000, -1.9, 3.3], + [1489971600000, 0.5, 4.4], + [1490058000000, 1.7, 6.6], + [1490144400000, 0.4, 5.1], + [1490230800000, -1.2, 3.5], + [1490317200000, 0.9, 7.4], + [1490403600000, 3.3, 8.4], + [1490490000000, 4.6, 10.1], + [1490572800000, 3.9, 11.8], + [1490659200000, 1.3, 8.9], + [1490745600000, 1.4, 9.8], + [1490832000000, -0.1, 2.4], + [1490918400000, 1.9, 6.8], + [1491004800000, 4.3, 12.2], + [1491091200000, 5.4, 9.3], + [1491177600000, 2.5, 11.1], + [1491264000000, 5.6, 10.2], + [1491350400000, 3.7, 7.6], + [1491436800000, 3.3, 7.4], + [1491523200000, 6.9, 12.0], + [1491609600000, 5.8, 10.9], + [1491696000000, 7.5, 12.1], + [1491782400000, 2.3, 9.7], + [1491868800000, 2.1, 7.1], + [1491955200000, 0.4, 6.9], + [1492041600000, 3.0, 7.0], + [1492128000000, -1.2, 8.9], + [1492214400000, 0.4, 7.5], + [1492300800000, 0.9, 4.4], + [1492387200000, -0.9, 8.2], + [1492473600000, -1.7, 9.6], + [1492560000000, 2.2, 7.5], + [1492646400000, 3.9, 9.1], + [1492732800000, 2.3, 6.5], + [1492819200000, 2.0, 7.4], + [1492905600000, 1.6, 6.9], + [1492992000000, 1.4, 4.7], + [1493078400000, -0.9, 9.7], + [1493164800000, 1.8, 7.9], + [1493251200000, 0.5, 10.6], + [1493337600000, 0.8, 11.0], + [1493424000000, 4.3, 11.6], + [1493510400000, -1.6, 11.7], + [1493596800000, -0.6, 14.3], + [1493683200000, 1.1, 16.1], + [1493769600000, 1.7, 17.1], + [1493856000000, 3.1, 17.6], + [1493942400000, 5.3, 19.2], + [1494028800000, 5.6, 20.3], + [1494115200000, 5.4, 18.0], + [1494201600000, 2.9, 12.1], + [1494288000000, 0.9, 8.6], + [1494374400000, 1.2, 7.7], + [1494460800000, 3.3, 10.1], + [1494547200000, 2.7, 14.1], + [1494633600000, 2.5, 16.6], + [1494720000000, 9.0, 12.7], + [1494806400000, 8.2, 17.0], + [1494892800000, 9.0, 11.8], + [1494979200000, 9.3, 16.9], + [1495065600000, 10.2, 12.1], + [1495152000000, 9.9, 20.3], + [1495238400000, 9.0, 23.1], + [1495324800000, 9.9, 16.9], + [1495411200000, 7.2, 12.5], + [1495497600000, 3.3, 18.1], + [1495584000000, 8.3, 12.9], + [1495670400000, 8.5, 13.1], + [1495756800000, 10.5, 16.1], + [1495843200000, 10.3, 22.3], + [1495929600000, 9.0, 18.2], + [1496016000000, 12.3, 15.6], + [1496102400000, 12.5, 16.3], + [1496188800000, 9.9, 13.6], + [1496275200000, 3.7, 14.8], + [1496361600000, 8.6, 14.3], + [1496448000000, 10.0, 18.3], + [1496534400000, 9.7, 13.0], + [1496620800000, 9.0, 14.3], + [1496707200000, 8.3, 14.3], + [1496793600000, 11.1, 18.4], + [1496880000000, 10.7, 16.6], + [1496966400000, 7.2, 16.3], + [1497052800000, 11.5, 15.3], + [1497139200000, 11.1, 15.1], + [1497225600000, 10.4, 16.5], + [1497312000000, 11.3, 14.6], + [1497398400000, 9.1, 17.6], + [1497484800000, 9.9, 15.5], + [1497571200000, 12.3, 14.9], + [1497657600000, 10.5, 15.1], + [1497744000000, 11.4, 18.0], + [1497830400000, 9.9, 14.8], + [1497916800000, 8.1, 12.4], + [1498003200000, 8.6, 15.5], + [1498089600000, 9.4, 13.0], + [1498176000000, 11.2, 13.0], + [1498262400000, 9.0, 15.3], + [1498348800000, 7.7, 13.6], + [1498435200000, 10.3, 13.6], + [1498521600000, 6.3, 18.0], + [1498608000000, 5.5, 21.7], + [1498694400000, 9.1, 23.2], + [1498780800000, 12.6, 25.4], + [1498867200000, 10.6, 19.6], + [1498953600000, 11.3, 14.5], + [1499040000000, 9.2, 16.1], + [1499126400000, 8.7, 17.9], + [1499212800000, 6.9, 19.5], + [1499299200000, 6.6, 20.0], + [1499385600000, 9.7, 15.7], + [1499472000000, 11.9, 14.2], + [1499558400000, 8.7, 14.2], + [1499644800000, 9.7, 18.4], + [1499731200000, 10.3, 16.8], + [1499817600000, 10.9, 15.5], + [1499904000000, 10.2, 16.8], + [1499990400000, 6.0, 18.9], + [1500076800000, 9.1, 19.1], + [1500163200000, 11.6, 15.7], + [1500249600000, 8.8, 12.4], + [1500336000000, 9.2, 16.3], + [1500422400000, 6.2, 23.0], + [1500508800000, 8.2, 26.2], + [1500595200000, 11.7, 20.3], + [1500681600000, 13.6, 24.1], + [1500768000000, 13.8, 25.3], + [1500854400000, 13.7, 18.6], + [1500940800000, 12.9, 20.8], + [1501027200000, 12.9, 20.5], + [1501113600000, 13.6, 18.2], + [1501200000000, 12.3, 17.5], + [1501286400000, 10.4, 17.0], + [1501372800000, 12.6, 19.0], + [1501459200000, 12.3, 18.1], + [1501545600000, 11.3, 14.4], + [1501632000000, 11.3, 17.2], + [1501718400000, 10.7, 21.4], + [1501804800000, 12.1, 15.2], + [1501891200000, 12.3, 17.0], + [1501977600000, 12.4, 18.7], + [1502064000000, 11.7, 16.3], + [1502150400000, 10.0, 15.2], + [1502236800000, 8.1, 19.3], + [1502323200000, 11.9, 17.5], + [1502409600000, 15.3, 17.6], + [1502496000000, 12.7, 15.8], + [1502582400000, 9.4, 14.8], + [1502668800000, 9.3, 15.2], + [1502755200000, 10.6, 13.6], + [1502841600000, 10.8, 15.6], + [1502928000000, 10.2, 15.8], + [1503014400000, 12.3, 18.2], + [1503100800000, 11.7, 18.0], + [1503187200000, 11.5, 16.9], + [1503273600000, 11.5, 17.7], + [1503360000000, 7.6, 18.5], + [1503446400000, 6.7, 19.8], + [1503532800000, 8.3, 17.2], + [1503619200000, 7.7, 20.5], + [1503705600000, 8.1, 19.4], + [1503792000000, 6.7, 17.9], + [1503878400000, 12.6, 15.8], + [1503964800000, 9.0, 16.5], + [1504051200000, 10.3, 16.7], + [1504137600000, 9.2, 17.3], + [1504224000000, 10.3, 14.8], + [1504310400000, 5.3, 17.6], + [1504396800000, 5.2, 16.7], + [1504483200000, 7.1, 19.2], + [1504569600000, 10.3, 14.0], + [1504656000000, 11.9, 15.1], + [1504742400000, 11.9, 14.2], + [1504828800000, 9.6, 15.9], + [1504915200000, 9.1, 14.0], + [1505001600000, 8.3, 13.7], + [1505088000000, 6.8, 15.0], + [1505174400000, 6.8, 14.3], + [1505260800000, 7.1, 15.5], + [1505347200000, 5.1, 15.3], + [1505433600000, 6.7, 16.8], + [1505520000000, 4.0, 16.1], + [1505606400000, 3.5, 15.8], + [1505692800000, 8.1, 12.7], + [1505779200000, 10.4, 13.4], + [1505865600000, 7.4, 11.8], + [1505952000000, 4.6, 11.6], + [1506038400000, 9.4, 13.3], + [1506124800000, 7.2, 16.0], + [1506211200000, 6.2, 13.7], + [1506297600000, 6.7, 19.5], + [1506384000000, 7.8, 17.1], + [1506470400000, 10.3, 16.9], + [1506556800000, 11.9, 18.2], + [1506643200000, 10.0, 18.9], + [1506729600000, 9.8, 13.5], + [1506816000000, 9.1, 16.0], + [1506902400000, 9.8, 15.9], + [1506988800000, 8.6, 9.7], + [1507075200000, 6.3, 11.2], + [1507161600000, 6.0, 9.5], + [1507248000000, 8.3, 11.9], + [1507334400000, 5.6, 10.0], + [1507420800000, 0.7, 10.0], + [1507507200000, -0.8, 8.9], + [1507593600000, 1.2, 7.7], + [1507680000000, 5.3, 7.1], + [1507766400000, 5.8, 9.6], + [1507852800000, 4.8, 8.1], + [1507939200000, 7.5, 12.7], + [1508025600000, 7.9, 14.7], + [1508112000000, 7.6, 15.7], + [1508198400000, 6.2, 8.0], + [1508284800000, 0.5, 6.6], + [1508371200000, -1.3, 5.6], + [1508457600000, 3.8, 10.9], + [1508544000000, 6.8, 11.2], + [1508630400000, 7.2, 11.0], + [1508716800000, 3.4, 9.8], + [1508803200000, 4.8, 7.4], + [1508889600000, 4.9, 6.5], + [1508976000000, 2.6, 6.8], + [1509062400000, 4.1, 9.2], + [1509148800000, 3.5, 10.2], + [1509235200000, 4.0, 6.8], + [1509325200000, 3.2, 5.1], + [1509411600000, 3.1, 5.2], + [1509498000000, 5.2, 6.7], + [1509584400000, 1.4, 9.4], + [1509670800000, 1.3, 6.3], + [1509757200000, 4.3, 7.4], + [1509843600000, 4.8, 6.9], + [1509930000000, 0.9, 6.8], + [1510016400000, 2.9, 6.9], + [1510102800000, 4.6, 8.7], + [1510189200000, 3.9, 9.2], + [1510275600000, 1.2, 4.7], + [1510362000000, 1.0, 3.9], + [1510448400000, 2.9, 6.5], + [1510534800000, -0.6, 5.7], + [1510621200000, 0.1, 1.8], + [1510707600000, 1.8, 5.2], + [1510794000000, 1.7, 5.2], + [1510880400000, 2.7, 6.8], + [1510966800000, 1.1, 5.2], + [1511053200000, -0.1, 3.6], + [1511139600000, 0.9, 2.7], + [1511226000000, -4.1, 0.8], + [1511312400000, -3.8, -0.3], + [1511398800000, -1.0, 5.4], + [1511485200000, 2.4, 4.5], + [1511571600000, 1.3, 3.9], + [1511658000000, -3.3, 2.8], + [1511744400000, -3.9, -0.7], + [1511830800000, -5.3, -0.6], + [1511917200000, -8.4, -5.1], + [1512003600000, -8.7, -5.4], + [1512090000000, -8.2, -2.8], + [1512176400000, -2.6, 3.8], + [1512262800000, 1.8, 4.3], + [1512349200000, 0.9, 3.8], + [1512435600000, 0.8, 5.9], + [1512522000000, 1.5, 3.0], + [1512608400000, 2.5, 7.3], + [1512694800000, 2.0, 4.9], + [1512781200000, -0.8, 2.6], + [1512867600000, -1.4, 0.2], + [1512954000000, -2.3, -0.9], + [1513040400000, -9.0, -2.2], + [1513126800000, -8.8, -2.6], + [1513213200000, -7.0, -3.9], + [1513299600000, -8.1, -5.5], + [1513386000000, -7.5, -3.4], + [1513472400000, -8.1, -1.8], + [1513558800000, -1.5, 1.9], + [1513645200000, 1.3, 2.2], + [1513731600000, 2.1, 6.0], + [1513818000000, 2.2, 7.1], + [1513904400000, 2.7, 5.9], + [1513990800000, 2.5, 8.6], + [1514077200000, 1.8, 5.6], + [1514163600000, 1.0, 3.1], + [1514250000000, 1.3, 1.6], + [1514336400000, 0.8, 1.3], + [1514422800000, -3.3, -1.4], + [1514509200000, -1.5, -0.2], + [1514595600000, -2.7, -1.0], + [1514682000000, -2.8, 0.3] +]} \ 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 new file mode 100644 index 00000000000..41448b70747 --- /dev/null +++ b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/empty.svg @@ -0,0 +1,2 @@ + +No data to displayHighcharts.com 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 new file mode 100644 index 00000000000..95afa9a4e4e --- /dev/null +++ b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/enabled-functions.svg @@ -0,0 +1 @@ +Highcharts.com \ 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 new file mode 100644 index 00000000000..a881782fd98 --- /dev/null +++ b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/lumo-dark.svg @@ -0,0 +1,2 @@ + + 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 new file mode 100644 index 00000000000..25dfe80026e --- /dev/null +++ b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/pie.svg @@ -0,0 +1,2 @@ + +Highcharts.com 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 new file mode 100644 index 00000000000..b506e5ce330 --- /dev/null +++ b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/src/test/resources/timeline.svg @@ -0,0 +1,2 @@ + +1m3m6mYTD1yAllHighcharts.com \ 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 new file mode 100644 index 00000000000..ce3f5ed36f6 --- /dev/null +++ b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/test/index.test.js @@ -0,0 +1,145 @@ +const { expect } = require('chai') +const { JSDOM } = require('jsdom'); +const mock = require('mock-fs') + +const jsdomExporter = require('../jsdom-exporter.js') + +/** + * + * @param {string} svgString + * @returns Document + */ +function parseSVG(svgString) { + const dom = new JSDOM(`${svgString}`); + return dom.window.document +} + +describe('jsdom-exporter', () => { + + beforeEach(() => mock()); + + afterEach(() => mock.restore()); + + it('should render based on Highchart options', async () => { + const result = await jsdomExporter({ chartConfiguration: { title: { text: 'TITLE' } } }); + const document = parseSVG(result.svgString); + + expect(document.querySelector('svg')).to.be.not.null; + expect(document.querySelector('.highcharts-title').textContent).to.be.equal('TITLE'); + }); + + it('should use default filename to write svg file', async () => { + const result = await jsdomExporter({ chartConfiguration: {} }); + + expect(result.outFile).to.contain('chart.svg'); + }); + + it('should accept outfile name to write svg file', async () => { + const result = await jsdomExporter({ chartConfiguration: {}, outFile: 'custom-file.svg' }); + + expect(result.outFile).to.contain('custom-file.svg'); + }); + + it('should accept width/height as exporting options', async () => { + const configuration = { chartConfiguration: {}, exportOptions: { width: 100, height: 100 } }; + const result = await jsdomExporter(configuration); + + const document = parseSVG(result.svgString); + const svgElement = document.querySelector('svg'); + + expect(svgElement.getAttribute('width')).to.be.equal('100'); + expect(svgElement.getAttribute('height')).to.be.equal('100'); + }); + + it('should accept theme as exporting options', async () => { + const configuration = { + chartConfiguration: {}, exportOptions: { + theme: { + chart: { + backgroundColor: "red" + } + } + } + }; + const result = await jsdomExporter(configuration); + const document = parseSVG(result.svgString); + + const backgroundColor = document.querySelector('.highcharts-background').getAttribute('fill'); + expect(backgroundColor).to.be.equal('red'); + }); + + it('should accept lang as exporting options', async () => { + const configuration = { chartConfiguration: {}, exportOptions: { lang: { noData: 'custom message' } } }; + const result = await jsdomExporter(configuration); + const document = parseSVG(result.svgString); + + expect(document.querySelector('.highcharts-no-data').textContent).to.be.equal('custom message'); + }); + + it('should not inflate functions if "executeFunctions" is not enabled', async () => { + const result = await jsdomExporter({ + chartConfiguration: { + xAxis: { + min: 0, + max: 360, + labels: { + _fn_formatter: `function () { return this.value + 'CUSTOM_LABEL'; }` + }, + tickInterval: 45 + }, + series: [1] + } + }); + const document = parseSVG(result.svgString); + expect(document.querySelector('.highcharts-xaxis-labels text').textContent).to.not.contain('CUSTOM_LABEL'); + }); + + it('should inflate functions if "executeFunctions" is enabled', async () => { + const result = await jsdomExporter({ + chartConfiguration: { + xAxis: { + min: 0, + max: 360, + labels: { + _fn_formatter: `function () { return this.value + 'CUSTOM_LABEL'; }` + }, + tickInterval: 45 + }, + series: [1] + }, exportOptions: { executeFunctions: true } + }); + const document = parseSVG(result.svgString); + expect(document.querySelector('.highcharts-xaxis-labels text').textContent).to.contain('CUSTOM_LABEL'); + }); + + it('should inflate js expression if "executeFunctions" is enabled', async () => { + const result = await jsdomExporter({ + chartConfiguration: { + xAxis: { + min: 0, + max: 360, + labels: { + _fn_formatter: `this.value + 'CUSTOM_LABEL'` + }, + tickInterval: 45 + }, + series: [1] + }, exportOptions: { executeFunctions: true } + }); + const document = parseSVG(result.svgString); + expect(document.querySelector('.highcharts-xaxis-labels text').textContent).to.contain('CUSTOM_LABEL'); + }); +}); + +describe('timeline', () => { + beforeEach(() => mock()); + + afterEach(() => mock.restore()); + + it('should render stock chart if timeline is set to `true`', async () => { + const result = await jsdomExporter({ chartConfiguration: {}, exportOptions: { timeline: true } }); + const document = parseSVG(result.svgString); + + expect(document.querySelector('.highcharts-navigator')).to.be.not.null; + }); +}); \ No newline at end of file diff --git a/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/webpack.config.js b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/webpack.config.js new file mode 100644 index 00000000000..99a2770fb1d --- /dev/null +++ b/vaadin-charts-flow-parent/vaadin-charts-flow-svg-generator/webpack.config.js @@ -0,0 +1,30 @@ +const path = require('path'); +const { IgnorePlugin } = require('webpack'); +const TerserPlugin = require('terser-webpack-plugin'); +/** @type {import('webpack').Configuration} */ +module.exports = { + entry: './jsdom-exporter.js', + target: 'node', + mode: 'production', + output: { + library: { + type: 'commonjs2' + }, + filename: 'jsdom-exporter-bundle.js', + path: path.resolve(__dirname, 'src/main/resources/META-INF/frontend/generated/') + }, + optimization: { + minimize: true, + minimizer: [ + new TerserPlugin({ + extractComments: false + }) + ] + }, + + plugins: [ + new IgnorePlugin({ resourceRegExp: /canvas/ }), + new IgnorePlugin({ resourceRegExp: /utf-8-validate/ }), + new IgnorePlugin({ resourceRegExp: /bufferutil/ }), + ] +} \ No newline at end of file