Skip to content

Commit 6c23aee

Browse files
committed
ui-space-time: zoom behavior mouse + slider
Signed-off-by: Valentin Chanas <[email protected]>
1 parent 7b6384d commit 6c23aee

File tree

1 file changed

+157
-0
lines changed

1 file changed

+157
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import React, { useEffect, useState } from 'react';
2+
3+
import { Slider } from '@osrd-project/ui-core';
4+
import type { Meta } from '@storybook/react';
5+
6+
import '@osrd-project/ui-core/dist/theme.css';
7+
8+
import { OPERATIONAL_POINTS, PATHS } from './lib/paths';
9+
import { PathLayer } from '../components/PathLayer';
10+
import { SpaceTimeChart } from '../components/SpaceTimeChart';
11+
import { type Point, type PathData, type OperationalPoint } from '../lib/types';
12+
import { getDiff } from '../utils/vectors';
13+
14+
const DEFAULT_WIDTH = 1200;
15+
const DEFAULT_HEIGHT = 550;
16+
17+
const MIN_ZOOM = 0;
18+
const MAX_ZOOM = 100;
19+
const MIN_ZOOM_MS_PER_PX = 600000;
20+
const MAX_ZOOM_MS_PER_PX = 625;
21+
type SpaceTimeHorizontalZoomWrapperProps = {
22+
offset: number;
23+
operationalPoints: OperationalPoint[];
24+
paths: (PathData & { color: string })[];
25+
};
26+
27+
const sliderToTimeScale = (slider: number) =>
28+
MIN_ZOOM_MS_PER_PX * Math.pow(MAX_ZOOM_MS_PER_PX / MIN_ZOOM_MS_PER_PX, Number(slider) / 100);
29+
30+
const SpaceTimeHorizontalZoomWrapper = ({
31+
offset,
32+
operationalPoints = [],
33+
paths = [],
34+
}: SpaceTimeHorizontalZoomWrapperProps) => {
35+
const [state, setState] = useState<{
36+
zoomValue: number;
37+
xOffset: number;
38+
yOffset: number;
39+
panning: null | { initialOffset: Point };
40+
}>({
41+
zoomValue: 36.18, // 7500 ms/px
42+
xOffset: offset, // px
43+
yOffset: 0,
44+
panning: null,
45+
});
46+
useEffect(() => {
47+
setState((prev) => ({ ...prev, xOffset: offset }));
48+
}, [offset]);
49+
const handleZoom = (zoomValue: number, position = DEFAULT_WIDTH / 2) => {
50+
if (MIN_ZOOM <= zoomValue && zoomValue <= MAX_ZOOM) {
51+
const oldTimeScale = sliderToTimeScale(state.zoomValue);
52+
const newTimeScale = sliderToTimeScale(zoomValue);
53+
// by default zooming expands the graph around timeOrigin
54+
// changing the offset to timeOriginShift alone keeps the left border in place.
55+
// subtracting centerShift keeps the center in place
56+
// (xOffset = how many px the timeOrigin is shifted to the right)
57+
const timeOriginShift = (state.xOffset * oldTimeScale) / newTimeScale;
58+
const centerShift = (position * oldTimeScale - position * newTimeScale) / newTimeScale;
59+
const newOffset = timeOriginShift - centerShift;
60+
setState((prev) => ({ ...prev, zoomValue, xOffset: newOffset }));
61+
}
62+
};
63+
const simpleOperationalPoints = operationalPoints.map(({ id, position }) => ({
64+
id,
65+
label: id,
66+
position,
67+
}));
68+
const spaceScale = [
69+
{
70+
from: 0,
71+
to: 75000,
72+
coefficient: 300,
73+
},
74+
];
75+
return (
76+
<div
77+
className="space-time-horizontal-zoom-wrapper"
78+
style={{
79+
height: `${DEFAULT_HEIGHT}px`,
80+
width: `${DEFAULT_WIDTH}px`,
81+
}}
82+
>
83+
<SpaceTimeChart
84+
className="inset-0 absolute h-full"
85+
spaceOrigin={0}
86+
xOffset={state.xOffset}
87+
yOffset={state.yOffset}
88+
timeOrigin={+new Date('2024/04/02')}
89+
operationalPoints={simpleOperationalPoints}
90+
timeScale={sliderToTimeScale(state.zoomValue)}
91+
spaceScales={spaceScale}
92+
onZoom={({ delta, position: { x } }) => {
93+
handleZoom(state.zoomValue + delta, x);
94+
}}
95+
onPan={({ initialPosition, position, isPanning }) => {
96+
const diff = getDiff(initialPosition, position);
97+
setState((s) => {
98+
// Stop panning:
99+
if (!isPanning) {
100+
return { ...s, panning: null };
101+
}
102+
// Start panning:
103+
else if (!s.panning) {
104+
return {
105+
...s,
106+
panning: {
107+
initialOffset: {
108+
x: s.xOffset,
109+
y: s.yOffset,
110+
},
111+
},
112+
};
113+
}
114+
// Keep panning:
115+
else {
116+
const { initialOffset } = s.panning;
117+
return {
118+
...s,
119+
xOffset: initialOffset.x + diff.x,
120+
yOffset: initialOffset.y + diff.y,
121+
};
122+
}
123+
});
124+
}}
125+
>
126+
{paths.map((path) => (
127+
<PathLayer key={path.id} path={path} color={path.color} />
128+
))}
129+
</SpaceTimeChart>
130+
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
131+
<Slider
132+
min={0}
133+
max={100}
134+
value={state.zoomValue}
135+
onChange={(e) => {
136+
handleZoom(Number(e.target.value));
137+
}}
138+
/>
139+
<div>offset: {state.xOffset}</div>
140+
<div>timescale: {sliderToTimeScale(state.zoomValue)} ms/px</div>
141+
</div>
142+
</div>
143+
);
144+
};
145+
146+
export default {
147+
title: 'SpaceTimeChart/Horiontal Zoom',
148+
component: SpaceTimeHorizontalZoomWrapper,
149+
} as Meta<typeof SpaceTimeHorizontalZoomWrapper>;
150+
151+
export const Default = {
152+
args: {
153+
offset: 0,
154+
operationalPoints: OPERATIONAL_POINTS,
155+
paths: PATHS.slice(2, 4),
156+
},
157+
};

0 commit comments

Comments
 (0)