├── .gitignore ├── .npmignore ├── .prettierignore ├── .travis.yml ├── LICENSE ├── README.md ├── babel.config.js ├── examples ├── browser.html ├── custom_chart_example.js ├── image_fill.js ├── short_url_example.js ├── signed_url.js ├── simple_example.js └── to_file_example.js ├── package.json ├── src └── index.ts ├── test ├── index.test.ts └── setupJest.js ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | venv 2 | build 3 | 4 | ### Vim ### 5 | [._]*.s[a-w][a-z] 6 | [._]s[a-w][a-z] 7 | *.un~ 8 | Session.vim 9 | .netrwhist 10 | *~ 11 | 12 | 13 | ### OSX ### 14 | .DS_Store 15 | .AppleDouble 16 | .LSOverride 17 | 18 | # Icon must end with two \r 19 | Icon 20 | 21 | 22 | # Thumbnails 23 | ._* 24 | 25 | # Files that might appear in the root of a volume 26 | .DocumentRevisions-V100 27 | .fseventsd 28 | .Spotlight-V100 29 | .TemporaryItems 30 | .Trashes 31 | .VolumeIcon.icns 32 | 33 | # Directories potentially created on remote AFP share 34 | .AppleDB 35 | .AppleDesktop 36 | Network Trash Folder 37 | Temporary Items 38 | .apdisk 39 | 40 | 41 | ### Python ### 42 | # Byte-compiled / optimized / DLL files 43 | __pycache__/ 44 | *.py[cod] 45 | *$py.class 46 | 47 | # C extensions 48 | *.so 49 | 50 | # Distribution / packaging 51 | .Python 52 | env/ 53 | build/ 54 | develop-eggs/ 55 | dist/ 56 | downloads/ 57 | eggs/ 58 | .eggs/ 59 | lib/ 60 | lib64/ 61 | parts/ 62 | sdist/ 63 | var/ 64 | *.egg-info/ 65 | .installed.cfg 66 | *.egg 67 | 68 | # PyInstaller 69 | # Usually these files are written by a python script from a template 70 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 71 | *.manifest 72 | *.spec 73 | 74 | # Installer logs 75 | pip-log.txt 76 | pip-delete-this-directory.txt 77 | 78 | # Unit test / coverage reports 79 | htmlcov/ 80 | .tox/ 81 | .coverage 82 | .coverage.* 83 | .cache 84 | nosetests.xml 85 | coverage.xml 86 | *,cover 87 | 88 | # Translations 89 | *.mo 90 | *.pot 91 | 92 | # Django stuff: 93 | *.log 94 | 95 | # Sphinx documentation 96 | docs/_build/ 97 | 98 | # PyBuilder 99 | target/ 100 | 101 | 102 | ### Node ### 103 | # Logs 104 | logs 105 | *.log 106 | npm-debug.log* 107 | 108 | # Runtime data 109 | pids 110 | *.pid 111 | *.seed 112 | 113 | # Directory for instrumented libs generated by jscoverage/JSCover 114 | lib-cov 115 | 116 | # Coverage directory used by tools like istanbul 117 | coverage 118 | 119 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 120 | .grunt 121 | 122 | # node-waf configuration 123 | .lock-wscript 124 | 125 | # Compiled binary addons (http://nodejs.org/api/addons.html) 126 | build/Release 127 | 128 | # Dependency directory 129 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 130 | node_modules 131 | 132 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | test/ 3 | examples/ -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 10 4 | cache: yarn 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Ian Webster 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | QuickChart for Javascript 2 | --- 3 | [![npm](https://img.shields.io/npm/v/quickchart-js)](https://www.npmjs.com/package/quickchart-js) 4 | [![npm](https://img.shields.io/npm/dt/quickchart-js)](https://www.npmjs.com/package/quickchart-js) 5 | [![Build Status](https://travis-ci.com/typpo/quickchart-js.svg?branch=master)](https://travis-ci.com/typpo/quickchart-js) 6 | 7 | This is a Javascript client for [quickchart.io](https://quickchart.io), a web service for generating static charts. View the main QuickChart repository [here](https://github.com/typpo/quickchart). 8 | 9 | # Installation 10 | 11 | If you are using npm: 12 | 13 | ``` 14 | npm install quickchart-js 15 | ``` 16 | 17 | # Usage 18 | 19 | This library provides a **QuickChart** object. Import it, instantiate it, and set your [Chart.js](https://www.chartjs.org) config: 20 | 21 | ```js 22 | const QuickChart = require('quickchart-js'); 23 | 24 | const myChart = new QuickChart(); 25 | myChart.setConfig({ 26 | type: 'bar', 27 | data: { labels: ['Hello world', 'Foo bar'], datasets: [{ label: 'Foo', data: [1, 2] }] }, 28 | }); 29 | ``` 30 | 31 | Use `getUrl()` on your quickchart object to get the encoded URL that renders your chart: 32 | 33 | ```js 34 | console.log(myChart.getUrl()); 35 | // Prints: https://quickchart.io/chart?c=%7Btype%3A%27bar%27%2Cdata%3A%7Blabels%3A%5B%27Hello+world%27%2C%27Foo+bar%27%5D%2Cdatasets%3A%5B%7Blabel%3A%27Foo%27%2Cdata%3A%5B1%2C2%5D%7D%5D%7D%7D&w=500&h=300&bkg=transparent&f=png 36 | ``` 37 | 38 | If you have a large or complicated chart, use `getShortUrl()` on your quickchart object to get a fixed-length URL using the quickchart.io web service: 39 | ```js 40 | const url = await myChart.getShortUrl(); 41 | console.log(url); 42 | // Prints: https://quickchart.io/chart/render/f-a1d3e804-dfea-442c-88b0-9801b9808401 43 | ``` 44 | 45 | Or write it to disk: 46 | ```js 47 | myChart.toFile('/tmp/mychart.png'); 48 | ``` 49 | 50 | The URLs produce this chart image: 51 | 52 | 53 | 54 | ## Creating a QuickChart object 55 | 56 | If you have an account ID and API key, authenticate using the QuickChart constructor: 57 | 58 | ```js 59 | const qc = new QuickChart(apiKey, accountId); 60 | ``` 61 | 62 | To use the free (community) version, leave it blank: 63 | 64 | ```js 65 | const qc = new QuickChart(); 66 | ``` 67 | 68 | ## Customizing your chart 69 | 70 | ### setConfig(chart: Object | string) 71 | 72 | Use this config to customize the Chart.js config object that defines your chart. You must set this before generating a URL! 73 | 74 | ### setWidth(width: int) 75 | 76 | Sets the width of the chart in pixels. Defaults to 500. 77 | 78 | ### setHeight(height: int) 79 | 80 | Sets the height of the chart in pixels. Defaults to 300. 81 | 82 | ### setFormat(format: string) 83 | 84 | Sets the format of the chart. Defaults to `png`. `svg` is also valid. 85 | 86 | ### setBackgroundColor(color: string) 87 | 88 | Sets the background color of the chart. Any valid HTML color works. Defaults to `#ffffff` (white). Also takes `rgb`, `rgba`, and `hsl` values. 89 | 90 | ### setDevicePixelRatio(ratio: float) 91 | 92 | Sets the device pixel ratio of the chart. This will multiply the number of pixels by the value. This is usually used for retina displays. Defaults to 1.0. 93 | 94 | ### setVersion(version: string) 95 | 96 | Sets the Chart.js version to use (e.g. `2.9.4` or `3.4.0`). Valid options are shown in the [documentation](https://quickchart.io/documentation/#parameters). 97 | 98 | ### setHost(host: string) 99 | 100 | Sets the host of generated URLs. `quickchart.io` by default. 101 | 102 | ### setScheme(scheme: string) 103 | 104 | Sets the scheme of generated URLs. `https` by default. 105 | 106 | ## Getting outputs 107 | 108 | There are two ways to get a URL for your chart object. 109 | 110 | ### getUrl(): string 111 | 112 | Returns a URL that will display the chart image when loaded. 113 | 114 | ### getShortUrl(): Promise 115 | 116 | Uses the quickchart.io web service to create a fixed-length chart URL that displays the chart image. The Promise resolves with a URL such as `https://quickchart.io/chart/render/f-a1d3e804-dfea-442c-88b0-9801b9808401`. 117 | 118 | Note that short URLs expire after a few days for users of the free service. You can [subscribe](https://quickchart.io/pricing/) to keep them around longer. 119 | 120 | ### getSignedUrl(): string 121 | 122 | Returns a URL that displays the chart image. It is signed with your user account to bypass rate limitations. 123 | 124 | ### toBinary(): Promise 125 | 126 | Creates a binary buffer that contains your chart image. 127 | 128 | ### toDataUrl(): Promise 129 | 130 | Returns a base 64 data URL beginning with `data:image/png;base64`. 131 | 132 | ### toFile(pathOrDescriptor: PathLike | FileHandle): Promise 133 | 134 | Given a filepath string or a writable file handle, creates a file containing your chart image. 135 | 136 | ## More examples 137 | 138 | Check out the `examples/` directory to see other usage. Here's a simple test that uses some of the custom parameters: 139 | 140 | ```js 141 | const qc = new QuickChart(); 142 | 143 | qc.setConfig({ 144 | type: 'bar', 145 | data: { labels: ['Hello world', 'Foo bar'], datasets: [{ label: 'Foo', data: [1, 2] }] }, 146 | }); 147 | qc.setWidth(500).setHeight(300).setBackgroundColor('transparent'); 148 | 149 | console.log(qc.getUrl()); 150 | // https://quickchart.io/chart?c=%7Btype%3A%27bar%27%2Cdata%3A%7Blabels%3A%5B%27Hello+world%27%2C%27Foo+bar%27%5D%2Cdatasets%3A%5B%7Blabel%3A%27Foo%27%2Cdata%3A%5B1%2C2%5D%7D%5D%7D%7D&w=500&h=300&bkg=transparent&f=png 151 | ``` 152 | 153 | Here's a more complicated chart that includes some Javascript: 154 | 155 | ```js 156 | qc.setConfig({ 157 | type: 'bar', 158 | data: { 159 | labels: ['January', 'February', 'March', 'April', 'May'], 160 | datasets: [ 161 | { 162 | label: 'Dogs', 163 | data: [50, 60, 70, 180, 190], 164 | }, 165 | ], 166 | }, 167 | options: { 168 | scales: { 169 | yAxes: [ 170 | { 171 | ticks: { 172 | callback: function (value) { 173 | return '$' + value; 174 | }, 175 | }, 176 | }, 177 | ], 178 | }, 179 | }, 180 | }); 181 | qc.setWidth(500).setHeight(300).setBackgroundColor('#0febc2'); 182 | 183 | console.log(qc.getUrl()); 184 | // https://quickchart.io/chart?c=%7Btype%3A%27bar%27%2Cdata%3A%7Blabels%3A%5B%27January%27%2C%27February%27%2C%27March%27%2C%27April%27%2C%27May%27%5D%2Cdatasets%3A%5B%7Blabel%3A%27Dogs%27%2Cdata%3A%5B50%2C60%2C70%2C180%2C190%5D%7D%5D%7D%2Coptions%3A%7Bscales%3A%7ByAxes%3A%5B%7Bticks%3A%7Bcallback%3Afunction+%28value%29+%7B%0A++return+%27%24%27+%2B+value%3B%0A%7D%7D%7D%5D%7D%7D%7D&w=500&h=300&bkg=%230febc2&f=png 185 | ``` 186 | 187 | As we customize these charts, the URLs are getting a little long for my liking. There's a `getShortUrl` function that uses the QuickChart.io web service to generate a short(er), fixed-length URL: 188 | 189 | ```js 190 | // Fill the chart with data from 0 to 100 191 | const data = [...Array(100).keys()]; 192 | qc.setConfig({ 193 | type: 'bar', 194 | data: { labels: ['Hello world', 'Foo bar'], datasets: [{ label: 'Foo', data }] }, 195 | }); 196 | 197 | async function printShortUrl() { 198 | const url = await qc.getShortUrl(); 199 | console.log(url); 200 | } 201 | printShortUrl(); 202 | // https://quickchart.io/chart/render/f-a1d3e804-dfea-442c-88b0-9801b9808401 203 | ``` 204 | 205 | ## Using built-in QuickChart functions 206 | 207 | QuickChart has builtin functions: `getImageFill`, `getGradientFill`, `getGradientFillHelper`, and `pattern.draw`. These functions can be accessed via the `QuickChart` class. For example: 208 | 209 | ```js 210 | const qc = new QuickChart(); 211 | qc.setConfig({ 212 | type: 'bar', 213 | data: { 214 | labels: ['Hello world', 'Foo bar'], 215 | datasets: [ 216 | { 217 | label: 'Foo', 218 | data: [1, 2], 219 | backgroundColor: QuickChart.getGradientFillHelper('horizontal', ['red', 'green']), 220 | }, 221 | ], 222 | }, 223 | }); 224 | ``` 225 | 226 | # Building the library 227 | 228 | To build this library locally, run: 229 | 230 | ``` 231 | yarn build 232 | ``` 233 | 234 | To run tests: 235 | 236 | ``` 237 | yarn test 238 | ``` 239 | 240 | If you're editing the library and running examples, you may want to continuously build the library in the background: 241 | 242 | ``` 243 | yarn build:watch 244 | 245 | # ... 246 | 247 | node examples/simple_example.js 248 | ``` 249 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'], 3 | }; 4 | -------------------------------------------------------------------------------- /examples/browser.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 18 | 19 | -------------------------------------------------------------------------------- /examples/custom_chart_example.js: -------------------------------------------------------------------------------- 1 | const QuickChart = require('../build/quickchart.cjs'); 2 | 3 | const qc = new QuickChart(); 4 | 5 | qc.setConfig({ 6 | type: 'bar', 7 | data: { 8 | labels: ['January', 'February', 'March', 'April', 'May'], 9 | datasets: [ 10 | { 11 | label: 'Dogs', 12 | data: [50, 60, 70, 180, 190], 13 | }, 14 | ], 15 | }, 16 | options: { 17 | scales: { 18 | yAxes: [ 19 | { 20 | ticks: { 21 | callback: function (value) { 22 | return '$' + value; 23 | }, 24 | }, 25 | }, 26 | ], 27 | }, 28 | }, 29 | }); 30 | qc.setWidth(500).setHeight(300).setBackgroundColor('#0febc2'); 31 | 32 | console.log(qc.getUrl()); 33 | // https://quickchart.io/chart?c=%7Btype%3A%27bar%27%2Cdata%3A%7Blabels%3A%5B%27January%27%2C%27February%27%2C%27March%27%2C%27April%27%2C%27May%27%5D%2Cdatasets%3A%5B%7Blabel%3A%27Dogs%27%2Cdata%3A%5B50%2C60%2C70%2C180%2C190%5D%7D%5D%7D%2Coptions%3A%7Bscales%3A%7ByAxes%3A%5B%7Bticks%3A%7Bcallback%3Afunction+%28value%29+%7B%0A++return+%27%24%27+%2B+value%3B%0A%7D%7D%7D%5D%7D%7D%7D&w=500&h=300&bkg=%230febc2&f=png 34 | -------------------------------------------------------------------------------- /examples/image_fill.js: -------------------------------------------------------------------------------- 1 | const QuickChart = require('../build/quickchart.cjs'); 2 | 3 | const config = { 4 | type: 'radar', 5 | data: { 6 | labels: ['A', 'B', 'C'], 7 | datasets: [ 8 | { 9 | backgroundColor: QuickChart.getImageFill( 10 | 'https://cdn.pixabay.com/photo/2017/08/30/01/05/milky-way-2695569__340.jpg', 11 | ), 12 | borderColor: 'green', 13 | borderWidth: 1, 14 | pointRadius: 0, 15 | data: [1, 2, 3], 16 | }, 17 | ], 18 | }, 19 | options: { 20 | legend: { 21 | display: false, 22 | }, 23 | scale: { 24 | ticks: { 25 | beginAtZero: true, 26 | }, 27 | angleLines: { 28 | display: false, 29 | }, 30 | pointLabels: { 31 | display: false, 32 | }, 33 | }, 34 | }, 35 | }; 36 | 37 | const myChart = new QuickChart(); 38 | myChart.setConfig(config); 39 | 40 | myChart.toFile('/tmp/chart.png'); 41 | -------------------------------------------------------------------------------- /examples/short_url_example.js: -------------------------------------------------------------------------------- 1 | const QuickChart = require('../build/quickchart.cjs'); 2 | 3 | const qc = new QuickChart(); 4 | 5 | // Fill the chart with data from 0 to 100 6 | const data = [...Array(100).keys()]; 7 | qc.setConfig({ 8 | type: 'bar', 9 | data: { labels: ['Hello world', 'Foo bar'], datasets: [{ label: 'Foo', data }] }, 10 | }); 11 | 12 | // Print the regular URL... 13 | console.log(qc.getUrl()); 14 | // https://quickchart.io/chart?c=%7Btype%3A%27bar%27%2Cdata%3A%7Blabels%3A%5B%27Hello+world%27%2C%27Foo+bar%27%5D%2Cdatasets%3A%5B%7Blabel%3A%27Foo%27%2Cdata%3A%5B0%2C1%2C2%2C3%2C4%2C5%2C6%2C7%2C8%2C9%2C10%2C11%2C12%2C13%2C14%2C15%2C16%2C17%2C18%2C19%2C20%2C21%2C22%2C23%2C24%2C25%2C26%2C27%2C28%2C29%2C30%2C31%2C32%2C33%2C34%2C35%2C36%2C37%2C38%2C39%2C40%2C41%2C42%2C43%2C44%2C45%2C46%2C47%2C48%2C49%2C50%2C51%2C52%2C53%2C54%2C55%2C56%2C57%2C58%2C59%2C60%2C61%2C62%2C63%2C64%2C65%2C66%2C67%2C68%2C69%2C70%2C71%2C72%2C73%2C74%2C75%2C76%2C77%2C78%2C79%2C80%2C81%2C82%2C83%2C84%2C85%2C86%2C87%2C88%2C89%2C90%2C91%2C92%2C93%2C94%2C95%2C96%2C97%2C98%2C99%5D%7D%5D%7D%7D&w=500&h=300&bkg=%23ffffff&f=png 15 | 16 | // That's a long URL! Maybe we want a shorter version (requires an HTTP request to QuickChart.io) 17 | async function printShortUrl() { 18 | const url = await qc.getShortUrl(); 19 | console.log(url); 20 | } 21 | printShortUrl(); 22 | // https://quickchart.io/chart/render/f-a1d3e804-dfea-442c-88b0-9801b9808401 23 | // Much shorter and more manageable :) 24 | -------------------------------------------------------------------------------- /examples/signed_url.js: -------------------------------------------------------------------------------- 1 | const QuickChart = require('../build/quickchart.cjs'); 2 | 3 | const qc = new QuickChart('abc123', '12345'); 4 | 5 | qc.setConfig({ 6 | type: 'bar', 7 | data: { labels: ['Hello world', 'Foo bar'], datasets: [{ label: 'Foo', data: [1, 2] }] }, 8 | }); 9 | qc.setWidth(500).setHeight(300).setBackgroundColor('transparent'); 10 | 11 | console.log(qc.getSignedUrl()); 12 | // https://quickchart.io/chart?c=%7Btype%3A%27bar%27%2Cdata%3A%7Blabels%3A%5B%27Hello+world%27%2C%27Foo+bar%27%5D%2Cdatasets%3A%5B%7Blabel%3A%27Foo%27%2Cdata%3A%5B1%2C2%5D%7D%5D%7D%7D&w=500&h=300&bkg=transparent&f=png&v=2.9.4&sig=0c4cf0eb43b200bf523407f403412932cdeccb8d2a9317731ff18c7b887b5c78&accountId=12345 13 | -------------------------------------------------------------------------------- /examples/simple_example.js: -------------------------------------------------------------------------------- 1 | const QuickChart = require('../build/quickchart.cjs'); 2 | 3 | const qc = new QuickChart(); 4 | 5 | qc.setConfig({ 6 | type: 'bar', 7 | data: { labels: ['Hello world', 'Foo bar'], datasets: [{ label: 'Foo', data: [1, 2] }] }, 8 | }); 9 | qc.setWidth(500).setHeight(300).setBackgroundColor('transparent'); 10 | 11 | console.log(qc.getUrl()); 12 | // https://quickchart.io/chart?c=%7Btype%3A%27bar%27%2Cdata%3A%7Blabels%3A%5B%27Hello+world%27%2C%27Foo+bar%27%5D%2Cdatasets%3A%5B%7Blabel%3A%27Foo%27%2Cdata%3A%5B1%2C2%5D%7D%5D%7D%7D&w=500&h=300&bkg=transparent&f=png 13 | -------------------------------------------------------------------------------- /examples/to_file_example.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | const QuickChart = require('../build/quickchart.cjs'); 4 | 5 | const qc = new QuickChart(); 6 | 7 | qc.setConfig({ 8 | type: 'bar', 9 | data: { labels: ['Hello world', 'Foo bar'], datasets: [{ label: 'Foo', data: [1, 2] }] }, 10 | }); 11 | qc.setWidth(500).setHeight(300).setBackgroundColor('transparent'); 12 | 13 | async function saveChart() { 14 | // Write file to disk 15 | await qc.toFile('/tmp/chart.png'); 16 | } 17 | saveChart(); 18 | 19 | console.log('Written to /tmp/chart.png'); 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quickchart-js", 3 | "version": "3.1.3", 4 | "description": "Javascript client for QuickChart.io", 5 | "main": "build/quickchart.cjs.js", 6 | "module": "build/quickchart.mjs", 7 | "browser": "build/quickchart.js", 8 | "types": "build/typescript/index.d.ts", 9 | "exports": { 10 | ".": { 11 | "require": "./build/quickchart.cjs.js", 12 | "import": "./build/quickchart.mjs", 13 | "types": "./build/typescript/index.d.ts" 14 | } 15 | }, 16 | "repository": "https://github.com/typpo/quickchart-js", 17 | "author": "Ian Webster", 18 | "license": "MIT", 19 | "prepublish": "yarn build", 20 | "scripts": { 21 | "test": "jest --testPathIgnorePatterns=examples/", 22 | "format": "prettier --single-quote --trailing-comma all --print-width 100 --write \"**/*.{js,ts}\"", 23 | "build": "tsc && yarn build:browser && yarn build:esm && yarn build:cjs", 24 | "build:browser": "esbuild src/index.ts --bundle --sourcemap --external:fs --external:crypto --target=es2015 --global-name=QuickChart --tsconfig=tsconfig.json --footer:js=\"QuickChart = QuickChart.default\" --outfile=build/quickchart.js", 25 | "build:cjs": "esbuild src/index.ts --format=cjs --sourcemap --tree-shaking=true --platform=node --target=node10.4 --global-name=QuickChart --tsconfig=tsconfig.json --footer:js=\"module.exports = module.exports.default;\" --outfile=build/quickchart.cjs.js", 26 | "build:esm": "esbuild src/index.ts --sourcemap --tree-shaking=true --platform=node --target=node10.4 --global-name=QuickChart --tsconfig=tsconfig.json --format=esm --outfile=build/quickchart.mjs", 27 | "build:watch": "esbuild src/index.ts --watch --sourcemap --tree-shaking=true --platform=node --target=node10.4 --global-name=QuickChart --tsconfig=tsconfig.json --footer:js=\"module.exports = module.exports.default;\" --outfile=build/quickchart.cjs.js", 28 | "prepublishOnly": "yarn build", 29 | "publish": "yarn publish" 30 | }, 31 | "resolutions": { 32 | "moment": "^2.29.4" 33 | }, 34 | "dependencies": { 35 | "cross-fetch": "^3.1.5", 36 | "javascript-stringify": "^2.1.0" 37 | }, 38 | "devDependencies": { 39 | "@babel/preset-env": "^7.16.4", 40 | "@babel/preset-typescript": "^7.16.0", 41 | "@types/chart.js": "^2.9.37", 42 | "@types/jest": "^27.0.3", 43 | "esbuild": "^0.14.1", 44 | "jest": "^27.4.3", 45 | "jest-fetch-mock": "^3.0.3", 46 | "prettier": "^2.3.2", 47 | "typescript": "^4.5.2" 48 | }, 49 | "jest": { 50 | "automock": false, 51 | "resetMocks": false, 52 | "setupFiles": [ 53 | "./test/setupJest.js" 54 | ] 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'cross-fetch'; 2 | import { stringify } from 'javascript-stringify'; 3 | 4 | import type { PathLike } from 'fs'; 5 | import type { FileHandle } from 'fs/promises'; 6 | import type { ChartConfiguration } from 'chart.js'; 7 | import type { Response } from 'cross-fetch'; 8 | 9 | const SPECIAL_FUNCTION_REGEX: RegExp = /['"]__BEGINFUNCTION__(.*?)__ENDFUNCTION__['"]/g; 10 | 11 | const USER_AGENT = `quickchart-js/3.1.0`; 12 | 13 | interface PostData { 14 | chart: string; 15 | width?: number; 16 | height?: number; 17 | format?: string; 18 | version?: string; 19 | backgroundColor?: string; 20 | devicePixelRatio?: number; 21 | key?: string; 22 | } 23 | 24 | interface GradientFillOption { 25 | offset: number; 26 | color: string; 27 | } 28 | 29 | interface GradientDimensionOption { 30 | width?: number; 31 | height?: number; 32 | } 33 | 34 | function doStringify(chartConfig: ChartConfiguration): string | undefined { 35 | const str = stringify(chartConfig); 36 | if (!str) { 37 | return undefined; 38 | } 39 | return str.replace(SPECIAL_FUNCTION_REGEX, '$1'); 40 | } 41 | 42 | function postJson(url: string, payload: PostData): Promise { 43 | return fetch(url, { 44 | method: 'POST', 45 | headers: { 46 | 'User-Agent': USER_AGENT, 47 | 'Content-Type': 'application/json', 48 | }, 49 | body: JSON.stringify(payload), 50 | }); 51 | } 52 | 53 | class QuickChart { 54 | private host: string; 55 | private scheme: string; 56 | private width: number; 57 | private height: number; 58 | private devicePixelRatio: number; 59 | private backgroundColor: string; 60 | private format: string; 61 | private version: string; 62 | 63 | private chart?: string; 64 | private apiKey?: string; 65 | private accountId?: string; 66 | 67 | constructor(apiKey?: string, accountId?: string) { 68 | this.apiKey = apiKey; 69 | this.accountId = accountId; 70 | 71 | this.host = 'quickchart.io'; 72 | this.scheme = 'https'; 73 | 74 | this.chart = undefined; 75 | this.width = 500; 76 | this.height = 300; 77 | this.devicePixelRatio = 1.0; 78 | this.backgroundColor = '#ffffff'; 79 | this.format = 'png'; 80 | this.version = '2.9.4'; 81 | } 82 | 83 | setConfig(chartConfig: string | ChartConfiguration): QuickChart { 84 | this.chart = typeof chartConfig === 'string' ? chartConfig : doStringify(chartConfig); 85 | return this; 86 | } 87 | 88 | setHost(host: string): QuickChart { 89 | this.host = host; 90 | return this; 91 | } 92 | 93 | setScheme(scheme: string): QuickChart { 94 | this.scheme = scheme; 95 | return this; 96 | } 97 | 98 | setWidth(width: number): QuickChart { 99 | this.width = width; 100 | return this; 101 | } 102 | 103 | setHeight(height: number): QuickChart { 104 | this.height = height; 105 | return this; 106 | } 107 | 108 | setBackgroundColor(color: string): QuickChart { 109 | this.backgroundColor = color; 110 | return this; 111 | } 112 | 113 | setDevicePixelRatio(ratio: number): QuickChart { 114 | this.devicePixelRatio = ratio; 115 | return this; 116 | } 117 | 118 | setFormat(fmt: string): QuickChart { 119 | this.format = fmt; 120 | return this; 121 | } 122 | 123 | setVersion(version: string): QuickChart { 124 | this.version = version; 125 | return this; 126 | } 127 | 128 | isValid(): boolean { 129 | if (!this.chart) { 130 | return false; 131 | } 132 | return true; 133 | } 134 | 135 | private getBaseUrl(): string { 136 | return `${this.scheme}://${this.host}`; 137 | } 138 | 139 | private getUrlObject(): URL { 140 | if (!this.isValid()) { 141 | throw new Error('You must call setConfig before getUrl'); 142 | } 143 | const ret = new URL(`${this.getBaseUrl()}/chart`); 144 | ret.searchParams.append('c', this.chart!); 145 | ret.searchParams.append('w', String(this.width)); 146 | ret.searchParams.append('h', String(this.height)); 147 | ret.searchParams.append('ref', 'qc-js'); 148 | if (this.devicePixelRatio !== 1.0) { 149 | ret.searchParams.append('devicePixelRatio', String(this.devicePixelRatio)); 150 | } 151 | if (this.backgroundColor) { 152 | ret.searchParams.append('bkg', this.backgroundColor); 153 | } 154 | if (this.format) { 155 | ret.searchParams.append('f', this.format); 156 | } 157 | if (this.version) { 158 | ret.searchParams.append('v', this.version); 159 | } 160 | if (this.apiKey) { 161 | ret.searchParams.append('key', this.apiKey); 162 | } 163 | return ret; 164 | } 165 | 166 | getUrl(): string { 167 | return this.getUrlObject().href; 168 | } 169 | 170 | getSignedUrl(): string { 171 | if (!this.accountId || !this.apiKey) { 172 | throw new Error( 173 | 'You must set accountId and apiKey in the QuickChart constructor to use getSignedUrl()', 174 | ); 175 | } 176 | const crypto = require('crypto'); 177 | const urlObj = this.getUrlObject(); 178 | const chartStr = urlObj.searchParams.get('c'); 179 | 180 | const signature = crypto.createHmac('sha256', this.apiKey).update(chartStr).digest('hex'); 181 | urlObj.searchParams.append('sig', signature); 182 | urlObj.searchParams.append('accountId', this.accountId); 183 | urlObj.searchParams.delete('key'); 184 | return urlObj.href; 185 | } 186 | 187 | getPostData(): PostData { 188 | if (!this.isValid()) { 189 | throw new Error('You must call setConfig creating post data'); 190 | } 191 | 192 | const { width, height, chart, format, version, backgroundColor, devicePixelRatio, apiKey } = 193 | this; 194 | const postData: PostData = { 195 | width, 196 | height, 197 | chart: chart!, 198 | }; 199 | if (format) { 200 | postData.format = format; 201 | } 202 | if (version) { 203 | postData.version = version; 204 | } 205 | if (backgroundColor) { 206 | postData.backgroundColor = backgroundColor; 207 | } 208 | if (devicePixelRatio) { 209 | postData.devicePixelRatio = devicePixelRatio; 210 | } 211 | if (apiKey) { 212 | postData.key = apiKey; 213 | } 214 | return postData; 215 | } 216 | 217 | async getShortUrl(): Promise { 218 | if (!this.isValid()) { 219 | throw new Error('You must call setConfig before getUrl'); 220 | } 221 | if (this.host !== 'quickchart.io') { 222 | throw new Error('Short URLs must use quickchart.io host'); 223 | } 224 | 225 | const resp = await postJson(`${this.getBaseUrl()}/chart/create`, this.getPostData()); 226 | if (!resp.ok) { 227 | const quickchartError = resp.headers.get('x-quickchart-error'); 228 | const details = quickchartError ? `\n${quickchartError}` : ''; 229 | throw new Error(`Chart shorturl creation failed with status code ${resp.status}${details}`); 230 | } 231 | 232 | const json = (await resp.json()) as undefined | { success?: boolean; url?: string }; 233 | if (!json || !json.success || !json.url) { 234 | throw new Error('Received failure response from chart shorturl endpoint'); 235 | } else { 236 | return json.url; 237 | } 238 | } 239 | 240 | async toBinary(): Promise { 241 | if (!this.isValid()) { 242 | throw new Error('You must call setConfig before getUrl'); 243 | } 244 | 245 | const resp = await postJson(`${this.getBaseUrl()}/chart`, this.getPostData()); 246 | if (!resp.ok) { 247 | const quickchartError = resp.headers.get('x-quickchart-error'); 248 | const details = quickchartError ? `\n${quickchartError}` : ''; 249 | throw new Error(`Chart creation failed with status code ${resp.status}${details}`); 250 | } 251 | const data = await resp.arrayBuffer(); 252 | return Buffer.from(data); 253 | } 254 | 255 | async toDataUrl(): Promise { 256 | const buf = await this.toBinary(); 257 | const b64buf = buf.toString('base64'); 258 | const type = this.format === 'svg' ? 'svg+xml' : 'png'; 259 | return `data:image/${type};base64,${b64buf}`; 260 | } 261 | 262 | async toFile(pathOrDescriptor: PathLike | FileHandle): Promise { 263 | const fs = require('fs'); 264 | const buf = await this.toBinary(); 265 | fs.writeFileSync(pathOrDescriptor, buf); 266 | } 267 | 268 | static getGradientFillHelper( 269 | direction: string, 270 | colors: string[], 271 | dimensions?: GradientDimensionOption, 272 | ): string { 273 | return `__BEGINFUNCTION__getGradientFillHelper(${JSON.stringify(direction)}, ${JSON.stringify( 274 | colors, 275 | )}, ${JSON.stringify(dimensions)})__ENDFUNCTION__`; 276 | } 277 | 278 | static getGradientFill( 279 | colorOptions: GradientFillOption[], 280 | linearGradient: [number, number, number, number], 281 | ): string { 282 | return `__BEGINFUNCTION__getGradientFill(${JSON.stringify(colorOptions)}, ${JSON.stringify( 283 | linearGradient, 284 | )})__ENDFUNCTION__`; 285 | } 286 | 287 | static getImageFill(url: string): string { 288 | return `__BEGINFUNCTION__getImageFill(${JSON.stringify(url)})__ENDFUNCTION__`; 289 | } 290 | 291 | static pattern = { 292 | draw: function ( 293 | shapeType: string, 294 | backgroundColor: string, 295 | patternColor: string, 296 | requestedSize: number, 297 | ): string { 298 | return `__BEGINFUNCTION__pattern.draw(${JSON.stringify(shapeType)}, ${JSON.stringify( 299 | backgroundColor, 300 | )}, ${JSON.stringify(patternColor)}, ${JSON.stringify(requestedSize)})__ENDFUNCTION__`; 301 | }, 302 | }; 303 | } 304 | 305 | export default QuickChart; 306 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | import fetchMock from 'jest-fetch-mock'; 2 | 3 | import QuickChart from '../src/index'; 4 | 5 | test('basic chart, no auth', () => { 6 | const qc = new QuickChart(); 7 | qc.setConfig({ 8 | type: 'bar', 9 | data: { labels: ['Hello world', 'Foo bar'], datasets: [{ label: 'Foo', data: [1, 2] }] }, 10 | }); 11 | 12 | expect(qc.getUrl()).toContain('Hello+world'); 13 | expect(qc.getUrl()).toContain('/chart?'); 14 | expect(qc.getUrl()).toContain('w=500'); 15 | expect(qc.getUrl()).toContain('h=300'); 16 | }); 17 | 18 | test('basic chart with custom host', () => { 19 | const qc = new QuickChart(); 20 | qc.setHost('foo.com'); 21 | qc.setScheme('http'); 22 | qc.setConfig({ 23 | type: 'bar', 24 | data: { labels: ['Hello world', 'Foo bar'], datasets: [{ label: 'Foo', data: [1, 2] }] }, 25 | }); 26 | 27 | expect(qc.getUrl()).toContain('http://foo.com/chart?'); 28 | expect(qc.getUrl()).toContain('Hello+world'); 29 | expect(qc.getUrl()).toContain('w=500'); 30 | expect(qc.getUrl()).toContain('h=300'); 31 | }); 32 | 33 | test('basic chart with auth', () => { 34 | const qc = new QuickChart('abc123', '12345'); 35 | qc.setConfig({ 36 | type: 'bar', 37 | data: { labels: ['Hello world', 'Foo bar'], datasets: [{ label: 'Foo', data: [1, 2] }] }, 38 | }); 39 | 40 | expect(qc.getUrl()).toContain('Hello+world'); 41 | expect(qc.getUrl()).toContain('/chart?'); 42 | expect(qc.getUrl()).toContain('w=500'); 43 | expect(qc.getUrl()).toContain('h=300'); 44 | expect(qc.getUrl()).toContain('key=abc123'); 45 | }); 46 | 47 | test('basic chart with auth, signed', () => { 48 | const qc = new QuickChart('abc123', '12345'); 49 | qc.setConfig({ 50 | type: 'bar', 51 | data: { labels: ['Hello world', 'Foo bar'], datasets: [{ label: 'Foo', data: [1, 2] }] }, 52 | }); 53 | 54 | const url = qc.getSignedUrl(); 55 | expect(url).toContain('Hello+world'); 56 | expect(url).toContain('/chart?'); 57 | expect(url).toContain('w=500'); 58 | expect(url).toContain('h=300'); 59 | 60 | expect(url).not.toContain('key='); 61 | expect(url).toContain('accountId=12345'); 62 | expect(url).toContain('sig='); 63 | }); 64 | 65 | test('basic chart, string', () => { 66 | const qc = new QuickChart(); 67 | qc.setConfig(`{ 68 | type: 'bar', 69 | data: { labels: ['Hello world', 'Foo bar'], datasets: [{ label: 'Foo', data: [1, 2] }] }, 70 | }`); 71 | 72 | expect(qc.getUrl()).toContain('Hello+world'); 73 | expect(qc.getUrl()).toContain('/chart?'); 74 | expect(qc.getUrl()).toContain('w=500'); 75 | expect(qc.getUrl()).toContain('h=300'); 76 | }); 77 | 78 | test('basic chart with gradient', () => { 79 | const qc = new QuickChart(); 80 | qc.setConfig({ 81 | type: 'bar', 82 | data: { 83 | labels: ['Hello world', 'Foo bar'], 84 | datasets: [ 85 | { 86 | label: 'Foo', 87 | data: [1, 2], 88 | backgroundColor: QuickChart.getGradientFillHelper('horizontal', ['red', 'green']), 89 | }, 90 | ], 91 | }, 92 | }); 93 | 94 | expect(qc.getUrl()).toContain('Hello+world'); 95 | expect(qc.getUrl()).toContain('/chart?'); 96 | expect(qc.getUrl()).toContain('w=500'); 97 | expect(qc.getUrl()).toContain('h=300'); 98 | expect(qc.getUrl()).toContain('getGradientFillHelper'); 99 | }); 100 | 101 | test('basic chart, width and height', () => { 102 | const qc = new QuickChart(); 103 | qc.setConfig({ 104 | type: 'bar', 105 | data: { labels: ['Hello world', 'Foo bar'], datasets: [{ label: 'Foo', data: [1, 2] }] }, 106 | }); 107 | 108 | qc.setWidth(800).setHeight(500); 109 | 110 | expect(qc.getUrl()).toContain('Hello+world'); 111 | expect(qc.getUrl()).toContain('w=800'); 112 | expect(qc.getUrl()).toContain('h=500'); 113 | }); 114 | 115 | test('basic chart, other params', () => { 116 | const qc = new QuickChart(); 117 | qc.setConfig({ 118 | type: 'bar', 119 | data: { labels: ['Hello world', 'Foo bar'], datasets: [{ label: 'Foo', data: [1, 2] }] }, 120 | }); 121 | 122 | qc.setBackgroundColor('#000000').setDevicePixelRatio(2.0).setFormat('svg').setVersion('3'); 123 | 124 | expect(qc.getUrl()).toContain('Hello+world'); 125 | expect(qc.getUrl()).toContain('devicePixelRatio=2'); 126 | expect(qc.getUrl()).toContain('f=svg'); 127 | expect(qc.getUrl()).toContain('bkg=%23000000'); 128 | expect(qc.getUrl()).toContain('v=3'); 129 | }); 130 | 131 | test('js chart', () => { 132 | const qc = new QuickChart(); 133 | qc.setConfig({ 134 | type: 'bar', 135 | data: { 136 | labels: ['January', 'February', 'March', 'April', 'May'], 137 | datasets: [ 138 | { 139 | label: 'Dogs', 140 | data: [50, 60, 70, 180, 190], 141 | }, 142 | ], 143 | }, 144 | options: { 145 | scales: { 146 | yAxes: [ 147 | { 148 | ticks: { 149 | // @ts-ignore 150 | callback: function (value) { 151 | return '$' + value; 152 | }, 153 | }, 154 | }, 155 | ], 156 | }, 157 | }, 158 | }); 159 | 160 | expect(qc.getUrl()).toContain('Dogs'); 161 | expect(qc.getUrl()).toContain('callback%3Afunction+%28value'); 162 | }); 163 | 164 | test('postdata for basic chart, no auth', () => { 165 | const qc = new QuickChart(); 166 | qc.setConfig({ 167 | type: 'bar', 168 | data: { labels: ['Hello world', 'Foo bar'], datasets: [{ label: 'Foo', data: [1, 2] }] }, 169 | }); 170 | 171 | const postData = qc.getPostData(); 172 | expect(postData.chart).toContain('Hello world'); 173 | expect(postData.width).toEqual(500); 174 | expect(postData.height).toEqual(300); 175 | expect(postData.format).toEqual('png'); 176 | expect(postData.backgroundColor).toEqual('#ffffff'); 177 | expect(postData.devicePixelRatio).toBeCloseTo(1); 178 | }); 179 | 180 | test('postdata for basic chart with auth', () => { 181 | const qc = new QuickChart('abc123', '12345'); 182 | qc.setConfig({ 183 | type: 'bar', 184 | data: { labels: ['Hello world', 'Foo bar'], datasets: [{ label: 'Foo', data: [1, 2] }] }, 185 | }); 186 | 187 | const postData = qc.getPostData(); 188 | expect(postData.chart).toContain('Hello world'); 189 | expect(postData.width).toEqual(500); 190 | expect(postData.height).toEqual(300); 191 | expect(postData.format).toEqual('png'); 192 | expect(postData.backgroundColor).toEqual('#ffffff'); 193 | expect(postData.devicePixelRatio).toBeCloseTo(1); 194 | expect(postData.key).toEqual('abc123'); 195 | }); 196 | 197 | test('postdata for basic chart with params', () => { 198 | const qc = new QuickChart(); 199 | qc.setConfig({ 200 | type: 'bar', 201 | data: { labels: ['Hello world', 'Foo bar'], datasets: [{ label: 'Foo', data: [1, 2] }] }, 202 | }); 203 | 204 | qc.setWidth(400) 205 | .setHeight(200) 206 | .setFormat('svg') 207 | .setBackgroundColor('transparent') 208 | .setDevicePixelRatio(2.0); 209 | 210 | const postData = qc.getPostData(); 211 | expect(postData.chart).toContain('Hello world'); 212 | expect(postData.width).toEqual(400); 213 | expect(postData.height).toEqual(200); 214 | expect(postData.format).toEqual('svg'); 215 | expect(postData.backgroundColor).toEqual('transparent'); 216 | expect(postData.devicePixelRatio).toBeCloseTo(2); 217 | }); 218 | 219 | test('postdata for js chart', () => { 220 | const qc = new QuickChart(); 221 | qc.setConfig({ 222 | type: 'bar', 223 | data: { 224 | labels: ['January', 'February', 'March', 'April', 'May'], 225 | datasets: [ 226 | { 227 | label: 'Dogs', 228 | data: [50, 60, 70, 180, 190], 229 | }, 230 | ], 231 | }, 232 | options: { 233 | scales: { 234 | yAxes: [ 235 | { 236 | ticks: { 237 | // @ts-ignore 238 | callback: function (value) { 239 | return '$' + value; 240 | }, 241 | }, 242 | }, 243 | ], 244 | }, 245 | }, 246 | }); 247 | 248 | qc.setWidth(400) 249 | .setHeight(200) 250 | .setFormat('svg') 251 | .setBackgroundColor('transparent') 252 | .setDevicePixelRatio(2.0); 253 | 254 | const postData = qc.getPostData(); 255 | expect(postData.chart).toContain('callback:function (val'); 256 | expect(postData.width).toEqual(400); 257 | expect(postData.height).toEqual(200); 258 | expect(postData.format).toEqual('svg'); 259 | expect(postData.backgroundColor).toEqual('transparent'); 260 | expect(postData.devicePixelRatio).toBeCloseTo(2); 261 | }); 262 | 263 | test('getShortUrl for chart, no auth', async () => { 264 | const mockResp = { 265 | success: true, 266 | url: 'https://quickchart.io/chart/render/9a560ba4-ab71-4d1e-89ea-ce4741e9d232', 267 | }; 268 | fetchMock.mockResponseOnce(JSON.stringify(mockResp)); 269 | 270 | const qc = new QuickChart(); 271 | qc.setConfig({ 272 | type: 'bar', 273 | data: { labels: ['Hello world', 'Foo bar'], datasets: [{ label: 'Foo', data: [1, 2] }] }, 274 | }); 275 | 276 | await expect(qc.getShortUrl()).resolves.toEqual(mockResp.url); 277 | }); 278 | 279 | test('getShortUrl for chart js error', async () => { 280 | fetchMock.mockResponseOnce(() => { 281 | throw new Error('Request timed out'); 282 | }); 283 | 284 | const qc = new QuickChart(); 285 | qc.setConfig({ 286 | type: 'bar', 287 | data: { labels: ['Hello world', 'Foo bar'], datasets: [{ label: 'Foo', data: [1, 2] }] }, 288 | }); 289 | 290 | await expect(qc.getShortUrl()).rejects.toThrow('Request timed out'); 291 | }); 292 | 293 | test('getShortUrl for chart bad status code', async () => { 294 | fetchMock.mockResponseOnce('', { 295 | status: 502, 296 | }); 297 | 298 | const qc = new QuickChart(); 299 | qc.setConfig({ 300 | type: 'bar', 301 | data: { labels: ['Hello world', 'Foo bar'], datasets: [{ label: 'Foo', data: [1, 2] }] }, 302 | }); 303 | 304 | await expect(qc.getShortUrl()).rejects.toThrow('failed with status code'); 305 | }); 306 | 307 | test('getShortUrl for chart bad status code with error detail', async () => { 308 | fetchMock.mockResponseOnce('', { 309 | status: 400, 310 | headers: { 311 | 'x-quickchart-error': 'foo bar', 312 | }, 313 | }); 314 | 315 | const qc = new QuickChart(); 316 | qc.setConfig({ 317 | type: 'bar', 318 | data: { labels: ['Hello world', 'Foo bar'], datasets: [{ label: 'Foo', data: [1, 2] }] }, 319 | }); 320 | 321 | await expect(qc.getShortUrl()).rejects.toThrow('foo bar'); 322 | }); 323 | 324 | test('getShortUrl api failure', async () => { 325 | fetchMock.mockResponseOnce( 326 | JSON.stringify({ 327 | success: false, 328 | }), 329 | ); 330 | 331 | const qc = new QuickChart(); 332 | qc.setConfig({ 333 | type: 'bar', 334 | data: { labels: ['Hello world', 'Foo bar'], datasets: [{ label: 'Foo', data: [1, 2] }] }, 335 | }); 336 | 337 | await expect(qc.getShortUrl()).rejects.toThrow('failure response'); 338 | expect(fetch).toHaveBeenCalled(); 339 | }); 340 | 341 | test('toBinary, no auth', async () => { 342 | const mockData = Buffer.from('bWVvdw==', 'base64'); 343 | // https://github.com/jefflau/jest-fetch-mock/issues/218 344 | fetchMock.mockResponseOnce(() => Promise.resolve({ body: mockData as unknown as string })); 345 | 346 | const qc = new QuickChart(); 347 | qc.setConfig({ 348 | type: 'bar', 349 | data: { labels: ['Hello world', 'Foo bar'], datasets: [{ label: 'Foo', data: [1, 2] }] }, 350 | }); 351 | 352 | await expect(qc.toBinary()).resolves.toEqual(mockData); 353 | }); 354 | 355 | test('toDataUrl, no auth', async () => { 356 | fetchMock.mockResponseOnce(() => 357 | Promise.resolve({ body: Buffer.from('bWVvdw==', 'base64') as unknown as string }), 358 | ); 359 | 360 | const qc = new QuickChart(); 361 | qc.setConfig({ 362 | type: 'bar', 363 | data: { labels: ['Hello world', 'Foo bar'], datasets: [{ label: 'Foo', data: [1, 2] }] }, 364 | }); 365 | 366 | await expect(qc.toDataUrl()).resolves.toEqual('data:image/png;base64,bWVvdw=='); 367 | }); 368 | 369 | test('no chart specified throws error', async () => { 370 | const qc = new QuickChart(); 371 | expect(() => { 372 | qc.getUrl(); 373 | }).toThrow(); 374 | }); 375 | -------------------------------------------------------------------------------- /test/setupJest.js: -------------------------------------------------------------------------------- 1 | const fetchMock = require('jest-fetch-mock'); 2 | fetchMock.enableMocks(); 3 | jest.setMock('cross-fetch', fetchMock); 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "build/typescript", 4 | "lib": ["es2015", "dom"], 5 | "declaration": true, 6 | "noUnusedLocals": false, 7 | "noImplicitAny": true, 8 | "strictNullChecks": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noImplicitReturns": true, 12 | "noImplicitThis": true, 13 | "noImplicitOverride": true, 14 | "alwaysStrict": true, 15 | "esModuleInterop": true 16 | }, 17 | "include": ["src/**/*"] 18 | } --------------------------------------------------------------------------------