-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
osrd-ui: speedspacechart: setup speedspacechart
- synchronize speedspacechart project with npm deployment specifications - add a readMe file - create SpeedSpaceChart main component - add a story for storyBook using sampleData.ts - create useCanvas custom hook in hooks.ts - create layers in components/layers/ - create /components/helpers/ with drawElements/ containing draw functions files, one for each king of element - create layersManager for interactive functions as zoom and reset - add X axis zoom - create details box following the reticle - add snapping to closest pr - extract tain details box from reticle draw function to his own component - adapt vertical lines to PRs position - add PRs names - add tests for utils functions
- Loading branch information
Showing
37 changed files
with
15,637 additions
and
11,826 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,33 @@ | ||
# ui-speedspacechart | ||
### SpeedSpaceChart | ||
The `SpeedSpaceChart` is a component that visualizes the speed and position data of a simulation. It's part of the `ui-speedspacechart` module in your workspace. | ||
|
||
### How it works | ||
The `SpeedSpaceChart` component takes mainly three props: `width`, `height`, and `data`. The `width` and `height` props define the size of the chart, while the `data` prop is an `OsrdSimulationState` object that contains the simulation data to be visualized. | ||
|
||
The `OsrdSimulationState` object includes various properties such as `consolidatedSimulation`wich is an array of SimulationTrain objects, each representing a train in the simulation. | ||
|
||
The `SpeedSpaceChart` component uses a `useState` hook to create a `store` state variable. This `store` contains the speed, stops, electrification, and slopes data extracted from the `data` prop, as well as the `ratioX` and `leftOffset` properties for scaling and positioning the chart, and a `cursor` property for tracking the cursor position. | ||
|
||
The `SpeedSpaceChart` component renders a `div` element that contains various layers for different parts of the chart. These layers include `CurveLayer`, `AxisLayerY`, `MajorGridY`, and others. Each layer is a separate and independant component that takes the `width`, `height`, and `store` props and is responsible for rendering a specific part of the chart. | ||
|
||
The `useCanvas` hook takes a drawing function, the `width` and `height` of the canvas, the `store`, and optionally a function to set the `store`. It returns a reference to the canvas element. The drawing function is called with the `canvas` context, the `width` and `height`, the `store`, and the `setStore` function (if provided). | ||
|
||
### Usage | ||
Here is an example of how to use the `SpeedSpaceChart` component: | ||
|
||
```js | ||
import SpeedSpaceChart from 'ui-speedspacechart/src/components/SpeedSpaceChart'; | ||
import OSRD_SAMPLE from 'ui-speedspacechart/src/stories/assets/sampleData'; | ||
|
||
const MyComponent = () => { | ||
return ( | ||
<SpeedSpaceChart | ||
width={1440} | ||
height={521.5} | ||
data={OSRD_SAMPLE} | ||
/> | ||
); | ||
}; | ||
``` | ||
|
||
In this example, `OSRD_SAMPLE` is a sample `OsrdSimulationState` object imported from `ui-speedspacechart/src/stories/assets/sampleData.ts`. In a real application, you would replace OSRD_SAMPLE with your actual simulation data. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
const generateBasePostcssConfig = require('../postcss-base.config.cjs'); | ||
module.exports = generateBasePostcssConfig(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,18 +1,3 @@ | ||
import typescript from '@rollup/plugin-typescript'; | ||
import terser from '@rollup/plugin-terser'; | ||
import eslint from '@rollup/plugin-eslint'; | ||
import generateBaseRollupConfig from '../rollup-base.config.js'; | ||
|
||
const formats = ['esm']; | ||
|
||
/** @type {import('rollup').RollupOptions} */ | ||
export default { | ||
input: 'src/index.ts', | ||
output: formats.map((format) => ({ | ||
file: `dist/index.${format}.js`, | ||
format, | ||
name: 'osrdspeedspacechart', | ||
sourcemap: true, | ||
})), | ||
plugins: [eslint(), typescript(), terser()], | ||
external: ['react', 'd3'], | ||
}; | ||
export default generateBaseRollupConfig('osrdcore', ['react']); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import { describe, expect, it, vi } from 'vitest'; | ||
import { | ||
getGraphOffsets, | ||
speedRangeValues, | ||
maxPositionValues, | ||
clearCanvas, | ||
} from '../components/utils'; | ||
import type { Store } from '../types/chartTypes'; | ||
import type { ConsolidatedPositionSpeedTime } from '../types/simulationTypes'; | ||
|
||
const time = new Date(); | ||
|
||
const speed: ConsolidatedPositionSpeedTime[] = [ | ||
{ speed: 10, position: 200, time }, | ||
{ speed: 20, position: 350, time }, | ||
{ speed: 30, position: 600, time }, | ||
]; | ||
|
||
const store: Store = { | ||
speed, | ||
stops: [], | ||
electrification: [], | ||
slopes: [], | ||
ratioX: 1, | ||
leftOffset: 0, | ||
cursor: { | ||
x: null, | ||
y: null, | ||
}, | ||
}; | ||
|
||
describe('getGraphOffsets', () => { | ||
it('should return the correct width and height offsets', () => { | ||
const width = 100; | ||
const height = 200; | ||
const { WIDTH_OFFSET, HEIGHT_OFFSET } = getGraphOffsets(width, height); | ||
expect(WIDTH_OFFSET).toBe(40); | ||
expect(HEIGHT_OFFSET).toBe(120); | ||
}); | ||
}); | ||
|
||
describe('speedRangeValues', () => { | ||
it('should return the correct minSpeed, maxSpeed and speedRange', () => { | ||
const { minSpeed, maxSpeed, speedRange } = speedRangeValues(store); | ||
expect(minSpeed).toBe(10); | ||
expect(maxSpeed).toBe(30); | ||
expect(speedRange).toBe(20); | ||
}); | ||
}); | ||
|
||
describe('maxPositionValues', () => { | ||
it('should return the correct maxPosition, RoundMaxPosition and intermediateTicksPosition', () => { | ||
const { maxPosition, RoundMaxPosition, intermediateTicksPosition } = maxPositionValues(store); | ||
expect(maxPosition).toBe(600); | ||
expect(RoundMaxPosition).toBe(30); | ||
expect(intermediateTicksPosition).toBe(15); | ||
}); | ||
|
||
it('should return 0 for maxPosition, RoundMaxPosition and intermediateTicksPosition when speed array is empty', () => { | ||
const { maxPosition, RoundMaxPosition, intermediateTicksPosition } = maxPositionValues({ | ||
...store, | ||
speed: [], | ||
}); | ||
expect(maxPosition).toBe(0); | ||
expect(RoundMaxPosition).toBe(0); | ||
expect(intermediateTicksPosition).toBe(0); | ||
}); | ||
}); | ||
|
||
describe('clearCanvas', () => { | ||
it('should clear the canvas', () => { | ||
const fn = () => vi.fn(); | ||
const ctx = { | ||
clearRect: fn(), | ||
} as unknown as CanvasRenderingContext2D; | ||
clearCanvas(ctx, 100, 200); | ||
expect(ctx.clearRect).toHaveBeenCalledWith(0, 0, 100, 200); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,36 +1,113 @@ | ||
import React, { useState } from 'react'; | ||
|
||
import CurveLayer from './layers/CurveLayer'; | ||
import FrontInteractivityLayer from './layers/FrontInteractivityLayer'; | ||
import { type ConsolidatedPositionSpeedTime, OsrdSimulationState } from '../types/simulationTypes'; | ||
import React, { useEffect, useState } from 'react'; | ||
import type { Store } from '../types/chartTypes'; | ||
import AxisLayerX from './layers/AxisLayerX'; | ||
import AxisLayerY from './layers/AxisLayerY'; | ||
import TickLayerX from './layers/TickLayerX'; | ||
import TickLayerY from './layers/TickLayerY'; | ||
import MajorGridY from './layers/MajorGridY'; | ||
import StepLayer from './layers/StepLayer'; | ||
import ReticleLayer from './layers/ReticleLayer'; | ||
import { resetZoom } from './helpers/layersManager'; | ||
import StepNamesLayer from './layers/StepNamesLayer'; | ||
import { getGraphOffsets } from './utils'; | ||
|
||
export type SpeedSpaceChartProps = { | ||
width: number; | ||
height: number; | ||
backgroundColor: string; | ||
data: OsrdSimulationState; | ||
}; | ||
|
||
export const SpeedSpaceChart = ({ width, height, data }: SpeedSpaceChartProps) => { | ||
const SpeedSpaceChart = ({ width, height, backgroundColor, data }: SpeedSpaceChartProps) => { | ||
const [store, setStore] = useState<Store>({ | ||
speed: data.consolidatedSimulation[0].speed as ConsolidatedPositionSpeedTime[], | ||
ratio: 1, | ||
speed: [], | ||
stops: [], | ||
electrification: [], | ||
slopes: [], | ||
ratioX: 1, | ||
leftOffset: 0, | ||
cursor: { | ||
x: null, | ||
y: null, | ||
}, | ||
}); | ||
|
||
const { WIDTH_OFFSET, HEIGHT_OFFSET } = getGraphOffsets(width, height); | ||
|
||
const [showDetailsBox, setShowDetailsBox] = useState(false); | ||
|
||
const reset = () => { | ||
setStore((prev) => ({ | ||
...prev, | ||
ratioX: 1, | ||
leftOffset: 0, | ||
})); | ||
resetZoom(); | ||
}; | ||
|
||
useEffect(() => { | ||
const storeData = { | ||
speed: (data.consolidatedSimulation[0].speed as ConsolidatedPositionSpeedTime[]) || [], | ||
stops: data.simulation.present.trains[0].base.stops || [], | ||
electrification: data.simulation.present.trains[0].electrification_ranges || [], | ||
slopes: data.simulation.present.trains[0].slopes || [], | ||
}; | ||
|
||
if (storeData.speed && storeData.stops && storeData.electrification && storeData.slopes) { | ||
setStore((prev) => ({ | ||
...prev, | ||
speed: storeData.speed, | ||
stops: storeData.stops, | ||
electrification: storeData.electrification, | ||
slopes: storeData.slopes, | ||
})); | ||
} | ||
}, [data]); | ||
|
||
return ( | ||
<div className="bg-white" style={{ width: `${width}px`, height: `${height}px` }} tabIndex={0}> | ||
<CurveLayer width={width - 60} height={height - 35} store={store} /> | ||
<div | ||
style={{ | ||
width: `${width}px`, | ||
height: `${height}px`, | ||
backgroundColor: `${backgroundColor}`, | ||
}} | ||
tabIndex={0} | ||
> | ||
<div className="flex justify-end absolute mt-8 ml-2" style={{ width: width }}> | ||
<button | ||
className="bg-blue-600 hover:bg-blue-700 text-white-100 p-1 mr-6 z-10 rounded-full w-8 h-8" | ||
onClick={() => reset()} | ||
> | ||
↩ | ||
</button> | ||
</div> | ||
<CurveLayer width={WIDTH_OFFSET} height={HEIGHT_OFFSET} store={store} /> | ||
<AxisLayerY width={width} height={height} store={store} /> | ||
<MajorGridY width={width} height={height} store={store} /> | ||
<AxisLayerX width={width} height={height} store={store} /> | ||
<StepLayer width={WIDTH_OFFSET} height={HEIGHT_OFFSET} store={store} /> | ||
<StepNamesLayer key={stop.name} width={WIDTH_OFFSET} height={HEIGHT_OFFSET} store={store} /> | ||
<TickLayerY width={width} height={height} store={store} /> | ||
<TickLayerX width={width} height={height} store={store} /> | ||
<ReticleLayer | ||
width={width} | ||
height={height} | ||
store={store} | ||
showDetailsBox={showDetailsBox} | ||
setShowDetailsBox={setShowDetailsBox} | ||
/> | ||
<FrontInteractivityLayer | ||
width={width - 60} | ||
height={height - 35} | ||
width={WIDTH_OFFSET} | ||
height={HEIGHT_OFFSET} | ||
store={store} | ||
setStore={setStore} | ||
setShowDetailsBox={setShowDetailsBox} | ||
/> | ||
</div> | ||
); | ||
}; | ||
|
||
export default SpeedSpaceChart; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import React from 'react'; | ||
import { MARGINS } from '../const'; | ||
|
||
type DetailsBoxProps = { | ||
width: number; | ||
height: number; | ||
curveX: number; | ||
curveY: number; | ||
marecoSpeedText: string; | ||
effortText: string; | ||
electricalProfileText: string; | ||
previousGradientText: number; | ||
modeText: string; | ||
}; | ||
|
||
const DetailsBox = ({ | ||
width, | ||
height, | ||
curveX, | ||
curveY, | ||
marecoSpeedText, | ||
effortText, | ||
electricalProfileText, | ||
previousGradientText, | ||
modeText, | ||
}: DetailsBoxProps) => { | ||
const { MARGIN_TOP, MARGIN_BOTTOM, MARGIN_LEFT, MARGIN_RIGHT } = MARGINS; | ||
|
||
let rightOffset = 0; | ||
let bottomOffset = 0; | ||
|
||
// find out if the box is going out off the right side of the canvas | ||
if (curveX + MARGIN_LEFT + 115 > width - MARGIN_RIGHT - 10) { | ||
rightOffset = 127; | ||
} | ||
// find out if the box is going out off the bottom side of the canvas | ||
if (curveY + MARGIN_TOP + 180 > height - MARGIN_BOTTOM - 10) { | ||
bottomOffset = 192; | ||
} | ||
|
||
const boxX = curveX + MARGIN_LEFT + 6 - rightOffset; | ||
const boxY = curveY + MARGIN_TOP + 6 - bottomOffset; | ||
|
||
return ( | ||
<div | ||
id="details-box" | ||
className={`absolute flex flex-col ${curveY ? 'block' : 'hidden'}`} | ||
style={{ | ||
marginTop: boxY, | ||
marginLeft: boxX, | ||
}} | ||
> | ||
{marecoSpeedText && <span id="details-box-text mareco-speed-text">{marecoSpeedText}</span>} | ||
<div id="base-speed-text"> | ||
<span>--</span> | ||
<span>±--</span> | ||
</div> | ||
{(modeText || effortText) && <hr />} | ||
{modeText && <span id="mode-text">{modeText}</span>} | ||
{effortText && <span id="effort-text">{effortText}</span>} | ||
{electricalProfileText && <span id="electrical-profile-text">{electricalProfileText}</span>} | ||
<span id="power-restriction">--</span> | ||
<hr /> | ||
<span id="previous-gradient-text">{`${previousGradientText} ‰`}</span> | ||
</div> | ||
); | ||
}; | ||
|
||
export default DetailsBox; |
Oops, something went wrong.