Skip to content

Commit 83ed2ca

Browse files
committed
Add draggable per-cell plot and cell click wiring
1 parent 35a6478 commit 83ed2ca

4 files changed

Lines changed: 134 additions & 6 deletions

File tree

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { useState, useRef, useEffect } from 'react';
2+
import PlotManager, { type PlotSignal } from './PlotManager';
3+
import { GripHorizontal } from 'lucide-react';
4+
5+
interface DraggablePlotProps {
6+
isOpen: boolean;
7+
onClose: () => void;
8+
signalInfo: PlotSignal | null;
9+
}
10+
11+
export default function DraggablePlot({ isOpen, onClose, signalInfo }: DraggablePlotProps) {
12+
const [position, setPosition] = useState({ x: 0, y: 0 });
13+
const isDragging = useRef(false);
14+
const dragStart = useRef({ x: 0, y: 0 });
15+
const windowRef = useRef<HTMLDivElement>(null);
16+
17+
// Reset position when opened with new signal?
18+
// Maybe keep position persistence? Let's keep persistence for now.
19+
20+
useEffect(() => {
21+
const handleMouseMove = (e: MouseEvent) => {
22+
if (!isDragging.current) return;
23+
24+
const dx = e.clientX - dragStart.current.x;
25+
const dy = e.clientY - dragStart.current.y;
26+
27+
// Calculate new position
28+
// Since we are applying transform translate relative to bottom-right,
29+
// positive X moves right, positive Y moves down.
30+
// But dragging "left" means negative delta.
31+
32+
// Actually, let's track absolute translation.
33+
setPosition(prev => ({
34+
x: prev.x + dx,
35+
y: prev.y + dy
36+
}));
37+
38+
dragStart.current = { x: e.clientX, y: e.clientY };
39+
};
40+
41+
const handleMouseUp = () => {
42+
isDragging.current = false;
43+
};
44+
45+
if (isOpen) {
46+
window.addEventListener('mousemove', handleMouseMove);
47+
window.addEventListener('mouseup', handleMouseUp);
48+
}
49+
50+
return () => {
51+
window.removeEventListener('mousemove', handleMouseMove);
52+
window.removeEventListener('mouseup', handleMouseUp);
53+
};
54+
}, [isOpen]);
55+
56+
const handleMouseDown = (e: React.MouseEvent) => {
57+
isDragging.current = true;
58+
dragStart.current = { x: e.clientX, y: e.clientY };
59+
e.preventDefault(); // Prevent text selection
60+
};
61+
62+
if (!isOpen || !signalInfo) return null;
63+
64+
return (
65+
<div
66+
ref={windowRef}
67+
className="fixed bottom-4 right-4 z-50 flex flex-col bg-data-module-bg rounded-lg shadow-2xl border border-gray-700 w-[600px] overflow-hidden"
68+
style={{
69+
transform: `translate(${position.x}px, ${position.y}px)`,
70+
// Ensure it doesn't go off screen too much? simpler to just let user move it.
71+
}}
72+
>
73+
{/* Drag Handle */}
74+
<div
75+
className="bg-gray-800 p-1 cursor-grab active:cursor-grabbing flex justify-center items-center hover:bg-gray-700 transition-colors"
76+
onMouseDown={handleMouseDown}
77+
>
78+
<GripHorizontal size={16} className="text-gray-400" />
79+
</div>
80+
81+
<div className="p-1">
82+
<PlotManager
83+
plotId="Cell Plot"
84+
signals={[signalInfo]}
85+
timeWindowMs={30000}
86+
onRemoveSignal={onClose}
87+
onClosePlot={onClose}
88+
/>
89+
</div>
90+
</div>
91+
);
92+
}

pecan/src/components/accumulator/CellGrid.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919

2020
interface CellGridProps {
2121
moduleId: ModuleId;
22+
onCellClick?: (moduleId: ModuleId, cellIndex: number) => void;
2223
}
2324

2425
interface CellStats {
@@ -28,10 +29,11 @@ interface CellStats {
2829
}
2930

3031
// Individual cell component with 1s throttled updates and highlight support
31-
function Cell({ moduleId, cellIndex, stats }: {
32+
function Cell({ moduleId, cellIndex, stats, onClick }: {
3233
moduleId: ModuleId;
3334
cellIndex: number;
3435
stats: CellStats;
36+
onClick?: () => void;
3537
}) {
3638
const { highlightTarget } = useAccumulatorContext();
3739
const cellRef = useRef<HTMLDivElement>(null);
@@ -77,10 +79,11 @@ function Cell({ moduleId, cellIndex, stats }: {
7779
className={`
7880
relative flex items-center justify-center
7981
rounded-sm text-xs font-mono font-semibold
80-
cursor-default transition-all duration-300
81-
${isHighlighted ? 'z-10 shadow-lg scale-105' : ''}
82+
cursor-pointer transition-all duration-300
83+
${isHighlighted ? 'z-10 shadow-lg scale-105' : 'hover:scale-105 hover:z-10 hover:shadow-md'}
8284
${!isHighlighted && isCritical ? 'animate-alert-pulse' : !isHighlighted && isWarning ? 'animate-warning-pulse' : ''}
8385
`}
86+
onClick={onClick}
8487
style={{
8588
backgroundColor: isHighlighted ? '#ffffff' : bgColor,
8689
color: isHighlighted ? '#000000' : '#ffffff'
@@ -154,7 +157,7 @@ function useCellStats(moduleId: ModuleId): Map<number, CellStats> {
154157
return cellStats;
155158
}
156159

157-
export default function CellGrid({ moduleId }: CellGridProps) {
160+
export default function CellGrid({ moduleId, onCellClick }: CellGridProps) {
158161
const cellIndices = useMemo(() =>
159162
Array.from({ length: CELLS_PER_MODULE }, (_, i) => i + 1),
160163
[]
@@ -170,6 +173,7 @@ export default function CellGrid({ moduleId }: CellGridProps) {
170173
moduleId={moduleId}
171174
cellIndex={cellIndex}
172175
stats={cellStats.get(cellIndex) || { current: null, min: null, max: null }}
176+
onClick={() => onCellClick?.(moduleId, cellIndex)}
173177
/>
174178
))}
175179
</div>

pecan/src/components/accumulator/ModuleCard.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
interface ModuleCardProps {
2525
moduleId: ModuleId;
2626
initialOpen?: boolean;
27+
onCellClick?: (moduleId: ModuleId, cellIndex: number) => void;
2728
}
2829

2930
interface ModuleStats {
@@ -122,7 +123,7 @@ function useModuleStats(moduleId: ModuleId): ModuleStats {
122123
return stats;
123124
}
124125

125-
export default function ModuleCard({ moduleId, initialOpen = false }: ModuleCardProps) {
126+
export default function ModuleCard({ moduleId, initialOpen = false, onCellClick }: ModuleCardProps) {
126127
const [isOpen, setIsOpen] = useState(initialOpen);
127128
const { voltageStats, tempStats, alertLevel } = useModuleStats(moduleId);
128129

@@ -195,7 +196,7 @@ export default function ModuleCard({ moduleId, initialOpen = false }: ModuleCard
195196
{/* Cell voltage grid */}
196197
<div>
197198
<h4 className="text-xs text-white mb-1 font-semibold">Cell Voltages</h4>
198-
<CellGrid moduleId={moduleId} />
199+
<CellGrid moduleId={moduleId} onCellClick={onCellClick} />
199200
</div>
200201

201202
{/* Thermistor bar */}

pecan/src/pages/Accumulator.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,35 @@ import {
1212
ChargingCurve,
1313
MODULE_IDS,
1414
AccumulatorProvider,
15+
getCellSignalInfo,
16+
type ModuleId,
1517
} from '../components/accumulator';
18+
import DraggablePlot from '../components/DraggablePlot';
19+
import { type PlotSignal } from '../components/PlotManager';
20+
import { dataStore } from '../lib/DataStore';
1621

1722
export default function Accumulator() {
1823
// Time window for charging curve (5 minutes default)
1924
const [chartTimeWindow, setChartTimeWindow] = useState(300000);
2025

26+
// Plot modal state
27+
const [plotSignal, setPlotSignal] = useState<PlotSignal | null>(null);
28+
29+
const handleCellClick = (moduleId: ModuleId, cellIndex: number) => {
30+
const { msgId, signalName } = getCellSignalInfo(moduleId, cellIndex);
31+
32+
// Try to get friendly message name from store, or construct one
33+
const latest = dataStore.getLatest(msgId);
34+
const messageName = latest?.messageName || `Module ${moduleId} Voltage`;
35+
36+
setPlotSignal({
37+
msgID: msgId,
38+
signalName: signalName,
39+
messageName: messageName,
40+
unit: 'V'
41+
});
42+
};
43+
2144
return (
2245
<AccumulatorProvider>
2346
<div className="h-full overflow-y-auto bg-sidebar">
@@ -63,6 +86,7 @@ export default function Accumulator() {
6386
key={moduleId}
6487
moduleId={moduleId}
6588
initialOpen={true}
89+
onCellClick={handleCellClick}
6690
/>
6791
))}
6892
</div>
@@ -75,6 +99,13 @@ export default function Accumulator() {
7599
</p>
76100
</div>
77101
</div>
102+
103+
{/* Cell Plot Window (Draggable) */}
104+
<DraggablePlot
105+
isOpen={!!plotSignal}
106+
onClose={() => setPlotSignal(null)}
107+
signalInfo={plotSignal}
108+
/>
78109
</div>
79110
</AccumulatorProvider>
80111
);

0 commit comments

Comments
 (0)