Skip to content

Commit b021074

Browse files
authoredMay 10, 2022
feat(canvas): add custom ref to ScatterPlotCanvas and NetworkCanvas (#1953)
* Add forwardRef to ScatterPlotCanvas * Add story * Adding support for NetworkCanvas * Aligned function names * Formatting * Type fixes
1 parent 45c31e9 commit b021074

6 files changed

+171
-49
lines changed
 

‎packages/network/src/NetworkCanvas.tsx

+38-17
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
import { useCallback, useRef, useEffect, createElement, MouseEvent, useMemo } from 'react'
1+
import {
2+
ForwardedRef,
3+
forwardRef,
4+
useCallback,
5+
useRef,
6+
useEffect,
7+
createElement,
8+
MouseEvent,
9+
useMemo,
10+
} from 'react'
211
import { getDistance, getRelativeCursor, Container, useDimensions, useTheme } from '@nivo/core'
312
import { useTooltip } from '@nivo/tooltip'
413
import { useComputedAnnotations, renderAnnotationsToCanvas } from '@nivo/annotations'
@@ -17,7 +26,9 @@ import {
1726
type InnerNetworkCanvasProps<Node extends InputNode, Link extends InputLink> = Omit<
1827
NetworkCanvasProps<Node, Link>,
1928
'renderWrapper' | 'theme'
20-
>
29+
> & {
30+
canvasRef: ForwardedRef<HTMLCanvasElement>
31+
}
2132

2233
const InnerNetworkCanvas = <Node extends InputNode, Link extends InputLink>({
2334
width,
@@ -56,6 +67,7 @@ const InnerNetworkCanvas = <Node extends InputNode, Link extends InputLink>({
5667
defaultActiveNodeIds = canvasDefaultProps.defaultActiveNodeIds,
5768
nodeTooltip = canvasDefaultProps.nodeTooltip as NodeTooltip<Node>,
5869
onClick,
70+
canvasRef,
5971
}: InnerNetworkCanvasProps<Node, Link>) => {
6072
const canvasEl = useRef<HTMLCanvasElement | null>(null)
6173
const { margin, innerWidth, innerHeight, outerWidth, outerHeight } = useDimensions(
@@ -202,7 +214,10 @@ const InnerNetworkCanvas = <Node extends InputNode, Link extends InputLink>({
202214

203215
return (
204216
<canvas
205-
ref={canvasEl}
217+
ref={canvas => {
218+
canvasEl.current = canvas
219+
if (canvasRef && 'current' in canvasRef) canvasRef.current = canvas
220+
}}
206221
width={outerWidth * pixelRatio}
207222
height={outerHeight * pixelRatio}
208223
style={{
@@ -218,18 +233,24 @@ const InnerNetworkCanvas = <Node extends InputNode, Link extends InputLink>({
218233
)
219234
}
220235

221-
export const NetworkCanvas = <
222-
Node extends InputNode = InputNode,
223-
Link extends InputLink = InputLink
224-
>({
225-
theme,
226-
isInteractive = canvasDefaultProps.isInteractive,
227-
animate = canvasDefaultProps.animate,
228-
motionConfig = canvasDefaultProps.motionConfig,
229-
renderWrapper,
230-
...otherProps
231-
}: NetworkCanvasProps<Node, Link>) => (
232-
<Container {...{ isInteractive, animate, motionConfig, theme, renderWrapper }}>
233-
<InnerNetworkCanvas<Node, Link> isInteractive={isInteractive} {...otherProps} />
234-
</Container>
236+
export const NetworkCanvas = forwardRef(
237+
<Node extends InputNode = InputNode, Link extends InputLink = InputLink>(
238+
{
239+
theme,
240+
isInteractive = canvasDefaultProps.isInteractive,
241+
animate = canvasDefaultProps.animate,
242+
motionConfig = canvasDefaultProps.motionConfig,
243+
renderWrapper,
244+
...otherProps
245+
}: NetworkCanvasProps<Node, Link>,
246+
ref: ForwardedRef<HTMLCanvasElement>
247+
) => (
248+
<Container {...{ isInteractive, animate, motionConfig, theme, renderWrapper }}>
249+
<InnerNetworkCanvas<Node, Link>
250+
isInteractive={isInteractive}
251+
{...otherProps}
252+
canvasRef={ref}
253+
/>
254+
</Container>
255+
)
235256
)
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,28 @@
11
import { ResponsiveWrapper } from '@nivo/core'
2+
import { ForwardedRef, forwardRef } from 'react'
23
import { NetworkCanvasProps, InputNode, InputLink } from './types'
34
import { NetworkCanvas } from './NetworkCanvas'
45

5-
export const ResponsiveNetworkCanvas = <
6+
export const ResponsiveNetworkCanvas = forwardRef(function ResponsiveBarCanvas<
67
Node extends InputNode = InputNode,
78
Link extends InputLink = InputLink
89
>(
9-
props: Omit<NetworkCanvasProps<Node, Link>, 'height' | 'width'>
10-
) => (
11-
<ResponsiveWrapper>
12-
{({ width, height }) => (
13-
<NetworkCanvas<Node, Link> width={width} height={height} {...props} />
14-
)}
15-
</ResponsiveWrapper>
16-
)
10+
props: Omit<NetworkCanvasProps<Node, Link>, 'height' | 'width'>,
11+
ref: ForwardedRef<HTMLCanvasElement>
12+
) {
13+
return (
14+
<ResponsiveWrapper>
15+
{({ width, height }) => (
16+
<NetworkCanvas
17+
width={width}
18+
height={height}
19+
{...(props as Omit<
20+
NetworkCanvasProps<InputNode, InputLink>,
21+
'height' | 'width'
22+
>)}
23+
ref={ref}
24+
/>
25+
)}
26+
</ResponsiveWrapper>
27+
)
28+
})

‎packages/network/stories/networkCanvas.stories.tsx

+20
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
NodeTooltipProps,
1010
// @ts-ignore
1111
} from '../src'
12+
import { useRef } from 'react'
1213

1314
export default {
1415
component: NetworkCanvas,
@@ -70,3 +71,22 @@ export const CustomNodeRenderer = () => (
7071
export const OnClickHandler = () => (
7172
<NetworkCanvas<Node, Link> {...commonProperties} onClick={action('onClick')} />
7273
)
74+
75+
export const CustomCanvasRef = () => {
76+
const ref = useRef(undefined)
77+
78+
const download = ref => {
79+
const canvas = ref.current
80+
const link = document.createElement('a')
81+
link.download = 'test.png'
82+
link.href = canvas.toDataURL('image/png')
83+
link.click()
84+
}
85+
86+
return (
87+
<>
88+
<NetworkCanvas<Node, Link> {...commonProperties} ref={ref} />
89+
<button onClick={() => download(ref)}>Download PNG</button>
90+
</>
91+
)
92+
}
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,28 @@
11
import { ResponsiveWrapper } from '@nivo/core'
2+
import { ForwardedRef, forwardRef } from 'react'
3+
24
import { ScatterPlotCanvas } from './ScatterPlotCanvas'
35
import { ScatterPlotCanvasProps, ScatterPlotDatum } from './types'
46

5-
export const ResponsiveScatterPlotCanvas = <RawDatum extends ScatterPlotDatum>(
6-
props: Omit<ScatterPlotCanvasProps<RawDatum>, 'width' | 'height'>
7-
) => (
8-
<ResponsiveWrapper>
9-
{({ width, height }) => (
10-
<ScatterPlotCanvas<RawDatum> width={width} height={height} {...props} />
11-
)}
12-
</ResponsiveWrapper>
13-
)
7+
export const ResponsiveScatterPlotCanvas = forwardRef(function ResponsiveScatterPlotCanvas<
8+
RawDatum extends ScatterPlotDatum
9+
>(
10+
props: Omit<ScatterPlotCanvasProps<RawDatum>, 'width' | 'height'>,
11+
ref: ForwardedRef<HTMLCanvasElement>
12+
) {
13+
return (
14+
<ResponsiveWrapper>
15+
{({ width, height }) => (
16+
<ScatterPlotCanvas
17+
width={width}
18+
height={height}
19+
{...(props as Omit<
20+
ScatterPlotCanvasProps<ScatterPlotDatum>,
21+
'height' | 'width'
22+
>)}
23+
ref={ref}
24+
/>
25+
)}
26+
</ResponsiveWrapper>
27+
)
28+
})

‎packages/scatterplot/src/ScatterPlotCanvas.tsx

+27-12
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
import { createElement, useRef, useState, useEffect, useCallback, useMemo } from 'react'
1+
import {
2+
ForwardedRef,
3+
createElement,
4+
forwardRef,
5+
useCallback,
6+
useEffect,
7+
useMemo,
8+
useRef,
9+
useState,
10+
} from 'react'
211
import { Container, useDimensions, useTheme, getRelativeCursor, isCursorInRect } from '@nivo/core'
312
import { renderAnnotationsToCanvas } from '@nivo/annotations'
413
import { CanvasAxisProps, renderAxesToCanvas, renderGridLinesToCanvas } from '@nivo/axes'
@@ -12,7 +21,9 @@ import { ScatterPlotCanvasProps, ScatterPlotDatum, ScatterPlotNodeData } from '.
1221
type InnerScatterPlotCanvasProps<RawDatum extends ScatterPlotDatum> = Omit<
1322
ScatterPlotCanvasProps<RawDatum>,
1423
'renderWrapper' | 'theme'
15-
>
24+
> & {
25+
canvasRef: ForwardedRef<HTMLCanvasElement>
26+
}
1627

1728
const InnerScatterPlotCanvas = <RawDatum extends ScatterPlotDatum>({
1829
data,
@@ -46,6 +57,7 @@ const InnerScatterPlotCanvas = <RawDatum extends ScatterPlotDatum>({
4657
onClick,
4758
tooltip = canvasDefaultProps.tooltip,
4859
legends = canvasDefaultProps.legends,
60+
canvasRef,
4961
}: InnerScatterPlotCanvasProps<RawDatum>) => {
5062
const canvasEl = useRef<HTMLCanvasElement | null>(null)
5163
const theme = useTheme()
@@ -270,7 +282,10 @@ const InnerScatterPlotCanvas = <RawDatum extends ScatterPlotDatum>({
270282

271283
return (
272284
<canvas
273-
ref={canvasEl}
285+
ref={canvas => {
286+
canvasEl.current = canvas
287+
if (canvasRef && 'current' in canvasRef) canvasRef.current = canvas
288+
}}
274289
width={outerWidth * pixelRatio}
275290
height={outerHeight * pixelRatio}
276291
style={{
@@ -286,13 +301,13 @@ const InnerScatterPlotCanvas = <RawDatum extends ScatterPlotDatum>({
286301
)
287302
}
288303

289-
export const ScatterPlotCanvas = <RawDatum extends ScatterPlotDatum>({
290-
isInteractive,
291-
renderWrapper,
292-
theme,
293-
...props
294-
}: ScatterPlotCanvasProps<RawDatum>) => (
295-
<Container {...{ isInteractive, renderWrapper, theme }} animate={false}>
296-
<InnerScatterPlotCanvas<RawDatum> {...props} />
297-
</Container>
304+
export const ScatterPlotCanvas = forwardRef(
305+
<RawDatum extends ScatterPlotDatum>(
306+
{ isInteractive, renderWrapper, theme, ...props }: ScatterPlotCanvasProps<RawDatum>,
307+
ref: ForwardedRef<HTMLCanvasElement>
308+
) => (
309+
<Container {...{ isInteractive, renderWrapper, theme }} animate={false}>
310+
<InnerScatterPlotCanvas<RawDatum> {...props} canvasRef={ref} />
311+
</Container>
312+
)
298313
)

‎packages/scatterplot/stories/ScatterPlotCanvas.stories.tsx

+41-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { useState, useCallback, useMemo } from 'react'
1+
import { useCallback, useMemo, useRef, useState } from 'react'
22
import omit from 'lodash/omit'
33
import { Meta } from '@storybook/react'
44
// @ts-ignore
5-
import { ScatterPlotCanvas, ResponsiveScatterPlotCanvas, ScatterPlotNodeData } from '../src'
5+
import { ResponsiveScatterPlotCanvas, ScatterPlotCanvas, ScatterPlotNodeData } from '../src'
66

77
export default {
88
component: ScatterPlotCanvas,
@@ -403,3 +403,42 @@ export const CustomTooltip = () => (
403403
)}
404404
/>
405405
)
406+
407+
export const CustomCanvasRef = () => {
408+
const ref = useRef(undefined)
409+
410+
const download = ref => {
411+
const canvas = ref.current
412+
const link = document.createElement('a')
413+
link.download = 'test.png'
414+
link.href = canvas.toDataURL('image/png')
415+
link.click()
416+
}
417+
418+
return (
419+
<>
420+
<ScatterPlotCanvas<SampleDatum>
421+
{...commonProps}
422+
ref={ref}
423+
tooltip={({ node }) => (
424+
<div
425+
style={{
426+
color: node.color,
427+
background: '#333',
428+
padding: '12px 16px',
429+
}}
430+
>
431+
<strong>
432+
{node.id} ({node.serieId})
433+
</strong>
434+
<br />
435+
{`x: ${node.formattedX}`}
436+
<br />
437+
{`y: ${node.formattedY}`}
438+
</div>
439+
)}
440+
/>
441+
<button onClick={() => download(ref)}>Download PNG</button>
442+
</>
443+
)
444+
}

0 commit comments

Comments
 (0)
Please sign in to comment.