Skip to content

Commit

Permalink
feat(geo): add support for projection translation/rotation
Browse files Browse the repository at this point in the history
  • Loading branch information
Raphaël Benitte authored and Raphaël Benitte committed Mar 27, 2019
1 parent 54c0040 commit a78b293
Show file tree
Hide file tree
Showing 12 changed files with 222 additions and 57 deletions.
12 changes: 3 additions & 9 deletions packages/geo/src/Choropleth.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,14 @@
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import React, { Component } from 'react'
import React from 'react'
import { ChoroplethPropTypes } from './props'
import GeoMap from './GeoMap'
import { enhanceChoropleth } from './enhance'

class Choropleth extends Component {
static propTypes = ChoroplethPropTypes
const Choropleth = props => <GeoMap {...props} />

render() {
const {} = this.props

return <GeoMap {...this.props} />
}
}
Choropleth.propTypes = ChoroplethPropTypes

Choropleth.displayName = 'Choropleth'

Expand Down
12 changes: 3 additions & 9 deletions packages/geo/src/ChoroplethCanvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,14 @@
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import React, { Component } from 'react'
import React from 'react'
import { ChoroplethCanvasPropTypes } from './props'
import GeoMapCanvas from './GeoMapCanvas'
import { enhanceChoropleth } from './enhance'

class ChoroplethCanvas extends Component {
static propTypes = ChoroplethCanvasPropTypes
const ChoroplethCanvas = props => <GeoMapCanvas {...props} />

render() {
const {} = this.props

return <GeoMapCanvas {...this.props} />
}
}
ChoroplethCanvas.propTypes = ChoroplethCanvasPropTypes

ChoroplethCanvas.displayName = 'ChoroplethCanvas'

Expand Down
9 changes: 2 additions & 7 deletions packages/geo/src/GeoMap.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,10 @@ class GeoMap extends Component {
const { tooltip, theme } = this.props
if (!tooltip) return

const tooltipContent = tooltip(feature)
const tooltipContent = tooltip(feature, theme)
if (!tooltipContent) return

showTooltip(
<div style={theme.tooltip.container}>
<div>{tooltipContent}</div>
</div>,
event
)
showTooltip(tooltipContent, event)
}

handleMouseEnter = showTooltip => (feature, event) => {
Expand Down
8 changes: 4 additions & 4 deletions packages/geo/src/GeoMapCanvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,11 @@ class GeoMapCanvas extends Component {

const feature = this.getFeatureFromMouseEvent(event)
if (feature && tooltip) {
const tooltipContent = tooltip(feature)
const tooltipContent = tooltip(feature, theme)
if (!tooltipContent) {
hideTooltip()
} else {
showTooltip(<div style={theme.tooltip.container}>{tooltipContent}</div>, event)
showTooltip(tooltipContent, event)
}
} else {
hideTooltip()
Expand All @@ -71,7 +71,7 @@ class GeoMapCanvas extends Component {

const feature = this.getFeatureFromMouseEvent(event)
if (feature) {
onClick(feature)
onClick(feature, event)
}
}

Expand Down Expand Up @@ -149,7 +149,7 @@ class GeoMapCanvas extends Component {
style={{
width: outerWidth,
height: outerHeight,
cursor: isInteractive ? 'crosshair' : 'normal',
cursor: isInteractive ? 'auto' : 'normal',
}}
onMouseMove={this.handleMouseMove(showTooltip, hideTooltip)}
onClick={this.handleClick}
Expand Down
2 changes: 1 addition & 1 deletion packages/geo/src/GeoMapFeature.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class GeoMapFeature extends Component {
return (
<path
key={feature.id}
style={{ cursor: 'crosshair' }}
style={{ cursor: 'auto' }}
fill={fillColor}
strokeWidth={borderWidth}
stroke={borderColor}
Expand Down
64 changes: 50 additions & 14 deletions packages/geo/src/enhance.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import React from 'react'
import { isFunction, get } from 'lodash'
import {
geoPath,
Expand All @@ -21,8 +22,15 @@ import {
geoNaturalEarth1,
geoGraticule,
} from 'd3-geo'
import { compose, defaultProps, withPropsOnChange, pure, setDisplayName } from 'recompose'
import { withDimensions, withTheme, guessQuantizeColorScale } from '@nivo/core'
import {
compose,
defaultProps,
withPropsOnChange,
pure,
setDisplayName,
withProps,
} from 'recompose'
import { withDimensions, withTheme, guessQuantizeColorScale, BasicTooltip } from '@nivo/core'
import {
GeoMapDefaultProps,
GeoMapCanvasDefaultProps,
Expand All @@ -47,18 +55,26 @@ const commonEnhancers = [
withTheme(),
withDimensions(),
withPropsOnChange(
['width', 'height', 'projectionType', 'projectionScale', 'projectionTranslation'],
[
'width',
'height',
'projectionType',
'projectionScale',
'projectionTranslation',
'projectionRotation',
],
({
width,
height,
projectionType,
projectionScale,
projectionTranslation: [translateX, translateY],
projectionRotation,
}) => {
const projection = projectionById[projectionType]()
.scale(projectionScale)
.translate([width * translateX, height * translateY])
.rotate([-10, 0])
.rotate(projectionRotation)

const pathHelper = geoPath(projection)

Expand Down Expand Up @@ -117,9 +133,19 @@ export const enhanceChoropleth = Component => {
return setDisplayName(Component.displayName)(
compose(
defaultProps(defaultComponentProps),
withPropsOnChange(['colors', 'unknownColor'], ({ colors, unknownColor }) => {
const colorScale = guessQuantizeColorScale(colors).domain([0, 1000000])

return {
fillColor: feature => {
if (feature.value === undefined) return unknownColor
return colorScale(feature.value)
},
}
}),
withPropsOnChange(
['features', 'data', 'matchOn', 'valueFrom'],
({ features, data, matchOn, valueFrom }) => {
['features', 'data', 'matchOn', 'valueFrom', 'fillColor'],
({ features, data, matchOn, valueFrom, fillColor }) => {
let predicate
if (isFunction(matchOn)) {
predicate = matchOn
Expand All @@ -139,26 +165,36 @@ export const enhanceChoropleth = Component => {
return {
features: features.map(feature => {
const datum = data.find(datum => predicate(feature, datum))
const value = valueAccessor(datum)

if (datum) {
return {
const featureWithData = {
...feature,
data: datum,
value: valueAccessor(datum),
value,
}
featureWithData.color = fillColor(featureWithData)

return featureWithData
}

return feature
}),
}
}
),
withPropsOnChange(['colors', 'unknownColor'], ({ colors, unknownColor }) => {
const colorScale = guessQuantizeColorScale(colors).domain([0, 1000000])

withProps(() => {
return {
fillColor: feature => {
if (feature.value === undefined) return unknownColor
return colorScale(feature.value)
tooltip: (feature, theme) => {
return (
<BasicTooltip
id={feature.properties.name}
value={feature.value}
color={feature.color}
enableChip={true}
theme={theme}
/>
)
},
}
})
Expand Down
2 changes: 2 additions & 0 deletions packages/geo/src/props.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const commonPropTypes = {
projectionType: PropTypes.oneOf(Object.keys(projectionById)).isRequired,
projectionScale: PropTypes.number.isRequired,
projectionTranslation: PropTypes.arrayOf(PropTypes.number).isRequired,
projectionRotation: PropTypes.arrayOf(PropTypes.number).isRequired,

enableGraticule: PropTypes.bool.isRequired,
graticuleLineWidth: PropTypes.number.isRequired,
Expand Down Expand Up @@ -75,6 +76,7 @@ const commonDefaultProps = {
projectionType: 'mercator',
projectionScale: 100,
projectionTranslation: [0.5, 0.5],
projectionRotation: [0, 0, 0],

enableGraticule: false,
graticuleLineWidth: 0.5,
Expand Down
34 changes: 30 additions & 4 deletions website/src/components/charts/geo/Choropleth.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ const initialSettings = {

projectionType: 'mercator',
projectionScale: 100,
projectionTranslation: [0.5, 0.5],
projectionRotation: [0, 0, 0],

enableGraticule: true,
graticuleLineWidth: 0.5,
Expand All @@ -60,7 +62,11 @@ const Choropleth = () => {
const [settings, setSettings] = useState(initialSettings)
const [data, setData] = useState(generateChoroplethData())
const onClick = useCallback((feature, event) => {
alert(`${feature.properties.name}\nclicked at x: ${event.clientX}, y: ${event.clientY}`)
alert(
`${feature.properties.name} (${feature.id})\nclicked at x: ${event.clientX}, y: ${
event.clientY
}`
)
})
const diceRoll = useCallback(() => setData(generateChoroplethData()), [setData])

Expand All @@ -85,9 +91,29 @@ const Choropleth = () => {
const description = (
<div className="chart-description">
<p className="description">
The responsive alternative of this component is <code>ResponsiveChoropleth</code>,
it also offers a canvas implementations, see{' '}
<Link to="/choropleth/canvas">ChoroplethCanvas</Link>.
The Choropleth component displays divided geographical areas shaded in relation to
some data variable. It's build on top of the <Link to="/geomap">GeoMap</Link>{' '}
component.
</p>
<p className="description">
Using this component requires some knowledge about the <code>d3-geo</code>
library, projections, geoJSON… please have a loot at the{' '}
<a href="https://github.com/d3/d3-geo" target="_blank" rel="noopener noreferrer">
official d3 documentation
</a>{' '}
for further information.
</p>
<p className="description">
Like for <Link to="/geomap">GeoMap</Link>, you must pass an array of features which
determine the geometries to render on the map, then you pass an array of data which,
each datum is merged with its corresponding feature using the <code>match</code>{' '}
property, the value is picked according to the <code>value</code> accessor.
</p>
<p className="description">
The responsive alternative of this component is <code>ResponsiveChoropleth</code>.
This component also have a canvas implementations,{' '}
<Link to="/choropleth/canvas">ChoroplethCanvas</Link>, which should be used when you
have complex geometries as it offers better performance.
</p>
</div>
)
Expand Down
17 changes: 13 additions & 4 deletions website/src/components/charts/geo/ChoroplethCanvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ const initialSettings = {

projectionType: 'mercator',
projectionScale: 100,
projectionTranslation: [0.5, 0.5],
projectionRotation: [0, 0, 0],

enableGraticule: true,
graticuleLineWidth: 0.5,
Expand All @@ -60,7 +62,11 @@ const ChoroplethCanvas = () => {
const [settings, setSettings] = useState(initialSettings)
const [data, setData] = useState(generateChoroplethData())
const onClick = useCallback((feature, event) => {
alert(`${feature.properties.name}\nclicked at x: ${event.clientX}, y: ${event.clientY}`)
alert(
`${feature.properties.name} (${feature.id})\nclicked at x: ${event.clientX}, y: ${
event.clientY
}`
)
})
const diceRoll = useCallback(() => setData(generateChoroplethData()), [setData])

Expand All @@ -83,9 +89,12 @@ const ChoroplethCanvas = () => {
const description = (
<div className="chart-description">
<p className="description">
The responsive alternative of this component is <code>ResponsiveChoropleth</code>,
it also offers a canvas implementations, see{' '}
<Link to="/choropleth/canvas">ChoroplethCanvas</Link>.
A canvas implementation of the <Link to="/choropleth">Choropleth</Link> component,
should be used used when you have complex geometries as it offers better performance
than its SVG counterpart.
</p>
<p className="description">
The responsive alternative of this component is <code>ResponsiveChoropleth</code>.
</p>
</div>
)
Expand Down
15 changes: 15 additions & 0 deletions website/src/components/charts/geo/GeoMap.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ const initialSettings = {

projectionType: 'mercator',
projectionScale: 100,
projectionTranslation: [0.5, 0.5],
projectionRotation: [0, 0, 0],

fillColor: '#eeeeee',
borderWidth: 0.5,
Expand Down Expand Up @@ -74,6 +76,19 @@ const GeoMap = () => {

const description = (
<div className="chart-description">
<p className="description">
This component can be used to draw maps, it takes an array of features which
determine the geometries to render on the map. It can be used to build more complex
maps such as the <Link to="/choropleth">Choropleth</Link>.
</p>
<p className="description">
Using this component requires some knowledge about the <code>d3-geo</code>
library, projections, geoJSON… please have a loot at the{' '}
<a href="https://github.com/d3/d3-geo" target="_blank" rel="noopener noreferrer">
official d3 documentation
</a>{' '}
for further information.
</p>
<p className="description">
The responsive alternative of this component is <code>ResponsiveGeoMap</code>, it
also offers a canvas implementations, see{' '}
Expand Down
Loading

0 comments on commit a78b293

Please sign in to comment.