diff --git a/packages/geo/package.json b/packages/geo/package.json index 8a2729eb2..5f75948e4 100644 --- a/packages/geo/package.json +++ b/packages/geo/package.json @@ -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", diff --git a/packages/geo/src/Choropleth.js b/packages/geo/src/Choropleth.js index ccf96e596..830458adb 100644 --- a/packages/geo/src/Choropleth.js +++ b/packages/geo/src/Choropleth.js @@ -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 -} + 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(, event), + [isInteractive, showTooltip, Tooltip] + ) + const handleMouseMove = useCallback( + (feature, event) => + isInteractive && Tooltip && showTooltip(, event), + [isInteractive, showTooltip, Tooltip] + ) + const handleMouseLeave = useCallback(() => isInteractive && hideTooltip(), [ + isInteractive, + hideTooltip, + ]) + + return ( + + {layers.map((layer, i) => { + if (layer === 'graticule') { + if (enableGraticule !== true) return null + + return ( + + ) + } + if (layer === 'features') { + return ( + + {boundFeatures.map(feature => ( + + ))} + + ) + } + if (layer === 'legends') { + return legends.map((legend, i) => { + return ( + + ) + }) + } + + return {layer({})} + })} + + ) +}) + +Choropleth.displayName = 'Choropleth' Choropleth.propTypes = ChoroplethPropTypes Choropleth.defaultProps = ChoroplethDefaultProps -export default Choropleth +export default withContainer(Choropleth) diff --git a/packages/geo/src/ChoroplethCanvas.js b/packages/geo/src/ChoroplethCanvas.js index f3ea4a753..e451768cc 100644 --- a/packages/geo/src/ChoroplethCanvas.js +++ b/packages/geo/src/ChoroplethCanvas.js @@ -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 + 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(, 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 ( + + ) +}) + +ChoroplethCanvas.displayName = 'ChoroplethCanvas' ChoroplethCanvas.propTypes = ChoroplethCanvasPropTypes ChoroplethCanvas.defaultProps = ChoroplethCanvasDefaultProps -export default ChoroplethCanvas +export default withContainer(ChoroplethCanvas) diff --git a/packages/geo/src/GeoMap.js b/packages/geo/src/GeoMap.js index 782999ae1..a7603ae4d 100644 --- a/packages/geo/src/GeoMap.js +++ b/packages/geo/src/GeoMap.js @@ -106,12 +106,13 @@ const GeoMap = memo(props => { ) } - return layer(props) + return {layer(props)} })} ) }) +GeoMap.displayName = 'GeoMap' GeoMap.propTypes = GeoMapPropTypes GeoMap.defaultProps = GeoMapDefaultProps diff --git a/packages/geo/src/GeoMapCanvas.js b/packages/geo/src/GeoMapCanvas.js index 0819524d4..564a083cb 100644 --- a/packages/geo/src/GeoMapCanvas.js +++ b/packages/geo/src/GeoMapCanvas.js @@ -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' @@ -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, @@ -118,6 +118,8 @@ const GeoMapCanvas = props => { getFillColor, getBorderWidth, getBorderColor, + features, + layers, ]) const [showTooltip, hideTooltip] = useTooltip() @@ -160,8 +162,9 @@ const GeoMapCanvas = props => { onClick={handleClick} /> ) -} +}) +GeoMapCanvas.displatName = 'GeoMapCanvas' GeoMapCanvas.propTypes = GeoMapCanvasPropTypes GeoMapCanvas.defaultProps = GeoMapCanvasDefaultProps diff --git a/packages/geo/src/hooks.js b/packages/geo/src/hooks.js index 9f636d12e..d26b8a44e 100644 --- a/packages/geo/src/hooks.js +++ b/packages/geo/src/hooks.js @@ -24,6 +24,8 @@ import { geoGraticule, } from 'd3-geo' import { guessQuantizeColorScale } from '@nivo/core' +import { useQuantizeColorScaleLegendData, BoxLegendSvg } from '@nivo/legends' + export const projectionById = { azimuthalEqualArea: geoAzimuthalEqualArea, azimuthalEquidistant: geoAzimuthalEquidistant, @@ -120,14 +122,15 @@ export const useChoropleth = ({ if (isFunction(valueFormat)) return valueFormat return format(valueFormat) }, [valueFormat]) - const getFillColor = useMemo(() => { - const colorScale = guessQuantizeColorScale(colors).domain([0, 1000000]) + const colorScale = useMemo(() => guessQuantizeColorScale(colors).domain([0, 1000000]), [colors]) + const getFillColor = useMemo(() => { return feature => { if (feature.value === undefined) return unknownColor return colorScale(feature.value) } - }, [colors, unknownColor]) + }, [colorScale, unknownColor]) + const boundFeatures = useMemo( () => features.map(feature => { @@ -152,8 +155,16 @@ export const useChoropleth = ({ [features, data, findMatchingDatum, getValue, valueFormatter, getFillColor] ) + const legendData = useQuantizeColorScaleLegendData({ + scale: colorScale, + valueFormat: valueFormatter, + }) + return { + colorScale, getFillColor, boundFeatures, + valueFormatter, + legendData, } } diff --git a/packages/geo/src/props.js b/packages/geo/src/props.js index 5c576312c..627d4803e 100644 --- a/packages/geo/src/props.js +++ b/packages/geo/src/props.js @@ -63,6 +63,9 @@ const commonChoroplethPropTypes = { valueFormat: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), colors: quantizeColorScalePropType.isRequired, unknownColor: PropTypes.string.isRequired, + layers: PropTypes.arrayOf( + PropTypes.oneOfType([PropTypes.oneOf(['graticule', 'features', 'legends']), PropTypes.func]) + ).isRequired, } export const ChoroplethPropTypes = { @@ -96,6 +99,7 @@ const commonDefaultProps = { onClick: () => {}, layers: ['graticule', 'features'], + legends: [], } export const GeoMapDefaultProps = { @@ -115,6 +119,7 @@ const commonChoroplethDefaultProps = { colors: 'PuBuGn', unknownColor: '#999', tooltip: ChoroplethTooltip, + layers: ['graticule', 'features', 'legends'], } export const ChoroplethDefaultProps = { diff --git a/packages/legends/index.d.ts b/packages/legends/index.d.ts index d30059160..d69522f21 100644 --- a/packages/legends/index.d.ts +++ b/packages/legends/index.d.ts @@ -85,4 +85,11 @@ declare module '@nivo/legends' { effects?: LegendEffect[] } + + export interface QuantileLegendProps { + scale: any + domain?: number[] + } + + export type QuantileLegendSvg = React.FunctionComponent } diff --git a/packages/legends/package.json b/packages/legends/package.json index a6d712805..af180fac1 100644 --- a/packages/legends/package.json +++ b/packages/legends/package.json @@ -9,13 +9,15 @@ }, "main": "./dist/nivo-legends.cjs.js", "module": "./dist/nivo-legends.esm.js", + "typings": "./index.d.ts", "files": [ "README.md", "LICENSE.md", - "index.d.ts", - "dist/" + "dist/", + "index.d.ts" ], "dependencies": { + "@nivo/core": "0.55.0", "lodash": "^4.17.4", "recompose": "^0.30.0" }, diff --git a/packages/legends/src/canvas/index.js b/packages/legends/src/canvas/index.js index d5381ef9e..b8fe8c5e6 100644 --- a/packages/legends/src/canvas/index.js +++ b/packages/legends/src/canvas/index.js @@ -51,6 +51,8 @@ export const renderLegendToCanvas = ( symbolSpacing = LegendSvgItem.defaultProps.symbolSpacing, // @todo add support for shapes // symbolShape = LegendSvgItem.defaultProps.symbolShape, + + theme, } ) => { const { width, height, padding } = computeDimensions({ @@ -103,7 +105,7 @@ export const renderLegendToCanvas = ( ctx.textAlign = textPropsMapping.align[labelAnchor] ctx.textBaseline = textPropsMapping.baseline[labelAlignment] - ctx.fillStyle = itemTextColor + ctx.fillStyle = itemTextColor || theme.legends.text.fill ctx.fillText(d.label, itemX + labelX, itemY + labelY) }) diff --git a/packages/legends/src/compute.js b/packages/legends/src/compute.js index 779bf47ba..e86995708 100644 --- a/packages/legends/src/compute.js +++ b/packages/legends/src/compute.js @@ -33,12 +33,12 @@ const zeroPadding = { } export const computeDimensions = ({ - itemCount, - itemWidth, - itemHeight, direction, itemsSpacing, padding: _padding, + itemCount, + itemWidth, + itemHeight, }) => { let padding if (isNumber(_padding)) { diff --git a/packages/legends/src/hooks.js b/packages/legends/src/hooks.js new file mode 100644 index 000000000..1f64b9f54 --- /dev/null +++ b/packages/legends/src/hooks.js @@ -0,0 +1,37 @@ +/* + * This file is part of the nivo project. + * + * Copyright 2016-present, Raphaƫl Benitte. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +import { useMemo } from 'react' + +export const useQuantizeColorScaleLegendData = ({ + scale, + domain: overriddenDomain, + reverse = false, + valueFormat = v => v, + separator = ' - ', +}) => { + return useMemo(() => { + const domain = overriddenDomain || scale.range() + + const items = domain.map((domainValue, index) => { + const [start, end] = scale.invertExtent(domainValue) + + return { + id: domainValue, + index, + extent: [start, end], + label: `${valueFormat(start)}${separator}${valueFormat(end)}`, + value: scale(start), + color: domainValue, + } + }) + if (reverse === true) items.reverse() + + return items + }, [overriddenDomain, scale, reverse]) +} diff --git a/packages/legends/src/index.js b/packages/legends/src/index.js index 79d3b5a8a..bb6030e78 100644 --- a/packages/legends/src/index.js +++ b/packages/legends/src/index.js @@ -10,3 +10,4 @@ export * from './svg' export * from './canvas' export * from './constants' export * from './props' +export * from './hooks' diff --git a/packages/legends/src/svg/BoxLegendSvg.js b/packages/legends/src/svg/BoxLegendSvg.js index c6dab5ae5..bef6b589c 100644 --- a/packages/legends/src/svg/BoxLegendSvg.js +++ b/packages/legends/src/svg/BoxLegendSvg.js @@ -60,8 +60,6 @@ const BoxLegendSvg = ({ onMouseLeave, effects, - - theme, }) => { const { width, height } = computeDimensions({ itemCount: data.length, @@ -106,7 +104,6 @@ const BoxLegendSvg = ({ onClick={onClick} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} - theme={theme} /> ) } diff --git a/packages/legends/src/svg/LegendSvg.js b/packages/legends/src/svg/LegendSvg.js index efb54f854..1f2638872 100644 --- a/packages/legends/src/svg/LegendSvg.js +++ b/packages/legends/src/svg/LegendSvg.js @@ -12,8 +12,6 @@ import LegendSvgItem from './LegendSvgItem' import { datumPropType, symbolPropTypes, interactivityPropTypes } from '../props' import { computeDimensions } from '../compute' import { - DIRECTION_COLUMN, - DIRECTION_ROW, DIRECTION_LEFT_TO_RIGHT, DIRECTION_RIGHT_TO_LEFT, DIRECTION_TOP_TO_BOTTOM, @@ -23,7 +21,6 @@ import { const LegendSvg = ({ data, - // position/layout x, y, direction, @@ -48,8 +45,6 @@ const LegendSvg = ({ onClick, onMouseEnter, onMouseLeave, - - theme, }) => { // eslint-disable-next-line no-unused-vars const { width, height, padding } = computeDimensions({ @@ -63,9 +58,9 @@ const LegendSvg = ({ let xStep = 0 let yStep = 0 - if (direction === DIRECTION_ROW) { + if (direction === 'row') { xStep = itemWidth + itemsSpacing - } else if (direction === DIRECTION_COLUMN) { + } else if (direction === 'column') { yStep = itemHeight + itemsSpacing } @@ -93,7 +88,6 @@ const LegendSvg = ({ onClick={onClick} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} - theme={theme} /> ))} @@ -103,10 +97,9 @@ const LegendSvg = ({ LegendSvg.propTypes = { data: PropTypes.arrayOf(datumPropType).isRequired, - // position/layout x: PropTypes.number.isRequired, y: PropTypes.number.isRequired, - direction: PropTypes.oneOf([DIRECTION_COLUMN, DIRECTION_ROW]).isRequired, + direction: PropTypes.oneOf(['row', 'column']).isRequired, padding: PropTypes.oneOfType([ PropTypes.number, PropTypes.shape({ @@ -118,7 +111,6 @@ LegendSvg.propTypes = { ]).isRequired, justify: PropTypes.bool.isRequired, - // items itemsSpacing: PropTypes.number.isRequired, itemWidth: PropTypes.number.isRequired, itemHeight: PropTypes.number.isRequired, @@ -137,13 +129,11 @@ LegendSvg.propTypes = { } LegendSvg.defaultProps = { - // position/layout padding: 0, justify: false, - // items itemsSpacing: 0, - itemDirection: DIRECTION_LEFT_TO_RIGHT, + itemDirection: 'left-to-right', itemTextColor: 'black', itemBackground: 'transparent', itemOpacity: 1, diff --git a/packages/legends/src/svg/LegendSvgItem.js b/packages/legends/src/svg/LegendSvgItem.js index 6ce3e3687..7aa388efc 100644 --- a/packages/legends/src/svg/LegendSvgItem.js +++ b/packages/legends/src/svg/LegendSvgItem.js @@ -6,15 +6,10 @@ * 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, { useState, useCallback } from 'react' import PropTypes from 'prop-types' import isFunction from 'lodash/isFunction' -import { - DIRECTION_LEFT_TO_RIGHT, - DIRECTION_RIGHT_TO_LEFT, - DIRECTION_TOP_TO_BOTTOM, - DIRECTION_BOTTOM_TO_TOP, -} from '../constants' +import { useTheme } from '@nivo/core' import { datumPropType, symbolPropTypes, interactivityPropTypes } from '../props' import { computeItemLayout } from '../compute' import { SymbolCircle, SymbolDiamond, SymbolSquare, SymbolTriangle } from './symbols' @@ -26,82 +21,54 @@ const symbolByShape = { triangle: SymbolTriangle, } -export default class LegendSvgItem extends Component { - static propTypes = { - data: datumPropType.isRequired, - - x: PropTypes.number.isRequired, - y: PropTypes.number.isRequired, - width: PropTypes.number.isRequired, - height: PropTypes.number.isRequired, - - textColor: PropTypes.string, - background: PropTypes.string, - opacity: PropTypes.number, - - direction: PropTypes.oneOf([ - DIRECTION_LEFT_TO_RIGHT, - DIRECTION_RIGHT_TO_LEFT, - DIRECTION_TOP_TO_BOTTOM, - DIRECTION_BOTTOM_TO_TOP, - ]).isRequired, - justify: PropTypes.bool.isRequired, - - ...symbolPropTypes, - ...interactivityPropTypes, - } - - static defaultProps = { - direction: DIRECTION_LEFT_TO_RIGHT, - justify: false, - - textColor: 'black', - background: 'transparent', - opacity: 1, - - // symbol - symbolShape: 'square', - symbolSize: 16, - symbolSpacing: 8, - symbolBorderWidth: 0, - symbolBorderColor: 'transparent', - - effects: [], - } - - state = { - style: {}, - } - - handleClick = event => { - const { onClick, data } = this.props - - if (onClick === undefined) return - onClick(data, event) - } - - handleMouseEnter = event => { - const { onMouseEnter, data, effects } = this.props - - if (effects.length > 0) { - const applyEffects = effects.filter(({ on }) => on === 'hover') - const style = applyEffects.reduce( - (acc, effect) => ({ - ...acc, - ...effect.style, - }), - {} - ) - this.setState({ style }) - } - - if (onMouseEnter === undefined) return - onMouseEnter(data, event) - } - - handleMouseLeave = () => { - const { onMouseLeave, data, effects } = this.props +const LegendSvgItem = ({ + x, + y, + width, + height, + data, + direction, + justify, + textColor, + background, + opacity, + + symbolShape, + symbolSize, + symbolSpacing, + symbolBorderWidth, + symbolBorderColor, + + onClick, + onMouseEnter, + onMouseLeave, + + effects, +}) => { + const [style, setStyle] = useState({}) + const theme = useTheme() + + const handleClick = useCallback(event => onClick && onClick(data, event), [onClick, data]) + const handleMouseEnter = useCallback( + event => { + if (effects.length > 0) { + const applyEffects = effects.filter(({ on }) => on === 'hover') + const style = applyEffects.reduce( + (acc, effect) => ({ + ...acc, + ...effect.style, + }), + {} + ) + setStyle(style) + } + if (onMouseEnter === undefined) return + onMouseEnter(data, event) + }, + [onMouseEnter, data, effects] + ) + const handleMouseLeave = useCallback(() => { if (effects.length > 0) { const applyEffects = effects.filter(({ on }) => on !== 'hover') const style = applyEffects.reduce( @@ -111,106 +78,119 @@ export default class LegendSvgItem extends Component { }), {} ) - this.setState({ style }) + setStyle(style) } if (onMouseLeave === undefined) return onMouseLeave(data, event) + }, [onMouseLeave, data, effects]) + + const { symbolX, symbolY, labelX, labelY, labelAnchor, labelAlignment } = computeItemLayout({ + direction, + justify, + symbolSize: style.symbolSize || symbolSize, + symbolSpacing, + width, + height, + }) + + const isInteractive = [onClick, onMouseEnter, onMouseLeave].some( + handler => handler !== undefined + ) + + let Symbol + if (isFunction(symbolShape)) { + Symbol = symbolShape + } else { + Symbol = symbolByShape[symbolShape] } - render() { - const { - x, - y, - width, - height, - data, - direction, - justify, - textColor, - background, - opacity, - - symbolShape, - symbolSize, - symbolSpacing, - symbolBorderWidth, - symbolBorderColor, - - onClick, - onMouseEnter, - onMouseLeave, - - theme, - } = this.props - const { style } = this.state - - const { symbolX, symbolY, labelX, labelY, labelAnchor, labelAlignment } = computeItemLayout( - { - direction, - justify, - symbolSize: style.symbolSize || symbolSize, - symbolSpacing, - width, - height, - } - ) - - const isInteractive = [onClick, onMouseEnter, onMouseLeave].some( - handler => handler !== undefined - ) - - let Symbol - if (isFunction(symbolShape)) { - Symbol = symbolShape - } else { - Symbol = symbolByShape[symbolShape] - } - - return ( - + + {React.createElement(Symbol, { + x: symbolX, + y: symbolY, + size: style.symbolSize || symbolSize, + fill: data.fill || data.color, + borderWidth: + style.symbolBorderWidth !== undefined + ? style.symbolBorderWidth + : symbolBorderWidth, + borderColor: style.symbolBorderColor || symbolBorderColor, + })} + - - {React.createElement(Symbol, { - x: symbolX, - y: symbolY, - size: style.symbolSize || symbolSize, - fill: data.fill || data.color, - borderWidth: - style.symbolBorderWidth !== undefined - ? style.symbolBorderWidth - : symbolBorderWidth, - borderColor: style.symbolBorderColor || symbolBorderColor, - })} - - {data.label} - - - ) - } + {data.label} + + + ) +} + +LegendSvgItem.displayName = 'LegendSvgItem' +LegendSvgItem.propTypes = { + data: datumPropType.isRequired, + + x: PropTypes.number.isRequired, + y: PropTypes.number.isRequired, + width: PropTypes.number.isRequired, + height: PropTypes.number.isRequired, + + textColor: PropTypes.string, + background: PropTypes.string, + opacity: PropTypes.number, + + direction: PropTypes.oneOf([ + 'left-to-right', + 'right-top-left', + 'top-to-bottom', + 'bottom-to-top', + ]).isRequired, + justify: PropTypes.bool.isRequired, + + ...symbolPropTypes, + ...interactivityPropTypes, } +LegendSvgItem.defaultProps = { + direction: 'left-to-right', + justify: false, + + textColor: 'black', + background: 'transparent', + opacity: 1, + + symbolShape: 'square', + symbolSize: 16, + symbolSpacing: 8, + symbolBorderWidth: 0, + symbolBorderColor: 'transparent', + + effects: [], +} + +export default LegendSvgItem diff --git a/packages/pie/src/PieCanvasRenderer.js b/packages/pie/src/PieCanvasRenderer.js index 1435c9b06..f5057f0b5 100644 --- a/packages/pie/src/PieCanvasRenderer.js +++ b/packages/pie/src/PieCanvasRenderer.js @@ -219,6 +219,7 @@ class PieCanvasRenderer extends Component { })), containerWidth: width, containerHeight: height, + theme, }) }) } diff --git a/packages/scatterplot/src/ScatterPlotCanvas.js b/packages/scatterplot/src/ScatterPlotCanvas.js index 96db120cc..b23bbdc25 100644 --- a/packages/scatterplot/src/ScatterPlotCanvas.js +++ b/packages/scatterplot/src/ScatterPlotCanvas.js @@ -224,6 +224,7 @@ class ScatterPlotCanvas extends Component { data: legendData, containerWidth: width, containerHeight: height, + theme, }) }) } diff --git a/website/src/components/charts/geo/Choropleth.js b/website/src/components/charts/geo/Choropleth.js index 6ed12aacd..fad2c3848 100644 --- a/website/src/components/charts/geo/Choropleth.js +++ b/website/src/components/charts/geo/Choropleth.js @@ -33,8 +33,8 @@ const initialSettings = { left: 0, }, - colors: 'YlGnBu', - unknownColor: '#152538', + colors: 'nivo', + unknownColor: '#666666', label: 'properties.name', value: 'value', @@ -47,7 +47,7 @@ const initialSettings = { enableGraticule: true, graticuleLineWidth: 0.5, - graticuleLineColor: '#152538', + graticuleLineColor: '#dddddd', borderWidth: 0.5, borderColor: '#152538', @@ -56,9 +56,44 @@ const initialSettings = { 'custom tooltip example': false, tooltip: null, + legends: [ + { + anchor: 'bottom-left', + direction: 'column', + justify: true, + translateX: 20, + translateY: -100, + itemsSpacing: 0, + itemWidth: 94, + itemHeight: 18, + itemDirection: 'left-to-right', + itemTextColor: '#444444', + itemOpacity: 0.85, + symbolSize: 18, + onClick: data => { + alert(JSON.stringify(data, null, ' ')) + }, + effects: [ + { + on: 'hover', + style: { + itemTextColor: '#000000', + itemOpacity: 1, + }, + }, + ], + }, + ], + theme: { ...nivoTheme, - background: '#223346', + background: '#ffffff', + legends: { + text: { + fontSize: 11, + fontWeight: 600, + }, + }, }, } @@ -97,8 +132,8 @@ const Choropleth = () => {

The Choropleth component displays divided geographical areas shaded in relation to - some data variable. It's build on top of the GeoMap{' '} - component. + some data variable. It's build on top of primitives from the{' '} + GeoMap component.

Using this component requires some knowledge about the d3-geo library, diff --git a/website/src/components/charts/geo/ChoroplethCanvas.js b/website/src/components/charts/geo/ChoroplethCanvas.js index f0aa1a7d5..48d627ed7 100644 --- a/website/src/components/charts/geo/ChoroplethCanvas.js +++ b/website/src/components/charts/geo/ChoroplethCanvas.js @@ -55,6 +55,23 @@ const initialSettings = { isInteractive: true, 'custom tooltip example': false, + legends: [ + { + anchor: 'bottom-left', + direction: 'column', + justify: true, + translateX: 20, + translateY: -60, + itemsSpacing: 0, + itemWidth: 86, + itemHeight: 18, + itemDirection: 'left-to-right', + itemTextColor: '#e6f6cf', + itemOpacity: 0.85, + symbolSize: 18, + }, + ], + theme: { ...nivoTheme, background: '#1f294a', diff --git a/website/src/components/charts/geo/props.js b/website/src/components/charts/geo/props.js index 4398cc79b..ab941fbe1 100644 --- a/website/src/components/charts/geo/props.js +++ b/website/src/components/charts/geo/props.js @@ -8,7 +8,7 @@ */ import React from 'react' import { GeoMapDefaultProps, ChoroplethDefaultProps } from '@nivo/geo' -import { marginProperties, defsProperties } from '../../../lib/componentProperties' +import { marginProperties, defsProperties, getLegendsProps } from '../../../lib/componentProperties' export default [ { @@ -366,10 +366,9 @@ export default [

), }, - /* { key: 'legends', - scopes: ['GeoMap', 'GeoMapCanvas'], + scopes: ['Choropleth', 'ChoroplethCanvas'], type: '{Array}', description: `Optional chart's legends.`, controlGroup: 'Legends', @@ -380,10 +379,10 @@ export default [ addLabel: 'add legend', shouldRemove: true, defaults: { - anchor: 'left', + anchor: 'center', direction: 'column', justify: false, - translateX: -100, + translateX: 0, translateY: 0, itemWidth: 100, itemHeight: 20, @@ -406,5 +405,4 @@ export default [ }, }, }, - */ ]