@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.
- package/package.json +9 -1
- package/public/assets/{index-C4co33rh.css → index-BFSy7dOW.css} +1 -1
- package/public/assets/index-BHhAwNqq.js +9 -0
- package/public/index.html +2 -2
- package/bun.lock +0 -157
- package/index.html +0 -22
- package/metadata.json +0 -7
- package/public/assets/index-DvzZHuhg.js +0 -9
- package/src/App.tsx +0 -346
- package/src/components/AnalogGauge.tsx +0 -243
- package/src/components/DataBox.tsx +0 -116
- package/src/components/MiniCompass.tsx +0 -120
- package/src/components/Sparkline.tsx +0 -343
- package/src/components/WeatherRadar.tsx +0 -280
- package/src/hooks/useDashboard.ts +0 -1161
- package/src/main.tsx +0 -10
- package/src/style.css +0 -705
- package/src/types.ts +0 -88
- package/src/utils/math.ts +0 -153
- package/src/utils/radarMath.ts +0 -89
- package/src/vite-env.d.ts +0 -1
- package/static/icons/dash.png +0 -0
- package/static/manifest.json +0 -17
- package/tsconfig.json +0 -20
- package/vite.config.ts +0 -22
|
@@ -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
|
-
};
|