From fc0f27ff314f783713c3497d625a3f8992038512 Mon Sep 17 00:00:00 2001 From: plouc Date: Thu, 16 Dec 2021 08:16:55 +0900 Subject: [PATCH] feat(api): create new @nivo/static & @nivo/express packages --- .gitignore | 3 +- Procfile | 2 +- api/app.ts | 44 +++++++++ api/misc/README.mustache | 53 ---------- api/misc/readmeData.js | 20 ---- api/nodemon.json | 6 +- api/package.json | 36 +------ api/src/app.ts | 98 ------------------- api/src/mapping/index.ts | 32 ------ api/tsconfig.json | 9 +- packages/express/LICENSE.md | 19 ++++ packages/express/README.md | 3 + packages/express/package.json | 39 ++++++++ packages/express/src/index.ts | 57 +++++++++++ .../express/src/memory-storage.ts | 2 +- .../express/src}/validation.ts | 15 +-- packages/express/tsconfig.json | 8 ++ packages/static/LICENSE.md | 19 ++++ packages/static/README.md | 3 + packages/static/package.json | 48 +++++++++ packages/static/src/index.ts | 3 + .../static/src/mappings}/bar.ts | 6 +- .../static/src/mappings}/calendar.ts | 6 +- .../static/src/mappings}/chord.ts | 8 +- .../static/src/mappings}/circle_packing.ts | 6 +- .../static/src/mappings}/common.ts | 24 +++-- .../static/src/mappings}/commons/colors.ts | 0 .../static/src/mappings}/commons/curves.ts | 0 .../src/mappings}/commons/dimensions.ts | 0 .../static/src/mappings}/commons/scales.ts | 0 .../static/src/mappings}/heatmap.ts | 6 +- packages/static/src/mappings/index.ts | 43 ++++++++ .../static/src/mappings}/line.ts | 8 +- .../static/src/mappings}/pie.ts | 6 +- .../static/src/mappings}/radar.ts | 8 +- .../static/src/mappings}/sankey.ts | 6 +- .../static/src/mappings}/sunburst.ts | 6 +- .../static/src/mappings}/treemap.ts | 6 +- .../lib => packages/static/src}/renderer.ts | 11 ++- {api => packages/static}/src/samples/index.ts | 20 ++-- {api/src/lib => packages/static/src}/types.ts | 0 packages/static/tsconfig.json | 8 ++ tsconfig.monorepo.json | 6 +- yarn.lock | 2 +- 44 files changed, 381 insertions(+), 324 deletions(-) create mode 100644 api/app.ts delete mode 100644 api/misc/README.mustache delete mode 100644 api/misc/readmeData.js delete mode 100644 api/src/app.ts delete mode 100644 api/src/mapping/index.ts create mode 100644 packages/express/LICENSE.md create mode 100644 packages/express/README.md create mode 100644 packages/express/package.json create mode 100644 packages/express/src/index.ts rename api/src/lib/storage.ts => packages/express/src/memory-storage.ts (88%) rename {api/src/lib => packages/express/src}/validation.ts (64%) create mode 100644 packages/express/tsconfig.json create mode 100644 packages/static/LICENSE.md create mode 100644 packages/static/README.md create mode 100644 packages/static/package.json create mode 100644 packages/static/src/index.ts rename {api/src/mapping => packages/static/src/mappings}/bar.ts (96%) rename {api/src/mapping => packages/static/src/mappings}/calendar.ts (95%) rename {api/src/mapping => packages/static/src/mappings}/chord.ts (93%) rename {api/src/mapping => packages/static/src/mappings}/circle_packing.ts (93%) rename {api/src/mapping => packages/static/src/mappings}/common.ts (72%) rename {api/src/mapping => packages/static/src/mappings}/commons/colors.ts (100%) rename {api/src/mapping => packages/static/src/mappings}/commons/curves.ts (100%) rename {api/src/mapping => packages/static/src/mappings}/commons/dimensions.ts (100%) rename {api/src/mapping => packages/static/src/mappings}/commons/scales.ts (100%) rename {api/src/mapping => packages/static/src/mappings}/heatmap.ts (94%) create mode 100644 packages/static/src/mappings/index.ts rename {api/src/mapping => packages/static/src/mappings}/line.ts (96%) rename {api/src/mapping => packages/static/src/mappings}/pie.ts (96%) rename {api/src/mapping => packages/static/src/mappings}/radar.ts (93%) rename {api/src/mapping => packages/static/src/mappings}/sankey.ts (96%) rename {api/src/mapping => packages/static/src/mappings}/sunburst.ts (93%) rename {api/src/mapping => packages/static/src/mappings}/treemap.ts (95%) rename {api/src/lib => packages/static/src}/renderer.ts (70%) rename {api => packages/static}/src/samples/index.ts (93%) rename {api/src/lib => packages/static/src}/types.ts (100%) create mode 100644 packages/static/tsconfig.json diff --git a/.gitignore b/.gitignore index b4e363b543..dd71256bf9 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,5 @@ build *.lerna_backup -/stats \ No newline at end of file +/stats +/api/app.js \ No newline at end of file diff --git a/Procfile b/Procfile index ca9e686335..eeee7260c9 100644 --- a/Procfile +++ b/Procfile @@ -1 +1 @@ -web: node api/build/app.js \ No newline at end of file +web: node api/app.js \ No newline at end of file diff --git a/api/app.ts b/api/app.ts new file mode 100644 index 0000000000..8e09343176 --- /dev/null +++ b/api/app.ts @@ -0,0 +1,44 @@ +import express from 'express' +import cors from 'cors' +import bodyParser from 'body-parser' +import compression from 'compression' +import winston from 'winston' +import expressWinston from 'express-winston' +import { nivo } from '@nivo/express' + +const app = express() + +app.enable('trust proxy') +app.set('json spaces', 4) +app.use(cors()) +app.use(bodyParser.json()) +app.use( + expressWinston.logger({ + transports: [new winston.transports.Console()], + format: winston.format.combine(winston.format.colorize(), winston.format.simple()), + meta: false, + expressFormat: true, + colorize: true, + }) +) +app.use(compression()) + +app.get('/status', (req, res) => { + res.status(200).json({ + status: 'ok', + uptime: `${process.uptime()} second(s)`, + protocol: req.protocol, + host: req.get('host'), + env: { + // @ts-ignore + NODE_ENV: process.NODE_ENV, + }, + }) +}) + +app.use('/nivo', nivo) + +const port = process.env.PORT || 3030 +app.listen(port, () => { + console.log(`nivo api listening on ${port}`) +}) diff --git a/api/misc/README.mustache b/api/misc/README.mustache deleted file mode 100644 index 9260e05461..0000000000 --- a/api/misc/README.mustache +++ /dev/null @@ -1,53 +0,0 @@ -# nivo-api - -Rendering API for [nivo](https://github.com/plouc/nivo) dataviz React/d3 components. - -A [demo](https://nivo-api.herokuapp.com/) is available on heroku, but may not respond depending on usage. - -## How it works - -The API expose some of the [nivo](https://github.com/plouc/nivo) charts by using -[React server side environment](https://facebook.github.io/react/docs/environments.html). - -First you will have to make a post request on the desired endpoint, for example: - -```sh -curl -X POST \ - --header 'Content-Type: application/json' \ - --header 'Accept: application/json' \ - -d '{ "width": 500, "height": 500, "data": [[223, 299, 345, 184], [123, 248, 65, 123], [412, 76, 187, 312], [97, 37, 502, 176]]}' \ - 'http://localhost:3030/charts/chord' - -{ - "id": "73633fea-160e-4118-a534-377c3ed85254", - "url": "http://localhost:3000/r/73633fea-160e-4118-a534-377c3ed85254" -} -``` - -The response contains a link to the chart - -``` -GET http://localhost:3000/r/73633fea-160e-4118-a534-377c3ed85254 -``` - -## Charts endpoints - -``` -{{#endpoints}} -POST {{{.}}} -{{/endpoints}} -``` - -## Charts samples - -{{#samples}} -- https://nivo-api.herokuapp.com/samples/{{{.}}}.svg -{{/samples}} - -## Repositories - -- [nivo](https://github.com/plouc/nivo) - the nivo library -- [nivo-api](https://github.com/plouc/nivo-api) - the nivo http api -- [nivo-api-docker](https://github.com/plouc/nivo-api-docker) - a Docker image for the nivo http api -- [nivo-generators](https://github.com/plouc/nivo-generators) - the data generators used for nivo-website and http API samples -- [nivo-website](https://github.com/plouc/nivo-website) - the source for the nivo website diff --git a/api/misc/readmeData.js b/api/misc/readmeData.js deleted file mode 100644 index 803dd4a0d0..0000000000 --- a/api/misc/readmeData.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict' - -const _ = require('lodash') -const mapping = require('../src/mapping') -const samples = require('../src/samples') - -const data = { - endpoints: [], - samples: [], -} - -_.forOwn(mapping, (config, type) => { - data.endpoints.push(`/charts/${type}`) -}) - -_.forOwn(samples, (config, id) => { - data.samples.push(id) -}) - -console.log(JSON.stringify(data)) diff --git a/api/nodemon.json b/api/nodemon.json index d014886e8d..3e4e0fa375 100644 --- a/api/nodemon.json +++ b/api/nodemon.json @@ -1,12 +1,12 @@ { "restartable": "rs", - "ignore": ["node_modules/", "dist/", "coverage/"], - "watch": ["src/"], + "ignore": ["node_modules/"], + "watch": ["app.ts"], "execMap": { "ts": "node -r ts-node/register" }, "env": { "NODE_ENV": "development" }, - "ext": "js,json,ts" + "ext": "ts" } \ No newline at end of file diff --git a/api/package.json b/api/package.json index 56628a60b4..1058a68b5b 100644 --- a/api/package.json +++ b/api/package.json @@ -1,6 +1,7 @@ { "name": "@nivo/api", "version": "0.74.1", + "private": true, "description": "Rendering API for nivo dataviz React/d3 components", "main": "src/app.js", "licenses": [ @@ -18,56 +19,29 @@ "node": "16.x" }, "devDependencies": { - "mustache": "^2.3.0", "nodemon": "^1.18.10" }, "dependencies": { - "@nivo/bar": "0.74.1", - "@nivo/bullet": "0.74.1", - "@nivo/calendar": "0.74.1", - "@nivo/chord": "0.74.1", - "@nivo/circle-packing": "0.74.1", - "@nivo/core": "0.74.1", - "@nivo/generators": "0.74.0", - "@nivo/heatmap": "0.74.1", - "@nivo/legends": "0.74.1", - "@nivo/line": "0.74.1", - "@nivo/parallel-coordinates": "0.74.1", - "@nivo/pie": "0.74.1", - "@nivo/radar": "0.74.1", - "@nivo/sankey": "0.74.1", - "@nivo/scatterplot": "0.74.1", - "@nivo/stream": "0.74.1", - "@nivo/sunburst": "0.74.1", - "@nivo/treemap": "0.74.1", - "@nivo/voronoi": "0.74.1", - "@nivo/waffle": "0.74.1", + "@nivo/express": "0.74.1", "@types/body-parser": "^1.19.2", "@types/cors": "^2.8.12", "@types/express": "^4.17.13", "@types/node": "^16.11.12", - "@types/react": "^17.0.37", - "@types/react-dom": "^17.0.11", - "@types/uuid": "^8.3.3", "body-parser": "^1.17.2", "compression": "^1.7.0", "cors": "2.8.5", "express": "4.17.1", "express-winston": "4.2.0", - "joi": "17.5.0", - "lodash": "^4.17.21", "react": "^17.0.2", "react-dom": "^17.0.2", "ts-node": "^10.4.0", "typescript": "^4.5.4", - "uuid": "^3.1.0", "winston": "3.3.3" }, "scripts": { - "readme": "node misc/readmeData.js | mustache - misc/README.mustache > README.md", - "start": "node build/app.ts", - "dev": "nodemon src/app.ts", - "build": "rm -rf build && tsc" + "start": "yarn build && node app.js", + "dev": "nodemon app.ts", + "build": "tsc" }, "publishConfig": { "access": "public" diff --git a/api/src/app.ts b/api/src/app.ts deleted file mode 100644 index 6b00ffc750..0000000000 --- a/api/src/app.ts +++ /dev/null @@ -1,98 +0,0 @@ -import express from 'express' -import { forOwn } from 'lodash' -import * as uuid from 'uuid' -import cors from 'cors' -import bodyParser from 'body-parser' -import compression from 'compression' -import winston from 'winston' -import expressWinston from 'express-winston' -import { renderChart } from './lib/renderer' -import * as storage from './lib/storage' -import { validate } from './lib/validation' -import samples from './samples' -import { chartsMapping, ChartType } from './mapping' - -const app = express() - -app.enable('trust proxy') -app.set('json spaces', 4) -app.use(cors()) -app.use(bodyParser.json()) -app.use( - expressWinston.logger({ - transports: [new winston.transports.Console()], - format: winston.format.combine(winston.format.colorize(), winston.format.simple()), - meta: false, - expressFormat: true, - colorize: true, - }) -) -app.use(compression()) - -app.get('/status', (req, res) => { - res.status(200).json({ - status: 'ok', - uptime: `${process.uptime()} second(s)`, - protocol: req.protocol, - host: req.get('host'), - env: { - // @ts-ignore - NODE_ENV: process.NODE_ENV, - }, - }) -}) - -forOwn(chartsMapping, ({ schema }, type: ChartType) => { - app.post(`/charts/${type}`, validate(schema), (req, res) => { - // @ts-ignore - const props = req.payload - const id = uuid.v4() - const url = `${req.protocol}://${req.get('host')}/r/${id}` - - storage.set(id, { - type, - props, - url, - }) - - res.status(201).json({ id, url }) - }) -}) - -app.get('/r', (req, res) => { - res.status(200).json(storage.dump()) -}) - -app.get('/r/:id', (req, res) => { - const { id } = req.params - - const config = storage.get(req.params.id) - if (!config) { - return res.status(404).send(`no chart found for id "${id}"`) - } - - const rendered = renderChart(config, req.query) - - res.set('Content-Type', 'image/svg+xml').status(200).send(rendered) -}) - -app.get('/samples', (req, res) => { - res.status(200).json({ - samples: Object.keys(samples).map(sample => { - return `${req.protocol}://${req.get('host')}/samples/${sample}.svg` - }), - }) -}) - -forOwn(samples, (config, id) => { - app.get(`/samples/${id}.svg`, (req, res) => { - const rendered = renderChart(config, req.query) - - res.set('Content-Type', 'image/svg+xml').status(200).send(rendered) - }) -}) - -const port = process.env.PORT || 3030 -app.listen(port, () => { - console.log(`nivo api listening on ${port}`) -}) diff --git a/api/src/mapping/index.ts b/api/src/mapping/index.ts deleted file mode 100644 index 0c50a44aa7..0000000000 --- a/api/src/mapping/index.ts +++ /dev/null @@ -1,32 +0,0 @@ -import bar from './bar' -import calendar from './calendar' -import chord from './chord' -import circle_packing from './circle_packing' -import heatmap from './heatmap' -import line from './line' -import pie from './pie' -import radar from './radar' -import sankey from './sankey' -import sunburst from './sunburst' -import treemap from './treemap' -import { FunctionComponent } from 'react' - -export const chartsMapping = { - bar, - circle_packing, - calendar, - chord, - heatmap, - line, - pie, - radar, - sankey, - sunburst, - treemap, -} as const - -type ExtractProps = T extends FunctionComponent ? P : never - -export type ChartType = keyof typeof chartsMapping -export type ChartComponent = typeof chartsMapping[T]['component'] -export type ChartProps = ExtractProps> diff --git a/api/tsconfig.json b/api/tsconfig.json index afecb8b9d7..b245d79a69 100644 --- a/api/tsconfig.json +++ b/api/tsconfig.json @@ -1,11 +1,12 @@ { "compilerOptions": { - "module": "commonjs", "target": "es5", - "rootDir": "./src", - "outDir": "build", + "rootDir": ".", + "module": "commonjs", + "moduleResolution": "node", "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true - } + }, + "include": ["*.ts"] } \ No newline at end of file diff --git a/packages/express/LICENSE.md b/packages/express/LICENSE.md new file mode 100644 index 0000000000..faa45389ec --- /dev/null +++ b/packages/express/LICENSE.md @@ -0,0 +1,19 @@ +Copyright (c) Raphaël Benitte + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/packages/express/README.md b/packages/express/README.md new file mode 100644 index 0000000000..062c91111a --- /dev/null +++ b/packages/express/README.md @@ -0,0 +1,3 @@ +# `@nivo/express` + +[![version](https://img.shields.io/npm/v/@nivo/express.svg?style=flat-square)](https://www.npmjs.com/package/@nivo/express) diff --git a/packages/express/package.json b/packages/express/package.json new file mode 100644 index 0000000000..1914891cd1 --- /dev/null +++ b/packages/express/package.json @@ -0,0 +1,39 @@ +{ + "name": "@nivo/express", + "version": "0.74.1", + "license": "MIT", + "author": { + "name": "Raphaël Benitte", + "url": "https://github.com/plouc" + }, + "repository": { + "type": "git", + "url": "https://github.com/plouc/nivo.git", + "directory": "packages/express" + }, + "main": "./dist/nivo-express.cjs.js", + "module": "./dist/nivo-express.es.js", + "typings": "./dist/types/index.d.ts", + "files": [ + "README.md", + "LICENSE.md", + "dist/", + "!dist/tsconfig.tsbuildinfo" + ], + "dependencies": { + "@nivo/static": "0.74.1", + "@types/express": "^4.17.13", + "@types/uuid": "^8.3.3", + "express": "^4.17.1", + "joi": "^17.5.0", + "lodash": "^4.17.21", + "uuid": "^3.1.0" + }, + "peerDependencies": { + "react": ">= 16.14.0 < 18.0.0", + "react-dom": ">= 16.14.0 < 18.0.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/express/src/index.ts b/packages/express/src/index.ts new file mode 100644 index 0000000000..faf6a3dba1 --- /dev/null +++ b/packages/express/src/index.ts @@ -0,0 +1,57 @@ +import { Router } from 'express' +import * as uuid from 'uuid' +import { forOwn } from 'lodash' +import { chartsMapping, ChartType, renderChart, samples } from '@nivo/static' +import { validate } from './validation' +import * as storage from './memory-storage' + +export const nivo = Router() + +nivo.get('/', (req, res) => { + res.status(200).json({ + samples: Object.keys(samples).map(sample => { + return `${req.protocol}://${req.get('host')}${req.originalUrl}/samples/${sample}.svg` + }), + }) +}) + +// @ts-ignore +forOwn(chartsMapping, ({ schema }, type: ChartType) => { + nivo.post(`/charts/${type}`, validate(schema), (req, res) => { + // @ts-ignore + const props = req.payload + const id = uuid.v4() + const url = `${req.protocol}://${req.get('host')}/r/${id}` + + storage.set(id, { + type, + props, + url, + }) + + res.status(201).json({ id, url }) + }) +}) + +nivo.get('/r/:id', (req, res) => { + const { id } = req.params + + const config = storage.get(req.params.id) + if (!config) { + return res.status(404).send(`no chart found for id "${id}"`) + } + + // @ts-ignore + const rendered = renderChart(config, req.query) + + res.set('Content-Type', 'image/svg+xml').status(200).send(rendered) +}) + +forOwn(samples, (config, id) => { + nivo.get(`/samples/${id}.svg`, (req, res) => { + // @ts-ignore + const rendered = renderChart(config, req.query) + + res.set('Content-Type', 'image/svg+xml').status(200).send(rendered) + }) +}) diff --git a/api/src/lib/storage.ts b/packages/express/src/memory-storage.ts similarity index 88% rename from api/src/lib/storage.ts rename to packages/express/src/memory-storage.ts index 2400e2b8ea..1ffe42f582 100644 --- a/api/src/lib/storage.ts +++ b/packages/express/src/memory-storage.ts @@ -1,4 +1,4 @@ -import { ChartProps, ChartType } from '../mapping' +import { ChartProps, ChartType } from '@nivo/static' export interface StorageEntry { type: T diff --git a/api/src/lib/validation.ts b/packages/express/src/validation.ts similarity index 64% rename from api/src/lib/validation.ts rename to packages/express/src/validation.ts index ff5ff92f54..aedcb4fe98 100644 --- a/api/src/lib/validation.ts +++ b/packages/express/src/validation.ts @@ -1,29 +1,32 @@ +import { Request, Response, NextFunction } from 'express' import { omit } from 'lodash' -import joi from 'joi' +import Joi from 'joi' export const validate = ( - schema: joi.Schema, + schema: Joi.Schema, options: { omit?: string[] } = {} ) => { const { omit: omitProps } = options - return (req, res, next) => { + return (req: Request, res: Response, next: NextFunction) => { let data = req.body if (omit) { + // @ts-ignore data = omit(data, omitProps) } try { - req.payload = joi.attempt(data, schema, null, { + // @ts-ignore + req.payload = schema.validate(data, { abortEarly: true, convert: true, }) next() - } catch (err) { - console.log('ERROR', err) + } catch (err: any) { return res.status(400).json({ + // @ts-ignore errors: err.details.map(({ message, path }) => { return `${message}${path ? ` (${path})` : ''}` }), diff --git a/packages/express/tsconfig.json b/packages/express/tsconfig.json new file mode 100644 index 0000000000..855b4b2b74 --- /dev/null +++ b/packages/express/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.types.json", + "compilerOptions": { + "outDir": "./dist/types", + "rootDir": "./src" + }, + "include": ["src/**/*"] +} diff --git a/packages/static/LICENSE.md b/packages/static/LICENSE.md new file mode 100644 index 0000000000..faa45389ec --- /dev/null +++ b/packages/static/LICENSE.md @@ -0,0 +1,19 @@ +Copyright (c) Raphaël Benitte + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/packages/static/README.md b/packages/static/README.md new file mode 100644 index 0000000000..d682116829 --- /dev/null +++ b/packages/static/README.md @@ -0,0 +1,3 @@ +# `@nivo/static` + +[![version](https://img.shields.io/npm/v/@nivo/static.svg?style=flat-square)](https://www.npmjs.com/package/@nivo/static) diff --git a/packages/static/package.json b/packages/static/package.json new file mode 100644 index 0000000000..129cad0444 --- /dev/null +++ b/packages/static/package.json @@ -0,0 +1,48 @@ +{ + "name": "@nivo/static", + "version": "0.74.1", + "license": "MIT", + "author": { + "name": "Raphaël Benitte", + "url": "https://github.com/plouc" + }, + "repository": { + "type": "git", + "url": "https://github.com/plouc/nivo.git", + "directory": "packages/static" + }, + "main": "./dist/nivo-static.cjs.js", + "module": "./dist/nivo-static.es.js", + "typings": "./dist/types/index.d.ts", + "files": [ + "README.md", + "LICENSE.md", + "dist/", + "!dist/tsconfig.tsbuildinfo" + ], + "dependencies": { + "@nivo/bar": "0.74.1", + "@nivo/calendar": "0.74.1", + "@nivo/chord": "0.74.1", + "@nivo/circle-packing": "0.74.1", + "@nivo/core": "0.74.1", + "@nivo/generators": "0.74.0", + "@nivo/heatmap": "0.74.1", + "@nivo/legends": "0.74.1", + "@nivo/line": "0.74.1", + "@nivo/pie": "0.74.1", + "@nivo/radar": "0.74.1", + "@nivo/sankey": "0.74.1", + "@nivo/sunburst": "0.74.1", + "@nivo/treemap": "0.74.1", + "joi": "^17.5.0", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "react": ">= 16.14.0 < 18.0.0", + "react-dom": ">= 16.14.0 < 18.0.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/static/src/index.ts b/packages/static/src/index.ts new file mode 100644 index 0000000000..a8c2ccc425 --- /dev/null +++ b/packages/static/src/index.ts @@ -0,0 +1,3 @@ +export * from './mappings' +export { renderChart } from './renderer' +export { samples } from './samples' diff --git a/api/src/mapping/bar.ts b/packages/static/src/mappings/bar.ts similarity index 96% rename from api/src/mapping/bar.ts rename to packages/static/src/mappings/bar.ts index 1ebae4ec21..cba3718d19 100644 --- a/api/src/mapping/bar.ts +++ b/packages/static/src/mappings/bar.ts @@ -1,7 +1,7 @@ import { FunctionComponent } from 'react' import Joi from 'joi' import { Bar, BarSvgProps } from '@nivo/bar' -import { OmitStrict } from '../lib/types' +import { OmitStrict } from '../types' import { custom, axes } from './common' import { dimensions } from './commons/dimensions' import { inheritedColor, ordinalColors } from './commons/colors' @@ -30,7 +30,7 @@ export type BarApiProps = OmitStrict< | 'initialHiddenIds' > -const barMapping = { +export const barMapping = { component: Bar as FunctionComponent, schema: Joi.object().keys({ width: dimensions.width, @@ -85,5 +85,3 @@ const barMapping = { margin: { top: 40, right: 50, bottom: 40, left: 50 }, }, } - -export default barMapping diff --git a/api/src/mapping/calendar.ts b/packages/static/src/mappings/calendar.ts similarity index 95% rename from api/src/mapping/calendar.ts rename to packages/static/src/mappings/calendar.ts index 08b143961d..1eba6a4094 100644 --- a/api/src/mapping/calendar.ts +++ b/packages/static/src/mappings/calendar.ts @@ -3,14 +3,14 @@ import Joi from 'joi' import { Calendar, CalendarSvgProps } from '@nivo/calendar' import { custom } from './common' import { dimensions } from './commons/dimensions' -import { OmitStrict } from '../lib/types' +import { OmitStrict } from '../types' export type CalendarApiProps = OmitStrict< CalendarSvgProps, 'isInteractive' | 'onClick' | 'tooltip' | 'renderWrapper' | 'role' > -const calendarMapping = { +export const calendarMapping = { component: Calendar as FunctionComponent, schema: Joi.object().keys({ data: custom.array().min(1).required(), @@ -57,5 +57,3 @@ const calendarMapping = { margin: { top: 40, right: 50, bottom: 40, left: 50 }, }, } - -export default calendarMapping diff --git a/api/src/mapping/chord.ts b/packages/static/src/mappings/chord.ts similarity index 93% rename from api/src/mapping/chord.ts rename to packages/static/src/mappings/chord.ts index a05d185eae..4132f2c24d 100644 --- a/api/src/mapping/chord.ts +++ b/packages/static/src/mappings/chord.ts @@ -2,10 +2,10 @@ import { FunctionComponent } from 'react' import Joi from 'joi' import { Dimensions } from '@nivo/core' import { Chord, ChordProps } from '@nivo/chord' -import { custom, blendMode } from './common' +import { custom } from './common' import { ordinalColors, inheritedColor } from './commons/colors' import { dimensions } from './commons/dimensions' -import { OmitStrict } from '../lib/types' +import { OmitStrict } from '../types' // filter out all dynamic properties export type ChordApiProps = OmitStrict< @@ -31,7 +31,7 @@ export type ChordApiProps = OmitStrict< | 'layers' > -const chordMapping = { +export const chordMapping = { component: Chord as unknown as FunctionComponent, schema: Joi.object().keys({ width: dimensions.width, @@ -75,5 +75,3 @@ const chordMapping = { margin: { top: 0, right: 0, bottom: 0, left: 0 }, }, } - -export default chordMapping diff --git a/api/src/mapping/circle_packing.ts b/packages/static/src/mappings/circle_packing.ts similarity index 93% rename from api/src/mapping/circle_packing.ts rename to packages/static/src/mappings/circle_packing.ts index 6d87b5682a..3dab9d976b 100644 --- a/api/src/mapping/circle_packing.ts +++ b/packages/static/src/mappings/circle_packing.ts @@ -4,7 +4,7 @@ import { CirclePacking, CirclePackingSvgProps } from '@nivo/circle-packing' import { custom } from './common' import { dimensions } from './commons/dimensions' import { inheritedColor, ordinalColors } from './commons/colors' -import { OmitStrict } from '../lib/types' +import { OmitStrict } from '../types' export type CirclePackingApiProps = OmitStrict< CirclePackingSvgProps, @@ -18,7 +18,7 @@ export type CirclePackingApiProps = OmitStrict< | 'motionConfig' > -const circlePackingMapping = { +export const circlePackingMapping = { component: CirclePacking as FunctionComponent, schema: Joi.object().keys({ data: custom.object().required(), @@ -48,5 +48,3 @@ const circlePackingMapping = { margin: { top: 0, right: 0, bottom: 0, left: 0 }, }, } - -export default circlePackingMapping diff --git a/api/src/mapping/common.ts b/packages/static/src/mappings/common.ts similarity index 72% rename from api/src/mapping/common.ts rename to packages/static/src/mappings/common.ts index debbe467a0..57e7a25c0e 100644 --- a/api/src/mapping/common.ts +++ b/packages/static/src/mappings/common.ts @@ -6,12 +6,14 @@ export const custom = Joi.extend( base: Joi.array(), coerce: { from: 'string', - method: (value, helpers) => { - if (typeof value !== 'string') return + method: (value: any) => { + if (typeof value !== 'string') return {} try { return { value: JSON.parse(value) } - } catch (ignoreErr) {} // eslint-disable-line no-empty + } catch (ignoreErr) { + return {} + } }, }, }, @@ -20,12 +22,14 @@ export const custom = Joi.extend( base: Joi.object(), coerce: { from: 'string', - method: (value, helpers) => { - if (typeof value !== 'string') return + method: (value: any) => { + if (typeof value !== 'string') return {} try { return { value: JSON.parse(value) } - } catch (ignoreErr) {} // eslint-disable-line no-empty + } catch (ignoreErr) { + return {} + } }, }, } @@ -46,10 +50,10 @@ export const axis = Joi.object() .allow(null) export const axes = { - axisTop: exports.axis, - axisRight: exports.axis, - axisBottom: exports.axis, - axisLeft: exports.axis, + axisTop: axis, + axisRight: axis, + axisBottom: axis, + axisLeft: axis, } export const blendMode = Joi.valid( diff --git a/api/src/mapping/commons/colors.ts b/packages/static/src/mappings/commons/colors.ts similarity index 100% rename from api/src/mapping/commons/colors.ts rename to packages/static/src/mappings/commons/colors.ts diff --git a/api/src/mapping/commons/curves.ts b/packages/static/src/mappings/commons/curves.ts similarity index 100% rename from api/src/mapping/commons/curves.ts rename to packages/static/src/mappings/commons/curves.ts diff --git a/api/src/mapping/commons/dimensions.ts b/packages/static/src/mappings/commons/dimensions.ts similarity index 100% rename from api/src/mapping/commons/dimensions.ts rename to packages/static/src/mappings/commons/dimensions.ts diff --git a/api/src/mapping/commons/scales.ts b/packages/static/src/mappings/commons/scales.ts similarity index 100% rename from api/src/mapping/commons/scales.ts rename to packages/static/src/mappings/commons/scales.ts diff --git a/api/src/mapping/heatmap.ts b/packages/static/src/mappings/heatmap.ts similarity index 94% rename from api/src/mapping/heatmap.ts rename to packages/static/src/mappings/heatmap.ts index 7f19ac2ac3..36c323d4d6 100644 --- a/api/src/mapping/heatmap.ts +++ b/packages/static/src/mappings/heatmap.ts @@ -6,7 +6,7 @@ import { custom } from './common' import { dimensions } from './commons/dimensions' import { inheritedColor } from './commons/colors' import { axes } from './common' -import { OmitStrict } from '../lib/types' +import { OmitStrict } from '../types' export type HeatMapApiProps = OmitStrict< HeatMapSvgProps & Dimensions, @@ -18,7 +18,7 @@ export type HeatMapApiProps = OmitStrict< | 'animate' > -const heatMapMapping = { +export const heatmapMapping = { component: HeatMap as unknown as FunctionComponent, schema: Joi.object().keys({ data: custom.array().min(1).required(), @@ -54,5 +54,3 @@ const heatMapMapping = { margin: { top: 60, right: 0, bottom: 0, left: 60 }, }, } - -export default heatMapMapping diff --git a/packages/static/src/mappings/index.ts b/packages/static/src/mappings/index.ts new file mode 100644 index 0000000000..8894892880 --- /dev/null +++ b/packages/static/src/mappings/index.ts @@ -0,0 +1,43 @@ +import { FunctionComponent } from 'react' +import { barMapping } from './bar' +import { circlePackingMapping } from './circle_packing' +import { calendarMapping } from './calendar' +import { chordMapping } from './chord' +import { heatmapMapping } from './heatmap' +import { lineMapping } from './line' +import { pieMapping } from './pie' +import { radarMapping } from './radar' +import { sankeyMapping } from './sankey' +import { sunburstMapping } from './sunburst' +import { treemapMapping } from './treemap' + +export const chartsMapping = { + bar: barMapping, + circle_packing: circlePackingMapping, + calendar: calendarMapping, + chord: chordMapping, + heatmap: heatmapMapping, + line: lineMapping, + pie: pieMapping, + radar: radarMapping, + sankey: sankeyMapping, + sunburst: sunburstMapping, + treemap: treemapMapping, +} as const + +type ExtractProps = T extends FunctionComponent ? P : never + +export type ChartType = keyof typeof chartsMapping +export type ChartComponent = typeof chartsMapping[T]['component'] +export type ChartProps = ExtractProps> + +export * from './bar' +export * from './calendar' +export * from './chord' +export * from './circle_packing' +export * from './heatmap' +export * from './line' +export * from './pie' +export * from './radar' +export * from './sankey' +export * from './treemap' diff --git a/api/src/mapping/line.ts b/packages/static/src/mappings/line.ts similarity index 96% rename from api/src/mapping/line.ts rename to packages/static/src/mappings/line.ts index 04131ee348..3e655766cb 100644 --- a/api/src/mapping/line.ts +++ b/packages/static/src/mappings/line.ts @@ -4,10 +4,10 @@ import { Line, LineSvgProps } from '@nivo/line' // @ts-ignore import { curvePropKeys } from '@nivo/core' import { custom, axes, blendMode } from './common' -import { scale } from './commons/scales' +// import { scale } from './commons/scales' import { ordinalColors, inheritedColor } from './commons/colors' import { dimensions } from './commons/dimensions' -import { OmitStrict } from '../lib/types' +import { OmitStrict } from '../types' import { FunctionComponent } from 'react' export type LineApiProps = OmitStrict< @@ -30,7 +30,7 @@ export type LineApiProps = OmitStrict< | 'motionConfig' > -const lineMapping = { +export const lineMapping = { component: Line as unknown as FunctionComponent, schema: Joi.object().keys({ data: custom @@ -109,5 +109,3 @@ const lineMapping = { margin: { top: 40, right: 50, bottom: 40, left: 50 }, }, } - -export default lineMapping diff --git a/api/src/mapping/pie.ts b/packages/static/src/mappings/pie.ts similarity index 96% rename from api/src/mapping/pie.ts rename to packages/static/src/mappings/pie.ts index 9ab040b781..f1d83e4cd2 100644 --- a/api/src/mapping/pie.ts +++ b/packages/static/src/mappings/pie.ts @@ -3,7 +3,7 @@ import Joi from 'joi' import { Pie, PieSvgProps } from '@nivo/pie' import { ordinalColors, inheritedColor } from './commons/colors' import { dimensions } from './commons/dimensions' -import { OmitStrict } from '../lib/types' +import { OmitStrict } from '../types' import { custom } from './common' export type PieApiProps = OmitStrict< @@ -23,7 +23,7 @@ export type PieApiProps = OmitStrict< | 'renderWrapper' > -const pieMapping = { +export const pieMapping = { component: Pie as FunctionComponent, schema: Joi.object().keys({ data: custom.array().min(1).required(), @@ -68,5 +68,3 @@ const pieMapping = { margin: { top: 40, right: 50, bottom: 40, left: 50 }, }, } - -export default pieMapping diff --git a/api/src/mapping/radar.ts b/packages/static/src/mappings/radar.ts similarity index 93% rename from api/src/mapping/radar.ts rename to packages/static/src/mappings/radar.ts index 253ddcccf6..49705cf399 100644 --- a/api/src/mapping/radar.ts +++ b/packages/static/src/mappings/radar.ts @@ -5,14 +5,14 @@ import { blendMode, custom } from './common' import { ordinalColors, inheritedColor } from './commons/colors' import { dimensions } from './commons/dimensions' import { closedCurve } from './commons/curves' -import { OmitStrict } from '../lib/types' +import { OmitStrict } from '../types' -type RadarApiProps = OmitStrict< +export type RadarApiProps = OmitStrict< RadarSvgProps, 'renderWrapper' | 'layers' | 'isInteractive' | 'sliceTooltip' | 'animate' | 'motionConfig' > -const radarMapping = { +export const radarMapping = { component: Radar as FunctionComponent, schema: Joi.object().keys({ data: custom.array().min(1).required(), @@ -50,5 +50,3 @@ const radarMapping = { margin: { top: 40, right: 40, bottom: 40, left: 40 }, }, } - -export default radarMapping diff --git a/api/src/mapping/sankey.ts b/packages/static/src/mappings/sankey.ts similarity index 96% rename from api/src/mapping/sankey.ts rename to packages/static/src/mappings/sankey.ts index 52df3e6306..7996caff7a 100644 --- a/api/src/mapping/sankey.ts +++ b/packages/static/src/mappings/sankey.ts @@ -1,7 +1,7 @@ import { FunctionComponent } from 'react' import Joi from 'joi' import { Sankey, SankeySvgProps, sankeyAlignmentPropKeys } from '@nivo/sankey' -import { OmitStrict } from '../lib/types' +import { OmitStrict } from '../types' import { ordinalColors, inheritedColor } from './commons/colors' import { dimensions } from './commons/dimensions' import { blendMode, custom } from './common' @@ -25,7 +25,7 @@ export type SankeyApiProps = OmitStrict< | 'ariaDescribedBy' > -const sankeyMapping = { +export const sankeyMapping = { component: Sankey as FunctionComponent, schema: Joi.object().keys({ width: dimensions.width, @@ -89,5 +89,3 @@ const sankeyMapping = { linkBlendMode: 'normal', }, } - -export default sankeyMapping diff --git a/api/src/mapping/sunburst.ts b/packages/static/src/mappings/sunburst.ts similarity index 93% rename from api/src/mapping/sunburst.ts rename to packages/static/src/mappings/sunburst.ts index 34f3a2ba15..9860768762 100644 --- a/api/src/mapping/sunburst.ts +++ b/packages/static/src/mappings/sunburst.ts @@ -4,7 +4,7 @@ import { Sunburst, SunburstSvgProps } from '@nivo/sunburst' import { custom } from './common' import { ordinalColors, inheritedColor } from './commons/colors' import { dimensions } from './commons/dimensions' -import { OmitStrict } from '../lib/types' +import { OmitStrict } from '../types' export type SunburstApiProps = OmitStrict< SunburstSvgProps, @@ -17,7 +17,7 @@ export type SunburstApiProps = OmitStrict< | 'renderWrapper' > -const sunburstMapping = { +export const sunburstMapping = { component: Sunburst as FunctionComponent, schema: Joi.object().keys({ data: custom.object().required(), @@ -47,5 +47,3 @@ const sunburstMapping = { margin: { top: 0, right: 0, bottom: 0, left: 0 }, }, } - -export default sunburstMapping diff --git a/api/src/mapping/treemap.ts b/packages/static/src/mappings/treemap.ts similarity index 95% rename from api/src/mapping/treemap.ts rename to packages/static/src/mappings/treemap.ts index 45db5a1514..cfb664ba24 100644 --- a/api/src/mapping/treemap.ts +++ b/packages/static/src/mappings/treemap.ts @@ -5,14 +5,14 @@ import { TreeMap, TreeMapSvgProps } from '@nivo/treemap' import { custom } from './common' import { ordinalColors, inheritedColor } from './commons/colors' import { dimensions } from './commons/dimensions' -import { OmitStrict } from '../lib/types' +import { OmitStrict } from '../types' export type TreeMapApiProps = OmitStrict< TreeMapSvgProps & Dimensions, 'isInteractive' | 'onMouseEnter' | 'onMouseMove' | 'onMouseLeave' | 'onClick' | 'animate' > -const treeMapMapping = { +export const treemapMapping = { component: TreeMap as unknown as FunctionComponent, schema: Joi.object().keys({ data: custom.object().required(), @@ -67,5 +67,3 @@ const treeMapMapping = { margin: { top: 0, right: 0, bottom: 0, left: 0 }, }, } - -export default treeMapMapping diff --git a/api/src/lib/renderer.ts b/packages/static/src/renderer.ts similarity index 70% rename from api/src/lib/renderer.ts rename to packages/static/src/renderer.ts index 9b5c77f520..37913fd6ed 100644 --- a/api/src/lib/renderer.ts +++ b/packages/static/src/renderer.ts @@ -1,7 +1,7 @@ import { pick } from 'lodash' -import React, { FunctionComponent } from 'react' +import { createElement } from 'react' import { renderToStaticMarkup } from 'react-dom/server' -import { ChartProps, chartsMapping, ChartType } from '../mapping' +import { ChartProps, chartsMapping, ChartType } from './mappings' const staticProps = { animate: false, @@ -18,10 +18,10 @@ export const renderChart = ( type: T props: ChartProps }, - override + override: Partial> ) => { const chart = chartsMapping[type] - const component = chart.component as FunctionComponent> + const component = chart.component const mergedProps = { ...staticProps, ...chart.defaults, @@ -29,7 +29,8 @@ export const renderChart = ( ...pick(override, chart.runtimeProps || []), } const rendered = renderToStaticMarkup( - React.createElement>(component, mergedProps) + // @ts-ignore + createElement(component, mergedProps) ) return `${rendered}` diff --git a/api/src/samples/index.ts b/packages/static/src/samples/index.ts similarity index 93% rename from api/src/samples/index.ts rename to packages/static/src/samples/index.ts index 8254e92c6b..d1bcf84d73 100644 --- a/api/src/samples/index.ts +++ b/packages/static/src/samples/index.ts @@ -6,12 +6,18 @@ import { generateWinesTastes, generateSankeyData, } from '@nivo/generators' -import { StorageEntry } from '../lib/storage' +import { ChartProps, ChartType, LineApiProps } from '../mappings' const keys = ['hot dogs', 'burgers', 'sandwich', 'kebab', 'fries', 'donut'] const moreKeys = [...keys, 'junk', 'sushi', 'ramen', 'curry', 'udon', 'bagel'] -const samples: Record, 'url'>> = { +export const samples: Record< + string, + { + type: ChartType + props: ChartProps + } +> = { bar: { type: 'bar', props: { @@ -85,7 +91,7 @@ const samples: Record, 'url'>> = { identity: 'country', cumulative: false, curve: 'monotoneX', - }, + } as LineApiProps, }, pie: { type: 'pie', @@ -100,8 +106,6 @@ const samples: Record, 'url'>> = { innerRadius: 0.5, padAngle: 0.5, cornerRadius: 5, - radialLabelsTextColor: 'inherit:darker(1.4)', - radialLabelsLinkColor: 'inherit', margin: { top: 100, right: 100, @@ -130,9 +134,8 @@ const samples: Record, 'url'>> = { height: 800, data: generateSankeyData({ nodeCount: 13, maxIterations: 2 }), colors: { scheme: 'paired' }, - nodePaddingX: 3, nodeOpacity: 1, - nodeWidth: 14, + nodeThickness: 14, nodeBorderWidth: 0, linkOpacity: 0.15, labelPadding: 20, @@ -156,7 +159,6 @@ const samples: Record, 'url'>> = { treemap: { type: 'treemap', props: { - type: 'treemap', width: 800, height: 500, data: generateLibTree(), @@ -169,5 +171,3 @@ const samples: Record, 'url'>> = { }, }, } - -export default samples diff --git a/api/src/lib/types.ts b/packages/static/src/types.ts similarity index 100% rename from api/src/lib/types.ts rename to packages/static/src/types.ts diff --git a/packages/static/tsconfig.json b/packages/static/tsconfig.json new file mode 100644 index 0000000000..855b4b2b74 --- /dev/null +++ b/packages/static/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.types.json", + "compilerOptions": { + "outDir": "./dist/types", + "rootDir": "./src" + }, + "include": ["src/**/*"] +} diff --git a/tsconfig.monorepo.json b/tsconfig.monorepo.json index 11ad01fbe6..cbde2854f8 100644 --- a/tsconfig.monorepo.json +++ b/tsconfig.monorepo.json @@ -33,6 +33,10 @@ { "path": "./packages/scatterplot" }, { "path": "./packages/sunburst" }, { "path": "./packages/stream" }, - { "path": "./packages/swarmplot" } + { "path": "./packages/swarmplot" }, + + // static rendering and express middleware + { "path": "./packages/static" }, + { "path": "./packages/express" } ] } diff --git a/yarn.lock b/yarn.lock index e90e86c49b..0cfdafa4d3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14301,7 +14301,7 @@ jimp@^0.14.0: "@jimp/types" "^0.14.0" regenerator-runtime "^0.13.3" -joi@17.5.0, joi@^17.4.2: +joi@^17.4.2, joi@^17.5.0: version "17.5.0" resolved "https://registry.yarnpkg.com/joi/-/joi-17.5.0.tgz#7e66d0004b5045d971cf416a55fb61d33ac6e011" integrity sha512-R7hR50COp7StzLnDi4ywOXHrBrgNXuUUfJWIR5lPY5Bm/pOD3jZaTwpluUXVLRWcoWZxkrHBBJ5hLxgnlehbdw==