Skip to content

Commit

Permalink
osrd-ui: speedspacechart: setup speedspacechart
Browse files Browse the repository at this point in the history
- 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
Yohh authored and kmer2016 committed Jun 6, 2024
1 parent b8ad222 commit 3f43b61
Show file tree
Hide file tree
Showing 37 changed files with 15,637 additions and 11,826 deletions.
19,343 changes: 11,047 additions & 8,296 deletions package-lock.json

Large diffs are not rendered by default.

34 changes: 33 additions & 1 deletion ui-speedspacechart/README.md
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.
36 changes: 26 additions & 10 deletions ui-speedspacechart/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,25 @@
"name": "@osrd-project/ui-speedspacechart",
"version": "0.0.18",
"license": "LGPL-3.0-or-later",
"private": true,
"bugs": "https://github.com/osrd-project/osrd-ui/issues",
"repository": {
"type": "git",
"url": "https://github.com/osrd-project/osrd-ui.git",
"directory": "ui-speedspacechart"
},
"publishConfig": {
"access": "public"
},
"type": "module",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts",
"main": "dist/index.esm.js",
"module": "./dist/index.esm.js",
"types": "./dist/index.d.ts",
"main": "./dist/index.esm.js",
"style": "dist/theme.css",
"files": [
"/dist"
],
"exports": {
"./dist/theme.css": "./dist/theme.css",
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.esm.js"
Expand All @@ -26,16 +30,28 @@
"rollup": "rollup -c",
"clean": "rimraf dist",
"build": "npm run rollup",
"watch": "NODE_ENV=development rollup -c -w",
"test": "vitest run --dir src/__tests__",
"prepublishOnly": "npm run clean && npm run build"
},
"dependencies": {
"@types/d3": "^7.4.3",
"classnames": "^2.5.1",
"d3": "^7.9.0",
"tailwindcss": "^3.4.1"
},
"peerDependencies": {
"react": ">=18.0",
"react-dom": ">=18.0"
"react": ">=18.0"
},
"devDependencies": {
"@types/d3": "^7.4.3"
"autoprefixer": "^10.4.17",
"postcss": "^8.4.37",
"postcss-assets": "^6.0.0",
"postcss-import": "^16.0.0",
"postcss-preset-env": "^9.5.2",
"rollup-plugin-livereload": "^2.0.5",
"rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-serve": "^1.1.1"
},
"dependencies": {
"d3": "^7.8.5"
}
"gitHead": "973ad1478be4544e1c97303b844903247d9a9cd7"
}
2 changes: 2 additions & 0 deletions ui-speedspacechart/postcss.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
const generateBasePostcssConfig = require('../postcss-base.config.cjs');
module.exports = generateBasePostcssConfig();
19 changes: 2 additions & 17 deletions ui-speedspacechart/rollup.config.js
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']);
79 changes: 79 additions & 0 deletions ui-speedspacechart/src/__tests__/utils.spec.ts
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);
});
});
95 changes: 86 additions & 9 deletions ui-speedspacechart/src/components/SpeedSpaceChart.tsx
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()}
>
&#8617;
</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;
69 changes: 69 additions & 0 deletions ui-speedspacechart/src/components/common/DetailsBox.tsx
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;
Loading

0 comments on commit 3f43b61

Please sign in to comment.