Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Feature/add zoom and pan support improved brush (vertical) and minimap#5934

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Open
Fefedu973 wants to merge31 commits intorecharts:main
base:main
Choose a base branch
Loading
fromFefedu973:feature/add-zoom-and-pan-support-improved-brush-(vertical)-and-minimap
Open
Show file tree
Hide file tree
Changes from1 commit
Commits
Show all changes
31 commits
Select commitHold shift + click to select a range
a5bd320
Improve zoom container
Fefedu973Jun 5, 2025
8dbe03d
Add zoom state slice and update zoom behavior
Fefedu973Jun 5, 2025
8db2762
Add diverse zoom/pan storybook examples
Fefedu973Jun 5, 2025
5a59f48
Improve zoom behavior
Fefedu973Jun 5, 2025
c2bb459
Fix zoom overlay position and story data
Fefedu973Jun 5, 2025
5f6bc16
Fix X-axis zoom on categorical scales
Fefedu973Jun 5, 2025
f69b69e
Improve zoom and touch support
Fefedu973Jun 5, 2025
e61ccde
Refine zoom behavior and examples
Fefedu973Jun 5, 2025
e0288d1
Fix zoom container and examples
Fefedu973Jun 5, 2025
0245fc0
feat(zoom): add disableAnimation flag and fix clipping
Fefedu973Jun 5, 2025
fc1623f
Fix zoom scale application and tick filtering
Fefedu973Jun 5, 2025
3aa38a9
Clip chart content in zoom container
Fefedu973Jun 5, 2025
b545fe4
Fix zoom animation flag and update examples
Fefedu973Jun 5, 2025
d6bf75f
Fix tooltip scale and disable zoom animations
Fefedu973Jun 5, 2025
cddf8c0
fix zoom animations and tooltip layering
Fefedu973Jun 5, 2025
7c4769b
Fix zoom animation toggle
Fefedu973Jun 5, 2025
b9017f6
Improve zoom container features
Fefedu973Jun 5, 2025
8bee435
Clamp zoomed scale output
Fefedu973Jun 5, 2025
d4cb761
Add drag-to-zoom and brush integration
Fefedu973Jun 5, 2025
f7f5654
Update applyZoomToScale.ts
Fefedu973Jun 5, 2025
c42d3e2
feat(zoom): add follow line option
Fefedu973Jun 5, 2025
d776757
Improve zoom follow line and examples
Fefedu973Jun 5, 2025
1a4ac02
Fix zoom offset and smooth brush
Fefedu973Jun 6, 2025
2141b23
Update Brush.tsx
Fefedu973Jun 6, 2025
5bb4a62
Update applyZoomToScale.ts
Fefedu973Jun 6, 2025
0d5c825
feat(brush): emit incremental updates
Fefedu973Jun 6, 2025
2561896
fix: correct touch typing and lint
Fefedu973Jun 6, 2025
1745ed6
Update zoomPanContext.tsx
Fefedu973Jun 6, 2025
e760360
update
Fefedu973Jun 9, 2025
183d9ff
Merge branch 'codex/ajouter-support-zoom-et-pan-sur-graphiques-cartes…
Fefedu973Jun 9, 2025
1ca0f59
final-update
Fefedu973Jun 9, 2025
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
PrevPrevious commit
NextNext commit
Improve zoom and touch support
  • Loading branch information
@Fefedu973
Fefedu973 committedJun 5, 2025
commitf69b69ef8edf2234bbaf35a92c25e35c9a0f9bd9
115 changes: 111 additions & 4 deletionssrc/context/zoomPanContext.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,6 +25,7 @@ export function ZoomPanContainer({ children, config }: { children: ReactNode; co
const { mode = 'x', minScale = 1, maxScale = 20, onZoomChange } = config;
const { width, height, left, top } = useOffset();
const overlayRef = useRef<SVGRectElement>(null);
const pointerSupported = typeof window !== 'undefined' && 'PointerEvent' in window;

const getLocalCoords = useCallback((e: { clientX: number; clientY: number }) => {
const rect = overlayRef.current?.getBoundingClientRect();
Expand All@@ -45,6 +46,7 @@ export function ZoomPanContainer({ children, config }: { children: ReactNode; co
centerX: number;
centerY: number;
} | null>(null);
const lastTap = useRef<number>(0);

const update = useCallback(
(next: ZoomState) => {
Expand DownExpand Up@@ -113,6 +115,34 @@ export function ZoomPanContainer({ children, config }: { children: ReactNode; co
[state.scaleX, state.scaleY, state.offsetX, state.offsetY, getLocalCoords],
);

const handleTouchStart = useCallback(
(e: React.TouchEvent<SVGGElement>) => {
if (pointerSupported) return;
Array.from(e.changedTouches).forEach(t => {
const local = getLocalCoords({ clientX: t.clientX, clientY: t.clientY });
pointers.current.set(t.identifier, local);
});
if (pointers.current.size === 1) {
const t = e.changedTouches[0];
const local = getLocalCoords({ clientX: t.clientX, clientY: t.clientY });
dragStart.current = { x: local.x, y: local.y };
}
if (pointers.current.size === 2) {
const [a, b] = Array.from(pointers.current.values());
pinchStart.current = {
distance: Math.hypot(a.x - b.x, a.y - b.y),
scaleX: state.scaleX,
scaleY: state.scaleY,
offsetX: state.offsetX,
offsetY: state.offsetY,
centerX: (a.x + b.x) / 2 - state.offsetX,
centerY: (a.y + b.y) / 2 - state.offsetY,
};
}
},
[pointerSupported, state.scaleX, state.scaleY, state.offsetX, state.offsetY, getLocalCoords],
);

const handlePointerMove = useCallback(
(e: React.PointerEvent<SVGGElement>) => {
const prev = pointers.current.get(e.pointerId);
Expand DownExpand Up@@ -166,17 +196,91 @@ export function ZoomPanContainer({ children, config }: { children: ReactNode; co
[state, update, minScale, maxScale, mode, getLocalCoords],
);

const handleTouchMove = useCallback(
(e: React.TouchEvent<SVGGElement>) => {
if (pointerSupported) return;
Array.from(e.changedTouches).forEach(t => {
const prev = pointers.current.get(t.identifier);
if (!prev) return;
const local = getLocalCoords({ clientX: t.clientX, clientY: t.clientY });
pointers.current.set(t.identifier, local);
});

if (pointers.current.size === 2 && pinchStart.current) {
const [a, b] = Array.from(pointers.current.values());
const dist = Math.hypot(a.x - b.x, a.y - b.y);
const ratio = dist / pinchStart.current.distance;

const next = { ...state };

if (mode === 'y') {
const newScaleY = clamp(pinchStart.current.scaleY * ratio, minScale, maxScale);
const rY = newScaleY / pinchStart.current.scaleY;
next.scaleY = newScaleY;
next.offsetY = pinchStart.current.offsetY - pinchStart.current.centerY * (rY - 1);
} else if (mode === 'xy') {
const newScale = clamp(pinchStart.current.scaleX * ratio, minScale, maxScale);
const r = newScale / pinchStart.current.scaleX;
next.scaleX = newScale;
next.scaleY = newScale;
next.offsetX = pinchStart.current.offsetX - pinchStart.current.centerX * (r - 1);
next.offsetY = pinchStart.current.offsetY - pinchStart.current.centerY * (r - 1);
} else {
const newScaleX = clamp(pinchStart.current.scaleX * ratio, minScale, maxScale);
const rX = newScaleX / pinchStart.current.scaleX;
next.scaleX = newScaleX;
next.offsetX = pinchStart.current.offsetX - pinchStart.current.centerX * (rX - 1);
}

update(next);
return;
}

if (dragStart.current) {
const t = e.changedTouches[0];
const local = getLocalCoords({ clientX: t.clientX, clientY: t.clientY });
const dx = local.x - dragStart.current.x;
const dy = local.y - dragStart.current.y;
dragStart.current = { x: local.x, y: local.y };
const next = {
...state,
offsetX: state.offsetX + dx,
offsetY: state.offsetY + dy,
};
update(next);
}
},
[pointerSupported, state, update, minScale, maxScale, mode, getLocalCoords],
);

const handleDoubleClick = useCallback(() => {
update({ scaleX: 1, scaleY: 1, offsetX: 0, offsetY: 0 });
}, [update]);

const handleTouchEnd = useCallback(
(e: React.TouchEvent<SVGGElement>) => {
if (pointerSupported) return;
Array.from(e.changedTouches).forEach(t => {
pointers.current.delete(t.identifier);
});
if (pointers.current.size < 2) pinchStart.current = null;
if (pointers.current.size === 0) dragStart.current = null;
const now = Date.now();
if (now - lastTap.current < 300) {
handleDoubleClick();
}
lastTap.current = now;
},
[pointerSupported, handleDoubleClick],
);

const handlePointerUp = useCallback((e?: React.PointerEvent<SVGGElement>) => {
if (e) (e.target as Element).releasePointerCapture?.(e.pointerId);
pointers.current.delete(e?.pointerId ?? 0);
if (pointers.current.size < 2) pinchStart.current = null;
if (pointers.current.size === 0) dragStart.current = null;
}, []);

const handleDoubleClick = useCallback(() => {
update({ scaleX: 1, scaleY: 1, offsetX: 0, offsetY: 0 });
}, [update]);

return (
<g
clipPath={clipPathId ? `url(#${clipPathId})` : undefined}
Expand All@@ -195,6 +299,9 @@ export function ZoomPanContainer({ children, config }: { children: ReactNode; co
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
onPointerLeave={handlePointerUp}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
onDoubleClick={handleDoubleClick}
/>
{children}
Expand Down
123 changes: 72 additions & 51 deletionssrc/state/selectors/axisSelectors.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -1025,12 +1025,17 @@ function applyZoomToScale(
return (scale as any).copy().domain([domainStart, domainEnd]);
}
if (typeof (scale as any).bandwidth === 'function') {
const domain = (scale as any).domain() as ReadonlyArray<any>;
const step = (scale as any).step ? (scale as any).step() : (scale as any).bandwidth();
const startIndex = Math.max(0, Math.floor((r0 - offset) / (step * zoomScale)));
const endIndex = Math.min(domain.length, Math.ceil((r1 - offset) / (step * zoomScale)));
const newDomain = domain.slice(startIndex, endIndex);
return (scale as any).copy().domain(newDomain).range([r0, r1]);
const base: any = scale;
const zoomed = (value: unknown) => base(value) * zoomScale + offset;
zoomed.domain = base.domain;
zoomed.range = () => [r0, r1];
if (typeof base.bandwidth === 'function') {
zoomed.bandwidth = () => base.bandwidth() * zoomScale;
}
if (typeof base.step === 'function') {
zoomed.step = () => base.step() * zoomScale;
}
return zoomed as RechartsScale;
}
return scale;
}
Expand DownExpand Up@@ -1727,39 +1732,49 @@ export const combineAxisTicks = (
};
});

return result.filter((row: TickItem) => !isNan(row.coordinate));
const [rStart, rEnd] = axisRange ?? [Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY];
return result.filter(
(row: TickItem) =>
!isNan(row.coordinate) && row.coordinate >= Math.min(rStart, rEnd) && row.coordinate <= Math.max(rStart, rEnd),
);
}

// When axis is a categorical axis, but the type of axis is number or the scale of axis is not "auto"
if (isCategorical && categoricalDomain) {
return categoricalDomain.map(
(entry: any, index: number): TickItem => ({
coordinate: scale(entry) + offset,
value: entry,
index,
offset,
}),
);
const [rStart, rEnd] = axisRange ?? [Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY];
return categoricalDomain
.map(
(entry: any, index: number): TickItem => ({
coordinate: scale(entry) + offset,
value: entry,
index,
offset,
}),
)
.filter(row => row.coordinate >= Math.min(rStart, rEnd) && row.coordinate <= Math.max(rStart, rEnd));
}

if (scale.ticks) {
return (
scale
.ticks(tickCount)
// @ts-expect-error why does the offset go here? The type does not require it
.map((entry: any): TickItem => ({ coordinate: scale(entry) + offset, value: entry, offset }))
);
const [rStart, rEnd] = axisRange ?? [Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY];
return scale
.ticks(tickCount)
.map((entry: any): TickItem => ({ coordinate: scale(entry) + offset, value: entry, offset }))
.filter(row => row.coordinate >= Math.min(rStart, rEnd) && row.coordinate <= Math.max(rStart, rEnd));
}

// When axis has duplicated text, serial numbers are used to generate scale
return scale.domain().map(
(entry: any, index: number): TickItem => ({
coordinate: scale(entry) + offset,
value: duplicateDomain ? duplicateDomain[entry] : entry,
index,
offset,
}),
);
const [rStart, rEnd] = axisRange ?? [Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY];
return scale
.domain()
.map(
(entry: any, index: number): TickItem => ({
coordinate: scale(entry) + offset,
value: duplicateDomain ? duplicateDomain[entry] : entry,
index,
offset,
}),
)
.filter(row => row.coordinate >= Math.min(rStart, rEnd) && row.coordinate <= Math.max(rStart, rEnd));
};
export const selectTicksOfAxis: (
state: RechartsRootState,
Expand DownExpand Up@@ -1804,34 +1819,40 @@ export const combineGraphicalItemTicks = (

// When axis is a categorical axis, but the type of axis is number or the scale of axis is not "auto"
if (isCategorical && categoricalDomain) {
return categoricalDomain.map(
(entry: any, index: number): TickItem => ({
coordinate: scale(entry) + offset,
value: entry,
index,
offset,
}),
);
const [rStart, rEnd] = axisRange ?? [Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY];
return categoricalDomain
.map(
(entry: any, index: number): TickItem => ({
coordinate: scale(entry) + offset,
value: entry,
index,
offset,
}),
)
.filter(row => row.coordinate >= Math.min(rStart, rEnd) && row.coordinate <= Math.max(rStart, rEnd));
}

if (scale.ticks) {
return (
scale
.ticks(tickCount)
// @ts-expect-error why does the offset go here? The type does not require it
.map((entry: any): TickItem => ({ coordinate: scale(entry) + offset, value: entry, offset }))
);
const [rStart, rEnd] = axisRange ?? [Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY];
return scale
.ticks(tickCount)
.map((entry: any): TickItem => ({ coordinate: scale(entry) + offset, value: entry, offset }))
.filter(row => row.coordinate >= Math.min(rStart, rEnd) && row.coordinate <= Math.max(rStart, rEnd));
}

// When axis has duplicated text, serial numbers are used to generate scale
return scale.domain().map(
(entry: any, index: number): TickItem => ({
coordinate: scale(entry) + offset,
value: duplicateDomain ? duplicateDomain[entry] : entry,
index,
offset,
}),
);
const [rStart, rEnd] = axisRange ?? [Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY];
return scale
.domain()
.map(
(entry: any, index: number): TickItem => ({
coordinate: scale(entry) + offset,
value: duplicateDomain ? duplicateDomain[entry] : entry,
index,
offset,
}),
)
.filter(row => row.coordinate >= Math.min(rStart, rEnd) && row.coordinate <= Math.max(rStart, rEnd));
};

export const selectTicksOfGraphicalItem: (
Expand Down

[8]ページ先頭

©2009-2025 Movatter.jp