({ pixelRatio, width, height, margin: partialMargin, data, groups, groupBy, identity, label, value, valueFormat, valueScale, size, spacing, layout, gap, forceStrength, simulationIterations, layers, renderNode, colors, colorBy, borderWidth, borderColor, enableGridX, gridXValues, enableGridY, gridYValues, axisTop, axisRight, axisBottom, axisLeft, annotations, isInteractive, onMouseEnter, onMouseMove, onMouseLeave, onClick, tooltip, debugMesh, }) => { const canvasEl = useRef(null) const [currentNode, setCurrentNode] = useState(null) const { margin, innerWidth, innerHeight, outerWidth, outerHeight } = useDimensions( width, height, partialMargin ) const theme = useTheme() const { nodes, xScale, yScale } = useSwarmPlot({ width: innerWidth, height: innerHeight, data, groups, groupBy, identity, label, value, valueFormat, valueScale, size, spacing, layout, gap, colors, colorBy, forceStrength, simulationIterations, }) const boundAnnotations = useSwarmPlotAnnotations(nodes, annotations) const computedAnnotations = useComputedAnnotations({ annotations: boundAnnotations, innerWidth, innerHeight, }) const getBorderWidth = useBorderWidth(borderWidth) const getBorderColor = useInheritedColor(borderColor, theme) const { delaunay, voronoi } = useVoronoiMesh({ points: nodes, width: innerWidth, height: innerHeight, debug: debugMesh, }) useEffect(() => { 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) layers.forEach(layer => { if (layer === 'grid' && theme.grid.line.strokeWidth > 0) { ctx.lineWidth = theme.grid.line.strokeWidth ctx.strokeStyle = theme.grid.line.stroke enableGridX && renderGridLinesToCanvas(ctx, { width: innerWidth, height: innerHeight, scale: xScale, axis: 'x', values: gridXValues, }) enableGridY && renderGridLinesToCanvas(ctx, { width: innerWidth, height: innerHeight, scale: yScale, axis: 'y', values: gridYValues, }) } if (layer === 'axes') { renderAxesToCanvas(ctx, { xScale, yScale, width: innerWidth, height: innerHeight, top: axisTop, right: axisRight, bottom: axisBottom, left: axisLeft, theme, }) } if (layer === 'nodes') { nodes.forEach(node => { renderNode(ctx, { node, getBorderWidth, getBorderColor, }) }) } if (layer === 'mesh' && debugMesh === true) { renderVoronoiToCanvas(ctx, voronoi) if (currentNode) { renderVoronoiCellToCanvas(ctx, voronoi, currentNode.index) } } if (layer === 'annotations') { renderAnnotationsToCanvas(ctx, { annotations: computedAnnotations, theme, }) } if (typeof layer === 'function') { layer(ctx, { nodes, innerWidth, innerHeight, outerWidth, outerHeight, margin, xScale, yScale, }) } }) }, [ canvasEl, innerWidth, innerHeight, outerWidth, outerHeight, margin, pixelRatio, theme, layers, nodes, xScale, yScale, getBorderWidth, getBorderColor, voronoi, currentNode, computedAnnotations, ]) const [showTooltip, hideTooltip] = useTooltip() const showNodeTooltip = useMemo(() => { if (tooltip) return (node, event) => showTooltip(tooltip({ node }), event) return (node, event) => showTooltip(<SwarmPlotTooltip node={node} />, event) }, [showTooltip, tooltip]) const getNodeFromMouseEvent = useCallback( event => { const [x, y] = getRelativeCursor(canvasEl.current, event) if (!isCursorInRect(margin.left, margin.top, innerWidth, innerHeight, x, y)) return null const nodeIndex = delaunay.find(x - margin.left, y - margin.top) return nodes[nodeIndex] }, [canvasEl, margin, innerWidth, innerHeight, delaunay, setCurrentNode] ) const handleMouseHover = useCallback( event => { const node = getNodeFromMouseEvent(event) setCurrentNode(node) onMouseMove && onMouseMove(node, event) if (node) { showNodeTooltip(node, event) if ((!currentNode || currentNode.id !== node.id) && onMouseEnter) { onMouseEnter(node, event) } if (currentNode && currentNode.id !== node.id && onMouseLeave) { onMouseLeave(currentNode, event) } } else { currentNode && onMouseLeave && onMouseLeave(currentNode, event) hideTooltip() } }, [ getNodeFromMouseEvent, currentNode, onMouseEnter, onMouseLeave, showNodeTooltip, hideTooltip, ] ) const handleMouseLeave = useCallback( event => { hideTooltip() setCurrentNode(null) onMouseLeave && onMouseLeave(currentNode, event) }, [hideTooltip, setCurrentNode, currentNode, onMouseLeave] ) const handleClick = useCallback( event => { const node = getNodeFromMouseEvent(event) node && onClick && onClick(node, event) }, [getNodeFromMouseEvent, onClick] ) return ( <canvas ref={canvasEl} width={outerWidth * pixelRatio} height={outerHeight * pixelRatio} style={{ width: outerWidth, height: outerHeight, cursor: isInteractive ? 'auto' : 'normal', }} onMouseEnter={isInteractive ? handleMouseHover : undefined} onMouseMove={isInteractive ? handleMouseHover : undefined} onMouseLeave={isInteractive ? handleMouseLeave : undefined} onClick={isInteractive ? handleClick : undefined} /> ) }
const GeoMap = memo(props => { const { width, height, margin: partialMargin, features, layers, projectionType, projectionScale, projectionTranslation, projectionRotation, fillColor, borderWidth, borderColor, enableGraticule, graticuleLineWidth, graticuleLineColor, isInteractive, onClick, tooltip: Tooltip, } = props const { margin, outerWidth, outerHeight } = useDimensions(width, height, partialMargin) const { graticule, path, getFillColor, getBorderWidth, getBorderColor } = useGeoMap({ width, height, projectionType, projectionScale, projectionTranslation, projectionRotation, fillColor, borderWidth, borderColor, }) 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"> {features.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> ) } return <Fragment key={i}>{layer(props)}</Fragment> })} </SvgWrapper> ) })
({ 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, }) 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> ) }
({ 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, {}) } }) }, [ 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} /> ) }
({ arc, startAngle, endAngle, borderWidth, getBorderColor, opacity, arcGenerator, setCurrent, isInteractive, onMouseEnter, onMouseMove, onMouseLeave, onClick, tooltip, }) => { const [showTooltip, hideTooltip] = useTooltip() const handleMouseEnter = useMemo(() => { if (!isInteractive) return undefined return event => { setCurrent(arc) showTooltip(React.createElement(tooltip, { arc }), event) onMouseEnter && onMouseEnter(arc, event) } }, [isInteractive, arc, onMouseEnter]) const handleMouseMove = useMemo(() => { if (!isInteractive) return undefined return event => { showTooltip(React.createElement(tooltip, { arc }), event) onMouseMove && onMouseMove(arc, event) } }, [isInteractive, arc, onMouseMove]) const handleMouseLeave = useMemo(() => { if (!isInteractive) return undefined return event => { setCurrent(null) hideTooltip() onMouseLeave && onMouseLeave(arc, event) } }, [isInteractive, arc, onMouseLeave]) const handleClick = useMemo(() => { if (!isInteractive || !onClick) return undefined return event => onClick(arc, event) }, [isInteractive, arc, onClick]) return ( <path d={arcGenerator({ startAngle, endAngle })} fill={arc.color} fillOpacity={opacity} strokeWidth={borderWidth} stroke={getBorderColor(arc)} strokeOpacity={opacity} onMouseEnter={handleMouseEnter} onMouseMove={handleMouseMove} onMouseLeave={handleMouseLeave} onClick={handleClick} /> ) }