├── .gitignore ├── .nvmrc ├── .prettierrc ├── LICENSE ├── README.md ├── babel.config.js ├── example ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── _redirects │ ├── favicon.ico │ ├── index.html │ └── manifest.json └── src │ ├── App.js │ ├── demo1 │ ├── demo.js │ ├── demo.svg │ └── demoProps.js │ ├── demo2 │ ├── demo.js │ ├── demo.svg │ └── params.js │ ├── demo3 │ ├── demo.js │ ├── demo.svg │ └── utils.js │ ├── index.css │ ├── index.js │ └── links.js ├── netlify.toml ├── package-lock.json ├── package.json ├── src ├── index.js └── shapes.js └── test ├── __snapshots__ └── point.test.js.snap └── point.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # See https://help.github.com/ignore-files/ for more about ignoring files. 3 | 4 | # dependencies 5 | node_modules 6 | 7 | # builds 8 | build 9 | dist 10 | .rpt2_cache 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v12.18.0 -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "jsxSingleQuote": true, 4 | "semi": false, 5 | "tabWidth": 2, 6 | "bracketSpacing": true, 7 | "jsxBracketSameLine": false, 8 | "arrowParens": "always", 9 | "trailingComma": "none" 10 | } 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Mithi Sevilla 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A Bare Minimum 2D Plotter 2 | 3 | > An extremely lightweight React component to declaratively (and elegantly) plot shapes on an inline SVG 4 | 5 | [![NPM](https://img.shields.io/npm/v/bare-minimum-2d.svg)](https://www.npmjs.com/package/bare-minimum-2d) 6 | [![MINIFIED](https://img.shields.io/bundlephobia/min/bare-minimum-2d@0.2.0?color=%2300BCD4&label=minified)](https://bundlephobia.com/result?p=bare-minimum-2d@0.2.0) 7 | [![GZIPPED](https://img.shields.io/bundlephobia/minzip/bare-minimum-2d@0.2.0?color=%2300BCD4&label=minified%20%2B%20gzipped)](https://bundlephobia.com/result?p=bare-minimum-2d@0.2.0) 8 | 9 | ## Update: External Plugins 🥳 10 | You can now use your own shape implementation by passing it as a plugin (see [plugin section](./README.md#plugins) below for more information). 11 | Below are a couple of plugins by [@fuddl](https://github.com/fuddl). 12 | 13 | * [`text-marker`](https://www.npmjs.com/package/bare-minimum-text-marker) ([Demo](https://fuddl.github.io/bare-minimum-text-marker/)) 14 | * [`quadratic-bezier`](https://www.npmjs.com/package/bare-minimum-quadratic-bezier) ([Demo](https://fuddl.github.io/bare-minimum-quadratic-bezier/)) 15 | 16 | ## Demo Applications 17 | 18 | | Responsive Illustrations | On-The-Fly Animations | Interactive Applications | 19 | | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | 20 | | [][demo_link1] | [][demo_link2] | [][demo_link3] | 21 | | [demo][demo_link1] | [demo][demo_link2] | [demo][demo_link3] | 22 | | [source code][source_link1] | [source code][source_link2] | [source code][source_link3] | 23 | 24 | [demo_link1]: https://bare-minimum-2d.netlify.app/demo1 25 | [demo_link2]: https://bare-minimum-2d.netlify.app/demo2 26 | [demo_link3]: https://bare-minimum-2d.netlify.app/demo3 27 | [source_link1]: https://github.com/mithi/bare-minimum-2d/blob/master/example/src/demo1/demo.js 28 | [source_link2]: https://github.com/mithi/bare-minimum-2d/blob/master/example/src/demo2/demo.js 29 | [source_link3]: https://github.com/mithi/bare-minimum-2d/blob/master/example/src/demo3/demo.js 30 | 31 | ## Install 32 | 33 | ```bash 34 | npm install --save bare-minimum-2d 35 | ``` 36 | 37 | ## Usage 38 | 39 | This is [an example](./example/src/demo1/demoProps.js) of what you can pass to a `BareMinimum2d` component. 40 | 41 | You pass it like so: 42 | 43 | ```jsx 44 | import BareMinimum2d from 'bare-minimum-2d' 45 | 46 |
47 | 48 |
49 | ``` 50 | 51 | The component takes the dimensions of its parent and is always centered 52 | 53 | ## Everything you need to know explained in two minutes 54 | 55 | A `BareMinimum2d` component only has two props: `container` and `data`. `container` is a small object with exactly four elements. `data` is an array containing objects. 56 | 57 | Example: 58 | 59 | ```jsx 60 | import BareMinimum2d from 'bare-minimum-2d' 61 | 62 | const container = { 63 | color: '#0000FF', 64 | opacity: 0.2, 65 | xRange: 300, 66 | yRange: 500 67 | } 68 | 69 | const data = [{ 70 | x: [0], 71 | y: [0], 72 | color: "#FFFFFF", 73 | opacity: 1.0, 74 | size: 10, 75 | type: 'points', 76 | id: 'center' 77 | }] 78 | 79 |
80 | 81 |
82 | ``` 83 | 84 | `container.color` and `container.opacity` specifies the canvas color of `BareMinimum2d`. 85 | 86 | The cartesian coordinate system of `BareMinimum` will follow the diagram below given `container.xRange` and `container.yRange`. Position (0, 0) will always be at the center of the rendered component. 87 | 88 | ```js 89 | yRange/2 90 | | 91 | | 92 | -xRange/2 -------(0,0)--------- xRange/2 93 | | 94 | | 95 | -yRange/2 96 | ``` 97 | 98 | Please take a look at more [complex example data prop](./example/src/demo1/demoProps.js) to get the idea. 99 | each element of the array `data` should be a hash-like objectwith a `type` key which should have a value that is one of 100 | the following: 101 | 102 | | points | ellipse | lines | polygon | 103 | | ------ | -------- | ------ | -------- | 104 | | plural | singular | plural | singular | 105 | 106 | Elements of the `data` array will be stacked based on the order they are declared. 107 | The first element will be at the most bottom layer while the last element of the array will be at the top. 108 | 109 | All attributes are ALWAYS required, nothing is optional because there are no default values. The `id` attribute must be unique for each element of the `data` array. 110 | 111 | #### Create your own 112 | 113 | You can add your own shapes as a plugin for example, here's an example plugin written by [@fuddl](https://github.com/fuddl) 114 | 115 | ```jsx 116 | 117 | const Triangle = ({ x, y, transforms, size, color, opacity, id, i }) => { 118 | const cx = transforms.tx(x) 119 | const cy = transforms.ty(y) 120 | const ySize = size * 0.8626 121 | return ( 122 | 134 | ) 135 | } 136 | 137 | const trianglesPlugin = { 138 | triangle: (element, transforms) => { 139 | const { size, color, opacity, id } = element 140 | return element.x.map((x, i) => ( 141 | 154 | )) 155 | } 156 | } 157 | ``` 158 | 159 | And you can use it like so: 160 | 161 | ```jsx 162 | const triangle = { 163 | "x": [-163.72675374383329], 164 | "y": [-154.33259574213795], 165 | "opacity": 1, 166 | "size": 60, 167 | "color": "#2196F3", 168 | "type": "triangles", 169 | "id":"points0" 170 | } 171 | 172 |
173 | 174 |
175 | ``` 176 | 177 | END 178 | 179 | ## Contributing 180 | 181 | 1. Clone this repository. 182 | 2. Add your changes 183 | 3. You can add a demo or update the demo based on your changes somewhere [here](https://github.com/mithi/bare-minimum-2d/tree/master/example/src) 184 | 4. After making your change go run the following command to see if it works as you expect. 185 | ``` 186 | npm install && npm run build && rm -rf node_modules && cd example && npm install && npm run start 187 | ``` 188 | 189 | PRs welcome! Please read the [contributing guidelines](https://github.com/mithi/mithi/wiki/Contributing) and the [commit style guide](https://github.com/mithi/mithi/wiki/Commit-style-guide)! 190 | 191 | ## License 192 | 193 | MIT © [Mithi](https://github.com/mithi) 194 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@babel/preset-env', '@babel/preset-react'], 3 | } 4 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | ## Demo Applications 2 | 3 | | Responsive Illustrations | Generate On-the-fly Animations | Interactive Applications | 4 | | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | 5 | | [][demo_link1] | [][demo_link2] | [][demo_link3] | 6 | | [demo][demo_link1] | [demo][demo_link2] | [demo][demo_link3] | 7 | 8 | [demo_link1]: https://bare-minimum-2d.netlify.app/demo1 9 | [demo_link2]: https://bare-minimum-2d.netlify.app/demo2 10 | [demo_link3]: https://bare-minimum-2d.netlify.app/demo3 11 | 12 | # https://bare-minimum-2d.netlify.app 13 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bare-minimum-2d-example", 3 | "homepage": ".", 4 | "version": "0.0.0", 5 | "private": true, 6 | "scripts": { 7 | "start": "react-scripts start", 8 | "build": "react-scripts build" 9 | }, 10 | "dependencies": { 11 | "@babel/preset-react": "^7.12.13", 12 | "bare-minimum-2d": "file:..", 13 | "react": "^16.13.1", 14 | "react-dom": "^16.13.1", 15 | "react-router-dom": "^5.2.0", 16 | "react-scripts": "^5.0.1" 17 | }, 18 | "devDependencies": { 19 | "@babel/plugin-syntax-object-rest-spread": "^7.8.3" 20 | }, 21 | "browserslist": [ 22 | ">0.2%", 23 | "not dead", 24 | "not ie <= 11", 25 | "not op_mini all" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /example/public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /example/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mithi/bare-minimum-2d/9ee17c765c88b16dce790e08931660aafd6d4557/example/public/favicon.ico -------------------------------------------------------------------------------- /example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | bare-minimum-2d 13 | 14 | 15 | 16 | 19 | 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /example/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "bare-minimum-2d", 3 | "name": "bare-minimum-2d", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /example/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { 3 | BrowserRouter as Router, 4 | Switch, 5 | Route, 6 | Link, 7 | Redirect 8 | } from 'react-router-dom' 9 | 10 | import Demo3 from './demo3/demo' 11 | import Demo2 from './demo2/demo' 12 | import Demo1 from './demo1/demo' 13 | import { URL_REPO } from './links' 14 | 15 | const stickyHome = { 16 | position: 'fixed', 17 | top: 0, 18 | margin: '10px', 19 | color: '#32ff7e' 20 | } 21 | 22 | const LandingPage = () => ( 23 |
24 | Demo1. 25 |
26 | Demo2. 27 |
28 | Demo3. 29 |
30 | 31 | Code Repository. 32 | 33 |

A BareMinimum2d Plotter

34 |

35 | BareMinimum2d is a low-level and lightweight React component you can use 36 |
37 | to render points, lines, ellipses, and polygons on the screen. 38 |
39 |

40 |

41 | Go check out the three demos linked above. If you're interested in it, 42 |
43 | you can checkout the repository (also linked above). 44 |

45 |
46 | ) 47 | 48 | const App = () => { 49 | return ( 50 | 51 |
52 |
53 | Home 54 |
55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 |
74 |
75 | ) 76 | } 77 | 78 | export default App 79 | -------------------------------------------------------------------------------- /example/src/demo1/demo.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import BareMinimum2d from 'bare-minimum-2d' 3 | import DEMO_PROPS from './demoProps' 4 | import { URL_SOURCE_CODE_DEMO1, URL_SOURCE_PROPS_DEMO1 } from '../links' 5 | import { useEffect, useState } from 'react' 6 | 7 | // Adapted from: https://usehooks.com/useWindowSize/ 8 | function useWindowSize() { 9 | // Initialize state with undefined width/height so server and client renders match 10 | const [windowSize, setWindowSize] = useState({ 11 | width: undefined, 12 | height: undefined 13 | }) 14 | 15 | useEffect(() => { 16 | const handleResize = () => 17 | setWindowSize({ 18 | width: window.innerWidth, 19 | height: window.innerHeight 20 | }) 21 | 22 | window.addEventListener('resize', handleResize) 23 | handleResize() // call it right away! 24 | 25 | return () => window.removeEventListener('resize', handleResize) 26 | }, []) 27 | 28 | return windowSize 29 | } 30 | 31 | /***** 32 | DEMO #1 33 | 34 | In this demo, simple points, lines, ellipses and polygons are drawn. 35 | 36 | This demo shows that the BareMinimum2d component 37 | takes the dimensions of its parent component 38 | and will always scale and be centered. 39 | 40 | Check out the data structure of the props passed 41 | to BareMinimum2d and the resulting svg 42 | which are located at this same directory. 43 | They're named demoProps.js and demo.svg respectively. 44 | 45 | This component which wraps < BareMinimum2d /> uses ResizeObserver 46 | to listen for changes in dimensions of its width 47 | (to display it to the user) when changed, whenever this occurs 48 | we take this opportunity to get the document window's height 49 | and sync it to the height of this component. 50 | 51 | *****/ 52 | 53 | const DemoSticky = ({ height, width }) => ( 54 |
55 |

56 | Resize the window. 57 |
58 | {height} x {width} 59 |
60 |

61 |

62 | BareMinimum2d takes the dimensions of 63 |
64 | its parent and it will always be centered. 65 |
66 |

67 |

68 | Use BareMinimum2d to specify as many polygons, 69 |
70 | lines, ellipses and points as you like. 71 |

72 | 73 | Source code 74 | 75 |
76 | 77 | Props 78 | 79 |
80 | ) 81 | 82 | const Demo = () => { 83 | const { width, height } = useWindowSize() 84 | 85 | return ( 86 |
87 | 88 | 89 |
90 | ) 91 | } 92 | 93 | export default Demo 94 | -------------------------------------------------------------------------------- /example/src/demo1/demo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /example/src/demo1/demoProps.js: -------------------------------------------------------------------------------- 1 | const BODY_COLOR = '#e84393' 2 | const COG_COLOR = '#32ff7e' 3 | const LEG_COLOR = '#FF5722' 4 | const PAPER_BG_COLOR = '#17212B' 5 | const GROUND_COLOR = '#0e2845' 6 | const POINT_SIZE = 30 7 | const LINE_SIZE = 25 8 | 9 | const container = { 10 | color: PAPER_BG_COLOR, 11 | opacity: 1.0, 12 | xRange: 1000, 13 | yRange: 1000 14 | } 15 | 16 | const jointsFemur = { 17 | x: [-166, -235, -166, 166, 235, 166], 18 | y: [161, 0, -161, -161, 0, 161], 19 | color: LEG_COLOR, 20 | opacity: 1.0, 21 | size: POINT_SIZE, 22 | type: 'points', 23 | id: 'joint-femur' 24 | } 25 | 26 | const centerPoint = { 27 | x: [0], 28 | y: [-20], 29 | color: COG_COLOR, 30 | opacity: 1.0, 31 | size: 1.5 * POINT_SIZE, 32 | type: 'points', 33 | id: 'center' 34 | } 35 | 36 | const headPoint = { 37 | x: [0], 38 | y: [-120], 39 | color: BODY_COLOR, 40 | opacity: 1.0, 41 | size: 1.5 * POINT_SIZE, 42 | type: 'points', 43 | id: 'head' 44 | } 45 | 46 | const jointsTibia = { 47 | x: [-269, -355, -269, 269, 335, 269], 48 | y: [224, 0, -224, -224, 0, 224], 49 | color: LEG_COLOR, 50 | opacity: 1.0, 51 | size: POINT_SIZE, 52 | type: 'points', 53 | id: 'joint-tibia' 54 | } 55 | 56 | const vertices = { 57 | x: [-90, -145, -90, 90, 145, 90], 58 | y: [115, -20, -115, -115, -20, 115], 59 | color: BODY_COLOR, 60 | opacity: 1.0, 61 | size: POINT_SIZE, 62 | type: 'points', 63 | id: 'body-vertices' 64 | } 65 | 66 | const legs = { 67 | x0: [-90, -145, -90, 90, 145, 90], 68 | y0: [115, -20, -115, -115, -20, 115], 69 | x1: [-269, -355, -269, 269, 335, 269], 70 | y1: [224, 0, -224, -224, 0, 224], 71 | color: LEG_COLOR, 72 | opacity: 0.9, 73 | size: LINE_SIZE, 74 | type: 'lines', 75 | id: 'six-legs' 76 | } 77 | 78 | const body = { 79 | x: [-90, -145, -90, 90, 145, 90], 80 | y: [115, -20, -115, -115, -20, 115], 81 | fillColor: BODY_COLOR, 82 | fillOpacity: 0.5, 83 | borderColor: BODY_COLOR, 84 | borderOpacity: 1.0, 85 | borderSize: LINE_SIZE, 86 | type: 'polygon', 87 | id: 'body' 88 | } 89 | 90 | const hexagon = { 91 | x: [-228, -230, -99, 229, 230, 99], 92 | y: [107, -91, -236, -107, 91, 236], 93 | fillColor: GROUND_COLOR, 94 | fillOpacity: 1.0, 95 | borderColor: GROUND_COLOR, 96 | borderOpacity: 0, 97 | borderSize: 0, 98 | type: 'polygon', 99 | id: 'blue-hexagon' 100 | } 101 | 102 | const ellipse1 = { 103 | cx: 400, 104 | cy: -150, 105 | rx: 25, 106 | ry: 45, 107 | theta: 45, 108 | fillColor: COG_COLOR, 109 | fillOpacity: 0.5, 110 | borderColor: COG_COLOR, 111 | borderOpacity: 1, 112 | borderSize: 5, 113 | type: 'ellipse', 114 | id: 'sampleEllipse' 115 | } 116 | 117 | const ellipse2 = { 118 | cx: 360, 119 | cy: -150, 120 | rx: 25, 121 | ry: 45, 122 | theta: -45, 123 | fillColor: COG_COLOR, 124 | fillOpacity: 0.5, 125 | borderColor: COG_COLOR, 126 | borderOpacity: 1.0, 127 | borderSize: 5, 128 | type: 'ellipse', 129 | id: 'sampleEllipse2' 130 | } 131 | 132 | const props = { 133 | container, 134 | data: [ 135 | hexagon, 136 | 137 | jointsFemur, 138 | jointsTibia, 139 | headPoint, 140 | ellipse1, 141 | ellipse2, 142 | legs, 143 | body, 144 | vertices, 145 | centerPoint 146 | ] 147 | } 148 | 149 | export default props 150 | -------------------------------------------------------------------------------- /example/src/demo2/demo.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import BareMinimum2d from 'bare-minimum-2d' 3 | import * as p from './params' 4 | import { URL_SOURCE_CODE_DEMO2 } from '../links' 5 | 6 | /***** 7 | DEMO #2 8 | This demo shows that BareMinimum2d can be used 9 | for on-the-fly animations. 10 | 11 | Lines are updated every 15 milliseconds generating an interesting pattern 12 | 13 | demo.svg in this directory is is a snapshot of 14 | of one of the animation frames generated 15 | 16 | *****/ 17 | 18 | const DemoSticky = ({ t }) => ( 19 |
20 |

21 | BareMinimum2d can be used for on-the-fly animations 22 |
23 | {t} 24 |

25 |

26 | 27 | Source code 28 | 29 |

30 |
31 | ) 32 | 33 | class Demo extends React.PureComponent { 34 | intervalID = null 35 | t = Math.floor(Math.random() * p.RANDOMNESS) 36 | state = { 37 | data: [] 38 | } 39 | 40 | componentDidMount() { 41 | this.intervalID = setInterval(this.animate, p.ANIMATION_DELAY) 42 | } 43 | 44 | componentWillUnmount() { 45 | clearInterval(this.intervalID) 46 | } 47 | 48 | animate = () => { 49 | this.setState({ 50 | data: [linesInFrame(this.t)] 51 | }) 52 | this.t = (this.t + 1) % p.MAX_T 53 | } 54 | 55 | render() { 56 | return ( 57 |
58 | 59 | 60 |
61 | ) 62 | } 63 | } 64 | 65 | const linesInFrame = (t) => { 66 | let [x0, x1, y0, y1] = [[], [], [], []] 67 | 68 | for (let i = 0; i < 100; i++) { 69 | x0.push(fx0(t + i)) 70 | y0.push(fy0(t + i)) 71 | x1.push(fx1(t + i) + p.OFFSET) 72 | y1.push(fy1(t + i) + p.OFFSET) 73 | } 74 | 75 | return { 76 | x0, 77 | y0, 78 | x1, 79 | y1, 80 | size: p.LINE_SIZE, 81 | color: p.LINE_COLOR, 82 | opacity: p.LINE_OPACITY, 83 | type: 'lines', 84 | id: 'animating-lines-with-patterns' 85 | } 86 | } 87 | 88 | const fx0 = (t) => 89 | Math.sin(t / 10) * 125 + Math.sin(t / 20) * 125 + Math.sin(t / 30) * 125 90 | 91 | const fy0 = (t) => 92 | Math.cos(t / 10) * 125 + Math.cos(t / 20) * 125 + Math.cos(t / 30) * 125 93 | 94 | const fx1 = (t) => 95 | Math.sin(t / 15) * 125 + Math.sin(t / 25) * 125 + Math.sin(t / 35) * 125 96 | 97 | const fy1 = (t) => 98 | Math.cos(t / 15) * 125 + Math.cos(t / 25) * 125 + Math.cos(t / 35) * 125 99 | 100 | export default Demo 101 | -------------------------------------------------------------------------------- /example/src/demo2/demo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /example/src/demo2/params.js: -------------------------------------------------------------------------------- 1 | export const MAX_T = 20000 2 | export const X_RANGE = 1000 3 | export const Y_RANGE = 2000 4 | export const OFFSET = 20 5 | export const BACKGROUND_COLOR = '#2f3542' 6 | export const LINE_COLOR = '#ff4757' 7 | export const BG_OPACITY = 1.0 8 | export const LINE_OPACITY = 0.75 9 | export const LINE_SIZE = 2 10 | export const ANIMATION_DELAY = 15 11 | export const RANDOMNESS = 1000 12 | export const IMAGE_SIZE = '800px' 13 | 14 | export const CONTAINER = { 15 | color: BACKGROUND_COLOR, 16 | opacity: BG_OPACITY, 17 | xRange: X_RANGE, 18 | yRange: Y_RANGE 19 | } 20 | -------------------------------------------------------------------------------- /example/src/demo3/demo.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import BareMinimum2d from 'bare-minimum-2d' 3 | import { URL_SOURCE_CODE_DEMO3 } from '../links' 4 | 5 | import { 6 | pickRandom, 7 | skewedRandom, 8 | rotatedLine, 9 | NINETEEN_COLORS, 10 | SIX_COLORS, 11 | THREE_SIZES 12 | } from './utils' 13 | 14 | /***** 15 | DEMO #3 16 | 17 | This demo shows that the BareMinimum2d component 18 | can be used for interactive applications 19 | 20 | The pinwheel's orientation, size and colors 21 | changes everytime you move your cursor 22 | 23 | A snapshot of an svg given a possible combination is 24 | also saved in this directory 'demo.svg' 25 | *****/ 26 | 27 | const Triangle = ({ x, y, transforms, size, color, opacity, id, i }) => { 28 | const cx = transforms.tx(x) 29 | const cy = transforms.ty(y) 30 | const ySize = size * 0.8626 31 | return ( 32 | 44 | ) 45 | } 46 | 47 | const trianglesPlugin = { 48 | triangles: (element, transforms) => { 49 | const { size, color, opacity, id } = element 50 | return element.x.map((x, i) => ( 51 | 64 | )) 65 | } 66 | } 67 | 68 | const DemoSticky = ({ x, y, theta }) => ( 69 |
70 |

71 | Move your cursor to spin the pinwheel. 72 |
73 | BareMinimum2d can be used for interactive applications. 74 |
75 | You can also add your pass your own shape implementation as a plugin. 76 |

77 |

78 | x: {x} 79 |
80 | y: {y} 81 |
theta:{((theta * 180) / Math.PI).toFixed(2)} 82 |

83 |

84 | 85 | Source code 86 | 87 |

88 |
89 | ) 90 | 91 | const CONTAINER = { 92 | color: '#b71540', 93 | opacity: 1.0, 94 | xRange: 1000, 95 | yRange: 1000 96 | } 97 | 98 | const R = 225 99 | const numberOfColors = 15 100 | 101 | class PinWheelShapesManager { 102 | savedColors = Array.apply(null, Array(numberOfColors)).map((_) => 103 | pickRandom(NINETEEN_COLORS) 104 | ) 105 | savedSizes = Array.apply(null, Array(numberOfColors)).map((_) => 106 | pickRandom(THREE_SIZES) 107 | ) 108 | 109 | savedColorsPolygon = Array.apply(null, Array(2)).map((_) => 110 | pickRandom(SIX_COLORS) 111 | ) 112 | L1 = { x0: -R, y0: 0, x1: R, y1: 0 } 113 | L2 = { x0: 0, y0: -R, x1: 0, y1: R } 114 | 115 | update(theta) { 116 | const newColors = this.savedColors.map((color) => 117 | skewedRandom(NINETEEN_COLORS, color) 118 | ) 119 | 120 | const newColorsPolygon = this.savedColorsPolygon.map((color) => 121 | skewedRandom(SIX_COLORS, color) 122 | ) 123 | const newSizes = this.savedSizes.map((size) => 124 | skewedRandom(THREE_SIZES, size) 125 | ) 126 | 127 | const line1 = rotatedLine(this.L1, -theta) 128 | const line2 = rotatedLine(this.L2, -theta) 129 | const line3 = rotatedLine(this.L1, theta) 130 | const line4 = rotatedLine(this.L2, theta) 131 | 132 | const polygon = { 133 | x: [line3.x0, line4.x0, line3.x1], 134 | y: [line3.y0, line4.y0, line4.y1], 135 | fillColor: newColorsPolygon[1], 136 | fillOpacity: 1, 137 | borderColor: newColorsPolygon[0], 138 | borderOpacity: 1.0, 139 | borderSize: newSizes[11], 140 | type: 'polygon', 141 | id: 'body' 142 | } 143 | 144 | const lines = { 145 | x0: [line1.x0, line2.x0], 146 | y0: [line1.y0, line2.y0], 147 | x1: [line1.x1, line2.x1], 148 | y1: [line1.y1, line2.y1], 149 | color: newColors[10], 150 | opacity: 1.0, 151 | size: newSizes[10], 152 | type: 'lines', 153 | id: 'two-lines-center-cross' 154 | } 155 | 156 | const linePointsX = [line1.x0, line1.x1, line2.x0, line2.x1, 0] 157 | const linePointsY = [line1.y0, line1.y1, line2.y0, line2.y1, 0] 158 | 159 | const pointsX = [...linePointsX, ...linePointsX] 160 | const pointsY = [...linePointsY, ...linePointsY] 161 | const newPoints = pointsX.map((pointX, i) => ({ 162 | x: [pointX], 163 | y: [pointsY[i]], 164 | opacity: 1.0, 165 | size: newSizes[i], 166 | color: newColors[i], 167 | type: i === 0 || i === 5 ? 'triangles' : 'points', 168 | id: 'points' + i 169 | })) 170 | 171 | this.savedColors = newColors 172 | this.savedSizes = newSizes 173 | this.savedColorsPolygon = newColorsPolygon 174 | 175 | return [polygon, lines, ...newPoints] 176 | } 177 | } 178 | class Demo extends React.Component { 179 | h = window.innerHeight 180 | theta = 0 181 | pinWheel = new PinWheelShapesManager() 182 | state = { 183 | x: 0, 184 | y: 0, 185 | data: [] 186 | } 187 | 188 | _onMouseMove(e) { 189 | const x = e.nativeEvent.offsetX 190 | const y = e.nativeEvent.offsetY 191 | const w = window.innerWidth 192 | const h = window.innerHeight 193 | 194 | const currentX = w / 2 - x 195 | const currentY = h / 2 - y 196 | this.theta = Math.atan2(currentY, currentX) 197 | 198 | this.h = h 199 | const data = this.pinWheel.update(this.theta) 200 | 201 | this.setState({ 202 | x: currentX, 203 | y: currentY, 204 | data 205 | }) 206 | } 207 | 208 | componentDidMount() { 209 | this.setState({ data: this.pinWheel.update(0) }) 210 | } 211 | render() { 212 | const { x, y, data } = this.state 213 | const divDimensionsStyle = { width: '100%', height: this.h } 214 | 215 | return ( 216 |
220 | 224 | 225 |
226 | ) 227 | } 228 | } 229 | 230 | export default Demo 231 | -------------------------------------------------------------------------------- /example/src/demo3/demo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /example/src/demo3/utils.js: -------------------------------------------------------------------------------- 1 | const pickRandom = (list) => list[Math.floor(Math.random() * list.length)] 2 | 3 | const skewedRandom = (list, element) => 4 | Math.random() > 0.99 ? pickRandom(list) : element 5 | 6 | const rotatedLine = (line, theta) => { 7 | const x0 = line.x0 * Math.cos(theta) - line.y0 * Math.sin(theta) 8 | const y0 = line.x0 * Math.sin(theta) + line.y0 * Math.cos(theta) 9 | const x1 = line.x1 * Math.cos(theta) - line.y1 * Math.sin(theta) 10 | const y1 = line.x1 * Math.sin(theta) + line.y1 * Math.cos(theta) 11 | return { x0, y0, x1, y1 } 12 | } 13 | 14 | const NINETEEN_COLORS = [ 15 | '#F44336', 16 | '#E91E63', 17 | '#9C27B0', 18 | '#673AB7', 19 | '#3F51B5', 20 | '#2196F3', 21 | '#00BCD4', 22 | '#009688', 23 | '#4CAF50', 24 | '#12CBC4', 25 | '#9980FA', 26 | '#FFEB3B', 27 | '#FFC107', 28 | '#FF9800', 29 | '#FF5722', 30 | '#A3CB38', 31 | '#B53471', 32 | '#D980FA', 33 | '#1289A7', 34 | '#32ff7e' 35 | ] 36 | 37 | const SIX_COLORS = [ 38 | '#4b4b4b', 39 | '#e317d5', 40 | '#0652DD', 41 | '#006266', 42 | '#EA2027', 43 | '#132914' 44 | ] 45 | 46 | const THREE_SIZES = [40, 60, 95] 47 | export { 48 | pickRandom, 49 | skewedRandom, 50 | rotatedLine, 51 | NINETEEN_COLORS, 52 | THREE_SIZES, 53 | SIX_COLORS 54 | } 55 | -------------------------------------------------------------------------------- /example/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 5 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | font-weight: bolder; 10 | background-color: #222f3e; 11 | color: #fff200; 12 | font-size: 10px; 13 | } 14 | 15 | code { 16 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 17 | monospace; 18 | } 19 | 20 | a { 21 | color: #dff9fb; 22 | } 23 | 24 | a:hover { 25 | color: #ff7979; 26 | } 27 | 28 | .sticky-div { 29 | position: 'fixed'; 30 | top: 20px; 31 | margin: 5px; 32 | padding: 5px; 33 | } 34 | 35 | #sticky-home { 36 | position: fixed; 37 | top: 0; 38 | color: #32ff7e; 39 | background-color: rgba(0, 0, 0, 0); 40 | } 41 | -------------------------------------------------------------------------------- /example/src/index.js: -------------------------------------------------------------------------------- 1 | import './index.css' 2 | 3 | import React from 'react' 4 | import ReactDOM from 'react-dom' 5 | import App from './App' 6 | 7 | ReactDOM.render(, document.getElementById('root')) 8 | -------------------------------------------------------------------------------- /example/src/links.js: -------------------------------------------------------------------------------- 1 | const URL_SOURCE_CODE_DEMO1 = 2 | 'https://github.com/mithi/bare-minimum-2d/blob/master/example/src/demo1/demo.js' 3 | 4 | const URL_SOURCE_CODE_DEMO2 = 5 | 'https://github.com/mithi/bare-minimum-2d/blob/master/example/src/demo2/demo.js' 6 | 7 | const URL_SOURCE_CODE_DEMO3 = 8 | 'https://github.com/mithi/bare-minimum-2d/blob/master/example/src/demo3/demo.js' 9 | 10 | const URL_SOURCE_PROPS_DEMO1 = 11 | 'https://github.com/mithi/bare-minimum-2d/blob/master/example/src/demo1/demoProps.js' 12 | 13 | const URL_REPO = 'https://github.com/mithi/bare-minimum-2d' 14 | 15 | export { 16 | URL_SOURCE_CODE_DEMO1, 17 | URL_SOURCE_CODE_DEMO2, 18 | URL_SOURCE_CODE_DEMO3, 19 | URL_SOURCE_PROPS_DEMO1, 20 | URL_REPO 21 | } 22 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "npm install && npm run build && rm -rf node_modules && cd example && npm install && npm run build" 3 | 4 | [build.environment] 5 | NODE_ENV = "development" 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bare-minimum-2d", 3 | "version": "0.2.1", 4 | "description": "An extremely lightweight React component for plotting points, lines, ellipses, and polygons on an inline SVG", 5 | "keywords": [ 6 | "svg", 7 | "data", 8 | "visualization", 9 | "graph", 10 | "plot", 11 | "chart", 12 | "graphing", 13 | "plotting", 14 | "react", 15 | "react-component", 16 | "data-visualization" 17 | ], 18 | "author": "mithi", 19 | "license": "MIT", 20 | "repository": "mithi/bare-minimum-2d", 21 | "main": "dist/index.js", 22 | "module": "dist/index.modern.js", 23 | "source": "src/index.js", 24 | "engines": { 25 | "node": ">=10" 26 | }, 27 | "scripts": { 28 | "build": "microbundle build --jsx React.createElement --format modern,cjs", 29 | "start": "microbundle watch --jsx React.createElement --format modern,cjs", 30 | "format": "prettier --config ./.prettierrc --write \"./src/*\"", 31 | "prepublishOnly": "npm run build", 32 | "predeploy": "cd example && npm install && npm run build", 33 | "test": "jest", 34 | "test-deploy": "npm run build && rm -rf node_modules && cd example && npm install && npm run build" 35 | }, 36 | "peerDependencies": { 37 | "react": "^16.14.0" 38 | }, 39 | "devDependencies": { 40 | "@babel/preset-react": "^7.12.13", 41 | "babel-jest": "^26.3.0", 42 | "cross-env": "^7.0.2", 43 | "jest": "^26.4.2", 44 | "microbundle": "^0.15.1", 45 | "prettier": "^2.2.1", 46 | "react": "^16.14.0", 47 | "react-test-renderer": "^16.14.0" 48 | }, 49 | "files": [ 50 | "dist" 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Polygon, Ellipse, Lines, Point } from './shapes' 3 | 4 | const elementTypeImplementations = { 5 | lines: (element, transforms) => ( 6 | 7 | ), 8 | polygon: (element, transforms) => ( 9 | 10 | ), 11 | ellipse: (element, transforms) => ( 12 | 13 | ), 14 | points: (element, transforms) => { 15 | const { size, color, opacity, id } = element 16 | return element.x.map((x, i) => ( 17 | 30 | )) 31 | } 32 | } 33 | 34 | const svgProps = { 35 | version: '1.1', 36 | baseProfile: 'full', 37 | xmlns: 'http://www.w3.org/2000/svg', 38 | preserveAspectRatio: 'xMidYMid slice', 39 | width: '100%', 40 | height: '100%' 41 | } 42 | 43 | const Paper = ({ color, opacity }) => ( 44 | 45 | ) 46 | 47 | const svgElements = (data, transforms, plugins) => { 48 | plugins = plugins || [] 49 | 50 | const extendedElementTypeImplementations = plugins.reduce( 51 | (extended, plugin) => { 52 | return { ...extended, ...plugin } 53 | }, 54 | elementTypeImplementations 55 | ) 56 | 57 | const elements = data.map((element) => { 58 | const implementation = extendedElementTypeImplementations[element.type] 59 | return implementation(element, transforms) 60 | }) 61 | 62 | return elements.flat() 63 | } 64 | /************************** 65 | * Minimal Plot 66 | **************************/ 67 | 68 | class BareMinimum2d extends React.PureComponent { 69 | xRange = null 70 | yRange = null 71 | 72 | /*** 73 | yRange/2 74 | | 75 | | 76 | -xRange/2 -------(0,0)--------- xRange/2 77 | | 78 | | 79 | -yRange/2 80 | ***/ 81 | 82 | transformX = (x) => x + this.xRange / 2 83 | transformY = (y) => this.yRange / 2 - y 84 | 85 | render() { 86 | const { container, data, plugins } = this.props 87 | 88 | this.xRange = container.xRange 89 | this.yRange = container.yRange 90 | 91 | const transforms = { tx: this.transformX, ty: this.transformY } 92 | 93 | const viewBox = `0 0 ${container.xRange} ${container.yRange}` 94 | const { color, opacity } = container 95 | 96 | return ( 97 | 98 | 99 | {svgElements(data, transforms, plugins)} 100 | 101 | ) 102 | } 103 | } 104 | 105 | export default BareMinimum2d 106 | -------------------------------------------------------------------------------- /src/shapes.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | /************************** 4 | * POINT 5 | **************************/ 6 | const Point = ({ x, y, transforms, size, color, opacity, id, i }) => ( 7 | 17 | ) 18 | 19 | /************************** 20 | * LINES 21 | **************************/ 22 | const Lines = ({ size, color, opacity, x0, x1, y0, y1, transforms, id }) => { 23 | const d = x0.reduce((currentD, rawX0, i) => { 24 | const [currentX0, currentX1, currentY0, currentY1] = [ 25 | transforms.tx(rawX0), 26 | transforms.tx(x1[i]), 27 | transforms.ty(y0[i]), 28 | transforms.ty(y1[i]) 29 | ] 30 | return `${currentD} M ${currentX0},${currentY0} L ${currentX1},${currentY1} ` 31 | }, '') 32 | 33 | return 34 | } 35 | 36 | /************************** 37 | * POLYGON 38 | **************************/ 39 | const Polygon = ({ 40 | x, 41 | y, 42 | fillColor, 43 | fillOpacity, 44 | borderColor, 45 | borderOpacity, 46 | borderSize, 47 | transforms, 48 | id 49 | }) => { 50 | const pointString = x.reduce((pointString, rawX, i) => { 51 | const currentX = transforms.tx(rawX) 52 | const currentY = transforms.ty(y[i]) 53 | return `${pointString}${currentX},${currentY} ` 54 | }, '') 55 | 56 | const props = { 57 | points: pointString, 58 | fill: fillColor, 59 | stroke: borderColor, 60 | strokeWidth: borderSize, 61 | strokeOpacity: borderOpacity, 62 | id, 63 | fillOpacity 64 | } 65 | return 66 | } 67 | 68 | /************************** 69 | * ELLIPSE 70 | **************************/ 71 | const Ellipse = ({ 72 | cx, 73 | cy, 74 | rx, 75 | ry, 76 | theta, 77 | fillColor, 78 | fillOpacity, 79 | borderColor, 80 | borderOpacity, 81 | borderSize, 82 | transforms, 83 | id 84 | }) => { 85 | const newCx = transforms.tx(cx) 86 | const newCy = transforms.ty(cy) 87 | 88 | const props = { 89 | cx: newCx, 90 | cy: newCy, 91 | rx, 92 | ry, 93 | fill: fillColor, 94 | stroke: borderColor, 95 | strokeWidth: borderSize, 96 | strokeOpacity: borderOpacity, 97 | id, 98 | fillOpacity, 99 | transform: `rotate(${theta}, ${newCx}, ${newCy})` 100 | } 101 | return 102 | } 103 | 104 | export { Point, Lines, Polygon, Ellipse } 105 | -------------------------------------------------------------------------------- /test/__snapshots__/point.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders correctly 1`] = ` 4 | 13 | 19 | 27 | 28 | `; 29 | -------------------------------------------------------------------------------- /test/point.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import renderer from 'react-test-renderer' 3 | import BareMinimum2d from 'bare-minimum-2d' 4 | 5 | const container = { 6 | color: '#0000FF', 7 | opacity: 0.2, 8 | xRange: 300, 9 | yRange: 500 10 | } 11 | 12 | const data = [{ 13 | x: [0], 14 | y: [-20], 15 | color: "#FFFFFF", 16 | opacity: 1.0, 17 | size: 10, 18 | type: 'points', 19 | id: 'sample' 20 | }] 21 | 22 | it('renders correctly', () => { 23 | const tree = renderer 24 | .create() 25 | .toJSON() 26 | expect(tree).toMatchSnapshot() 27 | }) 28 | --------------------------------------------------------------------------------