├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .dockerignore ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ ├── merge-dependabot.yml │ └── test.yml ├── .gitignore ├── .prettierrc.js ├── Dockerfile ├── Dockerfile.build ├── Dockerfile.test ├── README.md ├── input.json ├── jest.config.js ├── package.json ├── scripts ├── savePdf.sh ├── savePng.sh └── saveSvg.sh ├── setup.sh ├── src ├── app.ts ├── constants.ts ├── public │ └── fonts │ │ └── Roboto │ │ ├── LICENSE.txt │ │ └── Roboto.ttf └── server.ts ├── test ├── __snapshots__ │ └── index.test.ts.snap └── index.test.ts ├── tsconfig.json ├── vegaSpecs ├── bar.vg.json ├── bar.vl.json ├── specUseInvalidExternalLink.vl.json ├── specWithRelativeUrl.vg.json └── specWithRelativeUrl.vl.json ├── vercel.json └── yarn.lock /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.158.0/containers/typescript-node/.devcontainer/base.Dockerfile 2 | 3 | # [Choice] Node.js version: 14, 12, 10 4 | ARG VARIANT="14-buster" 5 | FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:0-${VARIANT} 6 | 7 | # [Optional] Uncomment this section to install additional OS packages. 8 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 9 | # && apt-get -y install --no-install-recommends 10 | 11 | # [Optional] Uncomment if you want to install an additional version of node using nvm 12 | # ARG EXTRA_NODE_VERSION=10 13 | # RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}" 14 | 15 | # [Optional] Uncomment if you want to install more global node packages 16 | # RUN su node -c "npm install -g " 17 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.158.0/containers/typescript-node 3 | { 4 | "name": "Node.js & TypeScript", 5 | "build": { 6 | "dockerfile": "Dockerfile", 7 | // Update 'VARIANT' to pick a Node version: 10, 12, 14 8 | "args": { 9 | "VARIANT": "14" 10 | } 11 | }, 12 | 13 | // Set *default* container specific settings.json values on container create. 14 | "settings": { 15 | "terminal.integrated.shell.linux": "/bin/zsh" 16 | }, 17 | 18 | // Add the IDs of extensions you want installed when the container is created. 19 | "extensions": [ 20 | "dbaeumer.vscode-eslint" 21 | ], 22 | 23 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 24 | // "forwardPorts": [], 25 | 26 | // Use 'postCreateCommand' to run commands after the container is created. 27 | // "postCreateCommand": "yarn install", 28 | 29 | // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 30 | "remoteUser": "node" 31 | } -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | extends: [ 4 | 'plugin:@typescript-eslint/recommended', 5 | 'prettier', 6 | ], 7 | parserOptions: { 8 | ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features 9 | sourceType: 'module', // Allows for the use of imports 10 | }, 11 | rules: { 12 | '@typescript-eslint/no-explicit-any': 'warn', 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /.github/workflows/merge-dependabot.yml: -------------------------------------------------------------------------------- 1 | name: Auto-merge Dependabot PRs 2 | on: 3 | schedule: 4 | - cron: '0 * * * *' 5 | jobs: 6 | auto_merge: 7 | name: Auto-merge Dependabot PRs 8 | 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: akheron/dependabot-cron-action@v1 13 | with: 14 | token: ${{ secrets.GITHUB_TOKEN }} 15 | auto-merge: 'minor' 16 | merge-method: 'rebase' 17 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v4 11 | 12 | - name: Setup Node 13 | uses: actions/setup-node@v4 14 | 15 | - name: Install Node dependencies 16 | run: yarn --frozen-lockfile 17 | 18 | - name: Install default fonts 19 | run: | 20 | echo msttcorefonts msttcorefonts/accepted-mscorefonts-eula select true | sudo debconf-set-selections 21 | sudo apt-get install msttcorefonts 22 | 23 | - run: yarn lint 24 | - run: yarn build 25 | # - run: yarn test 26 | # - run: docker build -f Dockerfile.test -t vega-render-test . 27 | # - run: docker run --rm vega-render-test 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vercel 2 | *.pdf 3 | *.png 4 | build/* 5 | dist/* 6 | node_modules/* 7 | plot.svg 8 | .idea/* 9 | yarn-error.log 10 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: 'all', 4 | singleQuote: true 5 | }; -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14-alpine 2 | 3 | WORKDIR /usr/src/app 4 | COPY package.json ./ 5 | 6 | # add libraries; sudo so non-root user added downstream can get sudo 7 | RUN apk add --no-cache \ 8 | sudo \ 9 | curl \ 10 | build-base \ 11 | g++ \ 12 | libpng \ 13 | libpng-dev \ 14 | jpeg-dev \ 15 | pango-dev \ 16 | cairo-dev \ 17 | giflib-dev \ 18 | python \ 19 | ; 20 | 21 | RUN npm install 22 | 23 | COPY . . 24 | 25 | EXPOSE 8090 26 | CMD [ "yarn", "start" ] 27 | -------------------------------------------------------------------------------- /Dockerfile.build: -------------------------------------------------------------------------------- 1 | FROM node:14-alpine 2 | 3 | WORKDIR /usr/src/app 4 | COPY package.json ./ 5 | 6 | # add libraries; sudo so non-root user added downstream can get sudo 7 | RUN apk add --no-cache \ 8 | sudo \ 9 | curl \ 10 | build-base \ 11 | g++ \ 12 | libpng \ 13 | libpng-dev \ 14 | jpeg-dev \ 15 | pango-dev \ 16 | cairo-dev \ 17 | giflib-dev \ 18 | python \ 19 | ; 20 | 21 | RUN npm install 22 | 23 | COPY . . 24 | 25 | EXPOSE 8090 26 | CMD [ "yarn", "build" ] 27 | CMD [ "yarn", "jest", "--updateSnapshot" ] 28 | 29 | -------------------------------------------------------------------------------- /Dockerfile.test: -------------------------------------------------------------------------------- 1 | FROM node:14-alpine 2 | 3 | WORKDIR /usr/src/app 4 | COPY package.json ./ 5 | 6 | # add libraries; sudo so non-root user added downstream can get sudo 7 | RUN apk add --no-cache \ 8 | sudo \ 9 | curl \ 10 | build-base \ 11 | g++ \ 12 | libpng \ 13 | libpng-dev \ 14 | jpeg-dev \ 15 | pango-dev \ 16 | cairo-dev \ 17 | giflib-dev \ 18 | python \ 19 | ; 20 | 21 | RUN npm install 22 | 23 | COPY . . 24 | 25 | EXPOSE 8090 26 | CMD [ "yarn", "build" ] 27 | CMD [ "yarn", "test" ] 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vega Service to Generate Images 2 | 3 | [![Build Status](https://github.com/vega/vega-render-service/workflows/Test/badge.svg)](https://github.com/vega/vega-render-service/actions) 4 | 5 | **Deprecated. Please use https://github.com/vega/vl-convert-service.** 6 | 7 | Deployed at https://render-vega.vercel.app/. We will update the service with new version of Vega and Vega-Lite and change the API. 8 | 9 | ## Dev Setup 10 | 11 | Install dependencies with `yarn`. 12 | 13 | ## Development Instructions 14 | 15 | 1. Clone the repository. 16 | ``` 17 | $ git clone git@github.com:vega/vega-render-service.git 18 | ``` 19 | 20 | 2. Install all dependencies. 21 | ``` 22 | $ yarn install 23 | ``` 24 | 25 | 3. Run the back-end server. 26 | ``` 27 | $ yarn start 28 | ``` 29 | 30 | 4. Run sample test command 31 | ``` 32 | $ scripts/savePdf.sh 33 | ``` 34 | 35 | 6. Go to the home route (which usually is `http://localhost:8090/`). Otherwise 36 | it will be mentioned in the console where the above command is run. 37 | 38 | ## Documentation 39 | 40 | You can find examples at in [`scripts`](https://github.com/vega/vega-render-service/tree/master/scripts). 41 | 42 | ``` 43 | /handle : to return a pdf/png/svg file 44 | Params: 45 | ++ Request Body: 46 | A JSON spec with the form of 47 | { 48 | "specs": { 49 | "$schema": "https://vega.github.io/schema/vega/v5.json", 50 | "width": 400, 51 | "height": 200, 52 | ... 53 | } 54 | } 55 | 56 | ++ Request Headers: 57 | Content-Type: application/json 58 | Accept: image/png OR application/pdf or image/svg 59 | 60 | ``` 61 | -------------------------------------------------------------------------------- /input.json: -------------------------------------------------------------------------------- 1 | {"$schema":"https://vega.github.io/schema/vega/v5.json","width":400,"height":200,"padding":5,"data":[{"name":"table","values":[{"category":"A","amount":28},{"category":"B","amount":55},{"category":"C","amount":43},{"category":"D","amount":91},{"category":"E","amount":81},{"category":"F","amount":53},{"category":"G","amount":19},{"category":"H","amount":87}]}],"signals":[{"name":"tooltip","value":{},"on":[{"events":"rect:mouseover","update":"datum"},{"events":"rect:mouseout","update":"{}"}]}],"scales":[{"name":"xscale","type":"band","domain":{"data":"table","field":"category"},"range":"width","padding":0.05,"round":true},{"name":"yscale","domain":{"data":"table","field":"amount"},"nice":true,"range":"height"}],"axes":[{"orient":"bottom","scale":"xscale"},{"orient":"left","scale":"yscale"}],"marks":[{"type":"rect","from":{"data":"table"},"encode":{"enter":{"x":{"scale":"xscale","field":"category"},"width":{"scale":"xscale","band":1},"y":{"scale":"yscale","field":"amount"},"y2":{"scale":"yscale","value":0}},"update":{"fill":{"value":"steelblue"}},"hover":{"fill":{"value":"red"}}}},{"type":"text","encode":{"enter":{"align":{"value":"center"},"baseline":{"value":"bottom"},"fill":{"value":"#333"}},"update":{"x":{"scale":"xscale","signal":"tooltip.category","band":0.5},"y":{"scale":"yscale","signal":"tooltip.amount","offset":-2},"text":{"signal":"tooltip.amount"},"fillOpacity":[{"test":"datum === tooltip","value":0},{"value":1}]}}}]} -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | }; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vega-render-service", 3 | "version": "0.1.0", 4 | "description": "Service to render Vega charts", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "prestart": "npm run build", 9 | "vercel-build": "./setup.sh && npm run build", 10 | "start": "node build/server.js", 11 | "test": "jest test/", 12 | "lint": "eslint .", 13 | "format": "eslint . --fix", 14 | "deploy": "vercel" 15 | }, 16 | "private": true, 17 | "author": "", 18 | "license": "ISC", 19 | "dependencies": { 20 | "body-parser": "^1.20.2", 21 | "canvas": "^2.11.2", 22 | "express": "^4.18.2", 23 | "vega": "^5.26.1", 24 | "vega-lite": "^5.14.1", 25 | "vega-schema-url-parser": "^2.2.0" 26 | }, 27 | "devDependencies": { 28 | "@types/body-parser": "^1.19.5", 29 | "@types/express": "^4.17.21", 30 | "@types/jest": "^29.5.10", 31 | "@types/node": "^20.8.10", 32 | "@typescript-eslint/eslint-plugin": "^6.13.1", 33 | "@typescript-eslint/parser": "^6.13.1", 34 | "cors": "^2.8.5", 35 | "eslint": "^8.55.0", 36 | "eslint-config-prettier": "^9.0.0", 37 | "eslint-plugin-prettier": "^5.0.1", 38 | "jest": "29.7.0", 39 | "vercel": "^32.5.0", 40 | "prettier": "^3.1.0", 41 | "supertest": "^6.3.3", 42 | "ts-jest": "^29.1.1", 43 | "typescript": "^5.3.2" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /scripts/savePdf.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | curl -X POST \ 4 | -H 'Accept: application/pdf' \ 5 | -H 'Content-Type: application/json' \ 6 | -d @./vegaSpecs/bar.vg.json http://localhost:8090/ \ 7 | >> plot.pdf -------------------------------------------------------------------------------- /scripts/savePng.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | curl -X POST \ 4 | -H 'Accept: image/png' \ 5 | -H 'Content-Type: application/json' \ 6 | -d @./vegaSpecs/bar.vg.json http://localhost:8090/ \ 7 | >> plot.png -------------------------------------------------------------------------------- /scripts/saveSvg.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | curl -X POST \ 4 | -H 'Accept: image/vegaSvg' \ 5 | -H 'Content-Type: application/json' \ 6 | -d @./vegaSpecs/bar.vl.json http://localhost:8090/ \ 7 | >> plot.svg -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | yum install gcc-c++ cairo-devel pango-devel libjpeg-turbo-devel giflib-devel libuuid-devel libmount-devel 2 | cp /lib64/{libuuid,libmount,libblkid}.so.1 node_modules/canvas/build/Release/ 3 | 4 | # See https://github.com/ivansevillaa/use-next-blurhash/issues/4#issuecomment-1311766520 5 | yum install wget 6 | 7 | wget https://github.com/NixOS/patchelf/archive/refs/tags/0.17.0.tar.gz 8 | tar -xf 0.17.0.tar.gz 9 | cd patchelf-0.17.0 10 | ./bootstrap.sh 11 | ./configure 12 | make 13 | make install 14 | cd .. 15 | 16 | wget https://zlib.net/fossils/zlib-1.2.9.tar.gz 17 | tar -xf zlib-1.2.9.tar.gz 18 | cd zlib-1.2.9 19 | sh configure 20 | make 21 | cp libz.so.1.2.9 ../node_modules/canvas/build/Release/libz.so.X 22 | cd .. 23 | 24 | patchelf --replace-needed /lib64/libz.so.1 libz.so.X ./node_modules/canvas/build/Release/libpng16.so.16 25 | patchelf --replace-needed libz.so.1 libz.so.X ./node_modules/canvas/build/Release/libpng16.so.16 -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import bodyparser from 'body-parser'; 2 | import { registerFont } from 'canvas'; 3 | import cors from 'cors'; 4 | import express, { Request, Response } from 'express'; 5 | import { Express } from 'express-serve-static-core'; 6 | import fs from 'fs'; 7 | import { URL } from 'url'; 8 | import * as vega from 'vega'; 9 | import { compile } from 'vega-lite'; 10 | import vegaUrlParser from 'vega-schema-url-parser'; 11 | import { ALLOWED_URLS, VEGA_DATA_BASE_URL } from './constants'; 12 | 13 | if (fs.existsSync(`${__dirname}/public/fonts/Roboto/Roboto.ttf`)) { 14 | registerFont(`${__dirname}/public/fonts/Roboto/Roboto.ttf`, { 15 | family: 'Roboto', 16 | }); 17 | } 18 | 19 | const app: Express = express(); 20 | app.use(bodyparser.urlencoded({ extended: true })); 21 | app.use(bodyparser.json()); 22 | app.use(cors()); 23 | app.use(express.static(`${__dirname}/public`)); 24 | 25 | app.use((req, res, next) => { 26 | res.header('Access-Control-Allow-Origin', '*'); 27 | res.header( 28 | 'Access-Control-Allow-Headers', 29 | 'Origin, X-Requested-With, Content-Type, Accept, Authorization', 30 | ); 31 | if (req.method === 'OPTIONS') { 32 | res.header('Access-Control-Allow-Methods', 'PUT, POST, GET'); 33 | return res.status(200).json({}); 34 | } 35 | next(); 36 | }); 37 | 38 | // define a route handler for the default home page 39 | app.get('/', (req: Request, res: Response) => { 40 | res 41 | .status(200) 42 | .send( 43 | '

Vega render service. Learn more at github.com/vega/vega-render-service.

', 44 | ); 45 | }); 46 | 47 | app.post('/', async (req: Request, res: Response) => { 48 | const contentType = req.header('Accept') ?? 'pdf'; 49 | const {body} = req; 50 | if (!body && !body.spec) { 51 | return res.status(400).end('Must provide Vega spec for render service'); 52 | } 53 | let spec = body.spec ?? body; 54 | const baseURL = body.baseURL ?? VEGA_DATA_BASE_URL; 55 | const { library } = vegaUrlParser(spec.$schema); 56 | 57 | switch (library) { 58 | case 'vega': 59 | break; 60 | case 'vega-lite': 61 | spec = compile(spec).spec; 62 | break; 63 | default: 64 | return res 65 | .status(400) 66 | .end('Invalid Schema, should be Vega or Vega-Lite.'); 67 | } 68 | 69 | const loader = vega.loader({ mode: 'http' }); 70 | const originalLoad = loader.load.bind(loader); 71 | const originalHttp = loader.http.bind(loader); 72 | 73 | loader.http = async (uri: string, options: any): Promise => { 74 | const parsedUri = new URL(uri); 75 | if ( 76 | ALLOWED_URLS.every( 77 | (allowedUrl) => !parsedUri.hostname.includes(allowedUrl), 78 | ) 79 | ) { 80 | res.status(400).send('External URI not allowed on this API'); 81 | throw new Error('External data url not allowed'); 82 | } 83 | return originalHttp(uri, options); 84 | }; 85 | 86 | loader.load = async (url, options) => { 87 | try { 88 | if (options) { 89 | return await originalLoad(url, { 90 | ...options, 91 | ...{ baseURL }, 92 | }); 93 | } 94 | return await originalLoad(url, { baseURL }); 95 | } catch { 96 | return await originalLoad(url, options); 97 | } 98 | }; 99 | 100 | const view = new vega.View(vega.parse(spec), { 101 | renderer: 'none', 102 | loader: loader, 103 | }); 104 | view.finalize(); 105 | switch (contentType) { 106 | case 'application/pdf': 107 | const pdf: HTMLCanvasElement = await view.toCanvas(undefined, { 108 | type: 'pdf', 109 | context: { textDrawingMode: 'glyph' }, 110 | }); 111 | const encodedPdf = (pdf as any).toBuffer(); 112 | if (res.headersSent) { 113 | return; 114 | } 115 | res.status(200).send(encodedPdf); 116 | break; 117 | case 'image/png': 118 | const png: HTMLCanvasElement = await view.toCanvas(); 119 | const encodedPng = (png as any).toBuffer(); 120 | if (res.headersSent) { 121 | return; 122 | } 123 | res.status(200).send(encodedPng); 124 | case 'image/vegaSvg': 125 | default: 126 | const svg = await view.toSVG(); 127 | if (res.headersSent) { 128 | return; 129 | } 130 | res.status(200).send(svg); 131 | break; 132 | } 133 | }); 134 | 135 | export default app; 136 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const VEGA_DATA_BASE_URL = 'https://vega.github.io/vega-datasets/'; 2 | export const ALLOWED_URLS: string[] = ['vega.github.io', VEGA_DATA_BASE_URL]; 3 | -------------------------------------------------------------------------------- /src/public/fonts/Roboto/LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /src/public/fonts/Roboto/Roboto.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vega/vega-render-service/867ad53b94dd7aa7d5e7d32f3f0b8a8b82de11dc/src/public/fonts/Roboto/Roboto.ttf -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import app from './app'; 2 | const port = 8090; // default port to listen 3 | 4 | app.listen(port, () => { 5 | console.log(`server started at http://localhost:${port}`); 6 | }); 7 | -------------------------------------------------------------------------------- /test/__snapshots__/index.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`API Request should POST SVG for Vega Specs 1`] = `"ABCDEFGH0102030405060708090100"`; 4 | 5 | exports[`API Request should POST SVG for Vega-Lite Specs 1`] = `"ABCDEFGHIa020406080100b"`; 6 | 7 | exports[`API Request should render Vega external verified base URL correctly 1`] = `"050100150200Horsepower05101520253035404550Miles_per_Gallon05101520Acceleration"`; 8 | 9 | exports[`API Request should render Vega-Lite external verified base URL correctly 1`] = `"JanFebMarAprMayJunJulAugSepOctNovDecdate (month)020406080100120Count of Recordsdrizzlefograinsnowsunweather"`; 10 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | 3 | import request from 'supertest'; 4 | import app from '../src/app'; 5 | import { VEGA_DATA_BASE_URL } from '../src/constants'; 6 | 7 | export const vegaSpec = require('../vegaSpecs/bar.vg.json'); 8 | export const vegaliteSpec = require('../vegaSpecs/bar.vl.json'); 9 | export const specUseExternalLink = require('../vegaSpecs/specUseInvalidExternalLink.vl.json'); 10 | export const vegaSpecWithRelativeUrl = require('../vegaSpecs/specWithRelativeUrl.vg.json'); 11 | export const vegaliteSpecWithRelativeUrl = require('../vegaSpecs/specWithRelativeUrl.vl.json'); 12 | 13 | describe('API Request', () => { 14 | test('It should response the GET method', async () => { 15 | const response = await request(app).get('/'); 16 | expect(response.statusCode).toBe(200); 17 | }); 18 | 19 | test('should POST SVG for Vega Specs', async () => { 20 | const response = await request(app).post('/').send({ spec: vegaSpec }); 21 | 22 | expect(response.statusCode).toBe(200); 23 | expect(response.text).toMatchSnapshot(); 24 | }); 25 | 26 | test('should POST SVG for Vega-Lite Specs', async () => { 27 | const response = await request(app).post('/').send({ spec: vegaliteSpec }); 28 | 29 | expect(response.statusCode).toBe(200); 30 | expect(response.text).toMatchSnapshot(); 31 | }); 32 | 33 | test('should return error status for external link', async () => { 34 | const response = await request(app) 35 | .post('/') 36 | .send({ spec: specUseExternalLink }); 37 | expect(response.statusCode).toBe(400); 38 | }); 39 | 40 | test('should return error if no spec specified', async () => { 41 | const response = await request(app).post('/').send({ vegaSpec }); 42 | expect(response.statusCode).toBe(400); 43 | }); 44 | 45 | test('should render Vega external verified base URL correctly', async () => { 46 | const response = await request(app).post('/').send({ 47 | spec: vegaSpecWithRelativeUrl, 48 | baseURL: VEGA_DATA_BASE_URL, 49 | }); 50 | expect(response.statusCode).toBe(200); 51 | expect(response.text).toMatchSnapshot(); 52 | }); 53 | 54 | test('should render Vega-Lite external verified base URL correctly', async () => { 55 | const response = await request(app).post('/').send({ 56 | spec: vegaliteSpecWithRelativeUrl, 57 | baseURL: VEGA_DATA_BASE_URL, 58 | }); 59 | expect(response.statusCode).toBe(200); 60 | expect(response.text).toMatchSnapshot(); 61 | }); 62 | 63 | test('should render Vega-Lite external invalid base URL correctly', async () => { 64 | const response = await request(app).post('/').send({ 65 | spec: vegaliteSpecWithRelativeUrl, 66 | baseURL: 'http://google.com', 67 | }); 68 | expect(response.statusCode).toBe(400); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "lib": ["es6", "DOM"], 6 | "esModuleInterop": true, 7 | "outDir": "build", 8 | "skipLibCheck": true 9 | }, 10 | "include": [ 11 | "src/*.ts", 12 | "config" 13 | ], 14 | "exclude": [ 15 | "node_modules", 16 | "test/**/*.ts" 17 | ] 18 | } -------------------------------------------------------------------------------- /vegaSpecs/bar.vg.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://vega.github.io/schema/vega/v5.json", 3 | "description": "A basic bar chart example, with value labels shown upon mouse hover.", 4 | "width": 400, 5 | "height": 200, 6 | "padding": 5, 7 | 8 | "data": [ 9 | { 10 | "name": "table", 11 | "values": [ 12 | {"category": "A", "amount": 28}, 13 | {"category": "B", "amount": 55}, 14 | {"category": "C", "amount": 43}, 15 | {"category": "D", "amount": 91}, 16 | {"category": "E", "amount": 81}, 17 | {"category": "F", "amount": 53}, 18 | {"category": "G", "amount": 19}, 19 | {"category": "H", "amount": 87} 20 | ] 21 | } 22 | ], 23 | 24 | "signals": [ 25 | { 26 | "name": "tooltip", 27 | "value": {}, 28 | "on": [ 29 | {"events": "rect:mouseover", "update": "datum"}, 30 | {"events": "rect:mouseout", "update": "{}"} 31 | ] 32 | } 33 | ], 34 | 35 | "scales": [ 36 | { 37 | "name": "xscale", 38 | "type": "band", 39 | "domain": {"data": "table", "field": "category"}, 40 | "range": "width", 41 | "padding": 0.05, 42 | "round": true 43 | }, 44 | { 45 | "name": "yscale", 46 | "domain": {"data": "table", "field": "amount"}, 47 | "nice": true, 48 | "range": "height" 49 | } 50 | ], 51 | 52 | "axes": [ 53 | { "orient": "bottom", "scale": "xscale" }, 54 | { "orient": "left", "scale": "yscale" } 55 | ], 56 | 57 | "marks": [ 58 | { 59 | "type": "rect", 60 | "from": {"data":"table"}, 61 | "encode": { 62 | "enter": { 63 | "x": {"scale": "xscale", "field": "category"}, 64 | "width": {"scale": "xscale", "band": 1}, 65 | "y": {"scale": "yscale", "field": "amount"}, 66 | "y2": {"scale": "yscale", "value": 0} 67 | }, 68 | "update": { 69 | "fill": {"value": "steelblue"} 70 | }, 71 | "hover": { 72 | "fill": {"value": "red"} 73 | } 74 | } 75 | }, 76 | { 77 | "type": "text", 78 | "encode": { 79 | "enter": { 80 | "align": {"value": "center"}, 81 | "baseline": {"value": "bottom"}, 82 | "fill": {"value": "#333"} 83 | }, 84 | "update": { 85 | "x": {"scale": "xscale", "signal": "tooltip.category", "band": 0.5}, 86 | "y": {"scale": "yscale", "signal": "tooltip.amount", "offset": -2}, 87 | "text": {"signal": "tooltip.amount"}, 88 | "fillOpacity": [ 89 | {"test": "datum === tooltip", "value": 0}, 90 | {"value": 1} 91 | ] 92 | } 93 | } 94 | } 95 | ] 96 | } -------------------------------------------------------------------------------- /vegaSpecs/bar.vl.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://vega.github.io/schema/vega-lite/v5.json", 3 | "description": "A simple bar chart with embedded data.", 4 | "data": { 5 | "values": [ 6 | { 7 | "a": "A", 8 | "b": 28 9 | }, 10 | { 11 | "a": "B", 12 | "b": 55 13 | }, 14 | { 15 | "a": "C", 16 | "b": 43 17 | }, 18 | { 19 | "a": "D", 20 | "b": 91 21 | }, 22 | { 23 | "a": "E", 24 | "b": 81 25 | }, 26 | { 27 | "a": "F", 28 | "b": 53 29 | }, 30 | { 31 | "a": "G", 32 | "b": 19 33 | }, 34 | { 35 | "a": "H", 36 | "b": 87 37 | }, 38 | { 39 | "a": "I", 40 | "b": 52 41 | } 42 | ] 43 | }, 44 | "mark": "bar", 45 | "encoding": { 46 | "x": { 47 | "field": "a", 48 | "type": "ordinal" 49 | }, 50 | "y": { 51 | "field": "b", 52 | "type": "quantitative" 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /vegaSpecs/specUseInvalidExternalLink.vl.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://vega.github.io/schema/vega-lite/v5.json", 3 | "data": {"url": "https://raw.githubusercontentabcdxyz.com/vega/vega/master/docs/data/barley.json"}, 4 | "mark": "bar", 5 | "encoding": { 6 | "x": {"aggregate": "sum", "field": "yield", "type": "quantitative"}, 7 | "y": {"field": "variety", "type": "nominal"}, 8 | "color": {"field": "site", "type": "nominal"} 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /vegaSpecs/specWithRelativeUrl.vg.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://vega.github.io/schema/vega/v5.json", 3 | "description": "A basic scatter plot example depicting automobile statistics.", 4 | "width": 200, 5 | "height": 200, 6 | "padding": 5, 7 | 8 | "data": [ 9 | { 10 | "name": "source", 11 | "url": "data/cars.json", 12 | "transform": [ 13 | { 14 | "type": "filter", 15 | "expr": "datum['Horsepower'] != null && datum['Miles_per_Gallon'] != null && datum['Acceleration'] != null" 16 | } 17 | ] 18 | } 19 | ], 20 | 21 | "scales": [ 22 | { 23 | "name": "x", 24 | "type": "linear", 25 | "round": true, 26 | "nice": true, 27 | "zero": true, 28 | "domain": {"data": "source", "field": "Horsepower"}, 29 | "range": "width" 30 | }, 31 | { 32 | "name": "y", 33 | "type": "linear", 34 | "round": true, 35 | "nice": true, 36 | "zero": true, 37 | "domain": {"data": "source", "field": "Miles_per_Gallon"}, 38 | "range": "height" 39 | }, 40 | { 41 | "name": "size", 42 | "type": "linear", 43 | "round": true, 44 | "nice": false, 45 | "zero": true, 46 | "domain": {"data": "source", "field": "Acceleration"}, 47 | "range": [4,361] 48 | } 49 | ], 50 | 51 | "axes": [ 52 | { 53 | "scale": "x", 54 | "grid": true, 55 | "domain": false, 56 | "orient": "bottom", 57 | "tickCount": 5, 58 | "title": "Horsepower" 59 | }, 60 | { 61 | "scale": "y", 62 | "grid": true, 63 | "domain": false, 64 | "orient": "left", 65 | "titlePadding": 5, 66 | "title": "Miles_per_Gallon" 67 | } 68 | ], 69 | 70 | "legends": [ 71 | { 72 | "size": "size", 73 | "title": "Acceleration", 74 | "format": "s", 75 | "symbolStrokeColor": "#4682b4", 76 | "symbolStrokeWidth": 2, 77 | "symbolOpacity": 0.5, 78 | "symbolType": "circle" 79 | } 80 | ], 81 | 82 | "marks": [ 83 | { 84 | "name": "marks", 85 | "type": "symbol", 86 | "from": {"data": "source"}, 87 | "encode": { 88 | "update": { 89 | "x": {"scale": "x", "field": "Horsepower"}, 90 | "y": {"scale": "y", "field": "Miles_per_Gallon"}, 91 | "size": {"scale": "size", "field": "Acceleration"}, 92 | "shape": {"value": "circle"}, 93 | "strokeWidth": {"value": 2}, 94 | "opacity": {"value": 0.5}, 95 | "stroke": {"value": "#4682b4"}, 96 | "fill": {"value": "transparent"} 97 | } 98 | } 99 | } 100 | ] 101 | } 102 | -------------------------------------------------------------------------------- /vegaSpecs/specWithRelativeUrl.vl.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://vega.github.io/schema/vega-lite/v5.json", 3 | "data": {"url": "data/seattle-weather.csv"}, 4 | "mark": {"type": "bar", "cornerRadiusTopLeft": 3, "cornerRadiusTopRight": 3}, 5 | "encoding": { 6 | "x": {"timeUnit": "month", "field": "date", "type": "ordinal"}, 7 | "y": {"aggregate": "count", "type": "quantitative"}, 8 | "color": {"field": "weather", "type": "nominal"} 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [{ "src": "src/*.ts", "use": "@vercel/node" }], 4 | "routes": [{ 5 | "src": "/(.*)", "dest": "/src/server.ts", 6 | "methods": ["GET", "POST", "OPTIONS"], 7 | "headers": { 8 | "Access-Control-Allow-Origin": "*", 9 | "Access-Control-Allow-Headers": "Origin, X-Requested-With, Content-Type, Accept", 10 | "Access-Control-Allow-Credentials": "true" 11 | } 12 | }] 13 | } 14 | --------------------------------------------------------------------------------