Repository: OpenRailAssociation/osrd-ui
Pull request #977: ui-charts: add quadrilateral layer and associated story
Author: login: RostomBoudi
Created: 2025-03-12T14:25:42Z | Merged: 2025-03-14T16:24:05Z



Diff:
diff --git a/.gitignore b/.gitignore
index bd81dcac6..15fafc470 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,4 +11,3 @@ storybook-static
 # IDE
 .vscode/
 .idea/
-
diff --git a/ui-charts/src/spaceTimeChart/components/Quadrilateral.tsx b/ui-charts/src/spaceTimeChart/components/Quadrilateral.tsx
new file mode 100644
index 000000000..9ee5d6c98
--- /dev/null
+++ b/ui-charts/src/spaceTimeChart/components/Quadrilateral.tsx
@@ -0,0 +1,48 @@
+import { useCallback } from 'react';
+
+import { useDraw } from '../hooks/useCanvas';
+import { type DataPoint, type DrawingFunction } from '../lib/types';
+
+export type QuadrilateralProps = {
+  vertices: [DataPoint, DataPoint, DataPoint, DataPoint];
+  style: {
+    backgroundColor: string;
+    borderColor: string;
+    borderWidth: number;
+  };
+};
+
+/**
+ * Draws a quadrilateral in the space time chart
+ * Position correspond to each point of the quadrilateral
+ *
+ *      vertices[0]   ________ vertices[1]
+ *                  /        /
+ *     vertices[3] /________/ vertices[2]
+ *
+ */
+export const Quadrilateral = ({ vertices, style }: QuadrilateralProps) => {
+  const drawRegion = useCallback<DrawingFunction>(
+    (ctx, { getSpacePixel, getTimePixel }) => {
+      ctx.save();
+
+      ctx.fillStyle = style.backgroundColor ?? 'lightblue';
+      ctx.strokeStyle = style.borderColor ?? 'lightblue';
+      ctx.lineWidth = style.borderWidth ?? 1;
+
+      ctx.beginPath();
+      vertices.forEach((dataPoint) => {
+        ctx.lineTo(getTimePixel(dataPoint.time), getSpacePixel(dataPoint.position));
+      });
+      ctx.closePath();
+
+      ctx.fill();
+      ctx.stroke();
+      ctx.restore();
+    },
+    [vertices, style]
+  );
+  useDraw('background', drawRegion);
+
+  return null;
+};
diff --git a/ui-charts/src/spaceTimeChart/index.ts b/ui-charts/src/spaceTimeChart/index.ts
index 066f30ba8..eed8e6a94 100644
--- a/ui-charts/src/spaceTimeChart/index.ts
+++ b/ui-charts/src/spaceTimeChart/index.ts
@@ -19,3 +19,4 @@ export * from './components/Tooltip';
 export * from './components/OccupancyBlockLayer';
 export * from './components/WorkScheduleLayer';
 export * from './components/PatternRect';
+export * from './components/Quadrilateral';
diff --git a/ui-charts/src/spaceTimeChart/stories/quadrilateral.stories.tsx b/ui-charts/src/spaceTimeChart/stories/quadrilateral.stories.tsx
new file mode 100644
index 000000000..182b3191f
--- /dev/null
+++ b/ui-charts/src/spaceTimeChart/stories/quadrilateral.stories.tsx
@@ -0,0 +1,144 @@
+import React from 'react';
+
+import type { Meta } from '@storybook/react';
+import '@osrd-project/ui-charts/dist/theme.css';
+
+import { SpaceTimeChart, PathLayer, Quadrilateral, type QuadrilateralProps } from '..';
+import { OPERATIONAL_POINTS, PATHS, START_DATE } from './lib/paths';
+import { X_ZOOM_LEVEL, Y_ZOOM_LEVEL } from './lib/utils';
+import { HOUR } from '../lib/consts';
+
+type WrapperProps = {
+  xZoomLevel: number;
+  yZoomLevel: number;
+  xOffset: number;
+  yOffset: number;
+  spaceScaleType: 'linear' | 'proportional';
+  emptyData: boolean;
+};
+
+const QuadrilateralMock: QuadrilateralProps = {
+  vertices: [
+    { time: START_DATE.getTime() + HOUR, position: 3000 },
+    { time: START_DATE.getTime() + HOUR * 3, position: 3000 },
+    { time: START_DATE.getTime() + HOUR * 2, position: 11000 },
+    { time: START_DATE.getTime(), position: 11000 },
+  ],
+  style: {
+    backgroundColor: 'lightblue',
+    borderColor: 'red',
+    borderWidth: 1,
+  },
+};
+/**
+ * This story aims at showcasing how to render a SpaceTimeChart.
+ */
+const Wrapper = ({
+  xZoomLevel,
+  yZoomLevel,
+  xOffset,
+  yOffset,
+  spaceScaleType,
+  emptyData,
+}: WrapperProps) => {
+  const operationalPoints = emptyData ? [] : OPERATIONAL_POINTS;
+  const spaceScales = emptyData
+    ? []
+    : OPERATIONAL_POINTS.slice(0, -1).map((point, i) => ({
+        from: point.position,
+        to: OPERATIONAL_POINTS[i + 1].position,
+        ...(spaceScaleType === 'linear'
+          ? { size: 50 * yZoomLevel }
+          : { coefficient: 150 / yZoomLevel }),
+      }));
+  const paths = emptyData
+    ? [
+        {
+          id: `empty-train`,
+          label: `Train with no path`,
+          color: 'transparent',
+          points: [],
+        },
+      ]
+    : PATHS;
+
+  return (
+    <div className="absolute inset-0">
+      <SpaceTimeChart
+        className="h-full"
+        operationalPoints={operationalPoints}
+        spaceOrigin={0}
+        spaceScales={spaceScales}
+        timeOrigin={+START_DATE}
+        timeScale={60000 / xZoomLevel}
+        xOffset={xOffset}
+        yOffset={yOffset}
+      >
+        {paths.map((path) => (
+          <PathLayer
+            key={path.id}
+            path={path}
+            color={path.color}
+            border={path.border}
+            level={path.level || 2}
+          />
+        ))}
+        <Quadrilateral vertices={QuadrilateralMock.vertices} style={QuadrilateralMock.style} />
+      </SpaceTimeChart>
+    </div>
+  );
+};
+
+export default {
+  title: 'SpaceTimeChart/Quadrilateral',
+  component: Wrapper,
+  argTypes: {
+    xZoomLevel: {
+      name: 'X zoom level',
+      description: '(in pixels/minute)',
+      defaultValue: 0.4,
+      control: { type: 'range', min: 0.1, max: 75, step: 0.1 },
+    },
+    xOffset: {
+      name: 'X offset',
+      description: '(in pixels)',
+      defaultValue: 0,
+      control: { type: 'number', step: 10 },
+    },
+    yZoomLevel: {
+      name: 'Y zoom level',
+      options: ['linear', 'proportional'],
+      defaultValue: 1,
+      control: { type: 'range', min: 0.1, max: 10, step: 0.1 },
+    },
+    yOffset: {
+      name: 'Y offset',
+      description: '(in pixels)',
+      defaultValue: 0,
+      control: { type: 'number', step: 10 },
+    },
+    spaceScaleType: {
+      name: 'Space scaling type',
+      options: ['linear', 'proportional'],
+      defaultValue: 'linear',
+      control: { type: 'radio' },
+    },
+    emptyData: {
+      name: 'Use empty data',
+      defaultValue: false,
+      control: { type: 'boolean' },
+    },
+  },
+} as Meta<typeof Wrapper>;
+
+export const DefaultArgs = {
+  name: 'Default arguments',
+  args: {
+    xZoomLevel: X_ZOOM_LEVEL,
+    yZoomLevel: Y_ZOOM_LEVEL,
+    xOffset: 0,
+    yOffset: 0,
+    spaceScaleType: 'linear',
+    emptyData: false,
+  },
+};


Reviews:
user: login: anisometropie
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T14:19:37Z
user: login: RostomBoudi
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-14T14:50:58Z
user: login: RostomBoudi
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-14T14:54:10Z
user: login: RostomBoudi
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-14T14:54:58Z
user: login: anisometropie
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T15:17:59Z
user: login: Yohh
body: nice! thanks for this great first PR
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T15:22:54Z
user: login: anisometropie
body: Thanks! looking good, works well.

gratz on your first contribution, nice to see more people who want to bring the project to life!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T15:25:49Z
user: login: emersion
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-14T15:39:33Z
user: login: anisometropie
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T15:40:35Z
user: login: RostomBoudi
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-14T15:46:08Z
user: login: Yohh
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T15:47:39Z
user: login: emersion
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-14T15:52:54Z
user: login: Math-R
body: Looks good to me ! thanks for this first cool contribution ! 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T16:04:12Z

Review comments:
diff_hunk: @@ -0,0 +1,54 @@
+import { useCallback } from 'react';
+
+import { useDraw } from '../hooks/useCanvas';
+import { type DataPoint, type DrawingFunction } from '../lib/types';
+
+export type ParallelogramProps = {
+  spaceTimePoints: [DataPoint, DataPoint, DataPoint, DataPoint];
path: ui-charts/src/spaceTimeChart/components/Parallelogram.tsx
user: login: anisometropie
body: if we can use any arbitrary 4 points on the plane, we can technically draw any quadrilateral. 
I think `Quadrilateral` would be a better name for that component

![Screenshot_20250314_143740](https://github.com/user-attachments/assets/df3785a9-cacc-4ad9-a85f-bddadc81e074)


created_at: 2025-03-14T13:38:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,54 @@
+import { useCallback } from 'react';
+
+import { useDraw } from '../hooks/useCanvas';
+import { type DataPoint, type DrawingFunction } from '../lib/types';
+
+export type ParallelogramProps = {
+  spaceTimePoints: [DataPoint, DataPoint, DataPoint, DataPoint];
+  style: {
+    backgroundColor: string;
+    borderColor: string;
+    borderWidth: number;
+  };
+};
+
+/**
+ * Draws a parallelogram in the space time chart
+ * Position correspond to each point of the parallelogram
+ *
+ *      spaceTimePoints[0]   ________ spaceTimePoints[1]
+ *                         /        /
+ *     spaceTimePoints[3] /________/ spaceTimePoints[2]
+ *
+ * @param {ParallelogramProps} props
+ * @returns {null}
path: ui-charts/src/spaceTimeChart/components/Parallelogram.tsx
user: login: anisometropie
body: already given by TypeScript annotations
```suggestion
```
created_at: 2025-03-14T13:43:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,54 @@
+import { useCallback } from 'react';
+
+import { useDraw } from '../hooks/useCanvas';
+import { type DataPoint, type DrawingFunction } from '../lib/types';
+
+export type ParallelogramProps = {
+  spaceTimePoints: [DataPoint, DataPoint, DataPoint, DataPoint];
path: ui-charts/src/spaceTimeChart/components/Parallelogram.tsx
user: login: anisometropie
body: ```suggestion
  vertices: [DataPoint, DataPoint, DataPoint, DataPoint];
```
created_at: 2025-03-14T13:46:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,54 @@
+import { useCallback } from 'react';
+
+import { useDraw } from '../hooks/useCanvas';
+import { type DataPoint, type DrawingFunction } from '../lib/types';
+
+export type ParallelogramProps = {
+  spaceTimePoints: [DataPoint, DataPoint, DataPoint, DataPoint];
path: ui-charts/src/spaceTimeChart/components/Parallelogram.tsx
user: login: RostomBoudi
body: Indeed, renaming done (in the component, props and associated story)
created_at: 2025-03-14T14:50:58Z
author_association: NONE
diff_hunk: @@ -0,0 +1,54 @@
+import { useCallback } from 'react';
+
+import { useDraw } from '../hooks/useCanvas';
+import { type DataPoint, type DrawingFunction } from '../lib/types';
+
+export type ParallelogramProps = {
+  spaceTimePoints: [DataPoint, DataPoint, DataPoint, DataPoint];
+  style: {
+    backgroundColor: string;
+    borderColor: string;
+    borderWidth: number;
+  };
+};
+
+/**
+ * Draws a parallelogram in the space time chart
+ * Position correspond to each point of the parallelogram
+ *
+ *      spaceTimePoints[0]   ________ spaceTimePoints[1]
+ *                         /        /
+ *     spaceTimePoints[3] /________/ spaceTimePoints[2]
+ *
+ * @param {ParallelogramProps} props
+ * @returns {null}
path: ui-charts/src/spaceTimeChart/components/Parallelogram.tsx
user: login: RostomBoudi
body: Habits die hard ^^'
Done 👌 
created_at: 2025-03-14T14:54:10Z
author_association: NONE
diff_hunk: @@ -0,0 +1,54 @@
+import { useCallback } from 'react';
+
+import { useDraw } from '../hooks/useCanvas';
+import { type DataPoint, type DrawingFunction } from '../lib/types';
+
+export type ParallelogramProps = {
+  spaceTimePoints: [DataPoint, DataPoint, DataPoint, DataPoint];
path: ui-charts/src/spaceTimeChart/components/Parallelogram.tsx
user: login: RostomBoudi
body: Renaming done
created_at: 2025-03-14T14:54:58Z
author_association: NONE
diff_hunk: @@ -0,0 +1,54 @@
+import { useCallback } from 'react';
+
+import { useDraw } from '../hooks/useCanvas';
+import { type DataPoint, type DrawingFunction } from '../lib/types';
+
+export type ParallelogramProps = {
+  spaceTimePoints: [DataPoint, DataPoint, DataPoint, DataPoint];
path: ui-charts/src/spaceTimeChart/components/Parallelogram.tsx
user: login: anisometropie
body: thanks!
created_at: 2025-03-14T15:17:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,52 @@
+import { useCallback } from 'react';
+
+import { useDraw } from '../hooks/useCanvas';
+import { type DataPoint, type DrawingFunction } from '../lib/types';
+
+export type QuadrilateralProps = {
+  vertices: [DataPoint, DataPoint, DataPoint, DataPoint];
+  style: {
+    backgroundColor: string;
+    borderColor: string;
+    borderWidth: number;
+  };
+};
+
+/**
+ * Draws a quadrilateral in the space time chart
+ * Position correspond to each point of the quadrilateral
+ *
+ *      vertices[0]   ________ vertices[1]
+ *                  /        /
+ *     vertices[3] /________/ vertices[2]
+ *
+ */
+export const Quadrilateral = ({ vertices, style }: QuadrilateralProps) => {
+  const drawRegion = useCallback<DrawingFunction>(
+    (ctx, { getSpacePixel, getTimePixel }) => {
+      ctx.save();
+
+      ctx.fillStyle = style.backgroundColor ?? 'lightblue';
+      ctx.strokeStyle = style.borderColor ?? 'lightblue';
+      ctx.lineWidth = style.borderWidth ?? 1;
+
+      ctx.beginPath();
+      vertices.forEach((dataPoint, index) => {
+        if (index === 0) {
+          ctx.moveTo(getTimePixel(dataPoint.time), getSpacePixel(dataPoint.position));
+        } else {
+          ctx.lineTo(getTimePixel(dataPoint.time), getSpacePixel(dataPoint.position));
+        }
+      });
path: ui-charts/src/spaceTimeChart/components/Quadrilateral.tsx
user: login: Yohh
body: ```suggestion
      vertices.forEach((dataPoint) => {
          ctx.lineTo(getTimePixel(dataPoint.time), getSpacePixel(dataPoint.position));
      });
```

nit: you draw only one shape, no need to seek for first index here
created_at: 2025-03-14T15:22:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,52 @@
+import { useCallback } from 'react';
+
+import { useDraw } from '../hooks/useCanvas';
+import { type DataPoint, type DrawingFunction } from '../lib/types';
+
+export type QuadrilateralProps = {
+  vertices: [DataPoint, DataPoint, DataPoint, DataPoint];
+  style: {
+    backgroundColor: string;
+    borderColor: string;
+    borderWidth: number;
+  };
+};
+
+/**
+ * Draws a quadrilateral in the space time chart
+ * Position correspond to each point of the quadrilateral
+ *
+ *      vertices[0]   ________ vertices[1]
+ *                  /        /
+ *     vertices[3] /________/ vertices[2]
+ *
+ */
+export const Quadrilateral = ({ vertices, style }: QuadrilateralProps) => {
+  const drawRegion = useCallback<DrawingFunction>(
+    (ctx, { getSpacePixel, getTimePixel }) => {
+      ctx.save();
+
+      ctx.fillStyle = style.backgroundColor ?? 'lightblue';
+      ctx.strokeStyle = style.borderColor ?? 'lightblue';
+      ctx.lineWidth = style.borderWidth ?? 1;
+
+      ctx.beginPath();
+      vertices.forEach((dataPoint, index) => {
+        if (index === 0) {
+          ctx.moveTo(getTimePixel(dataPoint.time), getSpacePixel(dataPoint.position));
+        } else {
+          ctx.lineTo(getTimePixel(dataPoint.time), getSpacePixel(dataPoint.position));
+        }
+      });
path: ui-charts/src/spaceTimeChart/components/Quadrilateral.tsx
user: login: emersion
body: Have you tested this? I think we need to move the canvas' current point, before we can start drawing lines?
created_at: 2025-03-14T15:39:33Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,52 @@
+import { useCallback } from 'react';
+
+import { useDraw } from '../hooks/useCanvas';
+import { type DataPoint, type DrawingFunction } from '../lib/types';
+
+export type QuadrilateralProps = {
+  vertices: [DataPoint, DataPoint, DataPoint, DataPoint];
+  style: {
+    backgroundColor: string;
+    borderColor: string;
+    borderWidth: number;
+  };
+};
+
+/**
+ * Draws a quadrilateral in the space time chart
+ * Position correspond to each point of the quadrilateral
+ *
+ *      vertices[0]   ________ vertices[1]
+ *                  /        /
+ *     vertices[3] /________/ vertices[2]
+ *
+ */
+export const Quadrilateral = ({ vertices, style }: QuadrilateralProps) => {
+  const drawRegion = useCallback<DrawingFunction>(
+    (ctx, { getSpacePixel, getTimePixel }) => {
+      ctx.save();
+
+      ctx.fillStyle = style.backgroundColor ?? 'lightblue';
+      ctx.strokeStyle = style.borderColor ?? 'lightblue';
+      ctx.lineWidth = style.borderWidth ?? 1;
+
+      ctx.beginPath();
+      vertices.forEach((dataPoint, index) => {
+        if (index === 0) {
+          ctx.moveTo(getTimePixel(dataPoint.time), getSpacePixel(dataPoint.position));
+        } else {
+          ctx.lineTo(getTimePixel(dataPoint.time), getSpacePixel(dataPoint.position));
+        }
+      });
path: ui-charts/src/spaceTimeChart/components/Quadrilateral.tsx
user: login: anisometropie
body: Can’t this cause unexected behavior ?
created_at: 2025-03-14T15:40:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,52 @@
+import { useCallback } from 'react';
+
+import { useDraw } from '../hooks/useCanvas';
+import { type DataPoint, type DrawingFunction } from '../lib/types';
+
+export type QuadrilateralProps = {
+  vertices: [DataPoint, DataPoint, DataPoint, DataPoint];
+  style: {
+    backgroundColor: string;
+    borderColor: string;
+    borderWidth: number;
+  };
+};
+
+/**
+ * Draws a quadrilateral in the space time chart
+ * Position correspond to each point of the quadrilateral
+ *
+ *      vertices[0]   ________ vertices[1]
+ *                  /        /
+ *     vertices[3] /________/ vertices[2]
+ *
+ */
+export const Quadrilateral = ({ vertices, style }: QuadrilateralProps) => {
+  const drawRegion = useCallback<DrawingFunction>(
+    (ctx, { getSpacePixel, getTimePixel }) => {
+      ctx.save();
+
+      ctx.fillStyle = style.backgroundColor ?? 'lightblue';
+      ctx.strokeStyle = style.borderColor ?? 'lightblue';
+      ctx.lineWidth = style.borderWidth ?? 1;
+
+      ctx.beginPath();
+      vertices.forEach((dataPoint, index) => {
+        if (index === 0) {
+          ctx.moveTo(getTimePixel(dataPoint.time), getSpacePixel(dataPoint.position));
+        } else {
+          ctx.lineTo(getTimePixel(dataPoint.time), getSpacePixel(dataPoint.position));
+        }
+      });
path: ui-charts/src/spaceTimeChart/components/Quadrilateral.tsx
user: login: RostomBoudi
body: I also though it was mandatory 🤔, but I just tested it and it seems to work fine
created_at: 2025-03-14T15:46:08Z
author_association: NONE
diff_hunk: @@ -0,0 +1,52 @@
+import { useCallback } from 'react';
+
+import { useDraw } from '../hooks/useCanvas';
+import { type DataPoint, type DrawingFunction } from '../lib/types';
+
+export type QuadrilateralProps = {
+  vertices: [DataPoint, DataPoint, DataPoint, DataPoint];
+  style: {
+    backgroundColor: string;
+    borderColor: string;
+    borderWidth: number;
+  };
+};
+
+/**
+ * Draws a quadrilateral in the space time chart
+ * Position correspond to each point of the quadrilateral
+ *
+ *      vertices[0]   ________ vertices[1]
+ *                  /        /
+ *     vertices[3] /________/ vertices[2]
+ *
+ */
+export const Quadrilateral = ({ vertices, style }: QuadrilateralProps) => {
+  const drawRegion = useCallback<DrawingFunction>(
+    (ctx, { getSpacePixel, getTimePixel }) => {
+      ctx.save();
+
+      ctx.fillStyle = style.backgroundColor ?? 'lightblue';
+      ctx.strokeStyle = style.borderColor ?? 'lightblue';
+      ctx.lineWidth = style.borderWidth ?? 1;
+
+      ctx.beginPath();
+      vertices.forEach((dataPoint, index) => {
+        if (index === 0) {
+          ctx.moveTo(getTimePixel(dataPoint.time), getSpacePixel(dataPoint.position));
+        } else {
+          ctx.lineTo(getTimePixel(dataPoint.time), getSpacePixel(dataPoint.position));
+        }
+      });
path: ui-charts/src/spaceTimeChart/components/Quadrilateral.tsx
user: login: Yohh
body: > Have you tested this? I think we need to move the canvas' current point, before we can start drawing lines?

indeed
it seems moveTo and lineTo are the same, expect that lineTo draws, if it's the first point nothing will be draw 
created_at: 2025-03-14T15:47:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,52 @@
+import { useCallback } from 'react';
+
+import { useDraw } from '../hooks/useCanvas';
+import { type DataPoint, type DrawingFunction } from '../lib/types';
+
+export type QuadrilateralProps = {
+  vertices: [DataPoint, DataPoint, DataPoint, DataPoint];
+  style: {
+    backgroundColor: string;
+    borderColor: string;
+    borderWidth: number;
+  };
+};
+
+/**
+ * Draws a quadrilateral in the space time chart
+ * Position correspond to each point of the quadrilateral
+ *
+ *      vertices[0]   ________ vertices[1]
+ *                  /        /
+ *     vertices[3] /________/ vertices[2]
+ *
+ */
+export const Quadrilateral = ({ vertices, style }: QuadrilateralProps) => {
+  const drawRegion = useCallback<DrawingFunction>(
+    (ctx, { getSpacePixel, getTimePixel }) => {
+      ctx.save();
+
+      ctx.fillStyle = style.backgroundColor ?? 'lightblue';
+      ctx.strokeStyle = style.borderColor ?? 'lightblue';
+      ctx.lineWidth = style.borderWidth ?? 1;
+
+      ctx.beginPath();
+      vertices.forEach((dataPoint, index) => {
+        if (index === 0) {
+          ctx.moveTo(getTimePixel(dataPoint.time), getSpacePixel(dataPoint.position));
+        } else {
+          ctx.lineTo(getTimePixel(dataPoint.time), getSpacePixel(dataPoint.position));
+        }
+      });
path: ui-charts/src/spaceTimeChart/components/Quadrilateral.tsx
user: login: emersion
body: Right, just read the spec and it seems like `lineTo()` works just like `moveTo()` when there is no sub-path: https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-lineto
created_at: 2025-03-14T15:52:54Z
author_association: MEMBER

PR comments:
user: login: Yohh
created_at: 2025-03-14T15:25:30Z
body: can you reword your commit?

![image](https://github.com/user-attachments/assets/a457d0b8-a59e-4195-b271-eaf515bcc187)

author_association: CONTRIBUTOR
user: login: emersion
created_at: 2025-03-14T15:43:05Z
body: It seems this needs to be rebased on top of the latest `dev` commit, to resolve conflicts in `.gitignore`.
author_association: MEMBER