Skip to content

Commit 26e2e43

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

File tree

8 files changed

+228
-13
lines changed

8 files changed

+228
-13
lines changed

ui-speedspacechart/src/components/SpeedSpaceChart.tsx

+24-5
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

@@ -149,10 +160,13 @@ const SpeedSpaceChart = ({
149160
/>
150161
</div>
151162
)}
163+
{store.layersDisplay.declivities && (
164+
<DeclivityLayer width={WIDTH_OFFSET} height={HEIGHT_OFFSET} store={store} />
165+
)}
152166
<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} />
167+
<AxisLayerY width={adjustedWidthRightAxis} height={height} store={store} />
168+
<MajorGridY width={adjustedWidthRightAxis} height={height} store={store} />
169+
<AxisLayerX width={adjustedWidthRightAxis} height={height} store={store} />
156170
{store.layersDisplay.steps && (
157171
<>
158172
<StepLayer width={WIDTH_OFFSET} height={HEIGHT_OFFSET} store={store} />
@@ -180,14 +194,19 @@ const SpeedSpaceChart = ({
180194
/>
181195
)}
182196
<TickLayerX width={width} height={dynamicHeight} store={store} />
197+
198+
{store.layersDisplay.declivities && (
199+
<TickLayerYRight width={width} height={height} store={store} />
200+
)}
183201
<ReticleLayer
184-
width={width}
202+
width={adjustedWidthRightAxis}
185203
height={dynamicHeight}
186204
heightOffset={dynamicHeightOffset}
187205
store={store}
188206
showDetailsBox={showDetailsBox}
189207
setShowDetailsBox={setShowDetailsBox}
190208
/>
209+
191210
<FrontInteractivityLayer
192211
width={WIDTH_OFFSET}
193212
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,59 @@
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+
55+
// prevent overlapping with margin top
56+
ctx.clearRect(0, 0, width, MARGIN_TOP);
57+
ctx.clearRect(width - MARGIN_LEFT, height - MARGIN_BOTTOM, width, MARGIN_BOTTOM);
58+
ctx.clearRect(0, height - MARGIN_BOTTOM + 6, width - MARGIN_LEFT, MARGIN_BOTTOM);
59+
};
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)