Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/data/charts/zoom-and-pan/zoom-and-pan.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@

{{"demo": "ZoomSliderTooltip.js"}}

### Limits

The zoom slider uses the same limits as the zooming options. You can set the `minStart`, `maxEnd`, `minSpan`, and `maxSpan` properties on the axis config to restrict the zoom slider range.

Values outside the `minStart` and `maxEnd` range will not be displayed in the zoom slider.

Check warning on line 93 in docs/data/charts/zoom-and-pan/zoom-and-pan.md

View workflow job for this annotation

GitHub Actions / runner / vale

[vale] reported by reviewdog 🐶 [Google.Will] Avoid using 'will'. Raw Output: {"message": "[Google.Will] Avoid using 'will'.", "location": {"path": "docs/data/charts/zoom-and-pan/zoom-and-pan.md", "range": {"start": {"line": 93, "column": 50}}}, "severity": "WARNING"}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
Values outside the `minStart` and `maxEnd` range will not be displayed in the zoom slider.
The `minStart` and `maxEnd` define the extremum of the zoom slider.
Suggested change
Values outside the `minStart` and `maxEnd` range will not be displayed in the zoom slider.
The zoom slider does not display values outside the `minStart` and `maxEnd` range.


### Composition

When using composition, you can render the axes' sliders by rendering the `ChartZoomSlider` component.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
invertScale,
selectorChartAxis,
selectorChartAxisZoomOptionsLookup,
selectorChartDrawingArea,
useChartContext,
useDrawingArea,
useSelector,
Expand Down Expand Up @@ -81,9 +80,6 @@ export function ChartAxisZoomSliderActiveTrack({
return;
}

/* min and max values of zoom to ensure the pointer anchor in the slider is maintained */
let pointerZoomMin: number;
let pointerZoomMax: number;
let prevPointerZoom = 0;

const onPointerMove = rafThrottle((event: PointerEvent) => {
Expand All @@ -94,14 +90,12 @@ export function ChartAxisZoomSliderActiveTrack({
}

const point = getSVGPoint(element, event);
let pointerZoom = calculateZoomFromPoint(store.getSnapshot(), axisId, point);
const pointerZoom = calculateZoomFromPoint(store.getSnapshot(), axisId, point);

if (pointerZoom === null) {
return;
}

pointerZoom = Math.max(pointerZoomMin, Math.min(pointerZoomMax, pointerZoom));

const deltaZoom = pointerZoom - prevPointerZoom;
prevPointerZoom = pointerZoom;

Expand Down Expand Up @@ -133,8 +127,6 @@ export function ChartAxisZoomSliderActiveTrack({
}

prevPointerZoom = pointerDownZoom;
pointerZoomMin = pointerDownZoom - axisZoomData.start;
pointerZoomMax = 100 - (axisZoomData.end - pointerDownZoom);

document.addEventListener('pointerup', onPointerUp);
activePreviewRect.addEventListener('pointermove', onPointerMove);
Expand Down Expand Up @@ -190,21 +182,14 @@ export function ChartAxisZoomSliderActiveTrack({
const point = getSVGPoint(element, event);

instance.setZoomData((prevZoomData) => {
const { left, top, width, height } = selectorChartDrawingArea(store.getSnapshot());
const zoomOptions = selectorChartAxisZoomOptionsLookup(store.getSnapshot(), axisId);

return prevZoomData.map((zoom) => {
if (zoom.axisId === axisId) {
let newEnd: number;

if (axisDirection === 'x') {
newEnd = ((point.x - left) / width) * 100;
} else {
newEnd = ((top + height - point.y) / height) * 100;
}
const newEnd = calculateZoomFromPoint(store.getSnapshot(), axisId, point);

if (reverse) {
newEnd = 100 - newEnd;
if (newEnd === null) {
return zoom;
}

return {
Expand All @@ -227,15 +212,18 @@ export function ChartAxisZoomSliderActiveTrack({
let endThumbX: number;
let endThumbY: number;

const { minStart, maxEnd } = selectorChartAxisZoomOptionsLookup(store.getSnapshot(), axisId);
const range = maxEnd - minStart;

if (axisDirection === 'x') {
previewX = (zoomData.start / 100) * drawingArea.width;
previewX = ((zoomData.start - minStart) / range) * drawingArea.width;
previewY = 0;
previewWidth = (drawingArea.width * (zoomData.end - zoomData.start)) / 100;
previewWidth = (drawingArea.width * (zoomData.end - zoomData.start)) / range;
previewHeight = ZOOM_SLIDER_ACTIVE_TRACK_SIZE;

startThumbX = (zoomData.start / 100) * drawingArea.width;
startThumbX = ((zoomData.start - minStart) / range) * drawingArea.width;
startThumbY = 0;
endThumbX = (zoomData.end / 100) * drawingArea.width;
endThumbX = ((zoomData.end - minStart) / range) * drawingArea.width;
endThumbY = 0;

if (reverse) {
Expand All @@ -249,14 +237,14 @@ export function ChartAxisZoomSliderActiveTrack({
endThumbX -= previewThumbWidth / 2;
} else {
previewX = 0;
previewY = drawingArea.height - (zoomData.end / 100) * drawingArea.height;
previewY = drawingArea.height - ((zoomData.end - minStart) / range) * drawingArea.height;
previewWidth = ZOOM_SLIDER_ACTIVE_TRACK_SIZE;
previewHeight = (drawingArea.height * (zoomData.end - zoomData.start)) / 100;
previewHeight = (drawingArea.height * (zoomData.end - zoomData.start)) / range;

startThumbX = 0;
startThumbY = drawingArea.height - (zoomData.start / 100) * drawingArea.height;
startThumbY = drawingArea.height - ((zoomData.start - minStart) / range) * drawingArea.height;
endThumbX = 0;
endThumbY = drawingArea.height - (zoomData.end / 100) * drawingArea.height;
endThumbY = drawingArea.height - ((zoomData.end - minStart) / range) * drawingArea.height;

if (reverse) {
previewY = drawingArea.height - previewY - previewHeight;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,17 +67,16 @@ export function ChartAxisZoomSliderTrack({
}

const pointerDownPoint = getSVGPoint(element, event);
let zoomFromPointerDown = calculateZoomFromPoint(store.getSnapshot(), axisId, pointerDownPoint);
const zoomFromPointerDown = calculateZoomFromPoint(
store.getSnapshot(),
axisId,
pointerDownPoint,
);

if (zoomFromPointerDown === null) {
return;
}

const { minStart, maxEnd } = selectorChartAxisZoomOptionsLookup(store.getSnapshot(), axisId);

// Ensure the zoomFromPointerDown is within the min and max range
zoomFromPointerDown = Math.max(Math.min(zoomFromPointerDown, maxEnd), minStart);

const onPointerMove = rafThrottle(function onPointerMove(pointerMoveEvent: PointerEvent) {
const pointerMovePoint = getSVGPoint(element, pointerMoveEvent);
const zoomFromPointerMove = calculateZoomFromPoint(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect } from 'chai';
import { calculateZoomEnd, calculateZoomStart } from './zoom-utils';
import { calculateZoomEnd, calculateZoomStart, calculateZoomFromPointImpl } from './zoom-utils';
import { ZoomData } from '../../models';

describe('Zoom Utils', () => {
Expand Down Expand Up @@ -82,4 +82,70 @@ describe('Zoom Utils', () => {
expect(result).to.eq(15);
});
});

describe('calculateZoomFromPointImpl', () => {
const defaultDrawingArea = {
left: 100,
top: 100,
right: 300,
bottom: 200,
width: 200,
height: 100,
};

it('should calculate correct zoom value for x-axis (bottom position)', () => {
const result = calculateZoomFromPointImpl(
defaultDrawingArea,
{ position: 'bottom', reverse: false },
{ minStart: 0, maxEnd: 100 },
{ x: 200, y: 0 },
);

expect(result).to.eq(50);
});

it('should calculate correct zoom value for y-axis (left position)', () => {
const result = calculateZoomFromPointImpl(
defaultDrawingArea,
{ position: 'left', reverse: false },
{ minStart: 0, maxEnd: 100 },
{ x: 0, y: 100 },
);

expect(result).to.eq(100);
});

it('should handle reversed x-axis', () => {
const result = calculateZoomFromPointImpl(
defaultDrawingArea,
{ position: 'bottom', reverse: true },
{ minStart: 0, maxEnd: 100 },
{ x: 300, y: 0 },
);

expect(result).to.eq(0); // Should be at the start due to reverse
});

it('should handle reversed y-axis', () => {
const result = calculateZoomFromPointImpl(
defaultDrawingArea,
{ position: 'left', reverse: true },
{ minStart: 0, maxEnd: 100 },
{ x: 0, y: 180 },
);

expect(result).to.eq(80);
});

it('should handle custom min/max range', () => {
const result = calculateZoomFromPointImpl(
defaultDrawingArea,
{ position: 'bottom', reverse: false },
{ minStart: 20, maxEnd: 80 },
{ x: 150, y: 0 },
);

expect(result).to.eq(35);
});
});
});
Original file line number Diff line number Diff line change
@@ -1,31 +1,54 @@
import {
AxisId,
ChartState,
DefaultedXAxis,
DefaultedYAxis,
DefaultizedZoomOptions,
selectorChartAxisZoomOptionsLookup,
selectorChartDrawingArea,
selectorChartRawAxis,
ZoomData,
} from '@mui/x-charts/internals';
import { ChartDrawingArea } from '@mui/x-charts/hooks';

export function calculateZoomFromPoint(state: ChartState<any>, axisId: AxisId, point: DOMPoint) {
const { left, top, height, width } = selectorChartDrawingArea(state);
const axis = selectorChartRawAxis(state, axisId);

if (!axis) {
return null;
}

return calculateZoomFromPointImpl(
selectorChartDrawingArea(state),
axis,
selectorChartAxisZoomOptionsLookup(state, axisId),
point,
);
}

export function calculateZoomFromPointImpl(
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This separation makes it easier to unit test.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Did you also consider creating calculatePointFromZoomImpl() to simplify the computation in ChartAxisZoomSliderActiveTrack.tsx and have the conversion logic zoom <-> SVG in a single place

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I don't think I understood your question. calculatePointFromZoom, which uses calculatePointFromZoomImpl is already being used in ChartAxisZoomSliderActiveTrack. Where exactly do you think it would make sense to add it?

drawingArea: ChartDrawingArea,
axis: Pick<DefaultedXAxis | DefaultedYAxis, 'position' | 'reverse'>,
zoomOptions: Pick<DefaultizedZoomOptions, 'minStart' | 'maxEnd'>,
point: Pick<DOMPoint, 'x' | 'y'>,
) {
const { left, top, height, width } = drawingArea;
const { minStart, maxEnd } = zoomOptions;

const axisDirection = axis.position === 'right' || axis.position === 'left' ? 'y' : 'x';
const range = maxEnd - minStart;

let pointerZoom: number;
if (axisDirection === 'x') {
pointerZoom = ((point.x - left) / width) * 100;
pointerZoom = ((point.x - left) / width) * range;
} else {
pointerZoom = ((top + height - point.y) / height) * 100;
pointerZoom = ((top + height - point.y) / height) * range;
}

if (axis.reverse) {
pointerZoom = 100 - pointerZoom;
pointerZoom = maxEnd - pointerZoom;
} else {
pointerZoom += minStart;
}

return pointerZoom;
Expand Down
Loading