Skip to content

Commit e8f69a4

Browse files
committed
ui-speedspacechart: add declivities layer in gev v2
1 parent c187681 commit e8f69a4

File tree

9 files changed

+251
-16
lines changed

9 files changed

+251
-16
lines changed

ui-speedspacechart/src/__tests__/utils.spec.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
getAdaptiveHeight,
88
positionOnGraphScale,
99
getLinearLayerMarginTop,
10+
slopesValues,
1011
} from '../components/utils';
1112
import type { Store } from '../types/chartTypes';
1213
import type { ConsolidatedPositionSpeedTime } from '../types/simulationTypes';
@@ -60,7 +61,8 @@ describe('getGraphOffsets', () => {
6061
it('should return the correct width and height offsets', () => {
6162
const width = 100;
6263
const height = 200;
63-
const { WIDTH_OFFSET, HEIGHT_OFFSET } = getGraphOffsets(width, height);
64+
const declivities = true;
65+
const { WIDTH_OFFSET, HEIGHT_OFFSET } = getGraphOffsets(width, height, declivities);
6466
expect(WIDTH_OFFSET).toBe(40);
6567
expect(HEIGHT_OFFSET).toBe(120);
6668
});
@@ -94,6 +96,15 @@ describe('maxPositionValues', () => {
9496
});
9597
});
9698

99+
describe('slopesValues', () => {
100+
it('should return the correct minGradient and maxGradient', () => {
101+
const { minGradient, maxGradient, slopesRange } = slopesValues(store);
102+
expect(minGradient).toBe(-10);
103+
expect(maxGradient).toBe(50);
104+
expect(slopesRange).toBe(60);
105+
});
106+
});
107+
97108
describe('clearCanvas', () => {
98109
it('should clear the canvas', () => {
99110
const fn = () => vi.fn();

ui-speedspacechart/src/components/SpeedSpaceChart.tsx

+32-7
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ import PowerRestrictionsLayer from './layers/PowerRestrictionsLayer';
1818
import SettingsPanel from './common/SettingsPanel';
1919
import InteractionButtons from './common/InteractionButtons';
2020
import { LINEAR_LAYERS_HEIGHTS } from './const';
21+
import TickLayerYRight from './layers/TickLayerYRight';
22+
import DeclivityLayer from './layers/DeclivityLayer';
23+
import { MARGINS } from './const';
2124

2225
export type SpeedSpaceChartProps = {
2326
width: number;
@@ -85,9 +88,17 @@ const SpeedSpaceChart = ({
8588
isSettingsPanelOpened: false,
8689
});
8790

88-
const { WIDTH_OFFSET, HEIGHT_OFFSET } = getGraphOffsets(width, height);
91+
const { WIDTH_OFFSET, HEIGHT_OFFSET } = getGraphOffsets(
92+
width,
93+
height,
94+
store.layersDisplay.declivities
95+
);
8996
const dynamicHeight = getAdaptiveHeight(height, store.layersDisplay);
9097
const dynamicHeightOffset = getAdaptiveHeight(HEIGHT_OFFSET, store.layersDisplay);
98+
const { OFFSET_RIGHT_AXIS } = MARGINS;
99+
const adjustedWidthRightAxis = store.layersDisplay.declivities
100+
? width - OFFSET_RIGHT_AXIS
101+
: width;
91102

92103
const [showDetailsBox, setShowDetailsBox] = useState(false);
93104

@@ -136,11 +147,17 @@ const SpeedSpaceChart = ({
136147
}}
137148
tabIndex={0}
138149
>
139-
<div className="flex justify-end absolute base-margin-top" style={{ width: width }}>
150+
<div
151+
className="flex justify-end absolute base-margin-top"
152+
style={{ width: adjustedWidthRightAxis }}
153+
>
140154
<InteractionButtons reset={reset} openSettingsPanel={openSettingsPanel} store={store} />
141155
</div>
142156
{store.isSettingsPanelOpened && (
143-
<div className="flex justify-end absolute ml-2 base-margin-top" style={{ width: width }}>
157+
<div
158+
className="flex justify-end absolute ml-2 base-margin-top"
159+
style={{ width: adjustedWidthRightAxis }}
160+
>
144161
<SettingsPanel
145162
color={backgroundColor}
146163
store={store}
@@ -149,10 +166,13 @@ const SpeedSpaceChart = ({
149166
/>
150167
</div>
151168
)}
169+
{store.layersDisplay.declivities && (
170+
<DeclivityLayer width={WIDTH_OFFSET} height={HEIGHT_OFFSET} store={store} />
171+
)}
152172
<CurveLayer width={WIDTH_OFFSET} height={HEIGHT_OFFSET} store={store} />
153-
<AxisLayerY width={width} height={height} store={store} />
154-
<MajorGridY width={width} height={height} store={store} />
155-
<AxisLayerX width={width} height={height} store={store} />
173+
<AxisLayerY width={adjustedWidthRightAxis} height={height} store={store} />
174+
<MajorGridY width={adjustedWidthRightAxis} height={height} store={store} />
175+
<AxisLayerX width={adjustedWidthRightAxis} height={height} store={store} />
156176
{store.layersDisplay.steps && (
157177
<>
158178
<StepLayer width={WIDTH_OFFSET} height={HEIGHT_OFFSET} store={store} />
@@ -180,14 +200,19 @@ const SpeedSpaceChart = ({
180200
/>
181201
)}
182202
<TickLayerX width={width} height={dynamicHeight} store={store} />
203+
204+
{store.layersDisplay.declivities && (
205+
<TickLayerYRight width={width} height={height} store={store} />
206+
)}
183207
<ReticleLayer
184-
width={width}
208+
width={adjustedWidthRightAxis}
185209
height={dynamicHeight}
186210
heightOffset={dynamicHeightOffset}
187211
store={store}
188212
showDetailsBox={showDetailsBox}
189213
setShowDetailsBox={setShowDetailsBox}
190214
/>
215+
191216
<FrontInteractivityLayer
192217
width={WIDTH_OFFSET}
193218
height={dynamicHeightOffset}

ui-speedspacechart/src/components/const.ts

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { Store } from '../types/chartTypes';
22

3+
export const SLOPE_FILL_COLOR = '#CFDDCE';
4+
5+
export const RIGHT_TICK_HEIGHT_OFFSET = 2;
6+
37
export const MARGINS = {
48
MARGIN_LEFT: 48,
59
MARGIN_RIGHT: 12,
@@ -8,6 +12,8 @@ export const MARGINS = {
812
CURVE_MARGIN_TOP: 40,
913
CURVE_MARGIN_SIDES: 16,
1014
ELECTRICAL_PROFILES_MARGIN_TOP: 8,
15+
RIGHT_TICK_MARGINS: 60,
16+
OFFSET_RIGHT_AXIS: 42,
1117
};
1218

1319
export const LINEAR_LAYERS_HEIGHTS = {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { clearCanvas, slopesValues, maxPositionValues } from '../../utils';
2+
import type { Store } from '../../../types/chartTypes';
3+
import { MARGINS } from '../../const';
4+
import { SLOPE_FILL_COLOR } from '../../../components/const';
5+
6+
const { CURVE_MARGIN_SIDES, MARGIN_TOP, MARGIN_BOTTOM, RIGHT_TICK_MARGINS } = MARGINS;
7+
8+
export const drawDeclivity = (
9+
ctx: CanvasRenderingContext2D,
10+
width: number,
11+
height: number,
12+
store: Store
13+
) => {
14+
const { slopes, ratioX, leftOffset } = store;
15+
16+
const { maxGradient, slopesRange } = slopesValues(store);
17+
const { maxPosition } = maxPositionValues(store);
18+
19+
if (!slopes || slopes.length === 0) {
20+
console.error('Slopes data is missing or empty.');
21+
return;
22+
}
23+
24+
clearCanvas(ctx, width, height);
25+
26+
ctx.save();
27+
ctx.translate(leftOffset, 0);
28+
29+
// Calculate total height available for drawing, excluding the margins
30+
const availableHeight = height - MARGIN_TOP - MARGIN_BOTTOM - RIGHT_TICK_MARGINS / 2;
31+
32+
// Calculate the vertical center of the chart
33+
const centerY = MARGIN_TOP + RIGHT_TICK_MARGINS / 2 + availableHeight / 2;
34+
35+
ctx.fillStyle = SLOPE_FILL_COLOR;
36+
37+
try {
38+
const coef = ((width - CURVE_MARGIN_SIDES) / maxPosition) * ratioX;
39+
40+
for (let i = 0; i < slopes.length - 1; i++) {
41+
const current = slopes[i];
42+
const next = slopes[i + 1];
43+
44+
const x1 = current.position * coef + CURVE_MARGIN_SIDES / 2;
45+
const x2 = next.position * coef + CURVE_MARGIN_SIDES / 2;
46+
47+
const rectWidth = x2 - x1;
48+
49+
const rectHeight = (current.gradient / maxGradient) * (availableHeight / 2);
50+
51+
const rectY = current.gradient >= 0 ? centerY - rectHeight : centerY;
52+
53+
ctx.fillRect(x1, rectY, rectWidth, Math.abs(rectHeight));
54+
}
55+
} catch (error) {
56+
console.error('Error while drawing declivity:', error);
57+
}
58+
ctx.restore();
59+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { clearCanvas, slopesValues } from '../../utils';
2+
import type { Store } from '../../../types/chartTypes';
3+
import { MARGINS, RIGHT_TICK_HEIGHT_OFFSET } from '../../const';
4+
5+
const { MARGIN_LEFT, MARGIN_TOP, MARGIN_BOTTOM, RIGHT_TICK_MARGINS } = MARGINS;
6+
7+
export const drawTickYRight = (
8+
ctx: CanvasRenderingContext2D,
9+
width: number,
10+
height: number,
11+
store: Store
12+
) => {
13+
clearCanvas(ctx, width, height);
14+
15+
const { minGradient, maxGradient } = slopesValues(store);
16+
17+
// Calculate total height available for ticks excluding the margins
18+
const availableHeight = height - MARGIN_TOP - MARGIN_BOTTOM - RIGHT_TICK_MARGINS;
19+
20+
const tickSpacing = availableHeight / 12; // 12 intervals for 13 ticks
21+
22+
// Calculate the vertical center of the chart
23+
const centerY =
24+
MARGIN_TOP + RIGHT_TICK_MARGINS / 2 + availableHeight / 2 + RIGHT_TICK_HEIGHT_OFFSET;
25+
26+
const textOffsetX = width - MARGIN_LEFT + 10;
27+
const tickWidth = 6;
28+
29+
ctx.font = 'normal 12px IBM Plex Sans';
30+
31+
// Calculate gradient step to avoid decimals
32+
const maxAbsGradient = Math.max(maxGradient, minGradient);
33+
const roundedMaxAbsGradient = Math.ceil(maxAbsGradient / 6) * 6;
34+
const gradientStep = roundedMaxAbsGradient / 6;
35+
36+
ctx.beginPath();
37+
for (let i = -6; i <= 6; i++) {
38+
const tickValue = i * gradientStep;
39+
const tickY = centerY - i * tickSpacing;
40+
41+
ctx.moveTo(width - MARGIN_LEFT - tickWidth, tickY);
42+
ctx.lineTo(width - MARGIN_LEFT, tickY);
43+
ctx.strokeStyle = 'rgb(121, 118, 113)';
44+
ctx.stroke();
45+
46+
ctx.textAlign = 'left';
47+
const text = tickValue.toString();
48+
const textPositionYRight = tickY + 4;
49+
const opacity = 1;
50+
51+
ctx.fillStyle = `rgba(182, 179, 175, ${opacity})`;
52+
ctx.fillText(text, textOffsetX, textPositionYRight);
53+
54+
const maxTickY = centerY - 6 * tickSpacing;
55+
ctx.fillText('‰', width - MARGIN_LEFT, height / 12); // 12 ticks intervals
56+
console.log({ maxTickY, tickSpacing, height }, 'for ticks');
57+
}
58+
// prevent overlapping with margin top
59+
ctx.clearRect(0, 0, width, MARGIN_TOP);
60+
ctx.clearRect(width - MARGIN_LEFT, height - MARGIN_BOTTOM, width, MARGIN_BOTTOM);
61+
ctx.clearRect(0, height - MARGIN_BOTTOM + 6, width - MARGIN_LEFT, MARGIN_BOTTOM);
62+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import React from 'react';
2+
import type { Store } from '../../types/chartTypes';
3+
import { drawDeclivity } from '../helpers/drawElements/declivity';
4+
import { useCanvas } from '../hooks';
5+
6+
type DeclivityLayerProps = {
7+
width: number;
8+
height: number;
9+
store: Store;
10+
};
11+
12+
const DeclivityLayer = ({ width, height, store }: DeclivityLayerProps) => {
13+
const canvas = useCanvas(drawDeclivity, width, height, store);
14+
15+
return (
16+
<canvas
17+
id="declivity-layer"
18+
className="absolute rounded-t-xl"
19+
ref={canvas}
20+
width={width}
21+
height={height}
22+
/>
23+
);
24+
};
25+
26+
export default DeclivityLayer;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import React from 'react';
2+
import type { Store } from '../../types/chartTypes';
3+
4+
import { useCanvas } from '../hooks';
5+
import { drawTickYRight } from '../helpers/drawElements/tickYRight';
6+
7+
type TickLayerYRightProps = {
8+
width: number;
9+
height: number;
10+
store: Store;
11+
};
12+
13+
const TickLayerYRight = ({ width, height, store }: TickLayerYRightProps) => {
14+
const canvas = useCanvas(drawTickYRight, width, height, store);
15+
16+
return (
17+
<canvas
18+
id="tick-layer-y-right"
19+
className="absolute rounded-t-xl"
20+
ref={canvas}
21+
width={width}
22+
height={height}
23+
/>
24+
);
25+
};
26+
27+
export default TickLayerYRight;

ui-speedspacechart/src/components/utils.ts

+21-2
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,15 @@ type MaxPositionValues = {
1818
intermediateTicksPosition: number;
1919
};
2020

21-
export const getGraphOffsets = (width: number, height: number) => {
22-
const WIDTH_OFFSET = width - 60;
21+
type SlopesValues = {
22+
minGradient: number;
23+
maxGradient: number;
24+
slopesRange: number;
25+
maxPosition: number;
26+
};
27+
28+
export const getGraphOffsets = (width: number, height: number, declivities?: boolean) => {
29+
const WIDTH_OFFSET = declivities ? width - 102 : width - 60; // +2px so that the tick appears on the right of the chart
2330
const HEIGHT_OFFSET = height - 80;
2431
return { WIDTH_OFFSET, HEIGHT_OFFSET };
2532
};
@@ -188,3 +195,15 @@ export const checkLayerData = (store: Store, selection: (typeof LAYERS_SELECTION
188195
(selection === 'electricalProfiles' || selection === 'powerRestrictions') && !store[selection]
189196
);
190197
};
198+
/**
199+
* Given a store including a list of slopes, return the position and value of min and max slopes
200+
* @param store
201+
*/
202+
export const slopesValues = (store: Store): SlopesValues => {
203+
const slopes = store.slopes;
204+
const minGradient = Math.min(...slopes.map((data) => data.gradient));
205+
const maxGradient = Math.max(...slopes.map((data) => data.gradient));
206+
const slopesRange = maxGradient - minGradient;
207+
const maxPosition = Math.max(...slopes.map((data) => data.position));
208+
return { minGradient, maxGradient, slopesRange, maxPosition };
209+
};

ui-speedspacechart/src/styles/main.css

+6-6
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,18 @@
1313
}
1414

1515
.base-margin-top {
16-
margin-top: 1.6875rem;
16+
margin-top: 1.6875rem;
1717
}
1818

1919
#curve-layer,
2020
#step-layer,
21-
#front-interactivity-layer {
21+
#declivity-layer {
2222
margin-left: 3rem;
2323
margin-top: 1.6875rem;
24-
}
25-
26-
#curve-layer {
27-
background-color: rgb(255, 255, 255);
24+
box-shadow:
25+
0 0.0625rem 0,
26+
125rem 0 rgba(0, 0, 0, 0.19);
27+
box-shadow: 0 0.25rem 0.5625rem 0 rgba(0, 0, 0, 0.06);
2828
}
2929

3030
#front-interactivity-layer {

0 commit comments

Comments
 (0)