-
Notifications
You must be signed in to change notification settings - Fork 12
feat: support debouncing of the chart core tooltip #129
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: support debouncing of the chart core tooltip #129
Conversation
src/core/components/core-tooltip.tsx
Outdated
| const [expandedSeries, setExpandedSeries] = useState<ExpandedSeriesState>({}); | ||
| const tooltip = useSelector(api.tooltipStore, (s) => s); | ||
| if (!tooltip.visible || tooltip.group.length === 0) { | ||
| const [debouncedTooltip, setDebouncedTooltip] = useState<ReactiveTooltipState | null>(null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we e.g. move the code that subscribes to the tooltip data and does debouncing into a small utility hook above that component so to encapsulate the debouncing logic inside?
329a5ba to
c903809
Compare
src/core/interfaces.ts
Outdated
| enabled?: boolean; | ||
| placement?: "middle" | "outside" | "target"; | ||
| size?: "small" | "medium" | "large"; | ||
| renderDebounceDuration?: number; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
idea: I would rename this prop to just "debounce" as the "render" and "duration" parts can kind of be implied here. Additionally, I would consider making it then a union number | boolean. That is because while we cant add the debounce interval by default, ideally we still want the debounce interval to be consistent. This can be then achieved by using debounce: true to let the internal implementation use the internal constant for it.
| import { ChartAPI } from "../chart-api"; | ||
| import { ReactiveTooltipState } from "../chart-api/chart-extra-tooltip"; | ||
|
|
||
| export function useDebouncedTooltip(api: ChartAPI, renderDebounceDuration: number = 500) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we pass tooltip as a prop here instead of the api? In that case we can probably also make the argument type generic:
export function useDebounce<T>(value: T, debounceDuration = DEFAULT_DEBOUNCE_DURATION): null | T {
// ...
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thats a great suggestion, I'll make it generic and reuseable
| export function useDebouncedTooltip(api: ChartAPI, renderDebounceDuration: number = 500) { | ||
| const tooltip = useSelector(api.tooltipStore, (s) => s); | ||
| const [debouncedTooltip, setDebouncedTooltip] = useState<ReactiveTooltipState | null>(null); | ||
| const [shouldRender, setShouldRender] = useState(renderDebounceDuration <= 0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need this to be a state? Do we expect the debounce duration to change during components lifetime?
src/core/components/core-tooltip.tsx
Outdated
| if (!tooltip.visible || tooltip.group.length === 0) { | ||
| const debouncedTooltip = useDebouncedTooltip(api, renderDebounceDuration); | ||
|
|
||
| if (!debouncedTooltip?.visible || debouncedTooltip?.group?.length === 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we change that to !debouncedTooltip || !debouncedTooltip?.visible || debouncedTooltip?.group?.length === 0? This should allow unnecessary conditions around the debouncedTooltip var below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or can those be already removed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They need to stay with the !debouncedTooltip check at the front, this makes sure that if it's not debounced yet it doesn't render, and then if it is debounced it keeps the same behaviour
src/core/components/core-tooltip.tsx
Outdated
| const [expandedSeries, setExpandedSeries] = useState<ExpandedSeriesState>({}); | ||
| const tooltip = useSelector(api.tooltipStore, (s) => s); | ||
| if (!tooltip.visible || tooltip.group.length === 0) { | ||
| const debouncedTooltip = useDebouncedTooltip(api, renderDebounceDuration); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the point of defaulting the renderDebounceDuration to 500 inside the useDebouncedTooltip, when it defaults to 0 in this function anyways?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left it from when I was debugging. Nice catch!
| }); | ||
|
|
||
| expect(getTooltipContentMock).toHaveBeenCalledTimes(2); | ||
| expect(getTooltipContentMock).toHaveBeenCalledTimes(1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why this change?
| }); | ||
| }); | ||
|
|
||
| describe("debounce functionality", () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very good tests 👍
| }} | ||
| /> | ||
|
|
||
| <h2>Debounced Tooltip (500ms)</h2> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a chart here so that debounce can be tested on a page with it enabled and disabled
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that this is a page for visual regression tests. I would prefer not having anything there that is not meant for visual testing and might add noise and/or flakiness. Since this is a core-only feature, what about having a separate small page in pages/03-core?
src/internal/utils/use-debounce.tsx
Outdated
|
|
||
| import { DebouncedCall } from "./utils"; | ||
|
|
||
| export function useDebounce<T>(component: T, duration: number) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would suggest calling this function something like useDebouncedComponent or useDebouncedRender, since it is specific to rendering a component and not other types of calls
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggested (#129 (comment)) calling that useDebouncedValue because it can technically use any value as input, the component arg name is also misleading in that regard.
| }} | ||
| /> | ||
|
|
||
| <h2>Debounced Tooltip (500ms)</h2> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that this is a page for visual regression tests. I would prefer not having anything there that is not meant for visual testing and might add noise and/or flakiness. Since this is a core-only feature, what about having a separate small page in pages/03-core?
src/core/components/core-tooltip.tsx
Outdated
| const [expandedSeries, setExpandedSeries] = useState<ExpandedSeriesState>({}); | ||
| const tooltip = useSelector(api.tooltipStore, (s) => s); | ||
| if (!tooltip.visible || tooltip.group.length === 0) { | ||
| const debouncedTooltip = useDebouncedComponent(tooltip, debounce === true ? DEFAULT_DEBOUNCE : debounce || 0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The debouncedTooltip is not a component but a state. Should we rename the util to useDebouncedValue to avoid confusion? Likewise, inside the util we can have const [debouncedValue, setDebouncedValue] = useState<T | null>(null);
805cc3c
Description
Tooltip is being rendered on each hover which is slowing down the chart significantly.
By adding an option to debounce by using
renderDebounceDurationwe can stop it from being rendered each hover, but instead render it once the user stops moving their mouse, after the delay passed.Covered by unit tests and manual testing.
Backwards compatible by making the prop fully optional
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.