diff --git a/packages/legends/index.d.ts b/packages/legends/index.d.ts deleted file mode 100644 index f305656e1..000000000 --- a/packages/legends/index.d.ts +++ /dev/null @@ -1,104 +0,0 @@ -import * as React from 'react' - -declare module '@nivo/legends' { - interface ContainerDimensions { - containerHeight: number - containerWidth: number - } - - export type LegendAnchor = - | 'top' - | 'top-right' - | 'right' - | 'bottom-right' - | 'bottom' - | 'bottom-left' - | 'left' - | 'top-left' - | 'center' - - export type LegendDirection = 'row' | 'column' - - export type LegendItemDirection = - | 'left-to-right' - | 'right-to-left' - | 'top-to-bottom' - | 'bottom-to-top' - - export type Box = Partial<{ - bottom: number - left: number - right: number - top: number - }> - - export type LegendSymbolShape = 'circle' | 'diamond' | 'square' | 'triangle' - - export interface LegendMouseHandlerData { - id: string | number - label: string - color: string - } - - export type LegendMouseHandler = ( - data: LegendMouseHandlerData, - event: React.MouseEvent - ) => void - - export interface LegendEffect { - on: 'hover' - style: Partial<{ - itemTextColor: string - itemBackground: string - itemOpacity: number - symbolSize: number - symbolBorderWidth: number - symbolBorderColor: string - }> - } - - export interface LegendProps { - data?: { - id: string | number - label: string | number - color?: string - fill?: string - }[] - - anchor: LegendAnchor - direction: LegendDirection - justify?: boolean - padding?: number | Box - translateX?: number - translateY?: number - - itemWidth: number - itemHeight: number - itemDirection?: LegendItemDirection - itemsSpacing?: number - itemBackground?: string - itemTextColor?: string - itemOpacity?: number - symbolSize?: number - symbolSpacing?: number - symbolShape?: LegendSymbolShape | any - symbolBorderColor?: string - symbolBorderWidth?: number - textColor?: string - - onClick?: LegendMouseHandler - onMouseEnter?: LegendMouseHandler - onMouseLeave?: LegendMouseHandler - - effects?: LegendEffect[] - } - - export interface QuantileLegendProps { - scale: any - domain?: number[] - } - - export type QuantileLegendSvg = React.FunctionComponent - - export const BoxLegendSvg: React.FC -} diff --git a/packages/legends/package.json b/packages/legends/package.json index 79575b398..b0aa34763 100644 --- a/packages/legends/package.json +++ b/packages/legends/package.json @@ -14,16 +14,13 @@ }, "main": "./dist/nivo-legends.cjs.js", "module": "./dist/nivo-legends.es.js", - "typings": "./index.d.ts", + "typings": "./dist/types/index.d.ts", "files": [ "README.md", "LICENSE.md", "dist/", - "index.d.ts" + "!dist/tsconfig.tsbuildinfo" ], - "dependencies": { - "lodash": "^4.17.11" - }, "devDependencies": { "@nivo/core": "0.69.0" }, diff --git a/packages/legends/src/canvas.ts b/packages/legends/src/canvas.ts new file mode 100644 index 000000000..2cc4490b9 --- /dev/null +++ b/packages/legends/src/canvas.ts @@ -0,0 +1,96 @@ +import { computeDimensions, computePositionFromAnchor, computeItemLayout } from './compute' +import { LegendCanvasProps } from './types' + +const textAlignMapping = { + start: 'left', + middle: 'center', + end: 'right', +} as const + +export const renderLegendToCanvas = ( + ctx: CanvasRenderingContext2D, + { + data, + + containerWidth, + containerHeight, + translateX = 0, + translateY = 0, + anchor, + direction, + padding: _padding = 0, + justify = false, + + // items + itemsSpacing = 0, + itemWidth, + itemHeight, + itemDirection = 'left-to-right', + itemTextColor, + + // symbol + symbolSize = 16, + symbolSpacing = 8, + // @todo add support for shapes + // symbolShape = LegendSvgItem.defaultProps.symbolShape, + + theme, + }: LegendCanvasProps +) => { + const { width, height, padding } = computeDimensions({ + itemCount: data.length, + itemWidth, + itemHeight, + itemsSpacing, + direction, + padding: _padding, + }) + + const { x, y } = computePositionFromAnchor({ + anchor, + translateX, + translateY, + containerWidth, + containerHeight, + width, + height, + }) + + const xStep = direction === 'row' ? itemWidth + itemsSpacing : 0 + const yStep = direction === 'column' ? itemHeight + itemsSpacing : 0 + + ctx.save() + ctx.translate(x, y) + + ctx.font = `${theme.legends.text.fontSize}px ${theme.legends.text.fontFamily || 'sans-serif'}` + + data.forEach((d, i) => { + const itemX = i * xStep + padding.left + const itemY = i * yStep + padding.top + + const { symbolX, symbolY, labelX, labelY, labelAnchor, labelAlignment } = computeItemLayout( + { + direction: itemDirection, + justify, + symbolSize, + symbolSpacing, + width: itemWidth, + height: itemHeight, + } + ) + + ctx.fillStyle = d.color ?? 'black' + ctx.fillRect(itemX + symbolX, itemY + symbolY, symbolSize, symbolSize) + + ctx.textAlign = textAlignMapping[labelAnchor] + + if (labelAlignment === 'central') { + ctx.textBaseline = 'middle' + } + + ctx.fillStyle = itemTextColor ?? theme.legends.text.fill ?? 'black' + ctx.fillText(String(d.label), itemX + labelX, itemY + labelY) + }) + + ctx.restore() +} diff --git a/packages/legends/src/canvas/index.js b/packages/legends/src/canvas/index.js deleted file mode 100644 index f0c574d18..000000000 --- a/packages/legends/src/canvas/index.js +++ /dev/null @@ -1,116 +0,0 @@ -/* - * 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 { computeDimensions, computePositionFromAnchor, computeItemLayout } from '../compute' -import BoxLegendSvg from '../svg/BoxLegendSvg' -import LegendSvg from '../svg/LegendSvg' -import LegendSvgItem from '../svg/LegendSvgItem' -import { DIRECTION_COLUMN, DIRECTION_ROW } from '../constants' - -const textPropsMapping = { - align: { - start: 'left', - middle: 'center', - end: 'right', - }, - baseline: { - hanging: 'top', - middle: 'middle', - central: 'middle', - baseline: 'bottom', - }, -} - -export const renderLegendToCanvas = ( - ctx, - { - data, - - containerWidth, - containerHeight, - translateX = BoxLegendSvg.defaultProps.translateX, - translateY = BoxLegendSvg.defaultProps.translateY, - anchor, - direction, - padding: _padding = LegendSvg.defaultProps.padding, - justify = LegendSvgItem.defaultProps.justify, - - // items - itemsSpacing = LegendSvg.defaultProps.itemsSpacing, - itemWidth, - itemHeight, - itemDirection = LegendSvgItem.defaultProps.direction, - itemTextColor = LegendSvg.defaultProps.textColor, - - // symbol - symbolSize = LegendSvgItem.defaultProps.symbolSize, - symbolSpacing = LegendSvgItem.defaultProps.symbolSpacing, - // @todo add support for shapes - // symbolShape = LegendSvgItem.defaultProps.symbolShape, - - theme, - } -) => { - const { width, height, padding } = computeDimensions({ - itemCount: data.length, - itemWidth, - itemHeight, - itemsSpacing, - direction, - padding: _padding, - }) - - const { x, y } = computePositionFromAnchor({ - anchor, - translateX, - translateY, - containerWidth, - containerHeight, - width, - height, - }) - - let xStep = 0 - let yStep = 0 - if (direction === DIRECTION_ROW) { - xStep = itemWidth + itemsSpacing - } else if (direction === DIRECTION_COLUMN) { - yStep = itemHeight + itemsSpacing - } - - ctx.save() - ctx.translate(x, y) - - ctx.font = `${theme.legends.text.fontSize}px ${theme.legends.text.fontFamily || 'sans-serif'}` - - data.forEach((d, i) => { - const itemX = i * xStep + padding.left - const itemY = i * yStep + padding.top - - const { symbolX, symbolY, labelX, labelY, labelAnchor, labelAlignment } = computeItemLayout( - { - direction: itemDirection, - justify, - symbolSize, - symbolSpacing, - width: itemWidth, - height: itemHeight, - } - ) - - ctx.fillStyle = d.color - ctx.fillRect(itemX + symbolX, itemY + symbolY, symbolSize, symbolSize) - - ctx.textAlign = textPropsMapping.align[labelAnchor] - ctx.textBaseline = textPropsMapping.baseline[labelAlignment] - ctx.fillStyle = itemTextColor || theme.legends.text.fill - ctx.fillText(d.label, itemX + labelX, itemY + labelY) - }) - - ctx.restore() -} diff --git a/packages/legends/src/compute.js b/packages/legends/src/compute.ts similarity index 68% rename from packages/legends/src/compute.js rename to packages/legends/src/compute.ts index 0d683e317..68540a88e 100644 --- a/packages/legends/src/compute.js +++ b/packages/legends/src/compute.ts @@ -1,29 +1,7 @@ -/* - * 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 isNumber from 'lodash/isNumber' -import isPlainObject from 'lodash/isPlainObject' -import { - ANCHOR_BOTTOM, - ANCHOR_BOTTOM_LEFT, - ANCHOR_BOTTOM_RIGHT, - ANCHOR_CENTER, - ANCHOR_LEFT, - ANCHOR_RIGHT, - ANCHOR_TOP, - ANCHOR_TOP_RIGHT, - DIRECTION_BOTTOM_TO_TOP, - DIRECTION_COLUMN, - DIRECTION_LEFT_TO_RIGHT, - DIRECTION_RIGHT_TO_LEFT, - DIRECTION_ROW, - DIRECTION_TOP_TO_BOTTOM, -} from './constants' +import { BoxLegendSvgProps, LegendAnchor, LegendItemDirection } from './types' + +const isObject = (item: unknown): item is T => + typeof item === 'object' && !Array.isArray(item) && item !== null const zeroPadding = { top: 0, @@ -39,32 +17,33 @@ export const computeDimensions = ({ itemCount, itemWidth, itemHeight, -}) => { - let padding - if (isNumber(_padding)) { - padding = { - top: _padding, - right: _padding, - bottom: _padding, - left: _padding, - } - } else if (isPlainObject(_padding)) { - padding = { - ...zeroPadding, - ..._padding, - } - } else { - throw new TypeError(`Invalid property padding, must be one of: number, object`) +}: Pick & + Record<'itemsSpacing' | 'itemCount' | 'itemWidth' | 'itemHeight', number>) => { + if (typeof _padding !== 'number' && !isObject(_padding)) { + throw new Error('Invalid property padding, must be one of: number, object') } + const padding = + typeof _padding === 'number' + ? { + top: _padding, + right: _padding, + bottom: _padding, + left: _padding, + } + : { + ...zeroPadding, + ..._padding, + } + const horizontalPadding = padding.left + padding.right const verticalPadding = padding.top + padding.bottom let width = itemWidth + horizontalPadding let height = itemHeight + verticalPadding - let spacing = (itemCount - 1) * itemsSpacing - if (direction === DIRECTION_ROW) { + const spacing = (itemCount - 1) * itemsSpacing + if (direction === 'row') { width = itemWidth * itemCount + spacing + horizontalPadding - } else if (direction === DIRECTION_COLUMN) { + } else if (direction === 'column') { height = itemHeight * itemCount + spacing + verticalPadding } @@ -79,43 +58,46 @@ export const computePositionFromAnchor = ({ containerHeight, width, height, -}) => { +}: { anchor: LegendAnchor } & Record< + 'translateX' | 'translateY' | 'containerWidth' | 'containerHeight' | 'width' | 'height', + number +>) => { let x = translateX let y = translateY switch (anchor) { - case ANCHOR_TOP: + case 'top': x += (containerWidth - width) / 2 break - case ANCHOR_TOP_RIGHT: + case 'top-right': x += containerWidth - width break - case ANCHOR_RIGHT: + case 'right': x += containerWidth - width y += (containerHeight - height) / 2 break - case ANCHOR_BOTTOM_RIGHT: + case 'bottom-right': x += containerWidth - width y += containerHeight - height break - case ANCHOR_BOTTOM: + case 'bottom': x += (containerWidth - width) / 2 y += containerHeight - height break - case ANCHOR_BOTTOM_LEFT: + case 'bottom-left': y += containerHeight - height break - case ANCHOR_LEFT: + case 'left': y += (containerHeight - height) / 2 break - case ANCHOR_CENTER: + case 'center': x += (containerWidth - width) / 2 y += (containerHeight - height) / 2 break @@ -131,17 +113,20 @@ export const computeItemLayout = ({ symbolSpacing, width, height, -}) => { +}: { + direction: LegendItemDirection + justify: boolean +} & Record<'symbolSize' | 'symbolSpacing' | 'width' | 'height', number>) => { let symbolX let symbolY let labelX let labelY - let labelAnchor - let labelAlignment + let labelAnchor: 'start' | 'middle' | 'end' + let labelAlignment: 'alphabetic' | 'central' | 'text-before-edge' switch (direction) { - case DIRECTION_LEFT_TO_RIGHT: + case 'left-to-right': symbolX = 0 symbolY = (height - symbolSize) / 2 @@ -156,7 +141,7 @@ export const computeItemLayout = ({ } break - case DIRECTION_RIGHT_TO_LEFT: + case 'right-to-left': symbolX = width - symbolSize symbolY = (height - symbolSize) / 2 @@ -171,7 +156,7 @@ export const computeItemLayout = ({ } break - case DIRECTION_TOP_TO_BOTTOM: + case 'top-to-bottom': symbolX = (width - symbolSize) / 2 symbolY = 0 @@ -187,7 +172,7 @@ export const computeItemLayout = ({ } break - case DIRECTION_BOTTOM_TO_TOP: + case 'bottom-to-top': symbolX = (width - symbolSize) / 2 symbolY = height - symbolSize diff --git a/packages/legends/src/constants.js b/packages/legends/src/constants.js deleted file mode 100644 index 1336fec98..000000000 --- a/packages/legends/src/constants.js +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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. - */ -export const DIRECTION_ROW = 'row' -export const DIRECTION_COLUMN = 'column' - -export const ANCHOR_TOP = 'top' -export const ANCHOR_TOP_RIGHT = 'top-right' -export const ANCHOR_RIGHT = 'right' -export const ANCHOR_BOTTOM_RIGHT = 'bottom-right' -export const ANCHOR_BOTTOM = 'bottom' -export const ANCHOR_BOTTOM_LEFT = 'bottom-left' -export const ANCHOR_LEFT = 'left' -export const ANCHOR_TOP_LEFT = 'top-left' -export const ANCHOR_CENTER = 'center' - -export const DIRECTION_LEFT_TO_RIGHT = 'left-to-right' -export const DIRECTION_RIGHT_TO_LEFT = 'right-to-left' -export const DIRECTION_TOP_TO_BOTTOM = 'top-to-bottom' -export const DIRECTION_BOTTOM_TO_TOP = 'bottom-to-top' diff --git a/packages/legends/src/hooks.js b/packages/legends/src/hooks.ts similarity index 70% rename from packages/legends/src/hooks.js rename to packages/legends/src/hooks.ts index 1f64b9f54..ece0e956a 100644 --- a/packages/legends/src/hooks.js +++ b/packages/legends/src/hooks.ts @@ -1,22 +1,26 @@ -/* - * 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' +type Scale = { + (value: number): number + invertExtent: (value: number) => [number, number] + range: () => number[] +} + export const useQuantizeColorScaleLegendData = ({ scale, domain: overriddenDomain, reverse = false, valueFormat = v => v, separator = ' - ', +}: { + scale: Scale + domain?: number[] + reverse?: boolean + valueFormat?: (value: T) => T | U + separator?: string }) => { return useMemo(() => { - const domain = overriddenDomain || scale.range() + const domain = overriddenDomain ?? scale.range() const items = domain.map((domainValue, index) => { const [start, end] = scale.invertExtent(domainValue) diff --git a/packages/legends/src/index.js b/packages/legends/src/index.js deleted file mode 100644 index bb6030e78..000000000 --- a/packages/legends/src/index.js +++ /dev/null @@ -1,13 +0,0 @@ -/* - * 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. - */ -export * from './svg' -export * from './canvas' -export * from './constants' -export * from './props' -export * from './hooks' diff --git a/packages/legends/src/index.ts b/packages/legends/src/index.ts new file mode 100644 index 000000000..7785f9376 --- /dev/null +++ b/packages/legends/src/index.ts @@ -0,0 +1,5 @@ +export * from './svg' +export * from './canvas' +export * from './hooks' +export * from './props' +export * from './types' diff --git a/packages/legends/src/props.js b/packages/legends/src/props.js deleted file mode 100644 index f074579dd..000000000 --- a/packages/legends/src/props.js +++ /dev/null @@ -1,115 +0,0 @@ -/* - * 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 PropTypes from 'prop-types' -import { - ANCHOR_BOTTOM, - ANCHOR_BOTTOM_LEFT, - ANCHOR_BOTTOM_RIGHT, - ANCHOR_CENTER, - ANCHOR_LEFT, - ANCHOR_RIGHT, - ANCHOR_TOP, - ANCHOR_TOP_LEFT, - ANCHOR_TOP_RIGHT, - DIRECTION_BOTTOM_TO_TOP, - DIRECTION_COLUMN, - DIRECTION_LEFT_TO_RIGHT, - DIRECTION_RIGHT_TO_LEFT, - DIRECTION_ROW, - DIRECTION_TOP_TO_BOTTOM, -} from './constants' - -/** - * This can be used to add effect on legends on interaction. - */ -export const legendEffectPropType = PropTypes.shape({ - on: PropTypes.oneOfType([PropTypes.oneOf(['hover'])]).isRequired, - style: PropTypes.shape({ - itemTextColor: PropTypes.string, - itemBackground: PropTypes.string, - itemOpacity: PropTypes.number, - symbolSize: PropTypes.number, - symbolBorderWidth: PropTypes.number, - symbolBorderColor: PropTypes.string, - }).isRequired, -}) - -export const symbolPropTypes = { - symbolShape: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), - symbolSize: PropTypes.number, - symbolSpacing: PropTypes.number, - symbolBorderWidth: PropTypes.number, - symbolBorderColor: PropTypes.string, -} - -export const interactivityPropTypes = { - onClick: PropTypes.func, - onMouseEnter: PropTypes.func, - onMouseLeave: PropTypes.func, -} - -export const datumPropType = PropTypes.shape({ - id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, - label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, - color: PropTypes.string.isRequired, - fill: PropTypes.string, -}) - -/** - * The prop type is exported as a simple object instead of `PropTypes.shape` - * to be able to add extra properties. - * - * @example - * ```javascript - * import { LegendPropShape } from '@nivo/legends' - * - * const customLegendPropType = PropTypes.shape({ - * ...LegendPropShape, - * extra: PropTypes.any.isRequired, - * }) - * ``` - */ -export const LegendPropShape = { - data: PropTypes.arrayOf(datumPropType), - - // position & layout - anchor: PropTypes.oneOf([ - ANCHOR_TOP, - ANCHOR_TOP_RIGHT, - ANCHOR_RIGHT, - ANCHOR_BOTTOM_RIGHT, - ANCHOR_BOTTOM, - ANCHOR_BOTTOM_LEFT, - ANCHOR_LEFT, - ANCHOR_TOP_LEFT, - ANCHOR_CENTER, - ]).isRequired, - translateX: PropTypes.number, - translateY: PropTypes.number, - direction: PropTypes.oneOf([DIRECTION_ROW, DIRECTION_COLUMN]).isRequired, - - // item - itemsSpacing: PropTypes.number, - itemWidth: PropTypes.number.isRequired, - itemHeight: PropTypes.number.isRequired, - itemDirection: PropTypes.oneOf([ - DIRECTION_LEFT_TO_RIGHT, - DIRECTION_RIGHT_TO_LEFT, - DIRECTION_TOP_TO_BOTTOM, - DIRECTION_BOTTOM_TO_TOP, - ]), - itemTextColor: PropTypes.string, - itemBackground: PropTypes.string, - itemOpacity: PropTypes.number, - - ...symbolPropTypes, - ...interactivityPropTypes, - - effects: PropTypes.arrayOf(legendEffectPropType), -} diff --git a/packages/legends/src/props.ts b/packages/legends/src/props.ts new file mode 100644 index 000000000..105a68c98 --- /dev/null +++ b/packages/legends/src/props.ts @@ -0,0 +1,83 @@ +import PropTypes from 'prop-types' + +/** + * The prop type is exported as a simple object instead of `PropTypes.shape` + * to be able to add extra properties. + * + * @example + * ```javascript + * import { LegendPropShape } from '@nivo/legends' + * + * const customLegendPropType = PropTypes.shape({ + * ...LegendPropShape, + * extra: PropTypes.any.isRequired, + * }) + * ``` + */ +export const LegendPropShape = { + data: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + color: PropTypes.string, + fill: PropTypes.string, + }) + ), + + // position & layout + anchor: PropTypes.oneOf([ + 'top', + 'top-right', + 'right', + 'bottom-right', + 'bottom', + 'bottom-left', + 'left', + 'top-left', + 'center', + ]).isRequired, + translateX: PropTypes.number, + translateY: PropTypes.number, + direction: PropTypes.oneOf(['row', 'column']).isRequired, + + // item + itemsSpacing: PropTypes.number, + itemWidth: PropTypes.number.isRequired, + itemHeight: PropTypes.number.isRequired, + itemDirection: PropTypes.oneOf([ + 'left-to-right', + 'right-to-left', + 'top-to-bottom', + 'bottom-to-top', + ]), + itemTextColor: PropTypes.string, + itemBackground: PropTypes.string, + itemOpacity: PropTypes.number, + + symbolShape: PropTypes.oneOfType([ + PropTypes.oneOf(['circle', 'diamond', 'square', 'triangle']), + PropTypes.func, + ]), + symbolSize: PropTypes.number, + symbolSpacing: PropTypes.number, + symbolBorderWidth: PropTypes.number, + symbolBorderColor: PropTypes.string, + + onClick: PropTypes.func, + onMouseEnter: PropTypes.func, + onMouseLeave: PropTypes.func, + + effects: PropTypes.arrayOf( + PropTypes.shape({ + on: PropTypes.oneOfType([PropTypes.oneOf(['hover'])]).isRequired, + style: PropTypes.shape({ + itemTextColor: PropTypes.string, + itemBackground: PropTypes.string, + itemOpacity: PropTypes.number, + symbolSize: PropTypes.number, + symbolBorderWidth: PropTypes.number, + symbolBorderColor: PropTypes.string, + }).isRequired, + }) + ), +} diff --git a/packages/legends/src/svg/BoxLegendSvg.js b/packages/legends/src/svg/BoxLegendSvg.js deleted file mode 100644 index bef6b589c..000000000 --- a/packages/legends/src/svg/BoxLegendSvg.js +++ /dev/null @@ -1,164 +0,0 @@ -/* - * 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 React from 'react' -import PropTypes from 'prop-types' -import LegendSvg from './LegendSvg' -import { datumPropType, symbolPropTypes, interactivityPropTypes } from '../props' -import { computeDimensions, computePositionFromAnchor } from '../compute' -import { - DIRECTION_ROW, - DIRECTION_COLUMN, - DIRECTION_BOTTOM_TO_TOP, - DIRECTION_LEFT_TO_RIGHT, - DIRECTION_RIGHT_TO_LEFT, - DIRECTION_TOP_TO_BOTTOM, - ANCHOR_TOP, - ANCHOR_TOP_RIGHT, - ANCHOR_RIGHT, - ANCHOR_BOTTOM_RIGHT, - ANCHOR_BOTTOM, - ANCHOR_BOTTOM_LEFT, - ANCHOR_LEFT, - ANCHOR_TOP_LEFT, - ANCHOR_CENTER, -} from '../constants' - -const BoxLegendSvg = ({ - data, - - containerWidth, - containerHeight, - translateX, - translateY, - anchor, - direction, - padding, - justify, - - itemsSpacing, - itemWidth, - itemHeight, - itemDirection, - itemTextColor, - itemBackground, - itemOpacity, - - symbolShape, - symbolSize, - symbolSpacing, - symbolBorderWidth, - symbolBorderColor, - - onClick, - onMouseEnter, - onMouseLeave, - - effects, -}) => { - const { width, height } = computeDimensions({ - itemCount: data.length, - itemsSpacing, - itemWidth, - itemHeight, - direction, - padding, - }) - - const { x, y } = computePositionFromAnchor({ - anchor, - translateX, - translateY, - containerWidth, - containerHeight, - width, - height, - }) - - return ( - - ) -} - -BoxLegendSvg.propTypes = { - data: PropTypes.arrayOf(datumPropType).isRequired, - containerWidth: PropTypes.number.isRequired, - containerHeight: PropTypes.number.isRequired, - translateX: PropTypes.number.isRequired, - translateY: PropTypes.number.isRequired, - anchor: PropTypes.oneOf([ - ANCHOR_TOP, - ANCHOR_TOP_RIGHT, - ANCHOR_RIGHT, - ANCHOR_BOTTOM_RIGHT, - ANCHOR_BOTTOM, - ANCHOR_BOTTOM_LEFT, - ANCHOR_LEFT, - ANCHOR_TOP_LEFT, - ANCHOR_CENTER, - ]).isRequired, - direction: PropTypes.oneOf([DIRECTION_ROW, DIRECTION_COLUMN]).isRequired, - padding: PropTypes.oneOfType([ - PropTypes.number, - PropTypes.shape({ - top: PropTypes.number, - right: PropTypes.number, - bottom: PropTypes.number, - left: PropTypes.number, - }), - ]).isRequired, - justify: PropTypes.bool, - - itemWidth: PropTypes.number.isRequired, - itemHeight: PropTypes.number.isRequired, - itemDirection: PropTypes.oneOf([ - DIRECTION_LEFT_TO_RIGHT, - DIRECTION_RIGHT_TO_LEFT, - DIRECTION_TOP_TO_BOTTOM, - DIRECTION_BOTTOM_TO_TOP, - ]), - itemsSpacing: PropTypes.number.isRequired, - itemTextColor: PropTypes.string, - itemBackground: PropTypes.string, - itemOpacity: PropTypes.number, - - ...symbolPropTypes, - ...interactivityPropTypes, -} - -BoxLegendSvg.defaultProps = { - translateX: 0, - translateY: 0, - itemsSpacing: LegendSvg.defaultProps.itemsSpacing, - padding: LegendSvg.defaultProps.padding, -} - -export default BoxLegendSvg diff --git a/packages/legends/src/svg/BoxLegendSvg.tsx b/packages/legends/src/svg/BoxLegendSvg.tsx new file mode 100644 index 000000000..8561b852b --- /dev/null +++ b/packages/legends/src/svg/BoxLegendSvg.tsx @@ -0,0 +1,83 @@ +import React from 'react' +import { LegendSvg } from './LegendSvg' +import { BoxLegendSvgProps } from '../types' +import { computeDimensions, computePositionFromAnchor } from '../compute' + +export const BoxLegendSvg = ({ + data, + + containerWidth, + containerHeight, + translateX = 0, + translateY = 0, + anchor, + direction, + padding = 0, + justify, + + itemsSpacing = 0, + itemWidth, + itemHeight, + itemDirection, + itemTextColor, + itemBackground, + itemOpacity, + + symbolShape, + symbolSize, + symbolSpacing, + symbolBorderWidth, + symbolBorderColor, + + onClick, + onMouseEnter, + onMouseLeave, + + effects, +}: BoxLegendSvgProps) => { + const { width, height } = computeDimensions({ + itemCount: data.length, + itemsSpacing, + itemWidth, + itemHeight, + direction, + padding, + }) + + const { x, y } = computePositionFromAnchor({ + anchor, + translateX, + translateY, + containerWidth, + containerHeight, + width, + height, + }) + + return ( + + ) +} diff --git a/packages/legends/src/svg/LegendSvg.js b/packages/legends/src/svg/LegendSvg.js deleted file mode 100644 index 1f2638872..000000000 --- a/packages/legends/src/svg/LegendSvg.js +++ /dev/null @@ -1,142 +0,0 @@ -/* - * 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 React from 'react' -import PropTypes from 'prop-types' -import LegendSvgItem from './LegendSvgItem' -import { datumPropType, symbolPropTypes, interactivityPropTypes } from '../props' -import { computeDimensions } from '../compute' -import { - DIRECTION_LEFT_TO_RIGHT, - DIRECTION_RIGHT_TO_LEFT, - DIRECTION_TOP_TO_BOTTOM, - DIRECTION_BOTTOM_TO_TOP, -} from '../constants' - -const LegendSvg = ({ - data, - - x, - y, - direction, - padding: _padding, - justify, - effects, - - itemWidth, - itemHeight, - itemDirection, - itemsSpacing, - itemTextColor, - itemBackground, - itemOpacity, - - symbolShape, - symbolSize, - symbolSpacing, - symbolBorderWidth, - symbolBorderColor, - - onClick, - onMouseEnter, - onMouseLeave, -}) => { - // eslint-disable-next-line no-unused-vars - const { width, height, padding } = computeDimensions({ - itemCount: data.length, - itemWidth, - itemHeight, - itemsSpacing, - direction, - padding: _padding, - }) - - let xStep = 0 - let yStep = 0 - if (direction === 'row') { - xStep = itemWidth + itemsSpacing - } else if (direction === 'column') { - yStep = itemHeight + itemsSpacing - } - - return ( - - {data.map((data, i) => ( - - ))} - - ) -} - -LegendSvg.propTypes = { - data: PropTypes.arrayOf(datumPropType).isRequired, - - x: PropTypes.number.isRequired, - y: PropTypes.number.isRequired, - direction: PropTypes.oneOf(['row', 'column']).isRequired, - padding: PropTypes.oneOfType([ - PropTypes.number, - PropTypes.shape({ - top: PropTypes.number, - right: PropTypes.number, - bottom: PropTypes.number, - left: PropTypes.number, - }), - ]).isRequired, - justify: PropTypes.bool.isRequired, - - itemsSpacing: PropTypes.number.isRequired, - itemWidth: PropTypes.number.isRequired, - itemHeight: PropTypes.number.isRequired, - itemDirection: PropTypes.oneOf([ - DIRECTION_LEFT_TO_RIGHT, - DIRECTION_RIGHT_TO_LEFT, - DIRECTION_TOP_TO_BOTTOM, - DIRECTION_BOTTOM_TO_TOP, - ]).isRequired, - itemTextColor: PropTypes.string.isRequired, - itemBackground: PropTypes.string.isRequired, - itemOpacity: PropTypes.number.isRequired, - - ...symbolPropTypes, - ...interactivityPropTypes, -} - -LegendSvg.defaultProps = { - padding: 0, - justify: false, - - itemsSpacing: 0, - itemDirection: 'left-to-right', - itemTextColor: 'black', - itemBackground: 'transparent', - itemOpacity: 1, -} - -export default LegendSvg diff --git a/packages/legends/src/svg/LegendSvg.tsx b/packages/legends/src/svg/LegendSvg.tsx new file mode 100644 index 000000000..e8def96e9 --- /dev/null +++ b/packages/legends/src/svg/LegendSvg.tsx @@ -0,0 +1,74 @@ +import React from 'react' +import { LegendSvgItem } from './LegendSvgItem' +import { LegendSvgProps } from '../types' +import { computeDimensions } from '../compute' + +export const LegendSvg = ({ + data, + + x, + y, + direction, + padding: _padding = 0, + justify, + effects, + + itemWidth, + itemHeight, + itemDirection = 'left-to-right', + itemsSpacing = 0, + itemTextColor, + itemBackground = 'transparent', + itemOpacity = 1, + + symbolShape, + symbolSize, + symbolSpacing, + symbolBorderWidth, + symbolBorderColor, + + onClick, + onMouseEnter, + onMouseLeave, +}: LegendSvgProps) => { + const { padding } = computeDimensions({ + itemCount: data.length, + itemWidth, + itemHeight, + itemsSpacing, + direction, + padding: _padding, + }) + + const xStep = direction === 'row' ? itemWidth + itemsSpacing : 0 + const yStep = direction === 'column' ? itemHeight + itemsSpacing : 0 + + return ( + + {data.map((data, i) => ( + + ))} + + ) +} diff --git a/packages/legends/src/svg/LegendSvgItem.js b/packages/legends/src/svg/LegendSvgItem.js deleted file mode 100644 index cd04cba62..000000000 --- a/packages/legends/src/svg/LegendSvgItem.js +++ /dev/null @@ -1,196 +0,0 @@ -/* - * 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 React, { useState, useCallback } from 'react' -import PropTypes from 'prop-types' -import isFunction from 'lodash/isFunction' -import { useTheme } from '@nivo/core' -import { datumPropType, symbolPropTypes, interactivityPropTypes } from '../props' -import { computeItemLayout } from '../compute' -import { SymbolCircle, SymbolDiamond, SymbolSquare, SymbolTriangle } from './symbols' - -const symbolByShape = { - circle: SymbolCircle, - diamond: SymbolDiamond, - square: SymbolSquare, - triangle: SymbolTriangle, -} - -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( - event => { - if (effects.length > 0) { - const applyEffects = effects.filter(({ on }) => on !== 'hover') - const style = applyEffects.reduce( - (acc, effect) => ({ - ...acc, - ...effect.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] - } - - return ( - - - {React.createElement(Symbol, { - id: data.id, - 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} - - - ) -} - -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-to-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/legends/src/svg/LegendSvgItem.tsx b/packages/legends/src/svg/LegendSvgItem.tsx new file mode 100644 index 000000000..ec46a74ed --- /dev/null +++ b/packages/legends/src/svg/LegendSvgItem.tsx @@ -0,0 +1,147 @@ +import React, { useState, useCallback } from 'react' +import { useTheme } from '@nivo/core' +import { LegendSvgItemProps } from '../types' +import { computeItemLayout } from '../compute' +import { SymbolCircle, SymbolDiamond, SymbolSquare, SymbolTriangle } from './symbols' + +type Style = Partial<{ + itemBackground: string + itemOpacity: number + itemTextColor: string + symbolBorderColor: string + symbolBorderWidth: number + symbolSize: number +}> + +const symbolByShape = { + circle: SymbolCircle, + diamond: SymbolDiamond, + square: SymbolSquare, + triangle: SymbolTriangle, +} + +export const LegendSvgItem = ({ + x, + y, + width, + height, + data, + direction = 'left-to-right', + justify = false, + textColor, + background = 'transparent', + opacity = 1, + + symbolShape = 'square', + symbolSize = 16, + symbolSpacing = 8, + symbolBorderWidth = 0, + symbolBorderColor = 'transparent', + + onClick, + onMouseEnter, + onMouseLeave, + + effects, +}: LegendSvgItemProps) => { + const [style, setStyle] = useState