@sailingrotevista/rotevista-dash 8.0.1 → 8.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of @sailingrotevista/rotevista-dash might be problematic. Click here for more details.

@@ -1,116 +0,0 @@
1
- import React, { useRef } from 'react';
2
- import { BoxType } from '../types';
3
-
4
- interface DataBoxProps {
5
- type: BoxType;
6
- label: string;
7
- unit?: string;
8
- value?: string;
9
- valueColor?: string;
10
- isLargeValue?: boolean;
11
- unstable?: boolean;
12
- alarmClass?: string;
13
- isFocused?: boolean;
14
- isHercules?: boolean;
15
- children?: React.ReactNode;
16
- onLongPress?: () => void;
17
- onDoubleTap?: () => void;
18
- onSingleTap?: () => void;
19
- }
20
-
21
- export const DataBox: React.FC<DataBoxProps> = ({
22
- type,
23
- label,
24
- unit,
25
- value,
26
- valueColor,
27
- isLargeValue = false,
28
- unstable = false,
29
- alarmClass = '',
30
- isFocused = false,
31
- isHercules = false,
32
- children,
33
- onLongPress,
34
- onDoubleTap,
35
- onSingleTap,
36
- }) => {
37
- const pressTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
38
- const isLongPressActiveRef = useRef(false);
39
- const lastTapTimeRef = useRef(0);
40
- const tapTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
41
-
42
- const handlePointerDown = () => {
43
- isLongPressActiveRef.current = false;
44
- pressTimerRef.current = setTimeout(() => {
45
- isLongPressActiveRef.current = true;
46
- if (onLongPress) onLongPress();
47
- lastTapTimeRef.current = 0;
48
- }, 1000);
49
- };
50
-
51
- const handlePointerUp = () => {
52
- if (pressTimerRef.current) {
53
- clearTimeout(pressTimerRef.current);
54
- pressTimerRef.current = null;
55
- }
56
- if (isLongPressActiveRef.current) return;
57
-
58
- const currentTime = Date.now();
59
- const tapDelay = currentTime - lastTapTimeRef.current;
60
-
61
- if (tapDelay < 300 && tapDelay > 0) {
62
- if (tapTimeoutRef.current) clearTimeout(tapTimeoutRef.current);
63
- if (onDoubleTap) onDoubleTap();
64
- lastTapTimeRef.current = 0;
65
- } else {
66
- lastTapTimeRef.current = currentTime;
67
- tapTimeoutRef.current = setTimeout(() => {
68
- if (onSingleTap) onSingleTap();
69
- }, 250);
70
- }
71
- };
72
-
73
- const handlePointerLeave = () => {
74
- if (pressTimerRef.current) {
75
- clearTimeout(pressTimerRef.current);
76
- pressTimerRef.current = null;
77
- }
78
- isLongPressActiveRef.current = false;
79
- };
80
-
81
- const boxClasses = [
82
- 'data-box',
83
- `box-${type}`,
84
- isFocused ? 'is-focused' : '',
85
- isHercules ? 'box-hercules' : '',
86
- ]
87
- .filter(Boolean)
88
- .join(' ');
89
-
90
- return (
91
- <div
92
- className={boxClasses}
93
- onPointerDown={handlePointerDown}
94
- onPointerUp={handlePointerUp}
95
- onPointerLeave={handlePointerLeave}
96
- >
97
- <div className="label-row">
98
- <span className="label" id={type === 'sog' ? 'sog-vmg-label' : type === 'tws' ? 'tws-aws-label' : undefined}>
99
- {label}
100
- </span>
101
- {unit && <span className="unit">{unit}</span>}
102
- </div>
103
-
104
- {value !== undefined && (
105
- <span
106
- className={`${isLargeValue ? 'value-large' : 'value'} ${unstable ? 'unstable-data' : ''} ${alarmClass}`}
107
- id={type}
108
- style={valueColor ? { color: valueColor } : undefined}
109
- dangerouslySetInnerHTML={{ __html: value }}
110
- />
111
- )}
112
-
113
- {children}
114
- </div>
115
- );
116
- };
@@ -1,120 +0,0 @@
1
- import React from 'react';
2
-
3
- interface MiniCompassProps {
4
- boatRotDeg: number;
5
- windRotDeg: number;
6
- compassCwDot: { active: boolean; color: string };
7
- compassCcwDot: { active: boolean; color: string };
8
- }
9
-
10
- export const MiniCompass: React.FC<MiniCompassProps> = ({
11
- boatRotDeg,
12
- windRotDeg,
13
- compassCwDot,
14
- compassCcwDot,
15
- }) => {
16
- return (
17
- <svg className="mini-compass" viewBox="0 0 60 60">
18
- <defs>
19
- <radialGradient id="compass-plate-grad" cx="50%" cy="50%" r="50%">
20
- <stop offset="0%" stopColor="#ffffff" />
21
- <stop offset="85%" stopColor="#f4f6f8" />
22
- <stop offset="100%" stopColor="#e2e8f0" />
23
- </radialGradient>
24
- </defs>
25
-
26
- {/* Outer Dial Bezel and Inner Card */}
27
- <circle className="mini-compass-bezel" cx="30" cy="30" r="28.5" fill="#f8fafc" stroke="#cbd5e1" strokeWidth="1.8" />
28
- <circle className="mini-compass-dial" cx="30" cy="30" r="25.5" fill="url(#compass-plate-grad)" stroke="#94a3b8" strokeWidth="0.8" />
29
-
30
- {/* Graduation Ticks (every 30 degrees) */}
31
- {[0, 30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330].map((deg) => {
32
- const isMajor = deg % 90 === 0;
33
- return (
34
- <line
35
- key={`mini-tick-${deg}`}
36
- className={`mini-tick ${isMajor ? 'mini-tick-major' : 'mini-tick-minor'}`}
37
- x1="30"
38
- y1="5.5"
39
- x2="30"
40
- y2={isMajor ? '8.5' : '7.2'}
41
- stroke={isMajor ? '#475569' : '#94a3b8'}
42
- strokeWidth={isMajor ? 1.4 : 0.9}
43
- strokeLinecap="round"
44
- transform={`rotate(${deg}, 30, 30)`}
45
- />
46
- );
47
- })}
48
-
49
- {/* Cardinal Letters */}
50
- <text className="mini-cardinal-n" x="30" y="14" fill="#dc2626" fontSize="6.2" textAnchor="middle" fontWeight="900" fontFamily="system-ui, sans-serif">
51
- N
52
- </text>
53
- <text className="mini-cardinal" x="30" y="49" fill="#334155" fontSize="5.2" textAnchor="middle" fontWeight="800" fontFamily="system-ui, sans-serif">
54
- S
55
- </text>
56
- <text className="mini-cardinal" x="47.5" y="32" fill="#64748b" fontSize="5" textAnchor="middle" dominantBaseline="middle" fontWeight="800" fontFamily="system-ui, sans-serif">
57
- E
58
- </text>
59
- <text className="mini-cardinal" x="12.5" y="32" fill="#64748b" fontSize="5" textAnchor="middle" dominantBaseline="middle" fontWeight="800" fontFamily="system-ui, sans-serif">
60
- W
61
- </text>
62
-
63
- {/* Boat Heading Silhouette (Sharp nautical yacht hull pointing to HDG) */}
64
- <g id="twd-boat-wrap" transform={`rotate(${boatRotDeg}, 30, 30)`}>
65
- {/* Yacht Hull */}
66
- <path
67
- className="mini-boat-hull"
68
- d="M 30,19 C 26.6,24.5 25.5,31.5 26.5,37 C 27.8,38.8 32.2,38.8 33.5,37 C 34.5,31.5 33.4,24.5 30,19 Z"
69
- fill="#1e293b"
70
- stroke="#ffffff"
71
- strokeWidth="0.8"
72
- />
73
- {/* Cockpit / Mast Center Point */}
74
- <circle cx="30" cy="29" r="1.1" fill="#ffffff" />
75
- </g>
76
-
77
- {/* True Wind Direction Indicator (Bold tactical chevron & arrow pointing inward) */}
78
- <g id="twd-arrow" transform={`rotate(${windRotDeg}, 30, 30)`}>
79
- {/* Tactical Wind Chevron */}
80
- <path
81
- id="twd-wind-chevron"
82
- className="mini-wind-chevron"
83
- d="M 24.5,3.8 L 30,8.8 L 35.5,3.8"
84
- fill="none"
85
- stroke="#0284c7"
86
- strokeWidth="2.6"
87
- strokeLinecap="round"
88
- strokeLinejoin="round"
89
- />
90
- {/* Wind Inflow Stem */}
91
- <line
92
- x1="30"
93
- y1="8.8"
94
- x2="30"
95
- y2="13.2"
96
- stroke="#0284c7"
97
- strokeWidth="2.2"
98
- strokeLinecap="round"
99
- />
100
- {/* Strategic shift trend dots */}
101
- <circle
102
- id="trend-dot-cw"
103
- cx="37"
104
- cy="8.5"
105
- r="1.8"
106
- fill={compassCwDot.color}
107
- className={compassCwDot.active ? 'is-trending' : ''}
108
- />
109
- <circle
110
- id="trend-dot-ccw"
111
- cx="23"
112
- cy="8.5"
113
- r="1.8"
114
- fill={compassCcwDot.color}
115
- className={compassCcwDot.active ? 'is-trending' : ''}
116
- />
117
- </g>
118
- </svg>
119
- );
120
- };
@@ -1,343 +0,0 @@
1
- import React, { useMemo } from 'react';
2
- import { HistoryPoint, AppConfig, GraphMode } from '../types';
3
-
4
- interface SparklineProps {
5
- id: string;
6
- type: 'stw' | 'sog' | 'depth' | 'tws';
7
- data: HistoryPoint[];
8
- config: AppConfig;
9
- mode: GraphMode;
10
- isNavigating: boolean;
11
- displayMode: string;
12
- isFocused: boolean;
13
- onScaleChange?: (min: number, max: number) => void;
14
- }
15
-
16
- export const Sparkline: React.FC<SparklineProps> = ({
17
- id,
18
- type,
19
- data,
20
- config,
21
- mode,
22
- isNavigating,
23
- displayMode,
24
- isFocused,
25
- }) => {
26
- const w = 200;
27
- const h = 40;
28
-
29
- const visibleMinutes = (config.graphs.historyMinutes || 5) * (isNavigating ? 1 : 2);
30
- const viewportMs = visibleMinutes * 60000;
31
- const now = Date.now();
32
- const viewportStart = now - viewportMs;
33
-
34
- // 1. FILTER, SORT AND DEDUPLICATE STRICTLY BY TIME
35
- const visibleData = useMemo(() => {
36
- if (!data || data.length === 0) return [];
37
-
38
- // Filter points in viewport
39
- const inRange = data.filter(p => p && isFinite(p.val) && typeof p.time === 'number' && p.time >= viewportStart);
40
-
41
- // Sort ascending by time
42
- inRange.sort((a, b) => a.time - b.time);
43
-
44
- // Strictly ensure time increases monotonically (no duplicates, no backward steps)
45
- const sanitized: HistoryPoint[] = [];
46
- for (let i = 0; i < inRange.length; i++) {
47
- const pt = inRange[i];
48
- if (sanitized.length === 0 || pt.time > sanitized[sanitized.length - 1].time) {
49
- sanitized.push(pt);
50
- }
51
- }
52
- return sanitized;
53
- }, [data, viewportStart]);
54
-
55
- // 2. SCALE CALCULATION (Exact match with original charts.js calculateScale)
56
- const { min, max } = useMemo(() => {
57
- const s = config.scales[type] || { stdMax: 10, step: 2, hercSpan: 2 };
58
- if (!visibleData || visibleData.length === 0) {
59
- return { min: 0, max: s.stdMax };
60
- }
61
-
62
- const values = visibleData.map(p => p.val);
63
- const currentVal = values[values.length - 1];
64
-
65
- if (type === 'depth') {
66
- const shallowThreshold = Math.max(s.stdMax, 10);
67
- if (mode !== 'hercules') {
68
- const maxHistorico = Math.max(...values);
69
- if (currentVal <= shallowThreshold) {
70
- return { min: 0, max: shallowThreshold };
71
- }
72
- return { min: 0, max: Math.max(s.stdMax, Math.ceil(maxHistorico / s.step) * s.step) };
73
- } else {
74
- const roundStep = s.hercSpan;
75
- const localMin = Math.min(...values);
76
- const localMax = Math.max(...values);
77
- let targetMin = 0;
78
- if (localMin > shallowThreshold) {
79
- targetMin = Math.max(0, Math.floor(localMin / roundStep) * roundStep);
80
- }
81
- let targetMax = Math.ceil(localMax / roundStep) * roundStep;
82
- if (targetMax - targetMin < 4) {
83
- targetMax = targetMin + 4;
84
- }
85
- return { min: targetMin, max: targetMax };
86
- }
87
- }
88
-
89
- // STW, SOG, TWS
90
- if (mode !== 'hercules') {
91
- const maxHistorico = Math.max(...values);
92
- return { min: 0, max: Math.max(s.stdMax, Math.ceil(maxHistorico / s.step) * s.step) };
93
- } else {
94
- const roundStep = s.hercSpan;
95
- const oneThirdCount = Math.max(1, Math.floor(values.length / 3));
96
- const recentThirdData = values.slice(-oneThirdCount);
97
- const localMin = Math.min(...recentThirdData);
98
- const localMax = Math.max(...recentThirdData);
99
-
100
- let targetMin = Math.max(0, Math.floor(localMin / roundStep) * roundStep);
101
- let targetMax = Math.ceil(localMax / roundStep) * roundStep;
102
- if (targetMax - targetMin === 0) {
103
- targetMax = targetMin + roundStep;
104
- }
105
- return { min: targetMin, max: targetMax };
106
- }
107
- }, [visibleData, config.scales, type, mode]);
108
-
109
- const range = Math.max(0.1, max - min);
110
- const isDepth = type === 'depth';
111
- const isTws = type === 'tws';
112
-
113
- const colDanger = '#ff3b30';
114
- const colWarning = '#ff9800';
115
- const colTws = '#2c3e50';
116
- const colAws = '#5c6bc0';
117
- const colDepth = '#0088cc';
118
- const colStw = '#00C851';
119
- const colSog = '#ffbb33';
120
- const colVmg = '#00b8d4';
121
-
122
- const getColorProps = (val: number) => {
123
- const baseStroke = isFocused ? '4.2' : '1.6';
124
- const alertStroke = isFocused ? '4.8' : '2.2';
125
- const warnStroke = isFocused ? '4.4' : '1.8';
126
-
127
- let color = colTws;
128
- let opacity = '0.15';
129
- let stroke = baseStroke;
130
-
131
- if (isTws) {
132
- const baseWind = displayMode === 'AWS' ? colAws : colTws;
133
- const r1 = config.graphs.reef1 || 15;
134
- const r2 = config.graphs.reef2 || 20;
135
- const r3 = r2 + (r2 - r1);
136
-
137
- if (val >= r3) {
138
- color = '#9c27b0';
139
- opacity = '0.65';
140
- stroke = alertStroke;
141
- } else if (val >= r2) {
142
- color = colDanger;
143
- opacity = '0.55';
144
- stroke = alertStroke;
145
- } else if (val >= r1) {
146
- color = colWarning;
147
- opacity = '0.45';
148
- stroke = warnStroke;
149
- } else {
150
- color = baseWind;
151
- }
152
- } else if (isDepth) {
153
- if (val < config.alarms.depthDanger) {
154
- color = colDanger;
155
- opacity = '0.55';
156
- stroke = alertStroke;
157
- } else if (val < config.alarms.depthWarning) {
158
- color = colWarning;
159
- opacity = '0.45';
160
- stroke = warnStroke;
161
- } else {
162
- color = colDepth;
163
- }
164
- } else {
165
- if (id.includes('stw')) color = colStw;
166
- else if (id.includes('sog')) color = displayMode === 'VMG' ? colVmg : colSog;
167
- }
168
- return { color, opacity, stroke };
169
- };
170
-
171
- // Horizontal gridlines
172
- const horizontalGridY = [0.25, 0.5, 0.75].map(p => h - p * h);
173
-
174
- // Vertical time gridlines
175
- const gridInterval = visibleMinutes <= 15 ? 1 : 5;
176
- const verticalGridX: number[] = [];
177
- for (let m = gridInterval; m < visibleMinutes; m += gridInterval) {
178
- verticalGridX.push(w - (m / visibleMinutes) * w);
179
- }
180
-
181
- // Depth warning and danger lines
182
- const dangerY =
183
- isDepth && config.alarms.depthDanger >= min && config.alarms.depthDanger <= max
184
- ? h - ((config.alarms.depthDanger - min) / range) * h
185
- : null;
186
- const warningY =
187
- isDepth && config.alarms.depthWarning >= min && config.alarms.depthWarning <= max
188
- ? h - ((config.alarms.depthWarning - min) / range) * h
189
- : null;
190
- const alarmStrokeWidth = isFocused ? '4.8' : '1.8';
191
-
192
- // 3. CURVE AND AREA GEOMETRY GENERATION
193
- const { lineElements, areaPath, gradientStops } = useMemo(() => {
194
- if (visibleData.length < 2) {
195
- return { lineElements: [], areaPath: '', gradientStops: [] };
196
- }
197
-
198
- const lines: React.ReactNode[] = [];
199
- let path = '';
200
- const stops: Array<{ offset: number; color: string; opacity: string }> = [];
201
- let started = false;
202
-
203
- const expectedInterval = viewportMs / (config.graphs.samples || 60);
204
- const maxAllowedGap = Math.max(expectedInterval * 2.5, 90000);
205
-
206
- for (let i = 1; i < visibleData.length; i++) {
207
- const pA = visibleData[i - 1];
208
- const pB = visibleData[i];
209
-
210
- const x1 = Math.max(0, Math.min(w, ((pA.time - viewportStart) / viewportMs) * w));
211
- const x2 = Math.max(0, Math.min(w, ((pB.time - viewportStart) / viewportMs) * w));
212
- const y1 = h - Math.max(0, Math.min(1, (pA.val - min) / range)) * h;
213
- const y2 = h - Math.max(0, Math.min(1, (pB.val - min) / range)) * h;
214
-
215
- const deltaTime = pB.time - pA.time;
216
- const isHistoryGap = deltaTime <= 0 || deltaTime > maxAllowedGap;
217
-
218
- const props = getColorProps(pB.val);
219
- const offset1 = Math.max(0, Math.min(100, (x1 / w) * 100));
220
- const offset2 = Math.max(0, Math.min(100, (x2 / w) * 100));
221
-
222
- stops.push({ offset: offset1, color: props.color, opacity: props.opacity });
223
- stops.push({ offset: offset2, color: props.color, opacity: props.opacity });
224
-
225
- if (isHistoryGap) {
226
- if (started) {
227
- path += `L ${x1} ${h} `;
228
- started = false;
229
- }
230
- } else {
231
- lines.push(
232
- <line
233
- key={`seg-${i}`}
234
- x1={x1}
235
- y1={y1}
236
- x2={x2}
237
- y2={y2}
238
- stroke={props.color}
239
- strokeWidth={props.stroke}
240
- strokeLinecap="round"
241
- shapeRendering="geometricPrecision"
242
- vectorEffect="non-scaling-stroke"
243
- />
244
- );
245
-
246
- if (!started) {
247
- path += `M ${x1} ${h} L ${x1} ${y1} `;
248
- started = true;
249
- }
250
- path += `L ${x2} ${y2} `;
251
- }
252
- }
253
-
254
- if (started && visibleData.length > 0) {
255
- const last = visibleData[visibleData.length - 1];
256
- const lastX = Math.max(0, Math.min(w, ((last.time - viewportStart) / viewportMs) * w));
257
- path += `L ${lastX} ${h} Z`;
258
- }
259
-
260
- return { lineElements: lines, areaPath: path, gradientStops: stops };
261
- }, [visibleData, viewportStart, viewportMs, min, range, isFocused, displayMode]);
262
-
263
- const gradId = `grad-${id}`;
264
-
265
- return (
266
- <>
267
- <div className="scale-labels" id={`${type}-scale`}>
268
- <span>{Math.round(max)}</span>
269
- <span>{Math.round((min + max) / 2)}</span>
270
- <span>{Math.round(min)}</span>
271
- </div>
272
- <svg className="sparkline" id={id} viewBox="0 0 200 40" preserveAspectRatio="none">
273
- <defs>
274
- <linearGradient id={gradId} x1="0" y1="0" x2={w} y2="0" gradientUnits="userSpaceOnUse">
275
- {gradientStops.map((stop, idx) => (
276
- <stop key={idx} offset={`${stop.offset}%`} stopColor={stop.color} stopOpacity={stop.opacity} />
277
- ))}
278
- </linearGradient>
279
- </defs>
280
-
281
- {/* Horizontal gridlines */}
282
- {horizontalGridY.map((y, i) => (
283
- <line
284
- key={`hg-${i}`}
285
- x1="0"
286
- y1={y}
287
- x2={w}
288
- y2={y}
289
- stroke="rgba(0,0,0,0.12)"
290
- strokeWidth="0.5"
291
- vectorEffect="non-scaling-stroke"
292
- />
293
- ))}
294
-
295
- {/* Vertical time gridlines */}
296
- {verticalGridX.map((x, i) => (
297
- <line
298
- key={`vg-${i}`}
299
- x1={x}
300
- y1="0"
301
- x2={x}
302
- y2={h}
303
- stroke="rgba(0,0,0,0.08)"
304
- strokeWidth="0.4"
305
- vectorEffect="non-scaling-stroke"
306
- />
307
- ))}
308
-
309
- {/* Depth warning and danger lines */}
310
- {dangerY !== null && (
311
- <line
312
- x1="4"
313
- y1={dangerY}
314
- x2={w - 4}
315
- y2={dangerY}
316
- stroke="rgba(255, 59, 48, 0.95)"
317
- strokeWidth={alarmStrokeWidth}
318
- strokeDasharray="12, 6, 2, 6"
319
- vectorEffect="non-scaling-stroke"
320
- />
321
- )}
322
- {warningY !== null && (
323
- <line
324
- x1="4"
325
- y1={warningY}
326
- x2={w - 4}
327
- y2={warningY}
328
- stroke="rgba(255, 204, 0, 0.95)"
329
- strokeWidth={alarmStrokeWidth}
330
- strokeDasharray="6, 2, 6, 12"
331
- vectorEffect="non-scaling-stroke"
332
- />
333
- )}
334
-
335
- {/* Gradient fill area */}
336
- {areaPath && <path d={areaPath} fill={`url(#${gradId})`} stroke="none" />}
337
-
338
- {/* Sparkline segments */}
339
- {lineElements}
340
- </svg>
341
- </>
342
- );
343
- };