Skip to content

Commit 91c47d5

Browse files
committed
fix: better scroll/anchor perf, default intersectionObserve
1 parent f35946f commit 91c47d5

File tree

7 files changed

+2041
-1935
lines changed

7 files changed

+2041
-1935
lines changed

src/components/Chart.tsx

Lines changed: 97 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import React, { ComponentPropsWithoutRef } from 'react'
55

66
import useGetLatest from '../hooks/useGetLatest'
77
import useIsomorphicLayoutEffect from '../hooks/useIsomorphicLayoutEffect'
8-
import useRect from '../hooks/useRect'
98
import Area from '../seriesTypes/Area'
109
import Bar from '../seriesTypes/Bar'
1110
import Line from '../seriesTypes/Line'
@@ -35,7 +34,6 @@ import AxisLinear from './AxisLinear'
3534
import Cursors from './Cursors'
3635
import Tooltip from './Tooltip'
3736
import Voronoi from './Voronoi'
38-
import useIsScrolling from '../hooks/useIsScrolling'
3937

4038
//
4139

@@ -74,6 +72,9 @@ function defaultChartOptions<TDatum>(
7472
groupingMode: options.groupingMode ?? 'primary',
7573
showVoronoi: options.showVoronoi ?? false,
7674
defaultColors: options.defaultColors ?? defaultColorScheme,
75+
useIntersectionObserver: options.useIntersectionObserver ?? true,
76+
intersectionObserverRootMargin:
77+
options.intersectionObserverRootMargin ?? '1000px',
7778
}
7879
}
7980

@@ -84,28 +85,107 @@ export function Chart<TDatum>({
8485
...rest
8586
}: ComponentPropsWithoutRef<'div'> & { options: ChartOptions<TDatum> }) {
8687
const options = defaultChartOptions(userOptions)
87-
const [containerElement, setContainerElement] =
88+
const [chartElement, setContainerElement] =
8889
React.useState<HTMLDivElement | null>(null)
89-
const parentElement = containerElement?.parentElement
9090

91-
const isScrolling = useIsScrolling(200)
91+
const containerEl = chartElement?.parentElement
9292

93-
const { width, height } = useRect(parentElement, {
94-
enabled: !isScrolling,
95-
initialWidth: options.initialWidth,
96-
initialHeight: options.initialHeight,
97-
dimsOnly: true,
93+
const nearestScrollableParent = React.useMemo(() => {
94+
const run = (el?: Element | null): Element | null => {
95+
if (!el) {
96+
return null
97+
}
98+
99+
const grandParent = el.parentElement
100+
101+
if (!grandParent) {
102+
return null
103+
}
104+
105+
if (grandParent.scrollHeight > grandParent.clientHeight) {
106+
const { overflow } = window.getComputedStyle(grandParent)
107+
108+
if (overflow.includes('scroll') || overflow.includes('auto')) {
109+
return grandParent
110+
}
111+
}
112+
113+
return run(grandParent)
114+
}
115+
116+
return run(containerEl)
117+
}, [containerEl])
118+
119+
const [{ width, height }, setDims] = React.useState({
120+
width: options.initialWidth,
121+
height: options.initialHeight,
98122
})
99123

100124
useIsomorphicLayoutEffect(() => {
101-
if (parentElement) {
102-
const computed = window.getComputedStyle(parentElement)
125+
if (containerEl) {
126+
const computed = window.getComputedStyle(containerEl)
103127

104128
if (!['relative', 'absolute', 'fixed'].includes(computed.position)) {
105-
parentElement.style.position = 'relative'
129+
containerEl.style.position = 'relative'
106130
}
107131
}
108-
}, [parentElement])
132+
}, [containerEl])
133+
134+
React.useEffect(() => {
135+
if (!containerEl) {
136+
return
137+
}
138+
139+
const observer = new ResizeObserver(() => {
140+
const rect = containerEl?.getBoundingClientRect()
141+
142+
if (rect) {
143+
setDims({
144+
width: rect.width,
145+
height: rect.height,
146+
})
147+
}
148+
})
149+
150+
observer.observe(containerEl)
151+
152+
return () => {
153+
observer.unobserve(containerEl)
154+
}
155+
}, [containerEl])
156+
157+
const [isIntersecting, setIsIntersecting] = React.useState(true)
158+
159+
React.useEffect(() => {
160+
if (!containerEl || !options.useIntersectionObserver) return
161+
162+
let observer = new IntersectionObserver(
163+
entries => {
164+
for (let entry of entries) {
165+
if (entry.isIntersecting) {
166+
setIsIntersecting(true)
167+
} else {
168+
setIsIntersecting(false)
169+
}
170+
}
171+
},
172+
{
173+
root: nearestScrollableParent,
174+
rootMargin: options.intersectionObserverRootMargin,
175+
}
176+
)
177+
178+
observer.observe(containerEl)
179+
180+
return () => {
181+
observer.unobserve(containerEl)
182+
}
183+
}, [
184+
containerEl,
185+
nearestScrollableParent,
186+
options.intersectionObserverRootMargin,
187+
options.useIntersectionObserver,
188+
])
109189

110190
return (
111191
<div
@@ -119,7 +199,9 @@ export function Chart<TDatum>({
119199
height,
120200
}}
121201
>
122-
<ChartInner options={options} {...{ width, height }} />
202+
{isIntersecting ? (
203+
<ChartInner options={options} {...{ width, height }} />
204+
) : null}
123205
</div>
124206
)
125207
}

src/components/Cursors.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -199,9 +199,7 @@ function Cursor<TDatum>(props: {
199199

200200
const formattedValue = (axis as AxisTime<any>).formatters.cursor(latestValue)
201201

202-
const svgRect = useRect(svgRef.current, {
203-
enabled: show,
204-
})
202+
const svgRect = useRect(svgRef.current, show)
205203

206204
const immediatePos = !axis.isVertical ? lineStartX : lineStartY
207205
const immediate = usePrevious(immediatePos) === -1 && immediatePos > -1

src/components/Tooltip.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ import ReactDOM from 'react-dom'
44
import { useSpring, animated } from '@react-spring/web'
55

66
import { useAnchor } from '../hooks/useAnchor'
7-
// import useIsScrolling from '../hooks/useIsScrolling'
87
import useLatestWhen from '../hooks/useLatestWhen'
98
import usePortalElement from '../hooks/usePortalElement'
109
import usePrevious from '../hooks/usePrevious'
1110
import { Datum, ResolvedTooltipOptions, TooltipOptions } from '../types'
1211
//
1312
import useChartContext from '../utils/chartContext'
1413
import TooltipRenderer from './TooltipRenderer'
14+
import useRect from '../hooks/useRect'
1515

1616
//
1717

@@ -45,7 +45,7 @@ export default function Tooltip<TDatum>(): React.ReactPortal | null {
4545
primaryAxis,
4646
secondaryAxes,
4747
getDatumStatusStyle,
48-
// getSeriesStatusStyle,
48+
svgRef,
4949
} = useChartContext<TDatum>()
5050

5151
const [focusedDatum] = useFocusedDatumAtom()
@@ -69,6 +69,8 @@ export default function Tooltip<TDatum>(): React.ReactPortal | null {
6969

7070
const [tooltipEl, setTooltipEl] = React.useState<HTMLDivElement | null>()
7171

72+
const svgRect = useRect(svgRef.current, !!focusedDatum?.element)
73+
7274
const anchorEl = React.useMemo(() => {
7375
const anchorRect =
7476
latestFocusedDatum?.element?.getBoundingClientRect() ?? null
@@ -77,6 +79,8 @@ export default function Tooltip<TDatum>(): React.ReactPortal | null {
7779
return null
7880
}
7981

82+
if (!svgRect) return
83+
8084
const translateX = anchorRect.left ?? 0
8185
const translateY = anchorRect.top ?? 0
8286
const width = anchorRect.width ?? 0
@@ -101,9 +105,7 @@ export default function Tooltip<TDatum>(): React.ReactPortal | null {
101105
return box
102106
},
103107
}
104-
}, [latestFocusedDatum?.element])
105-
106-
// const isScrolling = useIsScrolling(200)
108+
}, [latestFocusedDatum?.element, svgRect])
107109

108110
const anchor = useAnchor({
109111
show: !!focusedDatum,

src/hooks/useAnchor.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,9 @@ export function useAnchor(options: {
8080
anchorEl: HasBoundingClientRect | null | undefined
8181
tooltipEl: HasBoundingClientRect | null | undefined
8282
}) {
83-
const portalDims = useRect(options.portalEl, { enabled: options.show })
84-
const anchorDims = useRect(options.anchorEl, { enabled: options.show })
85-
const tooltipDims = useRect(options.tooltipEl, {
86-
enabled: options.show,
87-
})
83+
const portalDims = useRect(options.portalEl, options.show)
84+
const anchorDims = useRect(options.anchorEl, options.show)
85+
const tooltipDims = useRect(options.tooltipEl, options.show)
8886

8987
const sides = React.useMemo(() => {
9088
const preSides = Array.isArray(options.side) ? options.side : [options.side]

src/hooks/useRect.ts

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,13 @@ export type HasBoundingClientRect = {
1010

1111
export default function useRect(
1212
node: HasBoundingClientRect | null | undefined,
13-
options: {
14-
enabled: boolean
15-
initialWidth?: number
16-
initialHeight?: number
17-
dimsOnly?: boolean
18-
}
13+
enabled: boolean
1914
) {
2015
const [element, setElement] = React.useState(node)
2116

2217
let [rect, setRect] = React.useState<DOMRect>({
23-
width: options.initialWidth ?? 0,
24-
height: options.initialHeight ?? 0,
18+
width: 0,
19+
height: 0,
2520
} as DOMRect)
2621

2722
const rectRef = React.useRef(rect)
@@ -37,36 +32,27 @@ export default function useRect(
3732
const initialRectSet = React.useRef(false)
3833

3934
useIsomorphicLayoutEffect(() => {
40-
if (options.enabled && element && !initialRectSet.current) {
35+
if (enabled && element && !initialRectSet.current) {
4136
initialRectSet.current = true
4237
setRect(element.getBoundingClientRect())
4338
}
44-
}, [element, options.enabled])
39+
}, [element, enabled])
4540

4641
React.useEffect(() => {
47-
if (!element || !options.enabled) {
42+
if (!element || !enabled) {
4843
return
4944
}
5045

5146
const observer = observeRect(element as Element, (newRect: DOMRect) => {
52-
if (options.dimsOnly) {
53-
if (
54-
rectRef.current.width !== newRect.width ||
55-
rectRef.current.height !== newRect.height
56-
) {
57-
setRect(newRect)
58-
}
59-
} else {
60-
setRect(newRect)
61-
}
47+
setRect(newRect)
6248
})
6349

6450
observer.observe()
6551

6652
return () => {
6753
observer.unobserve()
6854
}
69-
}, [element, options.dimsOnly, options.enabled])
55+
}, [element, enabled])
7056

7157
// const resolvedRect = React.useMemo(() => {
7258
// if (!element || !(element as Element).tagName) {

src/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ export type ChartOptions<TDatum> = {
4444
primaryCursor?: boolean | CursorOptions
4545
secondaryCursor?: boolean | CursorOptions
4646
tooltip?: boolean | TooltipOptions<TDatum>
47+
useIntersectionObserver?: boolean
48+
intersectionObserverRootMargin?:
49+
| `${number}px`
50+
| `${number}px ${number}px`
51+
| `${number}px ${number}px ${number}px ${number}px`
4752
}
4853

4954
export type RequiredChartOptions<TDatum> = TSTB.Object.Required<
@@ -56,6 +61,8 @@ export type RequiredChartOptions<TDatum> = TSTB.Object.Required<
5661
| 'defaultColors'
5762
| 'initialWidth'
5863
| 'initialHeight'
64+
| 'useIntersectionObserver'
65+
| 'intersectionObserverThreshold'
5966
>
6067

6168
export type ChartContextValue<TDatum> = {

0 commit comments

Comments
 (0)