Skip to content
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

[charts-pro] Zoom pointer improvements #17000

Open
wants to merge 52 commits into
base: master
Choose a base branch
from

Conversation

JCQuintas
Copy link
Member

@JCQuintas JCQuintas commented Mar 17, 2025

Main change

Use use-gesture to recognise the gestures. Right now the lib is a bit slow to have updates, but it has some solid behaviour detection.

Create a interaction listener plugin. This allows us to register events directly in the SVG.

The current implementation migrates most of the interactions to the schema below.

Screenshot 2025-03-19 at 22 40 10

Other changes

  • Add zoom and pan tests for BarChartPro, LineChartPro, and ScatterChartPro
    • Currently we can't properly test the pinch interaction. It seems RTL and use-gesture can't communicate properly there.
    • I've tried manual events as well (fireEvent/element.dispatchEvent), but couldn't figure out a way to make it work.
  • Divided the zoom behaviours into hooks implementing the usePanOnDrag, useZoomOnPinch and useZoomOnWheel hooks

@JCQuintas JCQuintas marked this pull request as ready for review March 19, 2025 21:41
Copy link
Member

@bernardobelchior bernardobelchior left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new implementation seems to simplify the logic a lot! Great job!

I still don't understand some parts of the interaction listener hook, so left a few questions

Comment on lines +108 to +119
const addMultipleInteractionListeners: AddMultipleInteractionListeners = React.useCallback(
(interactions, callback) => {
const cleanups = interactions.map((interaction) =>
// @ts-expect-error Overriding the type because the type of the callback is not inferred
addInteractionListener(interaction, callback),
);
return {
cleanup: () => cleanups.forEach((cleanup) => cleanup.cleanup()),
};
},
[addInteractionListener],
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this would work, but probably worth a shot:

Suggested change
const addMultipleInteractionListeners: AddMultipleInteractionListeners = React.useCallback(
(interactions, callback) => {
const cleanups = interactions.map((interaction) =>
// @ts-expect-error Overriding the type because the type of the callback is not inferred
addInteractionListener(interaction, callback),
);
return {
cleanup: () => cleanups.forEach((cleanup) => cleanup.cleanup()),
};
},
[addInteractionListener],
);
const addMultipleInteractionListeners = React.useCallback<AddMultipleInteractionListeners>(
(interactions, callback) => {
const cleanups = interactions.map((interaction) =>
addInteractionListener(interaction, callback),
);
return {
cleanup: () => cleanups.forEach((cleanup) => cleanup.cleanup()),
};
},
[addInteractionListener],
);

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That resolves to the same type, so it doesn't work.

@bernardobelchior
Copy link
Member

bernardobelchior commented Mar 20, 2025

Did a small test on iOS Safari and it seems to me that zooming in is too slow. Not in the sense of performance, but we might need to make it zoom a bit faster.

In desktop it's working perfectly 👍

@JCQuintas
Copy link
Member Author

Did a small test on iOS Safari and it seems to me that zooming in is too slow. Not in the sense of performance, but we might need to make it zoom a bit faster.

In desktop it's working perfectly 👍

Thanks for checking, will debug

Copy link
Member

@alexfauquette alexfauquette left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have a bug with controlled zoom. The dragging get accelerated.

I could not figure why by just reading the code

Capture.video.du.2025-03-20.10-54-05.mp4

Comment on lines +70 to +74
// scroll, we scroll exactly in the center of the svg
// And we do it 200 times which is the lowest number to trigger a zoom where both A and D are not visible
for (let i = 0; i < 200; i += 1) {
fireEvent.wheel(svg, { deltaY: -1, clientX: 50, clientY: 50 });
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's surprising that it can not be done with delatY: -200

Comment on lines +81 to +84
// scroll back
for (let i = 0; i < 200; i += 1) {
fireEvent.wheel(svg, { deltaY: 1, clientX: 50, clientY: 50 });
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If as you said 200 is the limit to only see B and C, then just a few scroll back should be enough

Comment on lines +32 to +36
const options = {
wrapper: ({ children }: { children?: React.ReactNode }) => (
<div style={{ width: 100, height: 130 }}>{children}</div>
),
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the advantage of the wrapper compared to just providing the width/height to the chart props?

Comment on lines 24 to 26
// Drag event is triggered by mobile touch or mouse drag.
const positionOnDragHandler = instance.addMultipleInteractionListeners(
['move', 'drag'],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like a hack to have the use-gesture whereas the pointermove event is exactly what we are looking for

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case it is probably better to use pointerMove indeed

Comment on lines +125 to +126
cleanInteractionHandler.cleanup();
setInteractionHandler.cleanup();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Effectively cleaner 👍

@@ -155,13 +158,15 @@ export const useChartCartesianAxis: ChartPlugin<UseChartCartesianAxisSignature<a
return () => {};
}

const handleMouseClick = (event: MouseEvent) => {
event.preventDefault();
const axisClickHandler = instance.addInteractionListener('drag', (state) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same feeling about forcing the usage of use-gesture events for stuff they are not planned for

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drag is meant to detect tap/click as well.

@github-actions github-actions bot added the PR: out-of-date The pull request has merge conflicts and can't be merged label Mar 21, 2025
Copy link

This pull request has conflicts, please resolve those before we can evaluate the pull request.

@JCQuintas JCQuintas force-pushed the pointer-events-improvement branch from 76ab4f5 to 0edea79 Compare March 21, 2025 14:03
Comment on lines 29 to 31
xAxis={[
{
zoom: true,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you'r lookig for what triggers the re-render. I'ts probably the xAxis

@github-actions github-actions bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged label Mar 25, 2025
@github-actions github-actions bot added the PR: out-of-date The pull request has merge conflicts and can't be merged label Mar 28, 2025
Copy link

This pull request has conflicts, please resolve those before we can evaluate the pull request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
component: charts This is the name of the generic UI component, not the React module! enhancement This is not a bug, nor a new feature PR: out-of-date The pull request has merge conflicts and can't be merged
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants