├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── karma.conf.js
├── package.json
├── src
├── lib
│ └── plotter.js
└── plot.jsx
└── tests
├── plot.spec.js
└── plotter.spec.js
/.gitignore:
--------------------------------------------------------------------------------
1 | **/.DS_Store
2 | .eslintrc
3 | dist/
4 | node_modules/
5 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | src/
2 | tests/
3 | karma.conf.js
4 | .travis.yml
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 angus croll
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 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## react-function-plot
2 |
3 | A React component to render a JS function as an SVG path
4 |
5 | ### Install
6 |
7 | ```
8 | npm install react-function-plot
9 | ```
10 |
11 | ### Usage
12 |
13 | ```js
14 | // commonJS...
15 | var Plot = require('react-function-plot');
16 | // ...or es2015
17 | import Plot from 'react-function-plot';
18 |
19 |
24 | ```
25 | The SVG element will autosize (and auto-center) within the Plot component. (The SVG container must be a square, so the height and width of the SVG element will be set to the lesser of the height and width of the Plot component)
26 |
27 | `fn` can be any JavaScript function that takes a single numeric argument...
28 | ```js
29 | // es5
30 | function(t) {return 0.9/t}
31 | // es2015
32 | x => x * x
33 | // or even
34 | n => Math.random();
35 | ```
36 | `className` is a required prop – it allows multiple `Plot` components to be used on the same page.
37 | ### Usage Example
38 |
39 | Click [here](http://anguscroll.com/react-function-plot/) to play with a working example. Here's [the source](https://github.com/angus-c/react-function-plot/blob/gh-pages/examples.jsx) for the
40 | example component.
41 |
42 | 
43 |
44 | ### Tests
45 |
46 | ```
47 | npm test
48 | ```
49 |
50 | (test coverage very limited so far)
51 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | module.exports = function(config) {
2 | config.set({
3 | browsers: ['Chrome'],
4 | files: [
5 | 'tests/*.spec.js'
6 | ],
7 | frameworks: ['mocha', 'chai'],
8 | preprocessors: {
9 | 'src/**/*.js': ['webpack'],
10 | 'src/**/*.jsx': ['webpack'],
11 | 'tests/*.spec.js': ['webpack']
12 | },
13 | plugins: [
14 | 'karma-chrome-launcher',
15 | 'karma-firefox-launcher',
16 | 'karma-chai',
17 | 'karma-mocha',
18 | 'karma-sourcemap-loader',
19 | 'karma-webpack',
20 | ],
21 | reporters: ['dots'],
22 | singleRun: false,
23 | webpack: {
24 | module: {
25 | loaders: [
26 | { test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel-loader?stage=0&optional=runtime' },
27 | ],
28 | },
29 | watch: true,
30 | },
31 | webpackServer: {
32 | noInfo: true,
33 | },
34 | });
35 | };
36 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-function-plot",
3 | "version": "0.0.14",
4 | "description": "react component to plot a function as an svg path",
5 | "author": "Angus Croll",
6 | "main": "dist/plot.js",
7 | "scripts": {
8 | "compile": "babel --stage 0 --optional runtime src --out-dir dist",
9 | "prepublish": "npm run compile",
10 | "test": "karma start --single-run",
11 | "testc": "karma start --auto-watch"
12 | },
13 | "license": "MIT",
14 | "repository": {
15 | "type": "git",
16 | "url": "https://github.com/angus-c/react-function-plot.git"
17 | },
18 | "keywords": [
19 | "react",
20 | "d3",
21 | "svg",
22 | "line",
23 | "function"
24 | ],
25 | "devDependencies": {
26 | "babel": "^5.1.13",
27 | "babel-core": "^5.1.11",
28 | "babel-loader": "^5.0.0",
29 | "babel-preset-es2015": "^6.0.15",
30 | "css-loader": "^0.20.1",
31 | "eslint": "^1.7.2",
32 | "eslint-plugin-react": "^3.6.2",
33 | "karma": "^0.13.15",
34 | "karma-chai": "^0.1.0",
35 | "karma-chrome-launcher": "^0.2.1",
36 | "karma-firefox-launcher": "^0.1.6",
37 | "karma-mocha": "^0.2.0",
38 | "style-loader": "^0.13.0"
39 | },
40 | "dependencies": {
41 | "babel-eslint": "^4.0.5",
42 | "babel-runtime": "^5.5.8",
43 | "classnames": "^2.2.0",
44 | "d3": "^3.5.6",
45 | "function-to-string": "^0.2.0",
46 | "react": "^0.14.1",
47 | "react-dom": "^0.14.1"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/lib/plotter.js:
--------------------------------------------------------------------------------
1 | import d3 from 'd3';
2 |
3 | const defaultThickness = 4;
4 |
5 | export default class Plotter {
6 | constructor({selector = 'body', thickness = defaultThickness}) {
7 | Object.assign(this, {selector, thickness});
8 | }
9 |
10 | addPath(fn) {
11 | this.d3Container = d3.select(this.selector);
12 |
13 | // render in a square SVG, adjust margins to center
14 | const width = this.d3Container[0][0].getBoundingClientRect().width;
15 | const height = this.d3Container[0][0].getBoundingClientRect().height;
16 | const marginOffset = Math.abs((width - height) / 2);
17 | const marginToAdjust = width > height ? 'margin-left' : 'margin-top';
18 | this.size = Math.min(width, height);
19 |
20 | const {size, thickness} = this;
21 |
22 | const svgContainer =
23 | this.d3Container
24 | .append('svg')
25 | .attr('width', size)
26 | .attr('height', size)
27 | .attr('style', `${marginToAdjust}: ${marginOffset}px`)
28 |
29 | svgContainer.append('path')
30 | .attr('d', this.getLineFunction(fn)([...new Array(Math.round(size))].map((_, i) => i)))
31 | .attr('stroke', 'blue')
32 | .attr('stroke-width', thickness)
33 | .attr('fill', 'none');
34 | }
35 |
36 | updatePath(fn) {
37 | const {size, thickness} = this;
38 |
39 | const svgContainer = this.d3Container.transition();
40 |
41 | svgContainer.select('path')
42 | .attr('d', this.getLineFunction(fn)([...new Array(Math.round(size))].map((_, i) => i)))
43 | }
44 |
45 | getLineFunction(fn) {
46 | const {size, thickness} = this;
47 | return d3.svg.line()
48 | .x(i => i)
49 | .y(i => size - thickness / 2 - size * fn(i / size) * (1 - thickness / size))
50 | .interpolate('linear');
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/plot.jsx:
--------------------------------------------------------------------------------
1 | import Plotter from './lib/plotter';
2 | import classnames from 'classnames';
3 | import React from 'react';
4 |
5 | class Plot extends React.Component {
6 | constructor(props) {
7 | super(props);
8 | this.plotSelector = ['.plot', this.props.className].join('.');
9 | this.plotter = new Plotter({selector: this.plotSelector, thickness: props.thickness});
10 | }
11 |
12 | static propTypes = {
13 | className: React.PropTypes.string.isRequired,
14 | fn: React.PropTypes.func,
15 | thickness: React.PropTypes.number
16 | }
17 |
18 | componentDidMount() {
19 | this.plotter.addPath(this.props.fn);
20 | }
21 |
22 | componentDidUpdate() {
23 | this.plotter.updatePath(this.props.fn);
24 | }
25 |
26 | render() {
27 | const className = classnames('plot', this.props.className);
28 | return (
29 |
30 | );
31 | }
32 | }
33 |
34 | export default Plot;
35 |
--------------------------------------------------------------------------------
/tests/plot.spec.js:
--------------------------------------------------------------------------------
1 | import { assert } from 'chai';
2 | import Plot from '../src/plot.jsx';
3 | import React from 'react/addons';
4 | import ReactDOM from 'react-dom';
5 |
6 | const ReactTestUtils = React.addons.TestUtils;
7 |
8 | class SinglePlotWrapper extends React.Component {
9 | render() {
10 | return (
11 |
x * x}
14 | height={300}
15 | width={300}
16 | thickness={4}
17 | />
18 | );
19 | }
20 | }
21 |
22 | class MultiplePlotWrapper extends React.Component {
23 | render() {
24 | return (
25 |
26 |
x * x}
29 | height={300}
30 | width={300}
31 | thickness={4}
32 | />
33 | x * x}
36 | height={300}
37 | width={300}
38 | thickness={4}
39 | />
40 |
41 | );
42 | }
43 | }
44 |
45 | describe('Plot', () => {
46 | let spw;
47 |
48 | describe('Single Plot renderer', () => {
49 | beforeEach(() => {
50 | spw = ReactTestUtils.renderIntoDocument();
51 | });
52 |
53 | it('renders a single plot', () => {
54 | const plots = ReactTestUtils.scryRenderedDOMComponentsWithClass(spw, "plot");
55 | assert.isDefined(plots);
56 | assert.equal(plots.length, 1);
57 | });
58 |
59 | it('(the plot) is a DIV', () => {
60 | const plotNode =
61 | ReactDOM.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithClass(spw, "plot"));
62 | assert.isDefined(plotNode);
63 | assert.equal(plotNode.tagName, 'DIV');
64 | });
65 | });
66 |
67 | describe('Multiple Plot renderer', () => {
68 | beforeEach(() => {
69 | spw = ReactTestUtils.renderIntoDocument();
70 | });
71 |
72 | it('renders two plots', () => {
73 | const plots = ReactTestUtils.scryRenderedDOMComponentsWithClass(spw, "plot");
74 | assert.isDefined(plots);
75 | assert.equal(plots.length, 2);
76 | });
77 |
78 | it('(each plot) is a DIV', () => {
79 | const plots = ReactTestUtils.scryRenderedDOMComponentsWithClass(spw, "plot");
80 | plots.forEach(plot => {
81 | const plotNode = ReactDOM.findDOMNode(plot);
82 | assert.equal(plotNode.tagName, 'DIV');
83 | });
84 | });
85 | });
86 | });
87 |
--------------------------------------------------------------------------------
/tests/plotter.spec.js:
--------------------------------------------------------------------------------
1 | describe('Plotter', () => {
2 | // TODO
3 | });
4 |
--------------------------------------------------------------------------------