Skip to content
This repository was archived by the owner on Aug 5, 2025. It is now read-only.

Commit f672fd7

Browse files
committed
ui-spacetimechart: add conflict tooltip
Signed-off-by: Simon Ser <[email protected]>
1 parent 0d8c3ad commit f672fd7

File tree

4 files changed

+153
-26
lines changed

4 files changed

+153
-26
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import React from 'react';
2+
3+
import type { Point } from '../lib/types';
4+
5+
export type ConflictTooltipProps = {
6+
position: Point;
7+
time: number;
8+
9+
spaceStart: number;
10+
spaceEnd: number;
11+
timeStart: number;
12+
timeEnd: number;
13+
type: string;
14+
trains: string[];
15+
};
16+
17+
const formatDistance = (meters: number) => {
18+
const km = meters / 1000;
19+
return km.toLocaleString('en-US', { minimumFractionDigits: 1, maximumFractionDigits: 1 });
20+
};
21+
22+
export const ConflictTooltip = ({
23+
position,
24+
time,
25+
spaceStart,
26+
spaceEnd,
27+
timeStart,
28+
timeEnd,
29+
type,
30+
trains,
31+
}: ConflictTooltipProps) => (
32+
<div className="spacetimechart-tooltip" style={{ left: position.x, top: position.y }}>
33+
<div className="time">{new Date(time).toLocaleTimeString()}</div>
34+
<div className="position-range">
35+
<div className="start-position">{formatDistance(spaceStart)}</div>
36+
<div className="end-position">{formatDistance(spaceEnd)}</div>
37+
</div>
38+
<div className="type-and-duration">
39+
<div>{type}</div>
40+
<div>{Math.round((timeEnd - timeStart) / 1000)}s</div>
41+
</div>
42+
<div className="trains">{trains.join(', ')}</div>
43+
</div>
44+
);

ui-spacetimechart/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import './styles/main.css';
33
export * from './components/SpaceTimeChart';
44
export * from './components/PathLayer';
55
export * from './components/ConflictLayer';
6+
export * from './components/ConflictTooltip';
67
export * from './components/OccupancyBlockLayer';
78
export * from './components/WorkScheduleLayer';
89
export * from './components/PatternRect';

ui-spacetimechart/src/stories/layers.stories.tsx

Lines changed: 63 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1-
import React from 'react';
1+
import React, { useState } from 'react';
22

33
import type { Meta, StoryObj } from '@storybook/react';
44

5-
import { ConflictLayer, OccupancyBlockLayer, SpaceTimeChart, PathLayer } from '../';
5+
import {
6+
type Conflict,
7+
ConflictLayer,
8+
ConflictTooltip,
9+
OccupancyBlockLayer,
10+
SpaceTimeChart,
11+
PathLayer,
12+
} from '../';
613
import { OPERATIONAL_POINTS, PATHS, START_DATE } from './lib/paths';
714
import { X_ZOOM_LEVEL, Y_ZOOM_LEVEL } from './lib/utils';
815
import {
@@ -12,6 +19,7 @@ import {
1219
OCCUPANCY_SEMAPHORE,
1320
OCCUPANCY_WARNING,
1421
} from '../lib/consts';
22+
import type { Point } from '../lib/types';
1523

1624
import '@osrd-project/ui-spacetimechart/dist/theme.css';
1725

@@ -36,6 +44,15 @@ const CONFLICTS = [
3644
},
3745
];
3846

47+
const CONFLICT_GROUP = {
48+
spaceStart: 12 * KILOMETER,
49+
spaceEnd: 41 * KILOMETER,
50+
timeStart: +START_DATE + 15 * MINUTE,
51+
timeEnd: +START_DATE + 37 * MINUTE,
52+
type: 'Spacing',
53+
trains: ['4655', '6079'],
54+
};
55+
3956
const OCCUPANCY_BLOCKS = [
4057
{
4158
timeStart: +START_DATE + 42 * MINUTE,
@@ -84,30 +101,50 @@ const OCCUPANCY_BLOCKS = [
84101
/**
85102
* This story aims at showcasing various additional layers.
86103
*/
87-
const Wrapper = () => (
88-
<div className="inset-0">
89-
<SpaceTimeChart
90-
className="inset-0 absolute overflow-hidden p-0 m-0"
91-
operationalPoints={OPERATIONAL_POINTS}
92-
spaceOrigin={0}
93-
spaceScales={OPERATIONAL_POINTS.slice(0, -1).map((point, i) => ({
94-
from: point.position,
95-
to: OPERATIONAL_POINTS[i + 1].position,
96-
size: 50 * Y_ZOOM_LEVEL,
97-
}))}
98-
timeOrigin={+new Date('2024/04/02')}
99-
timeScale={60000 / X_ZOOM_LEVEL}
100-
xOffset={0}
101-
yOffset={0}
102-
>
103-
{PATHS.map((path) => (
104-
<PathLayer key={path.id} path={path} color={path.color} />
105-
))}
106-
<ConflictLayer conflicts={CONFLICTS} />
107-
<OccupancyBlockLayer occupancyBlocks={OCCUPANCY_BLOCKS} />
108-
</SpaceTimeChart>
109-
</div>
110-
);
104+
const Wrapper = () => {
105+
const [hoveredConflict, setHoveredConflict] = useState<Conflict | null>(null);
106+
const [cursorPosition, setCursorPosition] = useState<Point | null>(null);
107+
const [cursorTime, setCursorTime] = useState<number>(0);
108+
109+
return (
110+
<div className="inset-0">
111+
<SpaceTimeChart
112+
className="inset-0 absolute overflow-hidden p-0 m-0"
113+
operationalPoints={OPERATIONAL_POINTS}
114+
spaceOrigin={0}
115+
spaceScales={OPERATIONAL_POINTS.slice(0, -1).map((point, i) => ({
116+
from: point.position,
117+
to: OPERATIONAL_POINTS[i + 1].position,
118+
size: 50 * Y_ZOOM_LEVEL,
119+
}))}
120+
timeOrigin={+new Date('2024/04/02')}
121+
timeScale={60000 / X_ZOOM_LEVEL}
122+
xOffset={0}
123+
yOffset={0}
124+
onHoveredChildUpdate={({ item }) => {
125+
let conflict = null;
126+
if (item?.element?.type === 'conflict') {
127+
conflict = CONFLICTS[item.element.conflictIndex];
128+
}
129+
setHoveredConflict(conflict);
130+
}}
131+
onMouseMove={({ position, data }) => {
132+
setCursorPosition(position);
133+
setCursorTime(data.time);
134+
}}
135+
>
136+
{PATHS.map((path) => (
137+
<PathLayer key={path.id} path={path} color={path.color} />
138+
))}
139+
<ConflictLayer conflicts={CONFLICTS} />
140+
<OccupancyBlockLayer occupancyBlocks={OCCUPANCY_BLOCKS} />
141+
{hoveredConflict && cursorPosition && (
142+
<ConflictTooltip {...CONFLICT_GROUP} position={cursorPosition} time={cursorTime} />
143+
)}
144+
</SpaceTimeChart>
145+
</div>
146+
);
147+
};
111148

112149
const meta = {
113150
title: 'SpaceTimeChart/Layers',
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,48 @@
11
@import 'tailwindcss/base';
22
@import 'tailwindcss/components';
33
@import 'tailwindcss/utilities';
4+
5+
.spacetimechart-tooltip {
6+
position: absolute;
7+
background-color: rgba(0, 0, 0, 0.85);
8+
box-shadow: 0 4px 6px -2px rgba(0, 0, 0, 0.28);
9+
color: white;
10+
font-size: 0.875rem;
11+
line-height: 20px;
12+
border-radius: 10px;
13+
padding: 6px 12px 7px 12px;
14+
margin: 12px;
15+
width: 213px;
16+
17+
.time {
18+
text-shadow: 0 0 6px rgba(27, 255, 122, 1);
19+
color: rgba(27, 255, 122, 1);
20+
text-align: center;
21+
margin-bottom: 3px;
22+
}
23+
24+
.position-range, .type-and-duration, .trains {
25+
font-weight: 600;
26+
}
27+
28+
.position-range {
29+
display: flex;
30+
justify-content: space-between;
31+
font-size: 0.75rem;
32+
line-height: 16px;
33+
margin-bottom: 15px;
34+
}
35+
36+
.type-and-duration {
37+
display: flex;
38+
justify-content: space-between;
39+
background-color: theme('colors.error.60');
40+
padding: 1px 6px 3px 6px;
41+
margin-bottom: 2px;
42+
}
43+
44+
.trains {
45+
overflow: hidden;
46+
text-overflow: ellipsis;
47+
}
48+
}

0 commit comments

Comments
 (0)