Skip to content

Commit

Permalink
feat(geo): add legend support to choropleth components
Browse files Browse the repository at this point in the history
  • Loading branch information
Raphaël Benitte authored and Raphaël Benitte committed Mar 28, 2019
1 parent e07f58f commit bb7a0a2
Show file tree
Hide file tree
Showing 21 changed files with 579 additions and 237 deletions.
5 changes: 2 additions & 3 deletions packages/geo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,10 @@
],
"dependencies": {
"@nivo/core": "0.55.0",
"@nivo/legends": "0.55.0",
"d3-format": "^1.3.2",
"d3-geo": "^1.11.3",
"lodash": "^4.17.4",
"react-motion": "^0.5.2",
"recompose": "^0.30.0"
"lodash": "^4.17.4"
},
"peerDependencies": {
"prop-types": ">= 15.5.10 < 16.0.0",
Expand Down
123 changes: 115 additions & 8 deletions packages/geo/src/Choropleth.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,125 @@
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import React from 'react'
import React, { memo, Fragment, useCallback } from 'react'
import { SvgWrapper, withContainer, useDimensions, useTheme, useTooltip } from '@nivo/core'
import { BoxLegendSvg } from '@nivo/legends'
import { ChoroplethPropTypes, ChoroplethDefaultProps } from './props'
import GeoMap from './GeoMap'
import { useChoropleth } from './hooks'
import GeoGraticule from './GeoGraticule'
import GeoMapFeature from './GeoMapFeature'
import { useGeoMap, useChoropleth } from './hooks'

const Choropleth = props => {
const { getFillColor, boundFeatures } = useChoropleth(props)
const Choropleth = memo(({ width, height, margin: partialMargin,
features, data, match, label, value, valueFormat,
projectionType, projectionScale, projectionTranslation, projectionRotation,
colors, unknownColor, borderWidth, borderColor,
enableGraticule, graticuleLineWidth, graticuleLineColor,
layers,
legends,
isInteractive, onClick, tooltip: Tooltip }) => {
const { margin, outerWidth, outerHeight } = useDimensions(width, height, partialMargin)
const { graticule, path, getBorderWidth, getBorderColor } = useGeoMap({
width,
height,
projectionType,
projectionScale,
projectionTranslation,
projectionRotation,
fillColor: () => {},
borderWidth,
borderColor,
})
const { getFillColor, boundFeatures, legendData } = useChoropleth({
features,
data,
match,
label,
value,
valueFormat,
colors,
unknownColor,
})

return <GeoMap {...props} features={boundFeatures} fillColor={getFillColor} />
}
const theme = useTheme()

const [showTooltip, hideTooltip] = useTooltip()
const handleClick = useCallback(
(feature, event) => isInteractive && onClick && onClick(feature, event),
[isInteractive, onClick]
)
const handleMouseEnter = useCallback(
(feature, event) =>
isInteractive && Tooltip && showTooltip(<Tooltip feature={feature} />, event),
[isInteractive, showTooltip, Tooltip]
)
const handleMouseMove = useCallback(
(feature, event) =>
isInteractive && Tooltip && showTooltip(<Tooltip feature={feature} />, event),
[isInteractive, showTooltip, Tooltip]
)
const handleMouseLeave = useCallback(() => isInteractive && hideTooltip(), [
isInteractive,
hideTooltip,
])

return (
<SvgWrapper width={outerWidth} height={outerHeight} margin={margin} theme={theme}>
{layers.map((layer, i) => {
if (layer === 'graticule') {
if (enableGraticule !== true) return null

return (
<GeoGraticule
key="graticule"
path={path}
graticule={graticule}
lineWidth={graticuleLineWidth}
lineColor={graticuleLineColor}
/>
)
}
if (layer === 'features') {
return (
<Fragment key="features">
{boundFeatures.map(feature => (
<GeoMapFeature
key={feature.id}
feature={feature}
path={path}
fillColor={getFillColor(feature)}
borderWidth={getBorderWidth(feature)}
borderColor={getBorderColor(feature)}
onMouseEnter={handleMouseEnter}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
onClick={handleClick}
/>
))}
</Fragment>
)
}
if (layer === 'legends') {
return legends.map((legend, i) => {
return (
<BoxLegendSvg
key={i}
containerWidth={width}
containerHeight={height}
data={legendData}
{...legend}
/>
)
})
}

return <Fragment key={i}>{layer({})}</Fragment>
})}
</SvgWrapper>
)
})

Choropleth.displayName = 'Choropleth'
Choropleth.propTypes = ChoroplethPropTypes
Choropleth.defaultProps = ChoroplethDefaultProps

export default Choropleth
export default withContainer(Choropleth)
164 changes: 156 additions & 8 deletions packages/geo/src/ChoroplethCanvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,166 @@
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import React from 'react'
import { ChoroplethCanvasPropTypes, ChoroplethCanvasDefaultProps } from './props'
import GeoMapCanvas from './GeoMapCanvas'
import { useChoropleth } from './hooks'
import React, { memo, useRef, useEffect, useCallback } from 'react'
import { geoContains } from 'd3-geo'
import { getRelativeCursor, withContainer, useDimensions, useTheme, useTooltip } from '@nivo/core'
import { renderLegendToCanvas } from '@nivo/legends'
import { ChoroplethCanvasDefaultProps, ChoroplethCanvasPropTypes } from './props'
import { useGeoMap, useChoropleth } from './hooks'

const ChoroplethCanvas = props => {
const { getFillColor, boundFeatures } = useChoropleth(props)
const getFeatureFromMouseEvent = (event, el, features, projection) => {
const [x, y] = getRelativeCursor(el, event)

return <GeoMapCanvas {...props} features={boundFeatures} fillColor={getFillColor} />
return features.find(f => geoContains(f, projection.invert([x, y])))
}

const ChoroplethCanvas = memo(({ width, height, margin: partialMargin, pixelRatio,
features, data, match, label, value, valueFormat,
projectionType, projectionScale, projectionTranslation, projectionRotation,
colors, unknownColor, borderWidth, borderColor,
enableGraticule, graticuleLineWidth, graticuleLineColor,
layers,
legends,
isInteractive, onClick, onMouseMove, tooltip: Tooltip }) => {
const canvasEl = useRef(null)
const theme = useTheme()
const { margin, outerWidth, outerHeight } = useDimensions(width, height, partialMargin)
const { projection, graticule, path, getBorderWidth, getBorderColor } = useGeoMap({
width,
height,
projectionType,
projectionScale,
projectionTranslation,
projectionRotation,
fillColor: () => {},
borderWidth,
borderColor,
})
const { getFillColor, boundFeatures, legendData } = useChoropleth({
features,
data,
match,
label,
value,
valueFormat,
colors,
unknownColor,
})

useEffect(() => {
if (!canvasEl) return

canvasEl.current.width = outerWidth * pixelRatio
canvasEl.current.height = outerHeight * pixelRatio

const ctx = canvasEl.current.getContext('2d')

ctx.scale(pixelRatio, pixelRatio)

ctx.fillStyle = theme.background
ctx.fillRect(0, 0, outerWidth, outerHeight)
ctx.translate(margin.left, margin.top)

path.context(ctx)

layers.forEach(layer => {
if (layer === 'graticule') {
if (enableGraticule === true) {
ctx.lineWidth = graticuleLineWidth
ctx.strokeStyle = graticuleLineColor
ctx.beginPath()
path(graticule())
ctx.stroke()
}
} else if (layer === 'features') {
boundFeatures.forEach(feature => {
ctx.beginPath()
path(feature)
ctx.fillStyle = getFillColor(feature)
ctx.fill()

const borderWidth = getBorderWidth(feature)
if (borderWidth > 0) {
ctx.strokeStyle = getBorderColor(feature)
ctx.lineWidth = borderWidth
ctx.stroke()
}
})
} else if (layer === 'legends') {
legends.forEach(legend => {
renderLegendToCanvas(ctx, {
...legend,
data: legendData,
containerWidth: width,
containerHeight: height,
theme,
})
})
} else {
layer(ctx, props)
}
})
}, [
canvasEl,
outerWidth,
outerHeight,
margin,
pixelRatio,
theme,
path,
graticule,
getFillColor,
getBorderWidth,
getBorderColor,
boundFeatures,
legends,
layers,
])

const [showTooltip, hideTooltip] = useTooltip()
const handleMouseMove = useCallback(() => {
if (!isInteractive || !Tooltip) return

const feature = getFeatureFromMouseEvent(event, canvasEl.current, boundFeatures, projection)
if (feature) {
showTooltip(<Tooltip feature={feature} />, event)
} else {
hideTooltip()
}
onMouseMove && onMouseMove(feature || null, event)
}, [showTooltip, hideTooltip, isInteractive, Tooltip, canvasEl, boundFeatures, projection])
const handleMouseLeave = useCallback(() => isInteractive && hideTooltip(), [
isInteractive,
hideTooltip,
])
const handleClick = useCallback(() => {
if (!isInteractive || !onClick) return

const feature = getFeatureFromMouseEvent(event, canvasEl.current, boundFeatures, projection)
if (feature) {
onClick(feature, event)
}
}, [isInteractive, canvasEl, boundFeatures, projection, onClick])

return (
<canvas
ref={canvasEl}
width={outerWidth * pixelRatio}
height={outerHeight * pixelRatio}
style={{
width: outerWidth,
height: outerHeight,
cursor: isInteractive ? 'auto' : 'normal',
}}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
onClick={handleClick}
/>
)
})

ChoroplethCanvas.displayName = 'ChoroplethCanvas'
ChoroplethCanvas.propTypes = ChoroplethCanvasPropTypes
ChoroplethCanvas.defaultProps = ChoroplethCanvasDefaultProps

export default ChoroplethCanvas
export default withContainer(ChoroplethCanvas)
3 changes: 2 additions & 1 deletion packages/geo/src/GeoMap.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,13 @@ const GeoMap = memo(props => {
)
}

return <Fragment key={i}>layer(props)</Fragment>
return <Fragment key={i}>{layer(props)}</Fragment>
})}
</SvgWrapper>
)
})

GeoMap.displayName = 'GeoMap'
GeoMap.propTypes = GeoMapPropTypes
GeoMap.defaultProps = GeoMapDefaultProps

Expand Down
11 changes: 7 additions & 4 deletions packages/geo/src/GeoMapCanvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import React, { useRef, useEffect, useCallback } from 'react'
import { getRelativeCursor, withContainer, useDimensions, useTheme, useTooltip } from '@nivo/core'
import React, { memo, useRef, useEffect, useCallback } from 'react'
import { geoContains } from 'd3-geo'
import { getRelativeCursor, withContainer, useDimensions, useTheme, useTooltip } from '@nivo/core'
import { GeoMapCanvasDefaultProps, GeoMapCanvasPropTypes } from './props'
import { useGeoMap } from './hooks'

Expand All @@ -18,7 +18,7 @@ const getFeatureFromMouseEvent = (event, el, features, projection) => {
return features.find(f => geoContains(f, projection.invert([x, y])))
}

const GeoMapCanvas = props => {
const GeoMapCanvas = memo(props => {
const {
width,
height,
Expand Down Expand Up @@ -118,6 +118,8 @@ const GeoMapCanvas = props => {
getFillColor,
getBorderWidth,
getBorderColor,
features,
layers,
])

const [showTooltip, hideTooltip] = useTooltip()
Expand Down Expand Up @@ -160,8 +162,9 @@ const GeoMapCanvas = props => {
onClick={handleClick}
/>
)
}
})

GeoMapCanvas.displatName = 'GeoMapCanvas'
GeoMapCanvas.propTypes = GeoMapCanvasPropTypes
GeoMapCanvas.defaultProps = GeoMapCanvasDefaultProps

Expand Down
Loading

0 comments on commit bb7a0a2

Please sign in to comment.