diff --git a/packages/heatmap/src/HeatMap.tsx b/packages/heatmap/src/HeatMap.tsx index 32b1764dc5..42f481b9fa 100644 --- a/packages/heatmap/src/HeatMap.tsx +++ b/packages/heatmap/src/HeatMap.tsx @@ -1,4 +1,4 @@ -import { ReactNode, Fragment, createElement } from 'react' +import { ReactNode, Fragment, createElement, useMemo } from 'react' import { SvgWrapper, Container, useDimensions } from '@nivo/core' import { Axes, Grid } from '@nivo/axes' import { AnchoredContinuousColorsLegendSvg } from '@nivo/legends' @@ -27,7 +27,7 @@ const InnerHeatMap = ({ width, height, margin: partialMargin, - // forceSquare = svgDefaultProps.forceSquare, + forceSquare = svgDefaultProps.forceSquare, xInnerPadding = svgDefaultProps.xInnerPadding, xOuterPadding = svgDefaultProps.xOuterPadding, yInnerPadding = svgDefaultProps.yInnerPadding, @@ -67,20 +67,31 @@ const InnerHeatMap = ({ ariaLabelledBy, ariaDescribedBy, }: InnerHeatMapProps) => { - const { margin, innerWidth, innerHeight, outerWidth, outerHeight } = useDimensions( - width, - height, - partialMargin - ) + const { + margin: _margin, + innerWidth: _innerWidth, + innerHeight: _innerHeight, + outerWidth, + outerHeight, + } = useDimensions(width, height, partialMargin) - const { xScale, yScale, cells, colorScale, activeCell, setActiveCell } = useHeatMap< - Datum, - ExtraProps - >({ - data, - valueFormat, + const { width: innerWidth, height: innerHeight, + offsetX, + offsetY, + xScale, + yScale, + cells, + colorScale, + activeCell, + setActiveCell, + } = useHeatMap({ + data, + valueFormat, + width: _innerWidth, + height: _innerHeight, + forceSquare, xInnerPadding, xOuterPadding, yInnerPadding, @@ -97,6 +108,15 @@ const InnerHeatMap = ({ hoverTarget, }) + const margin = useMemo( + () => ({ + ..._margin, + top: _margin.top + offsetY, + left: _margin.left + offsetX, + }), + [_margin, offsetX, offsetY] + ) + const layerById: Record = { grid: null, axes: null, diff --git a/packages/heatmap/src/HeatMapCanvas.tsx b/packages/heatmap/src/HeatMapCanvas.tsx index 631afa5ade..e0d0580eda 100644 --- a/packages/heatmap/src/HeatMapCanvas.tsx +++ b/packages/heatmap/src/HeatMapCanvas.tsx @@ -29,11 +29,11 @@ const InnerHeatMapCanvas = ) => { const canvasEl = useRef(null) - const { margin, innerWidth, innerHeight, outerWidth, outerHeight } = useDimensions( - width, - height, - partialMargin - ) + const { + margin: _margin, + innerWidth: _innerWidth, + innerHeight: _innerHeight, + outerWidth, + outerHeight, + } = useDimensions(width, height, partialMargin) - const { xScale, yScale, cells, activeCell, setActiveCell, colorScale } = useHeatMap< - Datum, - ExtraProps - >({ - data, - valueFormat, + const { width: innerWidth, height: innerHeight, + offsetX, + offsetY, + xScale, + yScale, + cells, + colorScale, + activeCell, + setActiveCell, + } = useHeatMap({ + data, + valueFormat, + width: _innerWidth, + height: _innerHeight, xInnerPadding, xOuterPadding, yInnerPadding, yOuterPadding, + forceSquare, sizeVariation, colors, emptyColor, @@ -96,6 +107,15 @@ const InnerHeatMapCanvas = ({ + ..._margin, + top: _margin.top + offsetY, + left: _margin.left + offsetX, + }), + [_margin, offsetX, offsetY] + ) + const boundAnnotations = useCellAnnotations(cells, annotations) const computedAnnotations = useComputedAnnotations({ annotations: boundAnnotations, diff --git a/packages/heatmap/src/compute.ts b/packages/heatmap/src/compute.ts index 4bdf097a21..67af3aa571 100644 --- a/packages/heatmap/src/compute.ts +++ b/packages/heatmap/src/compute.ts @@ -1,24 +1,69 @@ -import { scaleBand } from 'd3-scale' +import { scaleBand, scaleLinear } from 'd3-scale' import { castBandScale } from '@nivo/scales' -import { ComputedCell, HeatMapCommonProps, HeatMapDataProps, HeatMapDatum } from './types' +import { + ComputedCell, + HeatMapCommonProps, + HeatMapDataProps, + HeatMapDatum, + SizeVariationConfig, +} from './types' + +export const computeLayout = ({ + width: _width, + height: _height, + rows, + columns, + forceSquare, +}: { + width: number + height: number + rows: number + columns: number + forceSquare: boolean +}) => { + let width = _width + let height = _height + + let offsetX = 0 + let offsetY = 0 + + if (forceSquare) { + const cellWidth = Math.max(_width / columns, 0) + const cellHeight = Math.max(_height / rows, 0) + const cellSize = Math.min(cellWidth, cellHeight) + + width = cellSize * columns + height = cellSize * rows + + offsetX = (_width - width) / 2 + offsetY = (_height - height) / 2 + } + + return { + offsetX, + offsetY, + width, + height, + } +} export const computeCells = ({ data, - width, - height, + width: _width, + height: _height, xInnerPadding, xOuterPadding, yInnerPadding, yOuterPadding, + forceSquare, }: { data: HeatMapDataProps['data'] width: number height: number - xInnerPadding: HeatMapCommonProps['xInnerPadding'] - xOuterPadding: HeatMapCommonProps['xOuterPadding'] - yInnerPadding: HeatMapCommonProps['yInnerPadding'] - yOuterPadding: HeatMapCommonProps['yOuterPadding'] -}) => { +} & Pick< + HeatMapCommonProps, + 'xOuterPadding' | 'xInnerPadding' | 'yOuterPadding' | 'yInnerPadding' | 'forceSquare' +>) => { const xValuesSet = new Set() const serieIds: string[] = [] const allValues: number[] = [] @@ -47,6 +92,15 @@ export const computeCells = ( scaleBand() .domain(xValues) @@ -78,6 +132,10 @@ export const computeCells = number) => { + if (!size) return () => 1 + + const scale = scaleLinear() + .domain(size.values ? size.values : [min, max]) + .range(size.sizes) + + return (value: number | null) => { + if (value === null) return 1 + return scale(value) + } +} + +export const getCellAnnotationPosition = ( + cell: ComputedCell +) => ({ + x: cell.x, + y: cell.y, +}) + +export const getCellAnnotationDimensions = ( + cell: ComputedCell +) => ({ + size: Math.max(cell.width, cell.height), + width: cell.width, + height: cell.height, +}) diff --git a/packages/heatmap/src/hooks.ts b/packages/heatmap/src/hooks.ts index 533f45716d..513e07a244 100644 --- a/packages/heatmap/src/hooks.ts +++ b/packages/heatmap/src/hooks.ts @@ -1,5 +1,4 @@ import { useMemo, useCallback, useState } from 'react' -import { scaleLinear } from 'd3-scale' import { useTheme, usePropertyAccessor, useValueFormatter } from '@nivo/core' import { useInheritedColor, getContinuousColorScale } from '@nivo/colors' import { AnnotationMatcher, useAnnotations } from '@nivo/annotations' @@ -12,7 +11,12 @@ import { SizeVariationConfig, } from './types' import { commonDefaultProps } from './defaults' -import { computeCells } from './compute' +import { + computeCells, + computeSizeScale, + getCellAnnotationPosition, + getCellAnnotationDimensions, +} from './compute' export const useComputeCells = ({ data, @@ -22,15 +26,15 @@ export const useComputeCells = ['data'] width: number height: number - xInnerPadding: HeatMapCommonProps['xInnerPadding'] - xOuterPadding: HeatMapCommonProps['xOuterPadding'] - yInnerPadding: HeatMapCommonProps['yInnerPadding'] - yOuterPadding: HeatMapCommonProps['yOuterPadding'] -}) => +} & Pick< + HeatMapCommonProps, + 'xOuterPadding' | 'xInnerPadding' | 'yOuterPadding' | 'yInnerPadding' | 'forceSquare' +>) => useMemo( () => computeCells({ @@ -41,8 +45,18 @@ export const useComputeCells = number) => - useMemo(() => { - if (!size) return () => 1 + useMemo(() => computeSizeScale(size, min, max), [size, min, max]) - const scale = scaleLinear() - .domain(size.values ? size.values : [min, max]) - .range(size.sizes) - - return (value: number | null) => { - if (value === null) return 1 - return scale(value) - } - }, [size, min, max]) - -export const useHeatMap = < - Datum extends HeatMapDatum = DefaultHeatMapDatum, - ExtraProps extends object = Record ->({ - data, +const useCellsStyle = ({ + cells, + minValue, + maxValue, + sizeVariation, + colors, + emptyColor, + opacity, + activeOpacity, + inactiveOpacity, + borderColor, + label, + labelTextColor, valueFormat, - width, - height, - // forceSquare = commonDefaultProps.forceSquare, - xOuterPadding = commonDefaultProps.xOuterPadding, - xInnerPadding = commonDefaultProps.xInnerPadding, - yOuterPadding = commonDefaultProps.yOuterPadding, - yInnerPadding = commonDefaultProps.yInnerPadding, - sizeVariation = commonDefaultProps.sizeVariation, - colors = commonDefaultProps.colors as HeatMapCommonProps['colors'], - emptyColor = commonDefaultProps.emptyColor, - opacity = commonDefaultProps.opacity, - activeOpacity = commonDefaultProps.activeOpacity, - inactiveOpacity = commonDefaultProps.inactiveOpacity, - borderColor = commonDefaultProps.borderColor as HeatMapCommonProps['borderColor'], - label = commonDefaultProps.label as HeatMapCommonProps['label'], - labelTextColor = commonDefaultProps.labelTextColor as HeatMapCommonProps['labelTextColor'], - hoverTarget = commonDefaultProps.hoverTarget, + activeIds, }: { - data: HeatMapDataProps['data'] + cells: Omit< + ComputedCell, + 'formattedValue' | 'color' | 'opacity' | 'borderColor' | 'label' | 'labelTextColor' + >[] + minValue: number + maxValue: number valueFormat?: HeatMapCommonProps['valueFormat'] - width: number - height: number - forceSquare?: HeatMapCommonProps['forceSquare'] - xOuterPadding?: HeatMapCommonProps['xOuterPadding'] - xInnerPadding?: HeatMapCommonProps['xInnerPadding'] - yOuterPadding?: HeatMapCommonProps['yOuterPadding'] - yInnerPadding?: HeatMapCommonProps['yInnerPadding'] - sizeVariation?: HeatMapCommonProps['sizeVariation'] - colors?: HeatMapCommonProps['colors'] - emptyColor?: HeatMapCommonProps['emptyColor'] - opacity?: HeatMapCommonProps['opacity'] - activeOpacity?: HeatMapCommonProps['activeOpacity'] - inactiveOpacity?: HeatMapCommonProps['inactiveOpacity'] - borderColor?: HeatMapCommonProps['borderColor'] - label?: HeatMapCommonProps['label'] - labelTextColor?: HeatMapCommonProps['labelTextColor'] - hoverTarget?: HeatMapCommonProps['hoverTarget'] -}) => { - const [activeCell, setActiveCell] = useState | null>(null) - - const { cells, xScale, yScale, minValue, maxValue } = useComputeCells({ - data, - width, - height, - xOuterPadding, - xInnerPadding, - yOuterPadding, - yInnerPadding, - }) + activeIds: string[] +} & Pick< + HeatMapCommonProps, + | 'sizeVariation' + | 'colors' + | 'emptyColor' + | 'opacity' + | 'activeOpacity' + | 'inactiveOpacity' + | 'borderColor' + | 'label' + | 'labelTextColor' +>) => { + const getSize = useSizeScale(sizeVariation, minValue, maxValue) const colorScale = useMemo(() => { if (typeof colors === 'function') return null @@ -159,14 +144,6 @@ export const useHeatMap = < }) }, [colors, minValue, maxValue]) - const activeIds = useMemo(() => { - if (!activeCell) return [] - - const isHoverTarget = isHoverTargetByType[hoverTarget] - - return cells.filter(cell => isHoverTarget(cell, activeCell)).map(cell => cell.id) - }, [cells, activeCell, hoverTarget]) - const getColor = useCallback( (cell: Omit, 'color' | 'opacity' | 'borderColor'>) => { if (cell.value !== null) { @@ -178,14 +155,14 @@ export const useHeatMap = < }, [colors, colorScale, emptyColor] ) - const getSize = useSizeScale(sizeVariation, minValue, maxValue) const theme = useTheme() const getBorderColor = useInheritedColor(borderColor, theme) const getLabelTextColor = useInheritedColor(labelTextColor, theme) + const formatValue = useValueFormatter(valueFormat) const getLabel = usePropertyAccessor(label) - const computedCells = useMemo( + const styledCells = useMemo( () => cells.map(cell => { let computedOpacity = opacity @@ -226,53 +203,111 @@ export const useHeatMap = < ) return { - cells: computedCells, - xScale, - yScale, + cells: styledCells, colorScale, - activeCell, - setActiveCell, } +} - /* - const layoutConfig = useMemo(() => { - const columns = keys.length - const rows = data.length +export const useHeatMap = < + Datum extends HeatMapDatum = DefaultHeatMapDatum, + ExtraProps extends object = Record +>({ + data, + valueFormat, + width: _width, + height: _height, + xOuterPadding = commonDefaultProps.xOuterPadding, + xInnerPadding = commonDefaultProps.xInnerPadding, + yOuterPadding = commonDefaultProps.yOuterPadding, + yInnerPadding = commonDefaultProps.yInnerPadding, + forceSquare = commonDefaultProps.forceSquare, + sizeVariation = commonDefaultProps.sizeVariation, + colors = commonDefaultProps.colors as HeatMapCommonProps['colors'], + emptyColor = commonDefaultProps.emptyColor, + opacity = commonDefaultProps.opacity, + activeOpacity = commonDefaultProps.activeOpacity, + inactiveOpacity = commonDefaultProps.inactiveOpacity, + borderColor = commonDefaultProps.borderColor as HeatMapCommonProps['borderColor'], + label = commonDefaultProps.label as HeatMapCommonProps['label'], + labelTextColor = commonDefaultProps.labelTextColor as HeatMapCommonProps['labelTextColor'], + hoverTarget = commonDefaultProps.hoverTarget, +}: { + data: HeatMapDataProps['data'] + width: number + height: number +} & Partial< + Pick< + HeatMapCommonProps, + | 'valueFormat' + | 'xOuterPadding' + | 'xInnerPadding' + | 'yOuterPadding' + | 'yInnerPadding' + | 'forceSquare' + | 'sizeVariation' + | 'colors' + | 'emptyColor' + | 'opacity' + | 'activeOpacity' + | 'inactiveOpacity' + | 'borderColor' + | 'label' + | 'labelTextColor' + | 'hoverTarget' + > +>) => { + const [activeCell, setActiveCell] = useState | null>(null) - let cellWidth = Math.max((width - padding * (columns + 1)) / columns, 0) - let cellHeight = Math.max((height - padding * (rows + 1)) / rows, 0) + const { width, height, offsetX, offsetY, cells, xScale, yScale, minValue, maxValue } = + useComputeCells({ + data, + width: _width, + height: _height, + xOuterPadding, + xInnerPadding, + yOuterPadding, + yInnerPadding, + forceSquare, + }) - let offsetX = 0 - let offsetY = 0 - if (forceSquare === true) { - const cellSize = Math.min(cellWidth, cellHeight) - cellWidth = cellSize - cellHeight = cellSize + const activeIds = useMemo(() => { + if (!activeCell) return [] - offsetX = (width - ((cellWidth + padding) * columns + padding)) / 2 - offsetY = (height - ((cellHeight + padding) * rows + padding)) / 2 - } + const isHoverTarget = isHoverTargetByType[hoverTarget] - return { - cellWidth, - cellHeight, - offsetX, - offsetY, - } - }, [data, keys, width, height, padding, forceSquare]) - */ -} + return cells.filter(cell => isHoverTarget(cell, activeCell)).map(cell => cell.id) + }, [cells, activeCell, hoverTarget]) -const getCellAnnotationPosition = (cell: ComputedCell) => ({ - x: cell.x, - y: cell.y, -}) + const { cells: computedCells, colorScale } = useCellsStyle({ + cells, + minValue, + maxValue, + sizeVariation, + colors, + emptyColor, + opacity, + activeOpacity, + inactiveOpacity, + borderColor, + label, + labelTextColor, + valueFormat, + activeIds, + }) -const getCellAnnotationDimensions = (cell: ComputedCell) => ({ - size: Math.max(cell.width, cell.height), - width: cell.width, - height: cell.height, -}) + return { + width, + height, + offsetX, + offsetY, + cells: computedCells, + xScale, + yScale, + colorScale, + activeCell, + setActiveCell, + } +} export const useCellAnnotations = ( cells: ComputedCell[], diff --git a/packages/heatmap/src/index.ts b/packages/heatmap/src/index.ts index 7ad7f8d3ee..0cae12227e 100644 --- a/packages/heatmap/src/index.ts +++ b/packages/heatmap/src/index.ts @@ -5,3 +5,4 @@ export * from './ResponsiveHeatMapCanvas' export * from './hooks' export * from './defaults' export * from './types' +export * from './compute'