├── .babelrc ├── .gitignore ├── Procfile ├── README.md ├── forever.js ├── gulpfile.js ├── nodemon.json ├── package-lock.json ├── package.json ├── serverless.yml ├── src ├── LambdaHandlers.js ├── bin │ └── app.js ├── config.js ├── libs │ ├── ChartExamples.js │ ├── ChartHelpers.js │ ├── Helpers.js │ ├── Helpers.spec.js │ ├── Routes.js │ └── startServer.js ├── public │ ├── assets │ │ ├── restcharts_black.png │ │ ├── restcharts_black_rot.png │ │ ├── restcharts_black_rot_trans_15.png │ │ ├── restcharts_black_rot_trans_40.png │ │ ├── restcharts_black_trans_20.png │ │ ├── restcharts_black_trans_20_sm.png │ │ ├── restcharts_white.png │ │ └── restcharts_white_rot.png │ └── sass │ │ └── app.scss ├── routes │ ├── 0.._chart_[colon]type..all.js │ └── 999.._[star]..get.js └── tests │ ├── beach.jpg │ └── beach.png └── views └── index.pug /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", { 5 | "targets": { 6 | "node": "current" 7 | }, 8 | "useBuiltIns": "entry" 9 | } 10 | ] 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore 2 | *.log 3 | *.rdb 4 | *.serverless 5 | *.DS_Store 6 | /certs 7 | /logs 8 | /files 9 | /node_modules 10 | /libs 11 | /bin 12 | /routes 13 | /tasks 14 | /public 15 | /config.js 16 | /LambdaHandlers.js 17 | serverless.params.json 18 | serverless.env.yml 19 | *.env 20 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: npm start 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # :warning: DEPRECATED :warning: Use **[Image-Charts](https://www.image-charts.com/)** instead 👍 3 | 4 | ------------------------------ 5 | 6 | ## [RESTCharts](https://api.restcharts.com) 7 | 8 | Generate PNG images of charts easily through a simple REST-like API. 9 | 10 | Charts generated by [Highcharts](https://www.highcharts.com/) 11 | 12 | ![Red Area Chart](https://api.restcharts.com/chart/area?data=1,2,6,4,10,7,4,5,2,9,8&color=f00&height=150&width=200) 13 | 14 | Example: [https://api.restcharts.com/chart/area?data=1,2,6,4,10,7,4,5,2,9,8&color=f00](https://api.restcharts.com/chart/area?data=1,2,6,4,10,7,4,5,2,9,8&color=f00) 15 | 16 | ### API 17 | 18 | ```sh 19 | $ curl -X GET https://api.restcharts.com/chart/:type[?parameter1=val1¶meter2=val2] 20 | ``` 21 | 22 | or 23 | 24 | ```sh 25 | $ curl -X POST https://api.restcharts.com/chart/:type -d '{"parameter1": "val1", "parameter2": "val2", ...}' 26 | ``` 27 | 28 | ### Parameters 29 | 30 | #### Simple Params 31 | 32 | ##### URL parameters 33 | 34 | - `type`: The type of the chart you want to generate. See all types [here](https://www.highcharts.com/docs/chart-and-series-types/chart-types). 35 | 36 | #### Body/query string parameters 37 | 38 | - `data`: Comma-delimited list of your data that needs to be charted. 39 | This is a *required* parameter if the `raw` config option (see [Advanced](#advanced-params) below) 40 | is not provided or it is but a `series` array within the raw config is not provided. 41 | - `color`: The color of the line/bar/column/etc. of the chart. 42 | - `bg`: The background color of the area surrounding the chart (default: transparent, i.e. `rgba(0, 0, 0, 0)`) 43 | - `height`: The height of the generated chart. 44 | - `width`: The width of the generated chart. 45 | - `opacity`: If an area chart (or variation), will be the opacity of the area. 46 | - `linewidth`: If a line or spline chart, the line width of the lines. 47 | 48 | #### Advanced Params 49 | 50 | If you want to generate a chart using [any Highcharts options](https://api.highcharts.com/highcharts/) 51 | (the relevant options are the ones in the `Highcharts.chart()` method), 52 | you can provide a raw config object with any available options you'd like 53 | to provide for the chart type desired. Any configuration you have in the raw 54 | parameter will override the default options. Examples can be seen [here](https://api.restcharts.com/#Advanced%20Configuration). 55 | 56 | - `raw`: The JSON serialized config object. If the `data` parameter is not 57 | provided, a `series` array of data needs to be included here. 58 | 59 | #### Default Configuration 60 | 61 | The default Highcharts configuration object that is used if only simple parameters 62 | above are provided is as follows. We perform a deep [`Object.assign()`](https://github.com/saikojosh/Object-Assign-Deep) 63 | with this object as the target and the `raw` object overwriting anything in 64 | this object if it's provided in a request: 65 | 66 | ```js 67 | { 68 | chart: { 69 | type: `type`, 70 | backgroundColor: `bg`, 71 | margin: [ 0, 0, 0, 0 ], 72 | height: `height`, 73 | width: `width` 74 | }, 75 | plotOptions: { 76 | area: { 77 | fillOpacity: `opacity` 78 | } 79 | }, 80 | credits: { 81 | enabled: false 82 | }, 83 | xAxis: { 84 | visible: false 85 | }, 86 | yAxis: { 87 | visible: false 88 | }, 89 | legend: { 90 | enabled: false 91 | }, 92 | exporting: { 93 | enabled: false 94 | }, 95 | title: { 96 | text: '', 97 | }, 98 | series: [{ 99 | lineWidth: `linewidth`, 100 | color: `color`, 101 | data: `data`.map(d => ({ y: parseVal(d, 'integer'), marker: { enabled: false }})) 102 | }] 103 | } 104 | ``` 105 | 106 | ### Development 107 | 108 | #### Environment Variables 109 | 110 | The following environment variables is required for the app to be deployed and 111 | Highcharts installed correctly without problems. Please make sure you 112 | have a Highcharts license per their licensing requirements :) 113 | 114 | 1. ACCEPT_HIGHCHARTS_LICENSE=YES 115 | 116 | #### Build 117 | 118 | ```sh 119 | $ # To build the distribution files for the server 120 | $ gulp build 121 | $ # To run the dev server via nodemon 122 | $ npm run dev 123 | $ # To run tests 124 | $ npm test 125 | ``` 126 | 127 | #### AWS Lambda 128 | 129 | As of 2020-01-26, AWS Lambda's Runtime image lacks a dependency for PhantomJS to 130 | work which is what highcharts-export-server uses under the hood ([see the error](https://stackoverflow.com/questions/45129742/error-while-loading-shared-libraries-libfontconfig-so-1-on-cent-os)). To 131 | work around this issue follow these instructions before deploying 132 | to AWS Lambda. 133 | 134 | Don't forget about setting the `FONTCONFIG_PATH` environment to `/var/task/lib` 135 | in your Lambda environment variables. 136 | 137 | https://github.com/tarkal/highchart-lambda-export-server#building-from-scratch 138 | -------------------------------------------------------------------------------- /forever.js: -------------------------------------------------------------------------------- 1 | const forever = require('forever-monitor') 2 | 3 | const times = 10 4 | const child = new (forever.Monitor)('bin/app.js', { 5 | max: times, 6 | silent: false, 7 | args: [] 8 | }) 9 | 10 | child.on('exit', () => console.log(`index.js has exited after ${times} restarts`)) 11 | child.start() 12 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp') 2 | const babel = require('gulp-babel') 3 | const plumber = require('gulp-plumber') 4 | const concat = require('gulp-concat') 5 | const minify_css = require('gulp-clean-css') 6 | const sass = require('gulp-sass') 7 | 8 | gulp.task('assets', function() { 9 | return gulp.src("./src/public/assets/**/*") 10 | .pipe(gulp.dest("./public/assets/")) 11 | }) 12 | 13 | gulp.task('src', function() { 14 | return gulp.src("./src/**/*.js") 15 | .pipe(plumber()) 16 | .pipe(babel()) 17 | .pipe(gulp.dest("./")) 18 | }) 19 | 20 | gulp.task('styles', function() { 21 | return gulp.src([ 22 | './src/public/sass/**/*.scss', 23 | './src/public/sass/**/*.css' 24 | ]) 25 | .pipe(sass().on('error', sass.logError)) 26 | .pipe(concat('app.css')) 27 | .pipe(minify_css({ keepBreaks: true })) 28 | .pipe(gulp.dest('./public/css/')) 29 | }) 30 | 31 | gulp.task('build', gulp.parallel('assets', 'src', 'styles')) 32 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": [ 3 | "src/**/*" 4 | ], 5 | "events": { 6 | "restart": "gulp build" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "restcharts", 3 | "version": "0.4.1", 4 | "description": "Generate charts easily through a simple REST API.", 5 | "main": "forever.js", 6 | "engines": { 7 | "node": "12.14.0" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/whatl3y/restcharts.git" 12 | }, 13 | "scripts": { 14 | "dev": "nodemon --exec 'nf start'", 15 | "start": "node forever", 16 | "postinstall": "gulp build", 17 | "test": "mocha --require @babel/register --recursive './src/**/*.spec.js'" 18 | }, 19 | "devDependencies": { 20 | "foreman": "^3.0.1", 21 | "mocha": "^7.0.1", 22 | "nodemon": "^2.0.2" 23 | }, 24 | "dependencies": { 25 | "@babel/core": "^7.8.3", 26 | "@babel/preset-env": "^7.8.3", 27 | "@babel/register": "^7.8.3", 28 | "body-parser": "^1.19.0", 29 | "bootstrap": "^4.1.3", 30 | "bunyan": "^1.8.12", 31 | "cors": "^2.8.5", 32 | "express": "^4.17.1", 33 | "forever-monitor": "^2.0.0", 34 | "gulp": "^4.0.2", 35 | "gulp-babel": "^8.0.0", 36 | "gulp-clean-css": "^4.2.0", 37 | "gulp-concat": "^2.6.1", 38 | "gulp-plumber": "^1.2.1", 39 | "gulp-sass": "^4.0.2", 40 | "highcharts-export-server": "^2.0.24", 41 | "image-type": "^4.1.0", 42 | "minimist": "^1.2.0", 43 | "moment": "^2.24.0", 44 | "object-assign-deep": "^0.4.0", 45 | "pug": "^2.0.4", 46 | "request": "^2.88.0", 47 | "request-promise-native": "^1.0.8", 48 | "serverless": "^1.61.3", 49 | "serverless-http": "^2.3.1", 50 | "throng": "^4.0.0" 51 | }, 52 | "author": "Lance Whatley (https://www.lance.to)", 53 | "license": "MIT" 54 | } 55 | -------------------------------------------------------------------------------- /serverless.yml: -------------------------------------------------------------------------------- 1 | # Serverless definition file 2 | # -------------------------- 3 | # Defines Lambda functions to be deployed to AWS using the Serverless Framework. 4 | # http://serverless.com 5 | 6 | service: restcharts 7 | 8 | provider: 9 | name: aws 10 | runtime: nodejs12.x 11 | region: us-east-1 12 | memorySize: 512 13 | timeout: 10 14 | environment: ${file(serverless.env.yml):${self:provider.stage}} 15 | 16 | functions: 17 | app: 18 | handler: LambdaHandlers.handler 19 | events: 20 | - http: 21 | path: / 22 | method: ANY 23 | cors: true 24 | - http: 25 | path: /{any+} 26 | method: ANY 27 | cors: true 28 | -------------------------------------------------------------------------------- /src/LambdaHandlers.js: -------------------------------------------------------------------------------- 1 | import serverless from 'serverless-http' 2 | import startServer from './libs/startServer' 3 | 4 | const app = startServer(false) 5 | module.exports.handler = serverless(app, { binary: ['image/*'] }) 6 | -------------------------------------------------------------------------------- /src/bin/app.js: -------------------------------------------------------------------------------- 1 | /* Entry point for express web server 2 | * to listen for HTTP requests 3 | */ 4 | 5 | import throng from 'throng' 6 | import startServer from '../libs/startServer' 7 | import config from '../config' 8 | 9 | // entry point to start server 10 | // throng allows for multiple processes based on 11 | // concurrency configurations (i.e. num CPUs available.) 12 | throng({ 13 | workers: config.server.concurrency, 14 | lifetime: Infinity, 15 | grace: 8000, 16 | start: startServer 17 | }) 18 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | const appName = process.env.APP_NAME || "RESTCharts" 2 | 3 | export default { 4 | app: { 5 | name: appName 6 | }, 7 | 8 | server: { 9 | isProduction: process.env.NODE_ENV === 'production', 10 | port: process.env.PORT || 8080, 11 | concurrency: parseInt(process.env.WEB_CONCURRENCY || 1), 12 | host: process.env.HOSTNAME || "http://localhost:8080", 13 | api_host: process.env.API_HOSTNAME || "http://localhost:8080" 14 | }, 15 | 16 | logger: { 17 | options: { 18 | name: appName, 19 | level: process.env.LOGGING_LEVEL || "info", 20 | stream: process.stdout 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/libs/ChartExamples.js: -------------------------------------------------------------------------------- 1 | import config from '../config' 2 | 3 | const baseUrl = config.server.api_host 4 | 5 | const encodeJs = obj => encodeURI(JSON.stringify(obj)) 6 | 7 | export default [ 8 | { 9 | name: 'Area', 10 | examples: [ 11 | { name: 'Red Area Chart', url: `${baseUrl}/chart/area?data=1,2,6,4,10,7,4,5,2,9,8&color=f00` }, 12 | { name: 'Green Area Chart', url: `${baseUrl}/chart/area?data=5,1,2,9,8,3,0,0,4&color=00ff00` }, 13 | { name: 'Blue Area Chart', url: `${baseUrl}/chart/area?data=1,2,3,4,3,2,1&color=0000ff` }, 14 | { name: 'Red Area Transparent Chart', url: `${baseUrl}/chart/area?data=1,2,6,4,10,7,4,5,2,9,8&color=f00&opacity=0.2` }, 15 | { name: 'Green Area Transparent Chart', url: `${baseUrl}/chart/area?data=5,1,2,9,8,3,0,0,4&color=00ff00&opacity=0.2` }, 16 | { name: 'Blue Area Transparent Chart', url: `${baseUrl}/chart/area?data=1,2,3,4,3,2,1&color=0000ff&opacity=0.2` } 17 | ] 18 | }, { 19 | name: 'Area Spline', 20 | examples: [ 21 | { name: 'Red Area Spline Chart', url: `${baseUrl}/chart/areaspline?data=1,2,6,4,10,7,4,5,2,9,8&color=f00` }, 22 | { name: 'Green Area Spline Chart', url: `${baseUrl}/chart/areaspline?data=5,1,2,9,8,3,0,0,4&color=00ff00` }, 23 | { name: 'Blue Area Spline Chart', url: `${baseUrl}/chart/areaspline?data=1,2,3,4,3,2,1&color=0000ff` } 24 | ] 25 | }, { 26 | name: 'Bar', 27 | examples: [ 28 | { name: 'Red Bar Chart', url: `${baseUrl}/chart/bar?data=1,2,6,4,10,7,4,5,2,9,8&color=f00` }, 29 | { name: 'Green Bar Chart', url: `${baseUrl}/chart/bar?data=5,1,2,9,8,3,0,0,4&color=00ff00` }, 30 | { name: 'Blue Bar Chart', url: `${baseUrl}/chart/bar?data=1,2,3,4,3,2,1&color=0000ff` } 31 | ] 32 | }, { 33 | name: 'Column', 34 | examples: [ 35 | { name: 'Red Column Chart', url: `${baseUrl}/chart/column?data=1,2,6,4,10,7,4,5,2,9,8&color=f00` }, 36 | { name: 'Green Column Chart', url: `${baseUrl}/chart/column?data=5,1,2,9,8,3,0,0,4&color=00ff00` }, 37 | { name: 'Blue Column Chart', url: `${baseUrl}/chart/column?data=1,2,3,4,3,2,1&color=0000ff` } 38 | ] 39 | }, { 40 | name: 'Line', 41 | examples: [ 42 | { name: 'Red Line Chart', url: `${baseUrl}/chart/line?data=1,2,6,4,10,7,4,5,2,9,8&color=f00` }, 43 | { name: 'Green Line Chart', url: `${baseUrl}/chart/line?data=5,1,2,9,8,3,0,0,4&color=00ff00` }, 44 | { name: 'Blue Line Chart', url: `${baseUrl}/chart/line?data=1,2,3,4,3,2,1&color=0000ff` } 45 | ] 46 | }, { 47 | name: 'Spline', 48 | examples: [ 49 | { name: 'Red Spline Chart', url: `${baseUrl}/chart/spline?data=1,2,6,4,10,7,4,5,2,9,8&color=f00` }, 50 | { name: 'Green Spline Chart', url: `${baseUrl}/chart/spline?data=5,1,2,9,8,3,0,0,4&color=00ff00` }, 51 | { name: 'Blue Spline Chart', url: `${baseUrl}/chart/spline?data=1,2,3,4,3,2,1&color=0000ff` } 52 | ] 53 | }, { 54 | name: 'Advanced Configuration', 55 | examples: [ 56 | { name: 'Line 1 (with axes)', url: `${baseUrl}/chart/line?bg=fff&raw=${encodeJs({"chart":{"type":"line","margin":null},"title":{"text":"Monthly Average Temperature"},"subtitle":{"text":"Source: WorldClimate.com"},"xAxis":{"visible":true,"categories":["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]},"yAxis":{"visible":true,"title":{"text":"Temperature (°C)"}},"legend":{"enabled":true},"plotOptions":{"line":{"dataLabels":{"enabled":true},"enableMouseTracking":false}},"series":[{"name":"Tokyo","data":[7,6.9,9.5,14.5,18.4,21.5,25.2,26.5,23.3,18.3,13.9,9.6]},{"name":"London","data":[3.9,4.2,5.7,8.5,11.9,15.2,17,16.6,14.2,10.3,6.6,4.8]}]})}` }, 57 | { name: 'Line 1 (without axes)', url: `${baseUrl}/chart/line?bg=fff&raw=${encodeJs({"chart":{"type":"line"},"title":{"text":"Monthly Average Temperature"},"subtitle":{"text":"Source: WorldClimate.com"},"xAxis":{"categories":["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]},"yAxis":{"title":{"text":"Temperature (°C)"}},"legend":{"enabled":true},"plotOptions":{"line":{"dataLabels":{"enabled":true},"enableMouseTracking":false}},"series":[{"name":"Tokyo","data":[7,6.9,9.5,14.5,18.4,21.5,25.2,26.5,23.3,18.3,13.9,9.6]},{"name":"London","data":[3.9,4.2,5.7,8.5,11.9,15.2,17,16.6,14.2,10.3,6.6,4.8]}]})}` }, 58 | { name: 'Bubble 1 (with axes)', url: `${baseUrl}/chart/bubble?raw=${encodeJs({"chart":{"type":"bubble","margin":null,"plotBorderWidth":1,"zoomType":"xy"},"legend":{"enabled":false},"title":{"text":"Sugar and fat intake per country"},"xAxis":{"visible":true,"gridLineWidth":1,"title":{"text":"Daily fat intake"},"labels":{"format":"{value} gr"},"plotLines":[{"color":"black","dashStyle":"dot","width":2,"value":65,"label":{"rotation":0,"y":15,"style":{"fontStyle":"italic"},"text":"Safe fat intake 65g/day"},"zIndex":3}]},"yAxis":{"visible":true,"startOnTick":false,"endOnTick":false,"title":{"text":"Daily sugar intake"},"labels":{"format":"{value} gr"},"maxPadding":0.2,"plotLines":[{"color":"black","dashStyle":"dot","width":2,"value":50,"label":{"align":"right","style":{"fontStyle":"italic"},"text":"Safe sugar intake 50g/day","x":-10},"zIndex":3}]},"plotOptions":{"series":{"dataLabels":{"enabled":true,"format":"{point.name}"}}},"series":[{"data":[{"x":95,"y":95,"z":13.8,"name":"BE","country":"Belgium"},{"x":86.5,"y":102.9,"z":14.7,"name":"DE","country":"Germany"},{"x":80.8,"y":91.5,"z":15.8,"name":"FI","country":"Finland"},{"x":80.4,"y":102.5,"z":12,"name":"NL","country":"Netherlands"},{"x":80.3,"y":86.1,"z":11.8,"name":"SE","country":"Sweden"},{"x":78.4,"y":70.1,"z":16.6,"name":"ES","country":"Spain"},{"x":74.2,"y":68.5,"z":14.5,"name":"FR","country":"France"},{"x":73.5,"y":83.1,"z":10,"name":"NO","country":"Norway"},{"x":71,"y":93.2,"z":24.7,"name":"UK","country":"United Kingdom"},{"x":69.2,"y":57.6,"z":10.4,"name":"IT","country":"Italy"},{"x":68.6,"y":20,"z":16,"name":"RU","country":"Russia"},{"x":65.5,"y":126.4,"z":35.3,"name":"US","country":"United States"},{"x":65.4,"y":50.8,"z":28.5,"name":"HU","country":"Hungary"},{"x":63.4,"y":51.8,"z":15.4,"name":"PT","country":"Portugal"},{"x":64,"y":82.9,"z":31.3,"name":"NZ","country":"New Zealand"}]}]})}` }, 59 | { name: 'Bubble 1 (without axes)', url: `${baseUrl}/chart/bubble?raw=${encodeJs({"chart":{"type":"bubble","plotBorderWidth":1,"zoomType":"xy"},"legend":{"enabled":false},"title":{"text":"Sugar and fat intake per country"},"xAxis":{"gridLineWidth":1,"title":{"text":"Daily fat intake"},"labels":{"format":"{value} gr"},"plotLines":[{"color":"black","dashStyle":"dot","width":2,"value":65,"label":{"rotation":0,"y":15,"style":{"fontStyle":"italic"},"text":"Safe fat intake 65g/day"},"zIndex":3}]},"yAxis":{"startOnTick":false,"endOnTick":false,"title":{"text":"Daily sugar intake"},"labels":{"format":"{value} gr"},"maxPadding":0.2,"plotLines":[{"color":"black","dashStyle":"dot","width":2,"value":50,"label":{"align":"right","style":{"fontStyle":"italic"},"text":"Safe sugar intake 50g/day","x":-10},"zIndex":3}]},"plotOptions":{"series":{"dataLabels":{"enabled":true,"format":"{point.name}"}}},"series":[{"data":[{"x":95,"y":95,"z":13.8,"name":"BE","country":"Belgium"},{"x":86.5,"y":102.9,"z":14.7,"name":"DE","country":"Germany"},{"x":80.8,"y":91.5,"z":15.8,"name":"FI","country":"Finland"},{"x":80.4,"y":102.5,"z":12,"name":"NL","country":"Netherlands"},{"x":80.3,"y":86.1,"z":11.8,"name":"SE","country":"Sweden"},{"x":78.4,"y":70.1,"z":16.6,"name":"ES","country":"Spain"},{"x":74.2,"y":68.5,"z":14.5,"name":"FR","country":"France"},{"x":73.5,"y":83.1,"z":10,"name":"NO","country":"Norway"},{"x":71,"y":93.2,"z":24.7,"name":"UK","country":"United Kingdom"},{"x":69.2,"y":57.6,"z":10.4,"name":"IT","country":"Italy"},{"x":68.6,"y":20,"z":16,"name":"RU","country":"Russia"},{"x":65.5,"y":126.4,"z":35.3,"name":"US","country":"United States"},{"x":65.4,"y":50.8,"z":28.5,"name":"HU","country":"Hungary"},{"x":63.4,"y":51.8,"z":15.4,"name":"PT","country":"Portugal"},{"x":64,"y":82.9,"z":31.3,"name":"NZ","country":"New Zealand"}]}]})}` }, 60 | { name : 'Pie 1', url: `${baseUrl}/chart/pie?raw=${encodeJs({"chart":{"plotBackgroundColor":null,"plotBorderWidth":null,"plotShadow":false,"type":"pie"},"title":{"text":"Browser market shares January, 2015 to May, 2015"},"plotOptions":{"pie":{"allowPointSelect":true,"cursor":"pointer","dataLabels":{"enabled":false},"showInLegend":true}},"series":[{"name":"Brands","colorByPoint":true,"data":[{"name":"Microsoft Internet Explorer","y":56.33},{"name":"Chrome","y":24.03,"sliced":true,"selected":true},{"name":"Firefox","y":10.38},{"name":"Safari","y":4.77},{"name":"Opera","y":0.91},{"name":"Proprietary or Undetectable","y":0.2}]}]})}` }, 61 | ] 62 | } 63 | ] 64 | -------------------------------------------------------------------------------- /src/libs/ChartHelpers.js: -------------------------------------------------------------------------------- 1 | import objectAssignDeep from 'object-assign-deep' 2 | 3 | export default { 4 | getConfig(data, options={}, rawConfig={}) { 5 | const type = options.type || 'area' 6 | const color = parseVal(options.color, 'color') 7 | const bgColor = parseVal(options.bg || 'rgba(0, 0, 0, 0)', 'color') 8 | const height = parseVal(options.height, 'integer') 9 | const width = parseVal(options.width, 'integer') 10 | const opacity = parseVal(options.opacity, 'float') 11 | const lineWidth = parseVal(options.linewidth || 2, 'integer') 12 | 13 | return objectAssignDeep({ 14 | chart: { 15 | type: type, 16 | backgroundColor: bgColor, 17 | margin: [ 0, 0, 0, 0 ], 18 | height: height, 19 | width: width 20 | }, 21 | plotOptions: { 22 | area: { 23 | fillOpacity: opacity 24 | } 25 | }, 26 | credits: { 27 | enabled: false 28 | }, 29 | xAxis: { 30 | visible: false 31 | }, 32 | yAxis: { 33 | visible: false 34 | }, 35 | legend: { 36 | enabled: false 37 | }, 38 | exporting: { 39 | enabled: false 40 | }, 41 | title: { 42 | text: '', 43 | }, 44 | series: [{ 45 | lineWidth: lineWidth, 46 | color: color, 47 | data: data.map(d => ({ y: parseVal(d, 'integer'), marker: { enabled: false }})) 48 | }] 49 | }, rawConfig) 50 | }, 51 | 52 | async createChart(exporter, config) { 53 | return new Promise((resolve, reject) => { 54 | exporter.export({ type: 'png', options: config }, function(err, response) { 55 | if (err) 56 | return reject(err) 57 | 58 | const imageBase64 = response.data 59 | const imageBuffer = Buffer.from(imageBase64, 'base64') 60 | resolve(imageBuffer) 61 | }) 62 | }) 63 | } 64 | } 65 | 66 | export function parseVal(val, type='string') { 67 | if (val) { 68 | switch (type) { 69 | case 'color': 70 | return (val.indexOf('rgb') === 0) ? parseVal(val, 'string') : `#${parseVal(val, 'string')}` 71 | case 'float': 72 | return parseFloat(val) 73 | case 'integer': 74 | return parseInt(val) 75 | case 'string': 76 | return val.toString() 77 | } 78 | } 79 | 80 | return val 81 | } 82 | 83 | export function jsonParseFallback(obj, fallback={}) { 84 | try { 85 | const parsed = JSON.parse(obj) 86 | return parsed 87 | } catch(e) { 88 | return fallback 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/libs/Helpers.js: -------------------------------------------------------------------------------- 1 | export function flatten(ary) { 2 | const nestedFlattened = ary.map(v => { 3 | if (v instanceof Array) 4 | return flatten(v) 5 | return v 6 | }) 7 | return [].concat.apply([], nestedFlattened) 8 | } 9 | 10 | export function createNestedArrays(ary, length=10) { 11 | const aryCopy = ary.slice(0) 12 | let nestedArys = [] 13 | 14 | while (aryCopy.length > 0) { 15 | nestedArys.push(aryCopy.splice(0, length)) 16 | } 17 | return nestedArys 18 | } 19 | -------------------------------------------------------------------------------- /src/libs/Helpers.spec.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import { createNestedArrays, flatten } from './Helpers' 3 | 4 | describe('Helpers', () => { 5 | describe('#createNestedArrays', () => { 6 | it(`should return an array of arrays of specified length`, function() { 7 | const ary = new Array(100).fill(0).map((_, i) => i+1) 8 | const nested5 = createNestedArrays(ary, 5) 9 | const nested10 = createNestedArrays(ary, 10) 10 | const nested25 = createNestedArrays(ary, 25) 11 | 12 | assert.equal(20, nested5.length) 13 | assert.equal(5, nested5[0].length) 14 | assert.equal(5, nested5[19].length) 15 | assert.equal('undefined', typeof nested5[20]) 16 | assert.equal(1, nested5[0][0]) 17 | assert.equal(2, nested5[0][1]) 18 | assert.equal(3, nested5[0][2]) 19 | assert.equal(4, nested5[0][3]) 20 | assert.equal(5, nested5[0][4]) 21 | assert.equal('undefined', typeof nested5[0][5]) 22 | 23 | assert.equal(10, nested10.length) 24 | assert.equal(4, nested25.length) 25 | }) 26 | }) 27 | 28 | describe('#flatten', () => { 29 | it(`should flatten array with nested arrays`, function() { 30 | const nestedArray1 = [1, 2, [3, [4, 5]], [6, [7, 8, 9, [10]]]] 31 | const desiredArray1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 32 | 33 | const flattenedArray1 = flatten(nestedArray1) 34 | flattenedArray1.forEach((v, i) => { 35 | assert.equal(desiredArray1[i], v) 36 | }) 37 | }) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /src/libs/Routes.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | 4 | export default { 5 | _path: 'routes', 6 | 7 | get() { 8 | const files = fs.readdirSync(this._path) 9 | const routeFiles = files.filter(file => fs.lstatSync(path.join(this._path, file)).isFile()) 10 | const routes = routeFiles.map(file => { 11 | const routeInfo = file.replace(/\.js/g,"").replace(/_/g,"/").replace(/\[star\]/g,"*").replace(/\[colon\]/g,":").split("..") 12 | const routeOrder = Number(routeInfo[0] || 0) 13 | const routePath = routeInfo[1] 14 | const routeVerb = routeInfo[2] || 'get' 15 | 16 | return { 17 | verb: routeVerb, 18 | path: routePath, 19 | order: routeOrder, 20 | file: file, 21 | handler: require(path.join('..', this._path, file)).default 22 | } 23 | }) 24 | 25 | return routes.sort((r1, r2) => r1.order - r2.order) 26 | }, 27 | 28 | checkAndRedirect(req, res, defaultRedirectPath='/') { 29 | if (req.session && req.session.returnTo) 30 | return res.redirect(redirectTo) 31 | 32 | res.redirect(defaultRedirectPath) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/libs/startServer.js: -------------------------------------------------------------------------------- 1 | import http from 'http' 2 | import path from 'path' 3 | import bodyParser from 'body-parser' 4 | import express from 'express' 5 | import cors from 'cors' 6 | import bunyan from 'bunyan' 7 | import Routes from '../libs/Routes' 8 | import config from '../config' 9 | 10 | const app = express() 11 | const httpServer = http.Server(app) 12 | const log = bunyan.createLogger(config.logger.options) 13 | 14 | export default function startApp(shouldListenOnPort=true) { 15 | try { 16 | const routes = Routes.get() 17 | 18 | //view engine setup 19 | app.set('views', path.join(__dirname, '..', 'views')) 20 | app.set('view engine', 'pug') 21 | 22 | app.use(bodyParser.urlencoded({extended: true, limit: '1mb'})) 23 | app.use(bodyParser.json({limit: '1mb'})) 24 | 25 | // All routes should be CORS enabled 26 | app.use(cors()) 27 | 28 | //static files 29 | app.use('/public', express.static(path.join(__dirname, '..', '/public'))) 30 | 31 | //setup route handlers in the express app 32 | routes.forEach(route => { 33 | try { 34 | app[route.verb.toLowerCase()](route.path, route.handler) 35 | log.debug(`Successfully bound route to express; method: ${route.verb}; path: ${route.path}`) 36 | } catch(err) { 37 | log.error(err, `Error binding route to express; method: ${route.verb}; path: ${route.path}`) 38 | } 39 | }) 40 | 41 | if (shouldListenOnPort) 42 | httpServer.listen(config.server.port, () => log.info(`listening on *: ${config.server.port}`)) 43 | 44 | return app 45 | 46 | } catch(err) { 47 | log.error("Error starting server", err) 48 | process.exit() 49 | } 50 | 51 | //handle if the process suddenly stops 52 | process.on('SIGINT', () => { console.log('got SIGINT....'); process.exit() }) 53 | process.on('SIGTERM', () => { console.log('got SIGTERM....'); process.exit() }) 54 | } 55 | -------------------------------------------------------------------------------- /src/public/assets/restcharts_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/image-charts/restcharts/410d68a840eedec43c4563edc7f0248e74dc4317/src/public/assets/restcharts_black.png -------------------------------------------------------------------------------- /src/public/assets/restcharts_black_rot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/image-charts/restcharts/410d68a840eedec43c4563edc7f0248e74dc4317/src/public/assets/restcharts_black_rot.png -------------------------------------------------------------------------------- /src/public/assets/restcharts_black_rot_trans_15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/image-charts/restcharts/410d68a840eedec43c4563edc7f0248e74dc4317/src/public/assets/restcharts_black_rot_trans_15.png -------------------------------------------------------------------------------- /src/public/assets/restcharts_black_rot_trans_40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/image-charts/restcharts/410d68a840eedec43c4563edc7f0248e74dc4317/src/public/assets/restcharts_black_rot_trans_40.png -------------------------------------------------------------------------------- /src/public/assets/restcharts_black_trans_20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/image-charts/restcharts/410d68a840eedec43c4563edc7f0248e74dc4317/src/public/assets/restcharts_black_trans_20.png -------------------------------------------------------------------------------- /src/public/assets/restcharts_black_trans_20_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/image-charts/restcharts/410d68a840eedec43c4563edc7f0248e74dc4317/src/public/assets/restcharts_black_trans_20_sm.png -------------------------------------------------------------------------------- /src/public/assets/restcharts_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/image-charts/restcharts/410d68a840eedec43c4563edc7f0248e74dc4317/src/public/assets/restcharts_white.png -------------------------------------------------------------------------------- /src/public/assets/restcharts_white_rot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/image-charts/restcharts/410d68a840eedec43c4563edc7f0248e74dc4317/src/public/assets/restcharts_white_rot.png -------------------------------------------------------------------------------- /src/public/sass/app.scss: -------------------------------------------------------------------------------- 1 | @import '../../../node_modules/bootstrap/dist/css/bootstrap.css'; 2 | 3 | .example { 4 | a:hover { 5 | text-decoration: none; 6 | } 7 | } 8 | 9 | .margin-vert-medium { 10 | margin-top: 10px; 11 | margin-bottom: 10px; 12 | } 13 | 14 | .margin-vert-large { 15 | margin-top: 25px; 16 | margin-bottom: 25px; 17 | } 18 | 19 | .margin-vert-xlarge { 20 | margin-top: 50px; 21 | margin-bottom: 50px; 22 | } 23 | 24 | .small-width { 25 | max-width: 600px; 26 | } 27 | 28 | .header { 29 | margin-top: 50px; 30 | } 31 | 32 | .card { 33 | code { 34 | max-width: 100%; 35 | font-size: 9px; 36 | white-space: nowrap; 37 | overflow: hidden; 38 | text-overflow: ellipsis; 39 | 40 | &.wrap-always { 41 | white-space: normal; 42 | word-break: break-all; 43 | } 44 | 45 | &:hover { 46 | word-break: break-all; 47 | white-space: normal; 48 | overflow: visible; 49 | } 50 | } 51 | 52 | .card-block { 53 | padding: 0.7rem; 54 | } 55 | } 56 | 57 | .chart-example { 58 | // background-repeat: 'no-repeat'; 59 | // background-position: 'bottom'; 60 | // background-size: 'contain'; 61 | 62 | &.card-img-top { 63 | max-height: 150px; 64 | background: #f8f8f8; 65 | } 66 | 67 | &.large-img-top { 68 | max-height: 50vw; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/routes/0.._chart_[colon]type..all.js: -------------------------------------------------------------------------------- 1 | import exporter from 'highcharts-export-server' 2 | import ChartHelpers, { jsonParseFallback } from '../libs/ChartHelpers' 3 | 4 | //Set up a pool of PhantomJS workers 5 | exporter.initPool() 6 | 7 | export default function Chart(req, res) { 8 | return new Promise(async (resolve, reject) => { 9 | try { 10 | const routeParams = req.params 11 | const body = ((req.method.toLowerCase() == 'post') ? (req.body || req.query) : (req.query || req.body)) || {} 12 | const chartType = routeParams.type || body.type 13 | const rawConfig = jsonParseFallback(body.raw || {}, {}) 14 | const chartData = (body.data || '').split(',') 15 | 16 | delete(body.data) 17 | 18 | if (chartData.length <= 1 && !rawConfig.series) 19 | return res.status(400).json({ status: 400, error: `Please pass valid data. If you passed a 'raw' param with data populated in the 'series' key, there is likely something wrong with the JSON config you passed.` }) 20 | 21 | const finalConfig = ChartHelpers.getConfig(chartData, Object.assign(body, { type: chartType }), rawConfig) 22 | const imageBuffer = await ChartHelpers.createChart(exporter, finalConfig) 23 | 24 | res.setHeader('Content-Type', 'image/png') 25 | res.send(imageBuffer) 26 | resolve(imageBuffer) 27 | 28 | } catch(err) { 29 | res.status(500).json({ status: 500, error: err }) 30 | reject(err) 31 | } 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /src/routes/999.._[star]..get.js: -------------------------------------------------------------------------------- 1 | import bunyan from 'bunyan' 2 | import ChartExamples from '../libs/ChartExamples' 3 | import config from '../config' 4 | 5 | const log = bunyan.createLogger(config.logger.options) 6 | 7 | export default async function Index(req, res) { 8 | try { 9 | res.render('index', { 10 | data: { 11 | api_host: config.server.api_host, 12 | host: config.server.host, 13 | types: ChartExamples 14 | } 15 | }) 16 | 17 | } catch(err) { 18 | log.error("Error sending slack message", err) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/tests/beach.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/image-charts/restcharts/410d68a840eedec43c4563edc7f0248e74dc4317/src/tests/beach.jpg -------------------------------------------------------------------------------- /src/tests/beach.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/image-charts/restcharts/410d68a840eedec43c4563edc7f0248e74dc4317/src/tests/beach.png -------------------------------------------------------------------------------- /views/index.pug: -------------------------------------------------------------------------------- 1 | - data = data || {} 2 | - githubLink = "https://github.com/whatl3y/restcharts" 3 | 4 | html 5 | head 6 | meta(charset="utf-8") 7 | meta(http-equiv="X-UA-Compatible",content="IE=edge") 8 | meta(name="viewport",content="width=device-width") 9 | 10 | script(src="https://use.fontawesome.com/028467eb54.js") 11 | 12 | link(rel="stylesheet",href="https://cdn.rawgit.com/konpa/devicon/df6431e323547add1b4cf45992913f15286456d3/devicon.min.css") 13 | 14 | style 15 | include ../public/css/app.css 16 | 17 | title RESTCharts 18 | body 19 | div.container.small-width 20 | div.text-center.header 21 | div.margin-vert-large 22 | a(href=githubLink) 23 | i(style="font-size:4em").devicon-github-plain 24 | h1 RESTCharts 25 | div 26 | small Generate charts easier than ever! Generated by  27 | a(href="https://www.highcharts.com/") Highcharts 28 | 29 | hr 30 | 31 | div.text-center 32 | h3 What is RESTCharts? 33 | div. 34 | RESTCharts lets you generate dynamic Highcharts through 35 | a REST-like API, with the URL & query string params providing 36 | information such as the data to be charted, chart colors, 37 | area opacity, etc. 38 | div.card.margin-vert-xlarge 39 | div.card-block 40 | div As easy as... 41 | div 42 | code.wrap-always(style="font-size:14px;") 43 | a(href=data.api_host + '/chart/column?data=5,1,2,9,8,3,6')= data.api_host + '/chart/column?data=5,1,2,9,8,3,6' 44 | 45 | hr 46 | 47 | div 48 | h2.text-center(id='Parameters') API 49 | h5 Basic Route Format 50 | div.row 51 | div.col-10.offset-1 52 | div(style="font-size:10px;margin-bottom:10px") 53 | i. 54 | The following is an example with curl, but you can use any 55 | method of making an HTTP request. Also, CORS is enabled for 56 | making requests from the browser. 57 | div 58 | div 59 | code= '$ curl -X GET ' + data.api_host + '/chart/:type[?parameter1=val1¶meter2=val2&...]' 60 | div or 61 | div 62 | code= '$ curl -X POST ' + data.api_host + '/chart/:type -d \'{"parameter1": "val1", "parameter2": "val2", ...}\'' 63 | div.margin-vert-large.text-center 64 | strong 65 | a(href=githubLink). 66 | Complete API/parameter documentation available on github 67 | 68 | hr 69 | 70 | h2.text-center(id='Examples') Examples 71 | div 72 | each type in data.types 73 | div 74 | h3(id=type.name) 75 | a(href='#' + type.name)= type.name 76 | if type.name == 'Advanced Configuration' 77 | div 78 | small. 79 | The following are examples of using the `raw` parameter 80 | to pass a JSON serialized config object to have custom 81 | design or configuration for your chart. The following 82 | `raw` param has URI-encoded JSON within a GET request, 83 | but you can also pass this via a POST request as well. 84 | See the #[a(href=githubLink) docs] for more information. 85 | div.row 86 | each example in type.examples 87 | div.col-xs-12(class=(type.name == 'Advanced Configuration') ? 'col-md-12' : 'col-md-4') 88 | div.card.example.margin-vert-medium 89 | - //div.card-img-top.chart-example(style="background-image:url('" + example.url + "')") 90 | img.card-img-top.chart-example(class=(type.name == 'Advanced Configuration') ? 'large-img-top' : '',src=example.url) 91 | div.card-block 92 | a(href=example.url) 93 | div.card-title 94 | div= example.name 95 | p.card-text 96 | div.d-flex.flex-column.justify-content-center.align-items-center 97 | code 98 | i.fa.fa-hand-pointer-o 99 | span.no-underline= ' ' + example.url 100 | --------------------------------------------------------------------------------