From e99141d765d257e6535fbdb06f4bf5a4c66c5bd1 Mon Sep 17 00:00:00 2001 From: Valentin Chanas Date: Wed, 5 Feb 2025 14:58:03 +0100 Subject: [PATCH 1/6] up-spacetimechart: fix tme graduation Signed-off-by: Valentin Chanas --- .../src/components/TimeGraduations.tsx | 16 +---- .../src/stories/additional-data.stories.tsx | 2 +- .../src/utils/__tests__/canvas.spec.ts | 61 +++++++++++++++++++ ui-spacetimechart/src/utils/canvas.ts | 38 +++++++++++- 4 files changed, 101 insertions(+), 16 deletions(-) create mode 100644 ui-spacetimechart/src/utils/__tests__/canvas.spec.ts diff --git a/ui-spacetimechart/src/components/TimeGraduations.tsx b/ui-spacetimechart/src/components/TimeGraduations.tsx index 4774b2448..cb2774617 100644 --- a/ui-spacetimechart/src/components/TimeGraduations.tsx +++ b/ui-spacetimechart/src/components/TimeGraduations.tsx @@ -3,6 +3,7 @@ import { useCallback } from 'react'; import { useDraw } from '../hooks/useCanvas'; import { MINUTE } from '../lib/consts'; import { type DrawingFunction } from '../lib/types'; +import { displayElementsBasedOnZoom } from '../utils/canvas'; const TimeGraduations = () => { const drawingFunction = useCallback( @@ -37,20 +38,7 @@ const TimeGraduations = () => { return false; }); - // - Keys are times in ms - // - Values are the highest level on each time - const gridMarks: Record = {}; - timeRanges.map((range, i) => { - const gridlinesLevel = gridlinesLevels[i]; - - if (!gridlinesLevel) return; - - let t = Math.floor(minT / range) * range; - while (t <= maxT) { - gridMarks[t] = gridlinesLevel; - t += range; - } - }); + const gridMarks = displayElementsBasedOnZoom(minT, maxT, timeRanges, gridlinesLevels); // Render grid lines: for (const t in gridMarks) { diff --git a/ui-spacetimechart/src/stories/additional-data.stories.tsx b/ui-spacetimechart/src/stories/additional-data.stories.tsx index 4e4723688..f4a8053b5 100644 --- a/ui-spacetimechart/src/stories/additional-data.stories.tsx +++ b/ui-spacetimechart/src/stories/additional-data.stories.tsx @@ -201,7 +201,7 @@ const Wrapper = ({ swapAxis, spaceScaleType }: WrapperProps) => { ? { size: 50 * state.yZoomLevel } : { coefficient: 150 / state.yZoomLevel }), }))} - timeOrigin={+new Date('2024/04/02')} + timeOrigin={+new Date('2024-04-02T00:00:00')} timeScale={60000 / state.xZoomLevel} xOffset={state.xOffset} yOffset={state.yOffset} diff --git a/ui-spacetimechart/src/utils/__tests__/canvas.spec.ts b/ui-spacetimechart/src/utils/__tests__/canvas.spec.ts new file mode 100644 index 000000000..c783a5990 --- /dev/null +++ b/ui-spacetimechart/src/utils/__tests__/canvas.spec.ts @@ -0,0 +1,61 @@ +import { describe, it, expect } from 'vitest'; + +import { displayElementsBasedOnZoom } from '../canvas'; + +describe('displayElementsBasedOnZoom', () => { + it('should give 0h 1, 12h 2, 6h & 18h 3 in LOCAL timezone', () => { + const minT = 1712008800000; // 00:00:00 GMT+0200 + const maxT = 1712025400000; // 04:36:40 GMT+0200 + const timeRanges = [ + 10000, 30000, 60000, 300000, 900000, 1800000, 3600000, 10800000, 21600000, 43200000, 86400000, + ]; + const gridlinesLevels = [0, 0, 0, 0, 6, 5, 4, 3, 3, 2, 1]; + const result = displayElementsBasedOnZoom(minT, maxT, timeRanges, gridlinesLevels); + expect(result).toEqual({ + '1712008800000': 1, + '1712009700000': 6, + '1712010600000': 5, + '1712011500000': 6, + '1712012400000': 4, + '1712013300000': 6, + '1712014200000': 5, + '1712015100000': 6, + '1712016000000': 4, + '1712016900000': 6, + '1712017800000': 5, + '1712018700000': 6, + '1712019600000': 3, + '1712020500000': 6, + '1712021400000': 5, + '1712022300000': 6, + '1712023200000': 4, + '1712024100000': 6, + '1712025000000': 5, + }); + const times = Object.keys(result).map((t) => + new Date(Number(t)).toLocaleTimeString('fr-FR', { timeZone: 'Europe/Paris' }) + ); + + expect(times).toEqual([ + '00:00:00', + '00:15:00', + '00:30:00', + '00:45:00', + '01:00:00', + '01:15:00', + '01:30:00', + '01:45:00', + '02:00:00', + '02:15:00', + '02:30:00', + '02:45:00', + '03:00:00', + '03:15:00', + '03:30:00', + '03:45:00', + '04:00:00', + '04:15:00', + '04:30:00', + ]); + }); +}); diff --git a/ui-spacetimechart/src/utils/canvas.ts b/ui-spacetimechart/src/utils/canvas.ts index 68c3bfc87..f9f9b76b1 100644 --- a/ui-spacetimechart/src/utils/canvas.ts +++ b/ui-spacetimechart/src/utils/canvas.ts @@ -1,4 +1,4 @@ -import { clamp } from 'lodash'; +import { clamp, identity } from 'lodash'; import { type PathEnd, type Point, type RGBAColor, type RGBColor } from '../lib/types'; @@ -273,3 +273,39 @@ export function drawPathExtremity( drawPathStopExtremity(ctx, timePixel, spacePixel, swapAxis); } } + +/** + * + * @param minT number timestamp + * @param maxT number timestamp + * @param timeRanges time frames (24h, 12h, 6h, …) + * @param gridlinesLevels width of the lines for each time frame + * @returns Record + * Keys are times in ms + * Values are the highest level on each time + */ + +export function displayElementsBasedOnZoom( + minT: number, + maxT: number, + timeRanges: number[], + levels: number[], + formatter: (level: number, i: number) => T = identity +) { + const result: Record = {}; + const localOffset = -new Date(minT).getTimezoneOffset() * 60 * 1000; + timeRanges.forEach((range, i) => { + const gridlinesLevel = levels[i]; + + if (!gridlinesLevel) return; + + let t = Math.floor(minT / range) * range; + while (t <= maxT + localOffset) { + if (t >= minT + localOffset) { + result[t - localOffset] = formatter(gridlinesLevel, i); + } + t += range; + } + }); + return result; +} From e9fb37c85993165773cbabe4ac17e562e646fcc2 Mon Sep 17 00:00:00 2001 From: Valentin Chanas Date: Wed, 5 Feb 2025 14:58:30 +0100 Subject: [PATCH 2/6] ui-spacetimechart: fix time label Signed-off-by: Valentin Chanas --- ui-spacetimechart/src/stories/lib/components.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-spacetimechart/src/stories/lib/components.tsx b/ui-spacetimechart/src/stories/lib/components.tsx index 0a176630d..485ede84a 100644 --- a/ui-spacetimechart/src/stories/lib/components.tsx +++ b/ui-spacetimechart/src/stories/lib/components.tsx @@ -83,7 +83,7 @@ const DataLabel = ({ ) : ( <> -
Time: {new Date(data.time).toLocaleTimeString('en-GB', { timeZone: 'UTC' })}
+
Time: {new Date(data.time).toLocaleTimeString()}
Distance: {round(data.position).toLocaleString()} m
)} From b06e457f2dd9c39b01b95fa8cfb607bda746fc5f Mon Sep 17 00:00:00 2001 From: Valentin Chanas Date: Wed, 5 Feb 2025 15:26:01 +0100 Subject: [PATCH 3/6] ui-spacetimechart: fix time captions Signed-off-by: Valentin Chanas --- .../src/components/TimeCaptions.tsx | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/ui-spacetimechart/src/components/TimeCaptions.tsx b/ui-spacetimechart/src/components/TimeCaptions.tsx index 6c3dad7ce..c9e67ad4b 100644 --- a/ui-spacetimechart/src/components/TimeCaptions.tsx +++ b/ui-spacetimechart/src/components/TimeCaptions.tsx @@ -3,6 +3,7 @@ import { useCallback } from 'react'; import { useDraw } from '../hooks/useCanvas'; import { MINUTE } from '../lib/consts'; import { type DrawingFunction } from '../lib/types'; +import { displayElementsBasedOnZoom } from '../utils/canvas'; const MINUTES_FORMATTER = (t: number) => `:${new Date(t).getMinutes().toString().padStart(2, '0')}`; const HOURS_FORMATTER = (t: number, pixelsPerMinute: number) => { @@ -71,20 +72,17 @@ const TimeCaptions = () => { return false; }); - // - Keys are times in ms - // - Values are the highest level on each time - const labelMarks: Record = {}; - timeRanges.map((range, i) => { - const labelLevel = labelLevels[i]; - - if (!labelLevel) return; - - let t = Math.floor(minT / range) * range; - while (t <= maxT) { - if (labelLevel) labelMarks[t] = { level: labelLevel, rangeIndex: i }; - t += range; - } + const labelMarkFormatter = (labelLevel: number, i: number) => ({ + level: labelLevel, + rangeIndex: i, }); + const labelMarks = displayElementsBasedOnZoom( + minT, + maxT, + timeRanges, + labelLevels, + labelMarkFormatter + ); // Render caption background: ctx.fillStyle = background; From 9570090c6f038728400e5666c763a8786239bce4 Mon Sep 17 00:00:00 2001 From: Valentin Chanas Date: Mon, 3 Feb 2025 21:03:56 +0100 Subject: [PATCH 4/6] ui-spacetimechart: Simple rectangle --- .../src/components/PatternRect.tsx | 48 +++++--------- .../src/components/SimpleRect.tsx | 62 +++++++++++++++++++ 2 files changed, 76 insertions(+), 34 deletions(-) create mode 100644 ui-spacetimechart/src/components/SimpleRect.tsx diff --git a/ui-spacetimechart/src/components/PatternRect.tsx b/ui-spacetimechart/src/components/PatternRect.tsx index 97f51d686..f9574cecf 100644 --- a/ui-spacetimechart/src/components/PatternRect.tsx +++ b/ui-spacetimechart/src/components/PatternRect.tsx @@ -1,7 +1,6 @@ -import { useCallback } from 'react'; +import React from 'react'; -import { useDraw } from '../hooks/useCanvas'; -import { type DrawingFunction } from '../lib/types'; +import { SimpleRect } from './SimpleRect'; export type PatternRectProps = { timeStart: Date; @@ -21,36 +20,17 @@ export const PatternRect = ({ spaceEnd, imageElement, }: PatternRectProps) => { - const drawRegion = useCallback( - (ctx, { getSpacePixel, getTimePixel, spaceAxis }) => { - const timeStartPixel = getTimePixel(Number(timeStart)); - const endTimePixel = getTimePixel(Number(timeEnd)); - const spaceStartPixel = getSpacePixel(spaceStart); - const spaceEndPixel = getSpacePixel(spaceEnd); - - const areaSpaceSize = spaceEndPixel - spaceStartPixel; - const areaTimeSize = endTimePixel - timeStartPixel; - if (!areaSpaceSize || !areaTimeSize) return; - - const pattern = ctx.createPattern(imageElement, 'repeat'); - if (!pattern) { - return; - } - - ctx.save(); - ctx.fillStyle = pattern; - if (spaceAxis === 'x') { - ctx.translate(spaceStartPixel, timeStartPixel); - ctx.fillRect(0, 0, areaSpaceSize, areaTimeSize); - } else { - ctx.translate(timeStartPixel, spaceStartPixel); - ctx.fillRect(0, 0, areaTimeSize, areaSpaceSize); - } - ctx.restore(); - }, - [timeStart, timeEnd, spaceStart, spaceEnd, imageElement] + const drawPattern = (ctx: CanvasRenderingContext2D) => { + const pattern = ctx.createPattern(imageElement, 'repeat'); + return pattern; + }; + return ( + ); - useDraw('background', drawRegion); - - return null; }; diff --git a/ui-spacetimechart/src/components/SimpleRect.tsx b/ui-spacetimechart/src/components/SimpleRect.tsx new file mode 100644 index 000000000..cec5029c5 --- /dev/null +++ b/ui-spacetimechart/src/components/SimpleRect.tsx @@ -0,0 +1,62 @@ +import { useCallback } from 'react'; + +import { useDraw } from '../hooks/useCanvas'; +import { type DrawingFunction } from '../lib/types'; + +export type FillStyle = + | string + | CanvasPattern + | CanvasGradient + | ((ctx: CanvasRenderingContext2D) => string | CanvasPattern | CanvasGradient | null); + +export type PatternRectProps = { + timeStart: Date; + timeEnd: Date; + spaceStart: number; // mm + spaceEnd: number; // mm + fillStyle: FillStyle; +}; + +export const SimpleRect = ({ + timeStart, + timeEnd, + spaceStart, + spaceEnd, + fillStyle, +}: PatternRectProps) => { + const drawRegion = useCallback( + (ctx, { getSpacePixel, getTimePixel, spaceAxis }) => { + const timeStartPixel = getTimePixel(Number(timeStart)); + const endTimePixel = getTimePixel(Number(timeEnd)); + const spaceStartPixel = getSpacePixel(spaceStart); + const spaceEndPixel = getSpacePixel(spaceEnd); + + const areaSpaceSize = spaceEndPixel - spaceStartPixel; + const areaTimeSize = endTimePixel - timeStartPixel; + if (!areaSpaceSize || !areaTimeSize) return; + + ctx.save(); + if (typeof fillStyle === 'function') { + const pattern = fillStyle(ctx); + if (!pattern) { + return; + } + ctx.fillStyle = pattern; + } else { + ctx.fillStyle = fillStyle; + } + if (spaceAxis === 'x') { + ctx.translate(spaceStartPixel, timeStartPixel); + ctx.fillRect(0, 0, areaSpaceSize, areaTimeSize); + } else { + ctx.translate(timeStartPixel, spaceStartPixel); + ctx.fillRect(0, 0, areaTimeSize, areaSpaceSize); + } + ctx.restore(); + }, + [timeStart, timeEnd, spaceStart, spaceEnd, fillStyle] + ); + useDraw('background', drawRegion); + + return null; +}; From a27742a52e741d52db14a8344f9b87fb6e942518 Mon Sep 17 00:00:00 2001 From: Valentin Chanas Date: Mon, 3 Feb 2025 21:05:49 +0100 Subject: [PATCH 5/6] ui-spacetimechart: Simple rectangle --- .../src/stories/draw-rectangle.stories.tsx | 210 ++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 ui-spacetimechart/src/stories/draw-rectangle.stories.tsx diff --git a/ui-spacetimechart/src/stories/draw-rectangle.stories.tsx b/ui-spacetimechart/src/stories/draw-rectangle.stories.tsx new file mode 100644 index 000000000..420852259 --- /dev/null +++ b/ui-spacetimechart/src/stories/draw-rectangle.stories.tsx @@ -0,0 +1,210 @@ +import React, { useEffect, useMemo, useState } from 'react'; + +import { Button } from '@osrd-project/ui-core'; +import type { Meta } from '@storybook/react'; +import { clamp } from 'lodash'; + +import '@osrd-project/ui-core/dist/theme.css'; + +import { OPERATIONAL_POINTS, PATHS } from './lib/paths'; +import { PathLayer } from '../components/PathLayer'; +import { SimpleRect } from '../components/SimpleRect'; +import { SpaceTimeChart } from '../components/SpaceTimeChart'; +import { type Point, type PathData, type OperationalPoint } from '../lib/types'; +import { getDiff } from '../utils/vectors'; +import { MouseTracker } from './lib/components'; + +const DEFAULT_WIDTH = 900; +const DEFAULT_HEIGHT = 450; + +const MIN_ZOOM = 0; +const MAX_ZOOM = 100; +const MIN_ZOOM_MS_PER_PX = 600000; +const MAX_ZOOM_MS_PER_PX = 625; +const DEFAULT_ZOOM_MS_PER_PX = 7500; +type SpaceTimeHorizontalZoomWrapperProps = { + swapAxes: boolean; + spaceOrigin: number; + xOffset: number; + yOffset: number; + operationalPoints: OperationalPoint[]; + paths: (PathData & { color: string })[]; +}; + +const zoomValueToTimeScale = (slider: number) => + MIN_ZOOM_MS_PER_PX * Math.pow(MAX_ZOOM_MS_PER_PX / MIN_ZOOM_MS_PER_PX, slider / 100); + +const timeScaleToZoomValue = (timeScale: number) => + (100 * Math.log(timeScale / MIN_ZOOM_MS_PER_PX)) / + Math.log(MAX_ZOOM_MS_PER_PX / MIN_ZOOM_MS_PER_PX); + +/** + * This story demonstrates the behavior of drawing a rectangle with the mouse, and how it should behave in regards to the default zoom and pan. + */ +const DrawRectangleWrapper = ({ + swapAxes, + spaceOrigin, + xOffset, + yOffset, + operationalPoints = [], + paths = [], +}: SpaceTimeHorizontalZoomWrapperProps) => { + const [state, setState] = useState<{ + zoomValue: number; + xOffset: number; + yOffset: number; + panning: null | { initialOffset: Point }; + drawing: boolean; + rect: { + timeStart: Date; + timeEnd: Date; + spaceStart: number; // mm + spaceEnd: number; // mm + } | null; + }>({ + zoomValue: timeScaleToZoomValue(DEFAULT_ZOOM_MS_PER_PX), + xOffset, + yOffset, + panning: null, + drawing: false, + rect: null, + }); + useEffect(() => { + setState((prev) => ({ ...prev, xOffset })); + }, [xOffset]); + useEffect(() => { + setState((prev) => ({ ...prev, yOffset })); + }, [yOffset]); + const handleZoom = (zoomValue: number, position = DEFAULT_WIDTH / 2) => { + if (state.drawing) { + return; + } + const boundedXZoom = clamp(zoomValue, MIN_ZOOM, MAX_ZOOM); + const oldTimeScale = zoomValueToTimeScale(state.zoomValue); + const newTimeScale = zoomValueToTimeScale(boundedXZoom); + const newOffset = position - ((position - state.xOffset) * oldTimeScale) / newTimeScale; + setState((prev) => ({ ...prev, zoomValue: boundedXZoom, xOffset: newOffset })); + }; + const simpleOperationalPoints = operationalPoints.map(({ id, position }) => ({ + id, + label: id, + position, + })); + const timeOrigin = +new Date('2024-04-02T00:00:00'); + const timeScale = zoomValueToTimeScale(state.zoomValue); + const spaceScale = useMemo( + () => [ + { + from: -100000, + to: 100000, + coefficient: 300, + }, + ], + [] + ); + return ( +
+ { + handleZoom(state.zoomValue + delta, x); + }} + onPan={({ initialPosition, position, isPanning, data, initialData }) => { + console.log('onPan', position.y, data.position); + const diff = getDiff(initialPosition, position); + setState((prev) => { + // when releasing the mouse, onPan is called one last time with isPanning false + if (!isPanning) { + return { ...prev, panning: null, drawing: false }; + } + if (state.drawing) { + const rect = { + timeStart: new Date(initialData.time), + timeEnd: new Date(data.time), + spaceStart: initialData.position, + spaceEnd: data.position, + }; + + return { + ...prev, + rect, + }; + } + // Start panning: + else if (!prev.panning) { + return { + ...prev, + panning: { + initialOffset: { + x: prev.xOffset, + y: prev.yOffset, + }, + }, + }; + } + // Keep panning: + else { + const { initialOffset } = prev.panning; + return { + ...prev, + xOffset: initialOffset.x + diff.x, + yOffset: initialOffset.y + diff.y, + }; + } + }); + }} + > + {paths.map((path) => ( + + ))} + {state.rect && ( + + )} + + +
+ ); +}; + +export default { + title: 'SpaceTimeChart/Draw rectangle', + component: DrawRectangleWrapper, + tags: ['autodocs'], +} as Meta; + +export const Default = { + args: { + swapAxes: false, + spaceOrigin: 0, + xOffset: 0, + yOffset: 0, + operationalPoints: OPERATIONAL_POINTS, + paths: PATHS.slice(1, 2), + }, +}; From 37dbdc120d58d4cca3a96e825e9c885c43954f76 Mon Sep 17 00:00:00 2001 From: Valentin Chanas Date: Thu, 6 Feb 2025 09:56:16 +0100 Subject: [PATCH 6/6] possible fix to chcek --- .../TrackOccupancyDiagram.stories.tsx | 2 +- .../src/components/SimpleRect.tsx | 3 +- .../src/components/SpaceGraduations.tsx | 2 + .../src/components/SpaceTimeChart.tsx | 5 +- .../src/stories/additional-data.stories.tsx | 1 + .../src/stories/draw-rectangle.stories.tsx | 2 +- .../src/utils/__tests__/scales.spec.ts | 79 +++++++++++++++++++ ui-spacetimechart/src/utils/scales.ts | 15 +++- 8 files changed, 102 insertions(+), 7 deletions(-) create mode 100644 ui-spacetimechart/src/utils/__tests__/scales.spec.ts diff --git a/storybook/stories/TrackOccupancyDiagram/TrackOccupancyDiagram.stories.tsx b/storybook/stories/TrackOccupancyDiagram/TrackOccupancyDiagram.stories.tsx index f11775fea..f2d6736f7 100755 --- a/storybook/stories/TrackOccupancyDiagram/TrackOccupancyDiagram.stories.tsx +++ b/storybook/stories/TrackOccupancyDiagram/TrackOccupancyDiagram.stories.tsx @@ -149,7 +149,7 @@ const TrackOccupancyDiagram = ({ } const getTimePixel = getTimeToPixel(timeOrigin, timePixelOffset, timeScale); - const getSpacePixel = getSpaceToPixel(spacePixelOffset, spaceScaleTree); + const getSpacePixel = getSpaceToPixel(spaceOrigin, spacePixelOffset, spaceScaleTree); const getPoint = getDataToPoint(getTimePixel, getSpacePixel, timeAxis, spaceAxis); const getTime = getPixelToTime(timeOrigin, timePixelOffset, timeScale); const getSpace = getPixelToSpace(spaceOrigin, spacePixelOffset, spaceScaleTree); diff --git a/ui-spacetimechart/src/components/SimpleRect.tsx b/ui-spacetimechart/src/components/SimpleRect.tsx index cec5029c5..79737118d 100644 --- a/ui-spacetimechart/src/components/SimpleRect.tsx +++ b/ui-spacetimechart/src/components/SimpleRect.tsx @@ -25,11 +25,12 @@ export const SimpleRect = ({ fillStyle, }: PatternRectProps) => { const drawRegion = useCallback( - (ctx, { getSpacePixel, getTimePixel, spaceAxis }) => { + (ctx, { getSpacePixel, getTimePixel, spaceAxis, spaceScaleTree }) => { const timeStartPixel = getTimePixel(Number(timeStart)); const endTimePixel = getTimePixel(Number(timeEnd)); const spaceStartPixel = getSpacePixel(spaceStart); const spaceEndPixel = getSpacePixel(spaceEnd); + console.log('rect', spaceStart, spaceEnd, spaceStartPixel, spaceEndPixel, spaceScaleTree); const areaSpaceSize = spaceEndPixel - spaceStartPixel; const areaTimeSize = endTimePixel - timeStartPixel; diff --git a/ui-spacetimechart/src/components/SpaceGraduations.tsx b/ui-spacetimechart/src/components/SpaceGraduations.tsx index 4801d4c0d..53dd24ea3 100644 --- a/ui-spacetimechart/src/components/SpaceGraduations.tsx +++ b/ui-spacetimechart/src/components/SpaceGraduations.tsx @@ -24,6 +24,7 @@ const SpaceGraduations = () => { const styles = spaceGraduationsStyles[point.importanceLevel || 0]; if (!styles) return; + console.log('graduation'); ctx.strokeStyle = styles.color; ctx.lineWidth = styles.width; ctx.globalAlpha = styles.opacity || 1; @@ -34,6 +35,7 @@ const SpaceGraduations = () => { const spacePixel = getSpacePixel(point.position); + console.log('spacePixel', spacePixel); ctx.beginPath(); if (!swapAxis) { ctx.moveTo(0, spacePixel); diff --git a/ui-spacetimechart/src/components/SpaceTimeChart.tsx b/ui-spacetimechart/src/components/SpaceTimeChart.tsx index 189da8e2b..120d3c8f7 100644 --- a/ui-spacetimechart/src/components/SpaceTimeChart.tsx +++ b/ui-spacetimechart/src/components/SpaceTimeChart.tsx @@ -94,6 +94,7 @@ export const SpaceTimeChart = (props: SpaceTimeChartProps) => { const contextState: SpaceTimeChartContextType = useMemo(() => { const spaceScaleTree = spaceScalesToBinaryTree(spaceOrigin, spaceScales); + console.log('tree', spaceOrigin, spaceScales, spaceScaleTree); const timeAxis = !swapAxis ? 'x' : 'y'; const spaceAxis = !swapAxis ? 'y' : 'x'; @@ -109,8 +110,10 @@ export const SpaceTimeChart = (props: SpaceTimeChartProps) => { spacePixelOffset = xOffset; } + // console.log('spaceOrigin', spaceOrigin); + // console.table(spaceScaleTree); const getTimePixel = getTimeToPixel(timeOrigin, timePixelOffset, timeScale); - const getSpacePixel = getSpaceToPixel(spacePixelOffset, spaceScaleTree); + const getSpacePixel = getSpaceToPixel(spaceOrigin, spacePixelOffset, spaceScaleTree); const getPoint = getDataToPoint(getTimePixel, getSpacePixel, timeAxis, spaceAxis); const getTime = getPixelToTime(timeOrigin, timePixelOffset, timeScale); const getSpace = getPixelToSpace(spaceOrigin, spacePixelOffset, spaceScaleTree); diff --git a/ui-spacetimechart/src/stories/additional-data.stories.tsx b/ui-spacetimechart/src/stories/additional-data.stories.tsx index f4a8053b5..66abe8cdd 100644 --- a/ui-spacetimechart/src/stories/additional-data.stories.tsx +++ b/ui-spacetimechart/src/stories/additional-data.stories.tsx @@ -187,6 +187,7 @@ const Wrapper = ({ swapAxis, spaceScaleType }: WrapperProps) => { panning: null, }); + // console.log(state.xOffset, state.yOffset); return (
{ + it('origin 0', () => { + const spaceOrigin = 0; + const yOffset = 0; + const binaryTree = { from: 0, to: 10000, pixelFrom: 0, pixelTo: 200, coefficient: 50 }; + + const pixelToSpace = getPixelToSpace(spaceOrigin, yOffset, binaryTree); + expect(pixelToSpace(0)).toEqual(0); + expect(pixelToSpace(200)).toEqual(10000); + }); + it('origin 5000', () => { + const spaceOrigin = 5000; + const yOffset = 0; + const binaryTree = { from: 0, to: 10000, pixelFrom: 0, pixelTo: 200, coefficient: 50 }; + + const pixelToSpace = getPixelToSpace(spaceOrigin, yOffset, binaryTree); + expect(pixelToSpace(0)).toEqual(5000); + expect(pixelToSpace(200)).toEqual(15000); + }); + it('origin -5000', () => { + const spaceOrigin = -5000; + const yOffset = 0; + const binaryTree = { from: 0, to: 10000, pixelFrom: 0, pixelTo: 200, coefficient: 50 }; + + const pixelToSpace = getPixelToSpace(spaceOrigin, yOffset, binaryTree); + expect(pixelToSpace(0)).toEqual(-5000); + expect(pixelToSpace(200)).toEqual(5000); + }); +}); + +describe('getSpaceToPixel', () => { + it('origin 0', () => { + const spaceOrigin = 0; + const yOffset = 0; + const binaryTree = { from: 0, to: 10000, pixelFrom: 0, pixelTo: 200, coefficient: 50 }; + + const spaceToPixel = getSpaceToPixel(spaceOrigin, yOffset, binaryTree); + expect(spaceToPixel(0)).toEqual(0); + expect(spaceToPixel(5000)).toEqual(100); + expect(spaceToPixel(10000)).toEqual(200); + }); + + it('origin 5000', () => { + const spaceOrigin = 5000; + const yOffset = 0; + const binaryTree = { from: 0, to: 10000, pixelFrom: 0, pixelTo: 200, coefficient: 50 }; + + const spaceToPixel = getSpaceToPixel(spaceOrigin, yOffset, binaryTree); + expect(spaceToPixel(5000)).toEqual(0); + expect(spaceToPixel(10000)).toEqual(100); + expect(spaceToPixel(15000)).toEqual(200); + }); + + it('origin 5000, from 10000 to 20000', () => { + const spaceOrigin = 5000; + const yOffset = 0; + const binaryTree = { from: 10000, to: 20000, pixelFrom: 0, pixelTo: 200, coefficient: 50 }; + + const spaceToPixel = getSpaceToPixel(spaceOrigin, yOffset, binaryTree); + expect(spaceToPixel(10000)).toEqual(-100); + expect(spaceToPixel(15000)).toEqual(0); + expect(spaceToPixel(20000)).toEqual(100); + }); + + it('origin -5000', () => { + const spaceOrigin = -5000; + const yOffset = 0; + const binaryTree = { from: 0, to: 10000, pixelFrom: 0, pixelTo: 200, coefficient: 50 }; + + const spaceToPixel = getSpaceToPixel(spaceOrigin, yOffset, binaryTree); + expect(spaceToPixel(-5000)).toEqual(0); + expect(spaceToPixel(0)).toEqual(100); + expect(spaceToPixel(5000)).toEqual(200); + }); +}); diff --git a/ui-spacetimechart/src/utils/scales.ts b/ui-spacetimechart/src/utils/scales.ts index 8a16d8380..8b08121cd 100644 --- a/ui-spacetimechart/src/utils/scales.ts +++ b/ui-spacetimechart/src/utils/scales.ts @@ -28,7 +28,7 @@ export function spaceScalesToBinaryTree( spaceScales: SpaceScale[], skipSiblingReferences?: boolean ): NormalizedScaleTree { - let prev = spaceOrigin; + let prev = 0; // Step 1: Validate the scales if ( @@ -48,7 +48,7 @@ export function spaceScalesToBinaryTree( // Step 2: Normalize the scales const normalizedScales: NormalizedScale[] = []; let offset = 0; - prev = spaceOrigin; + prev = 0; for (let i = 0; i < spaceScales.length; i++) { const scale = spaceScales[i]; const coefficient = 'coefficient' in scale ? scale.coefficient : (scale.to - prev) / scale.size; @@ -159,12 +159,21 @@ export function getPixelToTime( } export function getSpaceToPixel( + spaceOrigin: number, pixelOffset: number, binaryTree: NormalizedScaleTree ): SpaceToPixel { return (position: number) => { const { from, pixelFrom, coefficient } = getNormalizedScaleAtPosition(position, binaryTree); - return pixelOffset + pixelFrom + (position - from) / coefficient; + console.log( + spaceOrigin, + pixelOffset, + binaryTree, + '=====>', + position, + pixelOffset + pixelFrom + (position - from - spaceOrigin) / coefficient + ); + return pixelOffset + pixelFrom + (position - from - spaceOrigin) / coefficient; }; }