diff --git a/.babelrc b/.babelrc index 6fbf25a..c31ad38 100644 --- a/.babelrc +++ b/.babelrc @@ -1,4 +1,7 @@ { "presets": ["es2015", "react"], - "plugins": ["transform-object-rest-spread"] + "plugins": [ + "transform-class-properties", + "transform-object-rest-spread" + ] } diff --git a/.eslintrc.json b/.eslintrc.json index 1257227..592f247 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -3,6 +3,8 @@ "extends": "@btmills/eslint-config-btmills/react", + "parser": "babel-eslint", + "env": { "browser": true } diff --git a/examples/arcs-example.jsx b/examples/arcs-example.jsx new file mode 100644 index 0000000..7eb80b1 --- /dev/null +++ b/examples/arcs-example.jsx @@ -0,0 +1,86 @@ +import React from 'react'; + +import Datamap from '../src'; +import Example from './example'; + +export default class ArcsExample extends React.Component { + + render() { + return ( + + + + ); + } + +} diff --git a/examples/basic-example.jsx b/examples/basic-example.jsx new file mode 100644 index 0000000..a428450 --- /dev/null +++ b/examples/basic-example.jsx @@ -0,0 +1,16 @@ +import React from 'react'; + +import Datamap from '../src'; +import Example from './example'; + +export default class BasicExample extends React.Component { + + render() { + return ( + + + + ); + } + +} diff --git a/examples/bubbles-example.jsx b/examples/bubbles-example.jsx new file mode 100644 index 0000000..53265dc --- /dev/null +++ b/examples/bubbles-example.jsx @@ -0,0 +1,63 @@ +import React from 'react'; + +import Datamap from '../src'; +import Example from './example'; + +export default class BubblesExample extends React.Component { + + render() { + return ( + + + `
Yield: ${data.yeild}\nExploded on ${data.date} by the ${data.country}` + }} + /> + + ); + } + +} diff --git a/examples/choropleth-example.jsx b/examples/choropleth-example.jsx new file mode 100644 index 0000000..2f270d6 --- /dev/null +++ b/examples/choropleth-example.jsx @@ -0,0 +1,63 @@ +/* global d3 */ + +import React from 'react'; + +import Datamap from '../src'; +import Example from './example'; + +const colors = d3.scale.category10(); + +export default class ChoroplethExample extends React.Component { + + state = { + data: { + USA: { fillKey: 'authorHasTraveledTo' }, + JPN: { fillKey: 'authorHasTraveledTo' }, + ITA: { fillKey: 'authorHasTraveledTo' }, + CRI: { fillKey: 'authorHasTraveledTo' }, + KOR: { fillKey: 'authorHasTraveledTo' }, + DEU: { fillKey: 'authorHasTraveledTo' } + } + }; + + componentDidMount() { + this.update(); + } + + componentWillUnmount() { + clearInterval(this.interval); + } + + update() { + this.interval = setInterval(() => { + this.setState({ + data: { + USA: colors(Math.random() * 10), + RUS: colors(Math.random() * 100), + AUS: { fillKey: 'authorHasTraveledTo' }, + BRA: colors(Math.random() * 50), + CAN: colors(Math.random() * 50), + ZAF: colors(Math.random() * 50), + IND: colors(Math.random() * 50) + } + }); + }, 2000); + } + + render() { + return ( + + + + ); + } + +} diff --git a/examples/example.jsx b/examples/example.jsx new file mode 100644 index 0000000..2d4e502 --- /dev/null +++ b/examples/example.jsx @@ -0,0 +1,25 @@ +import React from 'react'; + +export default class Example extends React.Component { + + static propTypes = { + children: React.PropTypes.element.isRequired, + label: React.PropTypes.string.isRequired, + mapStyle: React.PropTypes.object + }; + + render() { + return ( +
+

{this.props.label}

+
+ {React.Children.only(this.props.children)} +
+
+ ); + } + +} diff --git a/examples/index.jsx b/examples/index.jsx index 9d68346..bfa8e7c 100644 --- a/examples/index.jsx +++ b/examples/index.jsx @@ -1,45 +1,31 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import Datamap from '../src'; +import ArcsExample from './arcs-example'; +import BasicExample from './basic-example'; +import BubblesExample from './bubbles-example'; +import ChoroplethExample from './choropleth-example'; +import ProjectionsGraticulesExample from './projections-graticules-example.jsx'; +import StateLabelsExample from './state-labels-example'; +import ZoomExample from './zoom-example'; -const App = React.createClass({ - - displayName: 'App', - - getInitialState() { - return { - height: 300, - scope: 'world', - width: 500 - }; - }, - - update() { - this.setState(Object.assign({}, { - height: parseInt(this.refs.height.value, 10) || null, - scope: this.refs.scope.value || null, - width: parseInt(this.refs.width.value, 10) || null - }, window.example)); - }, +class App extends React.Component { render() { return (
-
- - - - -
-
- -
+ + + + + + +
); } -}); +} ReactDOM.render( , diff --git a/examples/projections-graticules-example.jsx b/examples/projections-graticules-example.jsx new file mode 100644 index 0000000..6cf2f6c --- /dev/null +++ b/examples/projections-graticules-example.jsx @@ -0,0 +1,71 @@ +/* global d3 */ + +import React from 'react'; + +import Datamap from '../src'; +import Example from './example'; + +const colors = d3.scale.category10(); + +export default class ProjectionsGraticulesExample extends React.Component { + + render() { + return ( + + + + ); + } + +} diff --git a/examples/screen.css b/examples/screen.css index 88e4a1c..757ce48 100644 --- a/examples/screen.css +++ b/examples/screen.css @@ -1,7 +1,13 @@ -* { +html { box-sizing: border-box; } +*, +*:before, +*:after { + box-sizing: inherit; +} + html, body, #app { @@ -15,57 +21,22 @@ body { font: 16px sans-serif; } -.App { - display: flex; - height: 100%; -} - -.App-options { +.Example { + align-items: center; display: flex; flex-direction: column; - padding: 10px; - align-items: stretch; - background: #fafafa; - border-right: 1px solid #e8e8e8; - z-index: 1; } -.App-options label { - padding: 10px; - display: flex; - align-items: center; - justify-content: flex-end; -} - -.App-options input { - width: 200px; - margin-left: 10px; - padding: 5px 10px; - font: inherit; - outline: 0; -} - -.App-options button { - margin: 10px; - padding: 10px; - background: #4183c4; - font: inherit; - color: white; - border: 0; - outline: 0; -} - -.App-options button:active { - background: #3673b0; -} - -.App-map { - flex: 1; - display: flex; - align-items: center; - justify-content: center; +.Example-map { + height: 500px; + width: 750px; } -.App-map > * { - border: 1px solid #eee; +.hoverinfo { + padding: 4px; + border-radius: 1px; + background-color: #fff; + box-shadow: 1px 1px 5px #ccc; + font-size: 12px; + border: 1px solid #ccc; } diff --git a/examples/state-labels-example.jsx b/examples/state-labels-example.jsx new file mode 100644 index 0000000..093bccf --- /dev/null +++ b/examples/state-labels-example.jsx @@ -0,0 +1,236 @@ +import React from 'react'; + +import Datamap from '../src'; +import Example from './example'; + +export default class StateLabelsExample extends React.Component { + + render() { + return ( + + + `
${geography.properties.name}\nElectoral Votes: ${data.electoralVotes}`, + highlightBorderWidth: 3 + }} + fills={{ + 'Republican': '#cc4731', + 'Democrat': '#306596', + 'Heavy Democrat': '#667faf', + 'Light Democrat': '#a9c0de', + 'Heavy Republican': '#ca5e5b', + 'Light Republican': '#eaa9a8', + 'defaultFill': '#eddc4e' + }} + data={{ + AZ: { + fillKey: 'Republican', + electoralVotes: 5 + }, + CO: { + fillKey: 'Light Democrat', + electoralVotes: 5 + }, + DE: { + fillKey: 'Democrat', + electoralVotes: 32 + }, + FL: { + fillKey: 'UNDECIDED', + electoralVotes: 29 + }, + GA: { + fillKey: 'Republican', + electoralVotes: 32 + }, + HI: { + fillKey: 'Democrat', + electoralVotes: 32 + }, + ID: { + fillKey: 'Republican', + electoralVotes: 32 + }, + IL: { + fillKey: 'Democrat', + electoralVotes: 32 + }, + IN: { + fillKey: 'Republican', + electoralVotes: 11 + }, + IA: { + fillKey: 'Light Democrat', + electoralVotes: 11 + }, + KS: { + fillKey: 'Republican', + electoralVotes: 32 + }, + KY: { + fillKey: 'Republican', + electoralVotes: 32 + }, + LA: { + fillKey: 'Republican', + electoralVotes: 32 + }, + MD: { + fillKey: 'Democrat', + electoralVotes: 32 + }, + ME: { + fillKey: 'Democrat', + electoralVotes: 32 + }, + MA: { + fillKey: 'Democrat', + electoralVotes: 32 + }, + MN: { + fillKey: 'Democrat', + electoralVotes: 32 + }, + MI: { + fillKey: 'Democrat', + electoralVotes: 32 + }, + MS: { + fillKey: 'Republican', + electoralVotes: 32 + }, + MO: { + fillKey: 'Republican', + electoralVotes: 13 + }, + MT: { + fillKey: 'Republican', + electoralVotes: 32 + }, + NC: { + fillKey: 'Light Republican', + electoralVotes: 32 + }, + NE: { + fillKey: 'Republican', + electoralVotes: 32 + }, + NV: { + fillKey: 'Heavy Democrat', + electoralVotes: 32 + }, + NH: { + fillKey: 'Light Democrat', + electoralVotes: 32 + }, + NJ: { + fillKey: 'Democrat', + electoralVotes: 32 + }, + NY: { + fillKey: 'Democrat', + electoralVotes: 32 + }, + ND: { + fillKey: 'Republican', + electoralVotes: 32 + }, + NM: { + fillKey: 'Democrat', + electoralVotes: 32 + }, + OH: { + fillKey: 'UNDECIDED', + electoralVotes: 32 + }, + OK: { + fillKey: 'Republican', + electoralVotes: 32 + }, + OR: { + fillKey: 'Democrat', + electoralVotes: 32 + }, + PA: { + fillKey: 'Democrat', + electoralVotes: 32 + }, + RI: { + fillKey: 'Democrat', + electoralVotes: 32 + }, + SC: { + fillKey: 'Republican', + electoralVotes: 32 + }, + SD: { + fillKey: 'Republican', + electoralVotes: 32 + }, + TN: { + fillKey: 'Republican', + electoralVotes: 32 + }, + TX: { + fillKey: 'Republican', + electoralVotes: 32 + }, + UT: { + fillKey: 'Republican', + electoralVotes: 32 + }, + WI: { + fillKey: 'Democrat', + electoralVotes: 32 + }, + VA: { + fillKey: 'Light Democrat', + electoralVotes: 32 + }, + VT: { + fillKey: 'Democrat', + electoralVotes: 32 + }, + WA: { + fillKey: 'Democrat', + electoralVotes: 32 + }, + WV: { + fillKey: 'Republican', + electoralVotes: 32 + }, + WY: { + fillKey: 'Republican', + electoralVotes: 32 + }, + CA: { + fillKey: 'Democrat', + electoralVotes: 32 + }, + CT: { + fillKey: 'Democrat', + electoralVotes: 32 + }, + AK: { + fillKey: 'Republican', + electoralVotes: 32 + }, + AR: { + fillKey: 'Republican', + electoralVotes: 32 + }, + AL: { + fillKey: 'Republican', + electoralVotes: 32 + } + }} + labels + /> + + ); + } + +} diff --git a/examples/zoom-example.jsx b/examples/zoom-example.jsx new file mode 100644 index 0000000..06e65d2 --- /dev/null +++ b/examples/zoom-example.jsx @@ -0,0 +1,95 @@ +/* global d3 */ + +import React from 'react'; + +import Datamap from '../src'; +import Example from './example'; + +const colors = d3.scale.category10(); + +export default class ZoomExample extends React.Component { + + setProjection(element) { + const projection = d3.geo.equirectangular() + .center([23, -3]) + .rotate([4.4, 0]) + .scale(400) + .translate([element.offsetWidth / 2, element.offsetHeight / 2]); + const path = d3.geo.path() + .projection(projection); + + return { path, projection }; + } + + render() { + return ( + + + `
Bubble for ${data.name}` + }} + /> + + ); + } + +} diff --git a/package.json b/package.json index 3fd54aa..4c3eb56 100644 --- a/package.json +++ b/package.json @@ -35,12 +35,14 @@ "datamaps": "^0.4.2" }, "devDependencies": { - "@btmills/eslint-config-btmills": "^0.1.3", + "@btmills/eslint-config-btmills": "^1.0.0-beta.6", "babel-cli": "^6.4.5", + "babel-eslint": "^6.1.0", + "babel-plugin-transform-class-properties": "^6.10.2", "babel-plugin-transform-object-rest-spread": "^6.3.13", "babel-preset-es2015": "^6.3.13", "babel-preset-react": "^6.3.13", - "eslint": "^1.10.3", - "eslint-plugin-react": "^3.16.1" + "eslint": "^2.13.1", + "eslint-plugin-react": "^5.2.2" } } diff --git a/src/datamap.jsx b/src/datamap.jsx index 506d6b9..a71b2f3 100644 --- a/src/datamap.jsx +++ b/src/datamap.jsx @@ -1,71 +1,104 @@ import React from 'react'; -import Datamap from 'datamaps'; +import Datamaps from 'datamaps'; -export default React.createClass({ +export default class Datamap extends React.Component { - displayName: 'Datamap', - - propTypes: { + static propTypes = { arc: React.PropTypes.array, arcOptions: React.PropTypes.object, bubbleOptions: React.PropTypes.object, bubbles: React.PropTypes.array, + data: React.PropTypes.object, graticule: React.PropTypes.bool, - labels: React.PropTypes.bool - }, + height: React.PropTypes.any, + labels: React.PropTypes.bool, + style: React.PropTypes.object, + updateChoroplethOptions: React.PropTypes.object, + width: React.PropTypes.any + }; componentDidMount() { this.drawMap(); - }, + } - componentWillReceiveProps() { - this.clear(); - }, + componentWillReceiveProps(newProps) { + if ( + this.props.height !== newProps.height + || this.props.width !== newProps.width + ) { + this.clear(); + } + } componentDidUpdate() { this.drawMap(); - }, + } componentWillUnmount() { this.clear(); - }, + } clear() { - const container = this.refs.container; + const { container } = this.refs; for (const child of Array.from(container.childNodes)) { container.removeChild(child); } - }, + + delete this.map; + } drawMap() { - const map = new Datamap(Object.assign({}, { ...this.props }, { - element: this.refs.container - })); + const { + arc, + arcOptions, + bubbles, + bubbleOptions, + data, + graticule, + labels, + updateChoroplethOptions, + ...props + } = this.props; + + let map = this.map; + + if (!map) { + map = this.map = new Datamaps({ + ...props, + data, + element: this.refs.container + }); + } else { + map.updateChoropleth(data, updateChoroplethOptions); + } - if (this.props.arc) { - map.arc(this.props.arc, this.props.arcOptions); + if (arc) { + map.arc(arc, arcOptions); } - if (this.props.bubbles) { - map.bubbles(this.props.bubbles, this.props.bubbleOptions); + if (bubbles) { + map.bubbles(bubbles, bubbleOptions); } - if (this.props.graticule) { + if (graticule) { map.graticule(); } - if (this.props.labels) { + if (labels) { map.labels(); } - }, + } render() { const style = { - position: 'relative' + height: '100%', + position: 'relative', + width: '100%', + ...this.props.style }; - return
; + return
; } -}); +}