-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
[charts-pro] Add range selection to zoom slider #17949
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] Add range selection to zoom slider #17949
Conversation
|
Thanks for adding a type label to the PR! 👍 |
| { axisId: 'x', start: 45, end: 55 }, | ||
| { axisId: 'x2', start: 30, end: 70 }, | ||
| { axisId: 'y', start: 10, end: 90 }, | ||
| { axisId: 'y', start: 40, end: 60 }, |
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.
Updated demo so it's easier to test the range selection
| event.stopPropagation(); | ||
|
|
||
| rect.setPointerCapture(event.pointerId); | ||
| document.addEventListener('pointerup', onPointerUp); |
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.
Couldn't make it work when pointerup listener was set on rect. The event wasn't being triggered in some cases.
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.
this is normal
|
Deploy preview: https://deploy-preview-17949--material-ui-x.netlify.app/ Bundle size reportTotal Size Change: @mui/x-charts-pro parsed: Show 32 more bundle changes@mui/x-charts-pro/ChartDataProviderPro parsed: |
| reverse: boolean; | ||
| } | ||
|
|
||
| function ChartAxisZoomSliderTrack({ |
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.
This file is getting huge. I'll move the components to other files in a follow-up.
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.
CodSpeed Performance ReportMerging #17949 will not alter performanceComparing Summary
|
bb1be02 to
4d3f704
Compare
4d3f704 to
e63476a
Compare
| theme.palette.mode === 'dark' | ||
| ? (theme.vars || theme).palette.grey[800] | ||
| : (theme.vars || theme).palette.grey[300], | ||
| cursor: 'crosshair', |
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.
A bit weird but ok 🤷
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.
Yeah, I don't love it either. I'm in contact with @noraleonte and @kenanyusuf to discuss what a better cursor would be.
We're having a hard time finding a better solution.
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.
There you go 😛
#17977
| const end = calculateZoomEnd( | ||
| pointerZoom, | ||
| { ...prevZoomData, start: pointerDownZoom }, | ||
| zoomOptions, | ||
| ); |
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.
Names are a bit confusing here, but I don't have suggestions 🤔
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've updated them. Do you think they're better now? If not, I can revert them
| event.stopPropagation(); | ||
|
|
||
| rect.setPointerCapture(event.pointerId); | ||
| document.addEventListener('pointerup', onPointerUp); |
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.
this is normal
| } | ||
|
|
||
| const pointerDownPoint = getSVGPoint(element, event); | ||
| let zoomFromPointerDown = calculateZoomFromPoint(store.getSnapshot(), axisId, pointerDownPoint); |
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.
maybe something like firstZoomPoint and currentZoomPoint for the move one?
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 initialZoomPoint
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'm not sure if firstZoomPoint is better because this isn't a point, it's the zoom value (ranging from 0 to 100) of the pointer down event. I could call it initialZoom, but not sure if that's less confusing 🤔
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'll merge this PR as it's been approved, but we can keep discussing and I'll update the code in a follow-up PR.
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.
My line of thought was that it is a point in the zoom "scale"
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.
Yeah, we can look at it that way. I'm afraid it might be confused with the SVGPoint that we're using in the same scope, that's why I initially tried suffixing it with Zoom
alexfauquette
left a comment
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.
Seems to be already merged 🙈
| } | ||
|
|
||
| const pointerDownPoint = getSVGPoint(element, event); | ||
| let zoomFromPointerDown = calculateZoomFromPoint(store.getSnapshot(), axisId, pointerDownPoint); |
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 initialZoomPoint
|
|
||
| let pointerMoved = false; | ||
|
|
||
| const onPointerMove = rafThrottle(function onPointerMove(pointerMoveEvent: PointerEvent) { |
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.
It's a bit weird to define the onPointerMove inside the onPointerDown
Does removing the pointerMove/pointerDown from document makes a difference in terms of performances?
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.
It's a bit weird to define the onPointerMove inside the onPointerDown
Why? I think it makes sense. We only need to listen to the pointer move events after a pointer down happens, so it makes sense that we only add the listener after a pointer down event.
We could add the event listener regardless, but then we're calling a function on every move just to do nothing. Seems a bit useless, IMO.
Does removing the pointerMove/pointerDown from document makes a difference in terms of performances?
It's probably negligible, I didn't test it.
| instance.setAxisZoomData(axisId, (prev) => ({ | ||
| ...prev, | ||
| start: zoomFromPointerDown, | ||
| end: zoomFromPointerDown, | ||
| })); |
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's the purpose of this setAxisZoomData?
The only effect is to clear the chart between pointer-down and pointer-move
On this topic, Echarts has a much better interaction. The pointer down and move only show the future range. And it's on pointer up that the zoom get applied. For this feature, they do not respect the min/max span.
Which seems better than having a jumping selection
Capture.video.du.2025-05-23.09-46-27.mp4
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's the purpose of this
setAxisZoomData?
It's a higher level function that setZoomData that allows setting the zoom data for a specific axis, instead of having to iterate over all axes to find the one you want and update it.
IMO, our setZoomData allows too much control. For example, you can disrepect a minStart or minSpan with it. I'm not sure we should allow users to do that.
On this topic, Echarts has a much better interaction. The pointer down and move only show the future range. And it's on pointer up that the zoom get applied.
We could do something like that as well. @kenanyusuf @noraleonte what's your take? Should I try to do something like this?
For this feature, they do not respect the min/max span.
Is that a good idea? What's the point of min/max span if you can disrepect it?
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.
Is that a good idea? What's the point of min/max span if you can disrepect it?
It's feasible to respect if with an interaction like: if the preview does not respect min/max span we ignore it, or have a different visualisation
Even if we dont not respect it with this interaction, it's usefull for the other interactions.
For me the minSpan is useful to avoid user zooming infinitely.
IMO, our setZoomData allows too much control. For example, you can disrepect a minStart or minSpan with it. I'm not sure we should allow users to do that.
It seems the setAxisZoomData does not respect minSpan since you can call it with start = end (which means span=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.
On this topic, Echarts has a much better interaction. The pointer down and move only show the future range. And it's on pointer up that the zoom get applied.
I don't think it is better though 😆
It feels a bit odd that the selection is only applied after pointer up
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.
It's feasible to respect if with an interaction like: if the preview does not respect min/max span we ignore it, or have a different visualisation
I'm ok with not respecting min/max span while dragging, but after pointer release we ensure those constraints are met.
Even if we dont not respect it with this interaction, it's usefull for the other interactions.
For me the minSpan is useful to avoid user zooming infinitely.
Yeah, but if the range selection doesn't respect the min/max span, then a user can zoom in too much.
Plus, I think it would be pretty unexpected to for the range selection to bypass min/max span. I think users wouldn't expect it.
It seems the
setAxisZoomDatadoes not respectminSpansince you can call it with start = end (which means span=0) 🙈
Yeah, at the moment we aren't respecting it. What I meant is that I think we should provide higher-level functions that respect it. E.g., instead of setAxisZoomData, you could have moveAxisZoomStart, moveAxisZoomEnd, moveAxisZoomRange, zoomIn, zoomOut, etc.
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 don't think it is better though 😆
It feels a bit odd that the selection is only applied after pointer up
UX-wise I think it's debatable, but for performance I think updating the chart only after pointer up would be much better.
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.
UX-wise, it's fairly similar to the brush zoom
like in rechart Applied on the overview instead of the drawing area
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 argue it is similar, but fairly different.
The brush zoom you are selecting part of your content. It makes sense for it to only apply the selection after the fact, since you would change the value being currently selected as you move the mouse.
The slider is a control, an abstraction over the zoom, which the user already implies has a meaning, and which in turn allows it to directly affect the content.
See that in the first case, the control is static, you are selecting the area you want to see. While in the second case, the control is dynamic, you are adapting the view to what you want to see.
Just like the gap example in funnel chart changes the gap instantly, so should the zoom slider change the zoom.
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 say that from a UX point of view. Due to performance reasons we can offer both if the case arises eventually. 😄

Part of #15383.
Adds a range selection to zoom slider. It respects all zoom options (e.g.,
minSpan,minStart).Screen.Recording.2025-05-21.at.16.48.10.mov
Some edge cases
Selection starts too close to the end and the cursor goes to the right, so the
minSpanof 10 wouldn't be respected. In this case, we need to move the start to the left.Screen.Recording.2025-05-21.at.16.49.29.mov
Selection starts outside
minStart. In this case, we move the start tominStartand adjust the end accordingly.Screen.Recording.2025-05-21.at.17.17.04.mov