diff --git a/app/scripts/components/world-map.js b/app/scripts/components/world-map.js new file mode 100644 index 000000000..93e4a3238 --- /dev/null +++ b/app/scripts/components/world-map.js @@ -0,0 +1,87 @@ +import React, {Component, PropTypes} from 'react' +import d3 from 'd3' +import topojson from 'topojson' +import {AutoSizer} from 'react-virtualized' +import ReactFauxDOM from 'react-faux-dom' + +import worldData from '../../data/world.json' + +export default class WorldMap extends Component { + static propTypes = { + coordinates: PropTypes.array.isRequired + }; + + _renderMap = ({width, height}) => { + const projection = d3.geo.equirectangular() + .scale(height / Math.PI) + .translate([width / 2, height / 2]) + .precision(0.1) + + const path = d3.geo.path() + .projection(projection) + + const graticule = d3.geo.graticule() + + const el = d3.select(ReactFauxDOM.createElement('svg')) + .attr('width', width) + .attr('height', height) + + // Fill Pattern + el.append('defs') + .append('pattern') + .attr('id', 'gridpattern') + .attr('x', 0) + .attr('y', 0) + .attr('width', 4) + .attr('height', 4) + .attr('patternUnits', 'userSpaceOnUse') + .append('circle') + .attr('cx', 0) + .attr('cy', 0) + .attr('r', 1) + .attr('style', 'stroke: none; fill: rgba(255, 255, 255, 0.7)') + + el.append('path') + .datum(graticule) + .attr('class', 'graticule') + .attr('d', path) + + el.insert('path', '.graticule') + .datum(topojson.feature(worldData, worldData.objects.land)) + .attr('d', path) + .attr('fill', 'url(#gridpattern)') + + el.insert('path', '.graticule') + .datum(topojson.mesh( + worldData, + worldData.objects.countries, + (a, b) => a !== b + )) + .attr('d', path) + .attr('fill', 'none') + .attr('stroke', 'none') + .attr('stroke-width', '0.5px') + + el.append('path') + .datum({ + type: 'MultiPoint', + coordinates: this.props.coordinates + }) + .attr('d', path.pointRadius((d) => 8)) + .attr('class', 'world-locations-base') + + el.append('path') + .datum({ + type: 'MultiPoint', + coordinates: this.props.coordinates + }) + .attr('d', path.pointRadius((d) => 2)) + .attr('class', 'world-locations-center') + + return el.node().toReact() + }; + + render () { + return {this._renderMap} + } +} diff --git a/app/scripts/components/world.js b/app/scripts/components/world.js index 4db1e5981..40423b6a1 100644 --- a/app/scripts/components/world.js +++ b/app/scripts/components/world.js @@ -1,11 +1,6 @@ import React, {Component, PropTypes} from 'react' -import d3 from 'd3' -import ReactFauxDOM from 'react-faux-dom' -import topojson from 'topojson' -import {AutoSizer} from 'react-virtualized' import {map, isEqual} from 'lodash' - -import worldData from '../../data/world.json' +import WorldMap from './world-map' export default class World extends Component { static propTypes = { @@ -18,93 +13,18 @@ export default class World extends Component { locations: {} }; - _renderMap = (locations) => { - return ({width, height}) => { - const projection = d3.geo.equirectangular() - .scale(height / Math.PI) - .translate([width / 2, height / 2]) - .precision(0.1) - - const path = d3.geo.path() - .projection(projection) - - const graticule = d3.geo.graticule() - - const el = d3.select(ReactFauxDOM.createElement('svg')) - .attr('width', width) - .attr('height', height) - - // Fill Pattern - el.append('defs') - .append('pattern') - .attr('id', 'gridpattern') - .attr('x', 0) - .attr('y', 0) - .attr('width', 4) - .attr('height', 4) - .attr('patternUnits', 'userSpaceOnUse') - .append('circle') - .attr('cx', 0) - .attr('cy', 0) - .attr('r', 1) - .attr('style', 'stroke: none; fill: rgba(255, 255, 255, 0.7)') - - el.append('path') - .datum(graticule) - .attr('class', 'graticule') - .attr('d', path) - - el.insert('path', '.graticule') - .datum(topojson.feature(worldData, worldData.objects.land)) - .attr('d', path) - .attr('fill', 'url(#gridpattern)') - - el.insert('path', '.graticule') - .datum(topojson.mesh( - worldData, - worldData.objects.countries, - (a, b) => a !== b - )) - .attr('d', path) - .attr('fill', 'none') - .attr('stroke', 'none') - .attr('stroke-width', '0.5px') - - el.append('path') - .datum({ - type: 'MultiPoint', - coordinates: locations - }) - .attr('d', path.pointRadius((d) => 8)) - .attr('class', 'world-locations-base') - - el.append('path') - .datum({ - type: 'MultiPoint', - coordinates: locations - }) - .attr('d', path.pointRadius((d) => 2)) - .attr('class', 'world-locations-center') - - return el.node().toReact() - } - }; - shouldComponentUpdate (nextProps) { return !isEqual(nextProps, this.props) } render () { - // Draw peer locations - const locations = map(this.props.locations, ({lon, lat}) => { + const coordinates = map(this.props.locations, ({lon, lat}) => { return [lon, lat] }) return (
- - {this._renderMap(locations)} - +
{this.props.peersCount}
Peers
diff --git a/test/components/world-map.spec.js b/test/components/world-map.spec.js new file mode 100644 index 000000000..20f08a502 --- /dev/null +++ b/test/components/world-map.spec.js @@ -0,0 +1,24 @@ +import {expect} from 'chai' +import {shallow, render} from 'enzyme' +import React from 'react' + +import WorldMap from '../../app/scripts/components/world-map' + +describe('WorldMap', () => { + const c = [[12, 14]] + + it('should render an svg', () => { + const el = render() + expect(el.find('svg').length).to.equal(1) + }) + + it('should have the correct coordiates', () => { + const el = shallow() + const p = el.instance().props + + c.forEach((coordinates, i) => { + expect(coordinates[0]).to.equal(p.coordinates[i][0]) + expect(coordinates[1]).to.equal(p.coordinates[i][1]) + }) + }) +}) diff --git a/test/components/world.spec.js b/test/components/world.spec.js new file mode 100644 index 000000000..22f0a7ff6 --- /dev/null +++ b/test/components/world.spec.js @@ -0,0 +1,22 @@ +import {expect} from 'chai' +import {shallow} from 'enzyme' +import React from 'react' + +import World from '../../app/scripts/components/world' + +describe('World', () => { + it('should have the correct defaults', () => { + const el = shallow() + expect(el.find('WorldMap').length).to.equal(1) + + var inst = el.instance() + expect(inst.props.peersCount).to.equal(0) + expect(Object.keys(inst.props.locations).length) + .to.equal(0) + }) + + it('should set peersCount', () => { + const el = shallow() + expect(el.find('.counter').node.props.children).to.equal(1337) + }) +})