Skip to content

Commit f7a45cd

Browse files
committed
wip
1 parent ea7af51 commit f7a45cd

7 files changed

Lines changed: 368 additions & 0 deletions

File tree

front/src/modules/simulationResult/components/SpaceTimeChartWrapper/SpaceTimeChartWrapper.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import {
2323
type Track,
2424
DEFAULT_ZOOM_MS_PER_PX,
2525
timeScaleToZoomValue,
26+
HourlyTimetableMarkers,
27+
SelectedPacedTrainInfoBars,
2628
} from '@osrd-project/ui-charts';
2729
import { Slider } from '@osrd-project/ui-core';
2830
import cx from 'classnames';
@@ -204,6 +206,12 @@ function formatDragOffset(ms: number): string {
204206
return `${sign} ${minutes} min`;
205207
}
206208

209+
// Format a duration as `Xh YYmin` (e.g. `1h 00min`).
210+
function formatHourlyDuration(duration: Duration): string {
211+
const totalMinutes = Math.round(duration.total('minute'));
212+
return `${Math.floor(totalMinutes / 60)}h ${(totalMinutes % 60).toString().padStart(2, '0')}min`;
213+
}
214+
207215
const SpaceTimeChartWrapper = ({
208216
operationalPoints,
209217
trainScheduleProjections,
@@ -668,6 +676,11 @@ const SpaceTimeChartWrapper = ({
668676
: undefined;
669677
const showCurvePanel = !!panelCounts;
670678

679+
const selectedPacedTrain =
680+
hourlyTimetableDuration && selectedTrain && isPacedTrainWithDetails(selectedTrain)
681+
? selectedTrain
682+
: undefined;
683+
671684
const handlePanelModeChange = (mode: PanelSelectionMode) => {
672685
setPanelSelectionMode(mode);
673686

@@ -929,6 +942,16 @@ const SpaceTimeChartWrapper = ({
929942
</>
930943
)}
931944
<TimeRangeObserver onChange={setChartTimeRange} />
945+
{hourlyTimetableDuration && (
946+
<HourlyTimetableMarkers duration={hourlyTimetableDuration.ms} />
947+
)}
948+
{selectedPacedTrain && hourlyTimetableDuration && (
949+
<SelectedPacedTrainInfoBars
950+
duration={hourlyTimetableDuration.ms}
951+
name={selectedPacedTrain.name}
952+
patternDurationLabel={formatHourlyDuration(hourlyTimetableDuration)}
953+
/>
954+
)}
932955
</SpaceTimeChart>
933956
{showCurvePanel && (
934957
<CurveSelectionSidePanel
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
.space-time-chart-container {
2+
overflow: hidden;
3+
}
4+
5+
.selected-paced-train-info {
6+
position: absolute;
7+
display: flex;
8+
align-items: center;
9+
gap: 8px;
10+
padding: 3px 8px 5px 8px;
11+
height: 24px;
12+
border: 1px solid rgba(89, 69, 37, 0.3);
13+
border-radius: 3px;
14+
background-color: rgba(238, 231, 217, 0.3);
15+
color: #000000;
16+
font-size: 14px;
17+
line-height: 16px;
18+
overflow: hidden;
19+
white-space: nowrap;
20+
pointer-events: none;
21+
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import React, { useContext, useMemo, useRef } from 'react';
2+
3+
import {
4+
type ChartPath,
5+
type Waypoint,
6+
HourlyTimetableMarkers,
7+
Manchette,
8+
PathLayer,
9+
SpaceTimeChart,
10+
SpaceTimeChartContext,
11+
useManchetteWithSpaceTimeChart,
12+
} from '@osrd-project/ui-charts';
13+
import type { Meta, StoryObj } from '@storybook/react-vite';
14+
15+
import '@osrd-project/ui-core/dist/theme.css';
16+
import '@osrd-project/ui-charts/dist/theme.css';
17+
18+
import { HOUR, MINUTE } from '../common/const';
19+
import { SAMPLE_CHART_PATHS, SAMPLE_WAYPOINTS } from './assets/sampleData';
20+
21+
import './hourly-timetable.scss';
22+
23+
const DEFAULT_HEIGHT = 561;
24+
const SELECTED_TRAIN_NAME = 'Mission name';
25+
26+
const formatDuration = (ms: number) => {
27+
const totalMinutes = Math.round(ms / MINUTE);
28+
return `${Math.floor(totalMinutes / 60)}h ${(totalMinutes % 60).toString().padStart(2, '0')}min`;
29+
};
30+
31+
type SelectedPacedTrainInfoBarsProps = {
32+
duration: number;
33+
origin?: number;
34+
name: string;
35+
timeWindowLabel: string;
36+
};
37+
38+
const SelectedPacedTrainInfoBars = ({
39+
duration,
40+
origin = 0,
41+
name,
42+
timeWindowLabel,
43+
}: SelectedPacedTrainInfoBarsProps) => {
44+
const { timeScale, timeOrigin, timePixelOffset, getTimePixel, width, swapAxis, captionSize } =
45+
useContext(SpaceTimeChartContext);
46+
47+
const bars = useMemo(() => {
48+
if (!duration || duration <= 0 || swapAxis) return [];
49+
const minT = timeOrigin - timeScale * timePixelOffset;
50+
const maxT = minT + timeScale * width;
51+
const firstIndex = Math.floor((minT - origin) / duration);
52+
const lastIndex = Math.ceil((maxT - origin) / duration);
53+
const result: { key: number; left: number; width: number }[] = [];
54+
for (let n = firstIndex; n < lastIndex; n++) {
55+
const left = getTimePixel(origin + n * duration);
56+
const w = getTimePixel(origin + (n + 1) * duration) - left;
57+
if (w > 0) result.push({ key: n, left, width: w });
58+
}
59+
return result;
60+
}, [duration, origin, timeScale, timeOrigin, timePixelOffset, getTimePixel, width, swapAxis]);
61+
62+
return (
63+
<>
64+
{bars.map(({ key, left, width: w }) => (
65+
<div
66+
key={key}
67+
className="selected-paced-train-info"
68+
style={{ left, width: w, bottom: captionSize + 17 }}
69+
>
70+
<span>{name}</span>
71+
<span>|</span>
72+
<span>{timeWindowLabel}</span>
73+
</div>
74+
))}
75+
</>
76+
);
77+
};
78+
79+
type WrapperProps = {
80+
waypoints: Waypoint[];
81+
paths: ChartPath[];
82+
selectedTrain: number;
83+
patternDurationHours: number;
84+
};
85+
86+
const Wrapper = ({ waypoints, paths, selectedTrain, patternDurationHours }: WrapperProps) => {
87+
const manchetteWithSpaceTimeChartRef = useRef<HTMLDivElement>(null);
88+
const patternDuration = patternDurationHours * HOUR;
89+
const origin = Math.min(...paths.map((p) => +p.points[0]?.time));
90+
91+
const { manchetteProps, spaceTimeChartProps, handleScroll } = useManchetteWithSpaceTimeChart({
92+
waypoints,
93+
manchetteWithSpaceTimeChartRef,
94+
defaultTimeOrigin: origin,
95+
});
96+
97+
const selectedPath = paths[selectedTrain]?.id;
98+
99+
return (
100+
<div className="ui-manchette-space-time-chart-wrapper">
101+
<div
102+
className="header bg-ambientB-5 w-full border-b border-grey-30"
103+
style={{ height: '40px' }}
104+
/>
105+
<div
106+
ref={manchetteWithSpaceTimeChartRef}
107+
className="manchette flex"
108+
style={{ height: `${DEFAULT_HEIGHT}px`, position: 'relative' }}
109+
onScroll={handleScroll}
110+
>
111+
<Manchette {...manchetteProps} />
112+
<div className="space-time-chart-container w-full sticky">
113+
<SpaceTimeChart className="inset-0 absolute h-full" {...spaceTimeChartProps}>
114+
{paths.map((path) => (
115+
<PathLayer
116+
key={path.id}
117+
path={path}
118+
color={path.color}
119+
level={path.id === selectedPath ? 1 : 2}
120+
/>
121+
))}
122+
<HourlyTimetableMarkers duration={patternDuration} origin={origin} />
123+
<SelectedPacedTrainInfoBars
124+
duration={patternDuration}
125+
origin={origin}
126+
name={SELECTED_TRAIN_NAME}
127+
timeWindowLabel={formatDuration(patternDuration)}
128+
/>
129+
</SpaceTimeChart>
130+
</div>
131+
</div>
132+
</div>
133+
);
134+
};
135+
136+
const meta: Meta<typeof Wrapper> = {
137+
title: 'Manchette with SpaceTimeChart/Hourly timetable',
138+
component: Wrapper,
139+
argTypes: {
140+
patternDurationHours: {
141+
name: 'Hourly pattern duration (h)',
142+
control: { type: 'range', min: 1, max: 6, step: 0.5 },
143+
},
144+
selectedTrain: {
145+
name: 'Selected train index',
146+
control: { type: 'number', min: 0, max: SAMPLE_CHART_PATHS.length - 1 },
147+
},
148+
},
149+
};
150+
151+
export default meta;
152+
153+
export const Default: StoryObj<typeof Wrapper> = {
154+
name: 'Hourly pattern selected',
155+
args: {
156+
waypoints: SAMPLE_WAYPOINTS,
157+
paths: SAMPLE_CHART_PATHS,
158+
selectedTrain: 1,
159+
patternDurationHours: 2,
160+
},
161+
};
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import React, { useContext, useMemo } from 'react';
2+
3+
import { SpaceTimeChartContext } from '../lib/context';
4+
5+
export type HourlyTimetableMarkersProps = {
6+
/** Length (in ms) of one hourly timetable pattern. */
7+
duration: number;
8+
/** Reference time (in ms) markers are anchored to. Defaults to `0`. */
9+
origin?: number;
10+
};
11+
12+
/**
13+
* Renders pin markers at the top and bottom of the SpaceTimeChart plot area at every
14+
* `origin + n * duration` visible in the current viewport.
15+
*/
16+
export const HourlyTimetableMarkers = ({ duration, origin = 0 }: HourlyTimetableMarkersProps) => {
17+
const { timeScale, timeOrigin, timePixelOffset, getTimePixel, width, swapAxis, captionSize } =
18+
useContext(SpaceTimeChartContext);
19+
20+
const positions = useMemo(() => {
21+
if (!duration || duration <= 0 || swapAxis) return [];
22+
const minT = timeOrigin - timeScale * timePixelOffset;
23+
const maxT = minT + timeScale * width;
24+
const firstIndex = Math.ceil((minT - origin) / duration);
25+
const lastIndex = Math.floor((maxT - origin) / duration);
26+
const result: number[] = [];
27+
for (let n = firstIndex; n <= lastIndex; n++) {
28+
result.push(getTimePixel(origin + n * duration));
29+
}
30+
return result;
31+
}, [duration, origin, timeScale, timeOrigin, timePixelOffset, getTimePixel, width, swapAxis]);
32+
33+
if (positions.length === 0) return null;
34+
35+
return (
36+
<>
37+
{positions.map((x, i) => (
38+
<div key={`top-${i}`} className="marker" style={{ left: x, top: 0 }} />
39+
))}
40+
{positions.map((x, i) => (
41+
<div
42+
key={`bot-${i}`}
43+
className="marker marker--flipped"
44+
style={{ left: x, bottom: captionSize }}
45+
/>
46+
))}
47+
</>
48+
);
49+
};
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import React, { useContext, useMemo } from 'react';
2+
3+
import { SpaceTimeChartContext } from '../lib/context';
4+
5+
export type SelectedPacedTrainInfoBarsProps = {
6+
/** Length (in ms) of one hourly timetable pattern. */
7+
duration: number;
8+
/** Reference time (in ms) at which the first bar starts. Defaults to `0`. */
9+
origin?: number;
10+
/** Name of the selected paced train. */
11+
name: string;
12+
/** Formatted label of the pattern duration (e.g. `6h 00min`). */
13+
patternDurationLabel: string;
14+
};
15+
16+
/**
17+
* Renders one info bar per hourly timetable pattern iteration visible in the viewport, each
18+
* spanning exactly the interval between two consecutive `HourlyTimetableMarkers`.
19+
*/
20+
export const SelectedPacedTrainInfoBars = ({
21+
duration,
22+
origin = 0,
23+
name,
24+
patternDurationLabel,
25+
}: SelectedPacedTrainInfoBarsProps) => {
26+
const { timeScale, timeOrigin, timePixelOffset, getTimePixel, width, swapAxis, captionSize } =
27+
useContext(SpaceTimeChartContext);
28+
29+
const bars = useMemo(() => {
30+
if (!duration || duration <= 0 || swapAxis) return [];
31+
const minT = timeOrigin - timeScale * timePixelOffset;
32+
const maxT = minT + timeScale * width;
33+
const firstIndex = Math.floor((minT - origin) / duration);
34+
const lastIndex = Math.ceil((maxT - origin) / duration);
35+
const result: { key: number; left: number; width: number }[] = [];
36+
for (let n = firstIndex; n < lastIndex; n++) {
37+
const left = getTimePixel(origin + n * duration);
38+
const w = getTimePixel(origin + (n + 1) * duration) - left;
39+
if (w > 0) result.push({ key: n, left, width: w });
40+
}
41+
return result;
42+
}, [duration, origin, timeScale, timeOrigin, timePixelOffset, getTimePixel, width, swapAxis]);
43+
44+
return (
45+
<>
46+
{bars.map(({ key, left, width: w }) => (
47+
<div
48+
key={key}
49+
className="selected-paced-train-info"
50+
style={{ left, width: w, bottom: captionSize + 17 }}
51+
>
52+
<span>{name}</span>
53+
<span>|</span>
54+
<span>{patternDurationLabel}</span>
55+
</div>
56+
))}
57+
</>
58+
);
59+
};

front/ui/ui-charts/src/spaceTimeChart/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ export * from './components/WorkScheduleLayer';
1010
export * from './components/PatternRect';
1111
export * from './components/Quadrilateral';
1212
export * from './components/ZoomRect';
13+
export * from './components/HourlyTimetableMarkers';
14+
export * from './components/SelectedPacedTrainInfoBars';
1315

1416
export { DEFAULT_THEME } from './lib/consts';
1517
export { SpaceTimeChartContext, SpaceTimeChartCanvasContext } from './lib/context';

0 commit comments

Comments
 (0)