Skip to content
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 our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

front: insert div in manchette with space time chart #960

Draft
wants to merge 2 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,7 @@ const meta: Meta<typeof Manchette> = {
export default meta;
type Story = StoryObj<typeof Manchette>;

const customDiv = (
<div style={{ height: 'auto', minHeight: 24, backgroundColor: 'rgba(152, 192, 245, 1)' }}>
Hello World
</div>
);
const customDiv = <div style={{ height: '100px', backgroundColor: '#EFF3F5' }} />;

export const Default: Story = {
args: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import React, { useRef } from 'react';
import React, { useCallback, useMemo, useRef, useState } from 'react';

import { PathLayer, SpaceTimeChart, type SpaceTimeChartProps } from '../../../spaceTimeChart';
import {
PathLayer,
SpaceTimeChart,
isInteractiveWaypoint,
type SpaceTimeChartProps,
} from '../../../spaceTimeChart';
import Manchette, {
type ProjectPathTrainResult,
type Waypoint,
type ManchetteProps,
type InteractiveWaypoint,
} from '../../Manchette';
import useManchetteWithSpaceTimeChart from '../hooks/useManchetteWithSpaceTimeChart';
import { clamp, keyBy } from 'lodash';
import { DrawingFunction } from 'ui-charts/src/spaceTimeChart/lib/types';
import { useDraw } from 'ui-charts/src/spaceTimeChart/hooks/useCanvas';

export type ManchetteWithSpaceTimeChartProps = {
waypoints: Waypoint[];
contents: (InteractiveWaypoint | React.ReactNode)[];
projectPathTrainResult: ProjectPathTrainResult[];
selectedTrain?: number;
height?: number;
Expand All @@ -19,14 +27,42 @@ export type ManchetteWithSpaceTimeChartProps = {
spaceTimeChartProps?: SpaceTimeChartProps;
};

/**
* This component renders a colored area where the line only has one track:
*/
const FlatStep = ({ position }: { position: number }) => {
const drawMonoTrackSpace = useCallback<DrawingFunction>(
(ctx, { getSpacePixel, width, height, spaceAxis }) => {
const spaceSize = spaceAxis === 'x' ? width : height;
const timeSize = spaceAxis === 'x' ? height : width;
const fromPixel = clamp(getSpacePixel(position), 0, spaceSize);
const toPixel = clamp(getSpacePixel(position, true), 0, spaceSize);
const monoLineSize = toPixel - fromPixel;
if (!monoLineSize) return;

ctx.fillStyle = '#EFF3F5';
if (spaceAxis === 'x') {
ctx.fillRect(fromPixel, 0, monoLineSize, timeSize);
} else {
ctx.fillRect(0, fromPixel, timeSize, monoLineSize);
}
},
[position]
);

useDraw('overlay', drawMonoTrackSpace);

return null;
};

/**
* A simple component to display a manchette and a space time chart.
*
* This only covers basic usage. For more advanced control over the manchette
* and space time chart, the useManchetteWithSpaceTimeChart() hook can be used.
*/
const ManchetteWithSpaceTimeChart = ({
waypoints,
contents,
projectPathTrainResult,
selectedTrain,
height = 561,
Expand All @@ -37,16 +73,47 @@ const ManchetteWithSpaceTimeChart = ({
}: ManchetteWithSpaceTimeChartProps) => {
const manchetteWithSpaceTimeChartRef = useRef<HTMLDivElement>(null);
const spaceTimeChartRef = useRef<HTMLDivElement>(null);
const waypoints = useMemo(() => contents.filter(isInteractiveWaypoint), [contents]);

const { manchetteProps, spaceTimeChartProps, handleScroll } = useManchetteWithSpaceTimeChart(
waypoints,
contents,
projectPathTrainResult,
manchetteWithSpaceTimeChartRef,
selectedTrain,
height,
spaceTimeChartRef
);

const fullSplitPoints = useMemo(() => {
const operationalPointsDict = keyBy(waypoints, 'id'); // Dictionnaire des waypoints

// Trouver les indices où un élément n'est PAS un InteractiveWaypoint (donc c'est un div)
const splitIndices = contents
.map((item, index) => (!isInteractiveWaypoint(item) ? index : -1))
.filter((index) => index !== -1); // Garde seulement les indices des splits

return splitIndices
.map((index) => {
// Chercher le dernier waypoint avant le split
let waypointIndex = index - 1;
while (waypointIndex >= 0 && !isInteractiveWaypoint(contents[waypointIndex])) {
waypointIndex--; // Remonte pour trouver le dernier waypoint valide
}

const waypoint = contents[waypointIndex] as InteractiveWaypoint | undefined;
if (!waypoint) return null;

return {
position: waypoint.position,
label: 'waypoint.label',
height: 100,
};
})
.filter(Boolean);
}, [contents, waypoints]);

console.log('fullSplitPoints', fullSplitPoints);

return (
<div className="manchette-space-time-chart-wrapper">
<div
Expand Down Expand Up @@ -74,9 +141,13 @@ const ManchetteWithSpaceTimeChart = ({
{...spaceTimeChartProps}
{...additionalSpaceTimeChartProps}
>
{spaceTimeChartProps.paths.map((path) => (
{spaceTimeChartProps.paths.map((path, index) => (
<PathLayer key={path.id} path={path} color={path.color} level={path.level} />
))}
{fullSplitPoints.map((point, index) => (
<FlatStep key={index} position={point!.position} />
))}

{children}
</SpaceTimeChart>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ export const filterVisibleElements = (
return displayedElements.sort((a, b) => a.position - b.position);
};

export const isInteractiveWaypoint = (
item: InteractiveWaypoint | React.ReactNode
): item is InteractiveWaypoint =>
item != null && typeof item === 'object' && 'id' in item && 'position' in item;

export const computeWaypointsToDisplay = (
waypoints: Waypoint[],
{ height, isProportional, yZoom }: WaypointsOptions
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';


import usePaths from './usePaths';
import type { SpaceScale, SpaceTimeChartProps } from '../../../spaceTimeChart';
import type { ProjectPathTrainResult, Waypoint } from '../../Manchette';
import type { ProjectPathTrainResult, Waypoint, InteractiveWaypoint } from '../../Manchette';
import { MAX_ZOOM_Y, MIN_ZOOM_Y, ZOOM_Y_DELTA, DEFAULT_ZOOM_MS_PER_PX } from '../consts';
import {
computeWaypointsToDisplay,
getScales,
zoomX,
zoomValueToTimeScale,
timeScaleToZoomValue,
isInteractiveWaypoint,
} from '../helpers';
import { getDiff } from '../utils/point';

Expand All @@ -27,8 +29,17 @@ type State = {
scales: SpaceScale[];
};

type SimplifiedWaypoint = {
id: string;
label: string;
position: number;
importanceLevel: number;
};

type SimplifiedWaypoints = (SimplifiedWaypoint | React.ReactNode)[];

const useManchettesWithSpaceTimeChart = (
waypoints: Waypoint[],
contents: (InteractiveWaypoint | React.ReactNode)[],
projectPathTrainResult: ProjectPathTrainResult[],
manchetteWithSpaceTimeChartContainer: React.RefObject<HTMLDivElement>,
selectedTrain?: number,
Expand All @@ -47,6 +58,7 @@ const useManchettesWithSpaceTimeChart = (
waypointsChart: [],
scales: [],
});
const waypoints = useMemo(() => contents.filter(isInteractiveWaypoint), [contents]);

const { xZoom, yZoom, xOffset, yOffset, scrollTo, panning, isProportional } = state;

Expand Down Expand Up @@ -148,9 +160,19 @@ const useManchettesWithSpaceTimeChart = (
[simplifiedWaypoints, height, isProportional, yZoom]
);

const contentsToDisplay = useMemo(
() =>
contents.map((content) =>
isInteractiveWaypoint(content)
? waypointsToDisplay.find((wp) => wp.id === content.id) || content
: content
),
[contents, waypointsToDisplay]
);

const manchetteProps = useMemo(
() => ({
contents: waypointsToDisplay,
contents: contentsToDisplay,
zoomYIn,
zoomYOut,
resetZoom,
Expand All @@ -159,7 +181,7 @@ const useManchettesWithSpaceTimeChart = (
isProportional,
yOffset,
}),
[waypointsToDisplay, zoomYIn, zoomYOut, resetZoom, toggleMode, yZoom, isProportional, yOffset]
[contentsToDisplay, zoomYIn, zoomYOut, resetZoom, toggleMode, yZoom, isProportional, yOffset]
);

const handleXZoom = useCallback(
Expand All @@ -174,7 +196,7 @@ const useManchettesWithSpaceTimeChart = (

const spaceTimeChartProps = useMemo(
() => ({
operationalPoints: simplifiedWaypoints,
contents: simplifiedWaypoints,
spaceScales: computedScales,
timeScale: zoomValueToTimeScale(xZoom),
paths,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default meta;

export const Default = {
args: {
waypoints: SAMPLE_WAYPOINTS,
contents: SAMPLE_WAYPOINTS,
projectPathTrainResult: SAMPLE_PATHS_DATA,
selectedTrain: 1,
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react';

import type { Meta } from '@storybook/react';

import '@osrd-project/ui-core/dist/theme.css';
import '@osrd-project/ui-charts/dist/theme.css';

import { SAMPLE_WAYPOINTS, SAMPLE_PATHS_DATA } from '../assets/sampleData';
import ManchetteWithSpaceTimeChart from '../components/ManchetteWithSpaceTimeChart';

const meta: Meta<typeof ManchetteWithSpaceTimeChart> = {
title: 'Manchette with SpaceTimeChart/split',
component: ManchetteWithSpaceTimeChart,
};

const customDiv = <div style={{ height: '100px', backgroundColor: '#EFF3F5' }} />;

const allWaypoints = SAMPLE_WAYPOINTS.map((waypoint, index) => {
if (index === 0 || index === 1 || index === 3) {
return [waypoint, customDiv];
}
return waypoint;
}).flat();

export default meta;

export const Default = {
args: {
contents: allWaypoints,
projectPathTrainResult: SAMPLE_PATHS_DATA,
selectedTrain: 1,
},
};
35 changes: 27 additions & 8 deletions ui-charts/src/spaceTimeChart/components/PathLayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ import {
type DataPoint,
DEFAULT_PATH_END,
type DrawingFunction,
type OperationalPoint,
type PathData,
type PickingDrawingFunction,
type Point,
type SpaceTimeChartContextType,
InteractiveWaypoint,
OperationalPoint,
} from '../lib/types';
import {
drawAliasedDisc,
Expand Down Expand Up @@ -68,6 +69,20 @@ export type PathLayerProps = {
level?: PathLevel;
};

export const isInteractiveWaypoint = (
item: InteractiveWaypoint | React.ReactNode
): item is InteractiveWaypoint | OperationalPoint =>
item != null && typeof item === 'object' && 'id' in item && 'position' in item;

export const isOp = (
item: InteractiveWaypoint | React.ReactNode | OperationalPoint
): item is OperationalPoint =>
item != null &&
typeof item === 'object' &&
'id' in item &&
'position' in item &&
'importanceLevel' in item;

/**
* This component handles drawing a Path inside a SpaceTimeChart. It renders:
* - The path itself
Expand Down Expand Up @@ -134,6 +149,7 @@ export const PathLayer = ({
},
[path]
);

/**
* This function returns the list of important points, where the mouse can snap.
*/
Expand All @@ -143,10 +159,11 @@ export const PathLayer = ({
getSpacePixel,
timeAxis,
spaceAxis,
operationalPoints,
contents,
}: SpaceTimeChartContextType): Point[] => {
const res: Point[] = [];
const stopPositions = new Set(operationalPoints.map((p) => p.position));
const waypoints = contents.filter(isInteractiveWaypoint);
const stopPositions = new Set(waypoints.map((p) => p.position));
path.points.forEach(({ position, time }) => {
if (stopPositions.has(position))
res.push({
Expand All @@ -163,8 +180,9 @@ export const PathLayer = ({
* This function draws the stops of the path on the operational points.
*/
const drawPauses = useCallback<DrawingFunction>(
(ctx, { getTimePixel, getSpacePixel, operationalPoints, swapAxis }) => {
const stopPositions = new Set(operationalPoints.map((p) => p.position));
(ctx, { getTimePixel, getSpacePixel, contents, swapAxis }) => {
const waypoints = contents.filter(isInteractiveWaypoint);
const stopPositions = new Set(waypoints.map((p) => p.position));
path.points.forEach(({ position, time }, i, a) => {
if (i) {
const { position: prevPosition, time: prevTime } = a[i - 1];
Expand Down Expand Up @@ -309,11 +327,12 @@ export const PathLayer = ({
);

const computePathLength = useCallback(
(operationalPoints: OperationalPoint[], segments: Point[]) => {
(contents: (InteractiveWaypoint | React.ReactNode)[], segments: Point[]) => {
let totalLength = 0;
const waypoints = contents.filter(isInteractiveWaypoint);

// Compute length of pauses
const stopPositions = new Set(operationalPoints.map((p) => p.position));
const stopPositions = new Set(waypoints.map((p) => p.position));
path.points.forEach(({ position, time }, i, pointsArray) => {
if (i > 0) {
const { position: prevPosition, time: prevTime } = pointsArray[i - 1];
Expand Down Expand Up @@ -371,7 +390,7 @@ export const PathLayer = ({

// Draw label:
if (!stcContext.hidePathsLabels) {
const pathLength = computePathLength(stcContext.operationalPoints, segments);
const pathLength = computePathLength(stcContext.contents, segments);
drawLabel(ctx, stcContext, path.label, color, segments, pathLength);
}
},
Expand Down
Loading