Skip to content

Commit

Permalink
feat(circle-packing): add support for mouse handlers to SVG and HTML …
Browse files Browse the repository at this point in the history
…implementations
  • Loading branch information
plouc authored and wyze committed Apr 26, 2021
1 parent 4fb658d commit 138eafb
Show file tree
Hide file tree
Showing 10 changed files with 296 additions and 67 deletions.
33 changes: 28 additions & 5 deletions packages/circle-packing/src/CirclePacking.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ import { CircleSvg } from './CircleSvg'
import { Labels } from './Labels'
import { LabelSvg } from './LabelSvg'

type InnerCirclePackingProps<RawDatum> = Partial<
Omit<
CirclePackingSvgProps<RawDatum>,
'data' | 'width' | 'height' | 'isInteractive' | 'animate' | 'motionConfig'
>
> &
Pick<CirclePackingSvgProps<RawDatum>, 'data' | 'width' | 'height' | 'isInteractive'>

const InnerCirclePacking = <RawDatum,>({
data,
id = defaultProps.id,
Expand All @@ -36,11 +44,14 @@ const InnerCirclePacking = <RawDatum,>({
labelsSkipRadius = defaultProps.labelsSkipRadius,
labelsTextColor = defaultProps.labelsTextColor as InheritedColorConfig<ComputedDatum<RawDatum>>,
layers = defaultProps.layers,
isInteractive,
onMouseEnter,
onMouseMove,
onMouseLeave,
onClick,
tooltip = defaultProps.tooltip,
role = defaultProps.role,
}: Partial<
Omit<CirclePackingSvgProps<RawDatum>, 'data' | 'width' | 'height' | 'animate' | 'motionConfig'>
> &
Pick<CirclePackingSvgProps<RawDatum>, 'data' | 'width' | 'height'>) => {
}: InnerCirclePackingProps<RawDatum>) => {
const { outerWidth, outerHeight, margin, innerWidth, innerHeight } = useDimensions(
width,
height,
Expand All @@ -67,7 +78,19 @@ const InnerCirclePacking = <RawDatum,>({
}

if (layers.includes('circles')) {
layerById.circles = <Circles key="circles" data={nodes} />
layerById.circles = (
<Circles<RawDatum>
key="circles"
nodes={nodes}
isInteractive={isInteractive}
onMouseEnter={onMouseEnter}
onMouseMove={onMouseMove}
onMouseLeave={onMouseLeave}
onClick={onClick}
component={CircleSvg}
tooltip={tooltip}
/>
)
}

if (enableLabels && layers.includes('labels')) {
Expand Down
16 changes: 9 additions & 7 deletions packages/circle-packing/src/CirclePackingCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ import { CirclePackingCanvasProps, ComputedDatum } from './types'
import { defaultProps } from './props'
import { useCirclePacking, useCirclePackingLabels } from './hooks'

type InnerCirclePackingCanvasProps<RawDatum> = Partial<
Omit<
CirclePackingCanvasProps<RawDatum>,
'data' | 'width' | 'height' | 'animate' | 'motionConfig'
>
> &
Pick<CirclePackingCanvasProps<RawDatum>, 'data' | 'width' | 'height'>

const InnerCirclePackingCanvas = <RawDatum,>({
data,
id = defaultProps.id,
Expand All @@ -28,13 +36,7 @@ const InnerCirclePackingCanvas = <RawDatum,>({
// layers = defaultProps.layers,
isInteractive,
role = defaultProps.role,
}: Partial<
Omit<
CirclePackingCanvasProps<RawDatum>,
'data' | 'width' | 'height' | 'animate' | 'motionConfig'
>
> &
Pick<CirclePackingCanvasProps<RawDatum>, 'data' | 'width' | 'height'>) => {
}: InnerCirclePackingCanvasProps<RawDatum>) => {
const canvasEl = useRef<HTMLCanvasElement | null>(null)
const theme = useTheme()

Expand Down
33 changes: 28 additions & 5 deletions packages/circle-packing/src/CirclePackingHtml.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ import { defaultProps } from './props'
import { Labels } from './Labels'
import { LabelHtml } from './LabelHtml'

type InnerCirclePackingHtmlProps<RawDatum> = Partial<
Omit<
CirclePackingHtmlProps<RawDatum>,
'data' | 'width' | 'height' | 'isInteractive' | 'animate' | 'motionConfig'
>
> &
Pick<CirclePackingHtmlProps<RawDatum>, 'data' | 'width' | 'height' | 'isInteractive'>

export const InnerCirclePackingHtml = <RawDatum,>({
data,
id = defaultProps.id,
Expand All @@ -30,11 +38,14 @@ export const InnerCirclePackingHtml = <RawDatum,>({
labelsSkipRadius = defaultProps.labelsSkipRadius,
labelsTextColor = defaultProps.labelsTextColor as InheritedColorConfig<ComputedDatum<RawDatum>>,
layers = defaultProps.layers,
isInteractive,
onMouseEnter,
onMouseMove,
onMouseLeave,
onClick,
tooltip = defaultProps.tooltip,
role = defaultProps.role,
}: Partial<
Omit<CirclePackingHtmlProps<RawDatum>, 'data' | 'width' | 'height' | 'animate' | 'motionConfig'>
> &
Pick<CirclePackingHtmlProps<RawDatum>, 'data' | 'width' | 'height'>) => {
}: InnerCirclePackingHtmlProps<RawDatum>) => {
const { outerWidth, outerHeight, margin, innerWidth, innerHeight } = useDimensions(
width,
height,
Expand All @@ -61,7 +72,19 @@ export const InnerCirclePackingHtml = <RawDatum,>({
}

if (layers.includes('circles')) {
layerById.circles = <Circles<RawDatum> key="circles" nodes={nodes} component={CircleHtml} />
layerById.circles = (
<Circles<RawDatum>
key="circles"
nodes={nodes}
isInteractive={isInteractive}
onMouseEnter={onMouseEnter}
onMouseMove={onMouseMove}
onMouseLeave={onMouseLeave}
onClick={onClick}
component={CircleHtml}
tooltip={tooltip}
/>
)
}

if (enableLabels && layers.includes('labels')) {
Expand Down
11 changes: 11 additions & 0 deletions packages/circle-packing/src/CirclePackingTooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from 'react'
import { BasicTooltip } from '@nivo/tooltip'
import { ComputedDatum } from './types'

export const CirclePackingTooltip = <RawDatum,>({
id,
formattedValue,
color,
}: ComputedDatum<RawDatum>) => (
<BasicTooltip id={id} value={formattedValue} enableChip={true} color={color} />
)
35 changes: 35 additions & 0 deletions packages/circle-packing/src/CircleSvg.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from 'react'
import { animated } from 'react-spring'
import { CircleProps } from './types'
import { useBoundMouseHandlers } from './hooks'

export const CircleSvg = <RawDatum,>({
node,
style,
onMouseEnter,
onMouseMove,
onMouseLeave,
onClick,
}: CircleProps<RawDatum>) => {
const handlers = useBoundMouseHandlers<RawDatum>(node, {
onMouseEnter,
onMouseMove,
onMouseLeave,
onClick,
})

return (
<animated.circle
key={node.id}
cx={style.x}
cy={style.y}
r={style.radius}
fill={style.color}
opacity={style.opacity}
onMouseEnter={handlers.onMouseEnter}
onMouseMove={handlers.onMouseMove}
onMouseLeave={handlers.onMouseLeave}
onClick={handlers.onClick}
/>
)
}
123 changes: 88 additions & 35 deletions packages/circle-packing/src/Circles.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React from 'react'
import { useTransition, animated, to, SpringValue } from 'react-spring'
import React, { createElement, useMemo, MouseEvent } from 'react'
import { useTransition, to, SpringValue } from 'react-spring'
import { useMotionConfig } from '@nivo/core'
import { ComputedDatum, CircleComponent } from './types'
import { useTooltip } from '@nivo/tooltip'
import { ComputedDatum, CircleComponent, MouseHandlers, CirclePackingCommonProps } from './types'

/**
* A negative radius value is invalid for an SVG circle,
Expand All @@ -11,37 +12,87 @@ import { ComputedDatum, CircleComponent } from './types'
export const interpolateRadius = (radiusValue: SpringValue<number>) =>
to([radiusValue], radius => Math.max(0, radius))

interface CirclesProps<RawDatum> {
type CirclesProps<RawDatum> = {
nodes: ComputedDatum<RawDatum>[]
component: CircleComponent<RawDatum>
}

export const Circles = <RawDatum,>({ nodes, component }: CirclesProps<RawDatum>) => {
const { animate, config: springConfig } = useMotionConfig()
isInteractive: CirclePackingCommonProps<RawDatum>['isInteractive']
tooltip: CirclePackingCommonProps<RawDatum>['tooltip']
} & MouseHandlers<RawDatum>

const enter = (node: ComputedDatum<RawDatum>) => ({
const getTransitionPhases = <RawDatum,>() => ({
enter: (node: ComputedDatum<RawDatum>) => ({
x: node.x,
y: node.y,
radius: 0,
color: node.color,
opacity: 0,
})

const update = (node: ComputedDatum<RawDatum>) => ({
}),
update: (node: ComputedDatum<RawDatum>) => ({
x: node.x,
y: node.y,
radius: node.radius,
color: node.color,
opacity: 1,
})

const leave = (node: ComputedDatum<RawDatum>) => ({
}),
leave: (node: ComputedDatum<RawDatum>) => ({
x: node.x,
y: node.y,
radius: 0,
color: node.color,
opacity: 0,
})
}),
})

export const Circles = <RawDatum,>({
nodes,
component,
isInteractive,
onMouseEnter,
onMouseMove,
onMouseLeave,
onClick,
tooltip,
}: CirclesProps<RawDatum>) => {
const { showTooltipFromEvent, hideTooltip } = useTooltip()

const handleMouseEnter = useMemo(() => {
if (!isInteractive) return undefined

return (node: ComputedDatum<RawDatum>, event: MouseEvent) => {
showTooltipFromEvent(createElement(tooltip, node), event)
onMouseEnter?.(node, event)
}
}, [isInteractive, showTooltipFromEvent, tooltip, onMouseEnter])

const handleMouseMove = useMemo(() => {
if (!isInteractive) return undefined

return (node: ComputedDatum<RawDatum>, event: MouseEvent) => {
showTooltipFromEvent(createElement(tooltip, node), event)
onMouseMove?.(node, event)
}
}, [isInteractive, showTooltipFromEvent, tooltip, onMouseMove])

const handleMouseLeave = useMemo(() => {
if (!isInteractive) return undefined

return (node: ComputedDatum<RawDatum>, event: MouseEvent) => {
hideTooltip()
onMouseLeave?.(node, event)
}
}, [isInteractive, hideTooltip, onMouseLeave])

const handleClick = useMemo(() => {
if (!isInteractive) return undefined

return (node: ComputedDatum<RawDatum>, event: MouseEvent) => {
onClick?.(node, event)
}
}, [isInteractive, onClick])

const { animate, config: springConfig } = useMotionConfig()

const transitionPhases = useMemo(() => getTransitionPhases<RawDatum>(), [])

const transition = useTransition<
ComputedDatum<RawDatum>,
Expand All @@ -52,30 +103,32 @@ export const Circles = <RawDatum,>({ nodes, component }: CirclesProps<RawDatum>)
color: string
opacity: number
}
>(data, {
key: datum => datum.id,
initial: update,
from: enter,
enter: update,
update,
leave,
>(nodes, {
key: node => node.id,
initial: transitionPhases.update,
from: transitionPhases.enter,
enter: transitionPhases.update,
update: transitionPhases.update,
leave: transitionPhases.leave,
config: springConfig,
immediate: !animate,
})

return (
<g>
{transition((transitionProps, datum) => {
return (
<animated.circle
key={datum.id}
cx={transitionProps.x}
cy={transitionProps.y}
r={interpolateRadius(transitionProps.radius)}
fill={transitionProps.color}
opacity={transitionProps.opacity}
/>
)
<>
{transition((transitionProps, node) => {
return React.createElement(component, {
key: node.id,
node,
style: {
...transitionProps,
radius: interpolateRadius(transitionProps.radius),
},
onMouseEnter: handleMouseEnter,
onMouseMove: handleMouseMove,
onMouseLeave: handleMouseLeave,
onClick: handleClick,
})
})}
</g>
)
Expand Down
Loading

0 comments on commit 138eafb

Please sign in to comment.