feat: Enhance ChartjsHistoricalPlot with simple and fullscreen modes

- Added `isSimplePlot` prop to toggle simple plot mode for faster navigation.
- Introduced `isFullscreen` prop to enable fullscreen mode for a compact layout.
- Implemented chart resizing on fullscreen toggle.
- Updated dataset configuration to respect simple plot settings.
- Modified chart controls to hide/show based on fullscreen state.
- Added fullscreen modal with navigation controls in PlotHistoricalSession.
- Updated system state JSON to reflect changes in active datasets and last update timestamp.
This commit is contained in:
Miguel 2025-08-16 20:36:56 +02:00
parent 3803cc92ae
commit 7738f1d241
4 changed files with 7871 additions and 4840 deletions

File diff suppressed because it is too large Load Diff

View File

@ -41,7 +41,9 @@ const ChartjsHistoricalPlot = ({
config = {},
onZoomToTimeRange,
onPanToTimeRange,
height = '400px'
height = '400px',
isSimplePlot = false, // NEW: Simple plot mode for faster navigation
isFullscreen = false // NEW: Fullscreen mode for compact layout
}) => {
const canvasRef = useRef(null);
const chartRef = useRef(null);
@ -103,6 +105,22 @@ const ChartjsHistoricalPlot = ({
setCurrentTimeRange(timeRange);
}, [timeRange]);
// Handle fullscreen resize - force chart resize when in fullscreen mode
useEffect(() => {
if (isFullscreen && chartRef.current) {
// Delay to ensure DOM is updated
const timer = setTimeout(() => {
console.log('🔄 Forcing chart resize for fullscreen mode');
if (chartRef.current) {
chartRef.current.resize();
// Also trigger window resize event
window.dispatchEvent(new Event('resize'));
}
}, 200);
return () => clearTimeout(timer);
}
}, [isFullscreen]);
// Cleanup chart on unmount
useEffect(() => {
console.log('📊 ChartjsHistoricalPlot mounted');
@ -253,6 +271,13 @@ const ChartjsHistoricalPlot = ({
console.log(`📊 Variable ${variable}: ${variableData[variable].length} points after processing`);
});
// Extract configuration options with defaults - respecting plot definition
const lineTension = (typeof config.line_tension === 'number') ? config.line_tension : 0.1;
const stepped = config.stepped === true;
const pointRadius = isSimplePlot ? 0 : (typeof config.point_radius === 'number') ? config.point_radius : 0;
const pointHoverRadius = (typeof config.point_hover_radius === 'number') ? config.point_hover_radius : 4;
const useStackedAxes = config.stacked === true;
// Create datasets for each variable
variables.forEach((variable, index) => {
const points = variableData[variable];
@ -265,12 +290,15 @@ const ChartjsHistoricalPlot = ({
backgroundColor: colors[index % colors.length] + '20',
borderWidth: 2,
fill: false,
tension: 0.1,
pointRadius: 0, // Let decimation handle point display
pointHoverRadius: 4,
tension: lineTension,
stepped: stepped,
pointRadius: pointRadius, // Respect plot configuration
pointHoverRadius: pointHoverRadius,
pointBackgroundColor: colors[index % colors.length],
pointBorderColor: colors[index % colors.length],
spanGaps: true
spanGaps: true,
// Assign Y axis based on stacked configuration
yAxisID: useStackedAxes ? `y-axis-${index}` : 'y'
});
} else {
console.log(`📊 No data for variable: ${variable}`);
@ -380,14 +408,53 @@ const ChartjsHistoricalPlot = ({
text: 'Time'
}
},
y: {
title: {
display: true,
text: 'Value'
},
min: plotConfig.y_min,
max: plotConfig.y_max
}
// Check if stacked Y-axes are enabled
...(plotConfig.stacked === true && data.datasets.length > 0
? // When stacked is enabled, create a separate Y axis for each dataset
data.datasets.reduce((axes, dataset, index) => {
axes[`y-axis-${index}`] = {
type: 'linear',
position: index % 2 === 0 ? 'left' : 'right',
title: {
display: true,
text: dataset.label
},
min: plotConfig.y_min,
max: plotConfig.y_max,
grid: {
drawOnChartArea: index === 0, // Only show grid for first axis
},
ticks: {
color: dataset.borderColor
}
};
return axes;
}, {})
: plotConfig.stacked === true && data.datasets.length === 0
? // Stacked is enabled but no datasets, add a placeholder axis
{
'y-axis-0': {
type: 'linear',
position: 'left',
title: {
display: true,
text: 'Value'
},
min: plotConfig.y_min,
max: plotConfig.y_max
}
}
: // Non-stacked mode with single Y axis
{
y: {
title: {
display: true,
text: 'Value'
},
min: plotConfig.y_min,
max: plotConfig.y_max
}
})
}
}
};
@ -501,80 +568,137 @@ const ChartjsHistoricalPlot = ({
}
return (
<Box bg={bgColor} borderRadius="md" p={2}>
{/* Chart Controls */}
<HStack mb={2} spacing={4}>
<HStack>
<FormLabel htmlFor="zoom-toggle" mb={0} fontSize="sm" color={textColor}>
Zoom/Pan:
<Box
height={height}
bg={bgColor}
borderRadius="md"
borderWidth={isFullscreen ? "0" : "1px"}
borderColor={borderColor}
position="relative"
>
{/* Chart Controls - Hide in fullscreen to match ChartjsPlot style */}
{!isFullscreen && (
<Box p={2}>
<HStack mb={2} spacing={4}>
<HStack>
<FormLabel htmlFor="zoom-toggle" mb={0} fontSize="sm" color={textColor}>
Zoom/Pan:
</FormLabel>
<Switch
id="zoom-toggle"
isChecked={isZoomEnabled}
onChange={toggleZoom}
size="sm"
colorScheme="blue"
/>
</HStack>
<Text fontSize="sm" color={textColor}>
{dataPointsCount.toLocaleString()} data points
{(() => {
const canvasWidth = canvasRef.current?.clientWidth || 800;
const maxPoints = dataPointsCount;
const decimationThreshold = canvasWidth * 2;
const shouldDecimate = maxPoints > decimationThreshold;
if (shouldDecimate) {
return (
<Text as="span" ml={2} fontSize="xs" color="blue.500">
(Min/Max decimation: {canvasWidth} px ÷ {maxPoints} pts)
</Text>
);
}
return null;
})()}
{dataPointsCount === 0 && chartRef.current && (
<Text as="span" ml={2} fontSize="xs" color="orange.500">
No data in current view - use pan/zoom to navigate
</Text>
)}
</Text>
</HStack>
</Box>
)}
{/* Zoom/Pan Toggle Switch - Only in fullscreen, positioned like ChartjsPlot */}
{isFullscreen && (
<HStack
position="absolute"
top={2}
left={2}
bg="rgba(255, 255, 255, 0.95)"
backdropFilter="blur(8px)"
borderWidth="1px"
borderColor="gray.200"
px={3}
py={2}
borderRadius="lg"
fontSize="xs"
zIndex={10}
spacing={2}
shadow="sm"
>
<FormLabel htmlFor="zoom-toggle-fs" mb={0} fontSize="xs" color="gray.700" fontWeight="medium">
🔍 Zoom/Pan
</FormLabel>
<Switch
id="zoom-toggle"
id="zoom-toggle-fs"
size="sm"
isChecked={isZoomEnabled}
onChange={toggleZoom}
size="sm"
colorScheme="blue"
/>
</HStack>
<Text fontSize="sm" color={textColor}>
{dataPointsCount.toLocaleString()} data points
{(() => {
const canvasWidth = canvasRef.current?.clientWidth || 800;
const maxPoints = dataPointsCount;
const decimationThreshold = canvasWidth * 2;
const shouldDecimate = maxPoints > decimationThreshold;
if (shouldDecimate) {
return (
<Text as="span" ml={2} fontSize="xs" color="blue.500">
(Min/Max decimation: {canvasWidth} px ÷ {maxPoints} pts)
</Text>
);
}
return null;
})()}
{dataPointsCount === 0 && chartRef.current && (
<Text as="span" ml={2} fontSize="xs" color="orange.500">
No data in current view - use pan/zoom to navigate
</Text>
)}
</Text>
</HStack>
)}
{/* Chart Canvas */}
<Box
height={height}
position="relative"
border="1px solid"
borderColor={borderColor}
{/* Canvas - Direct like ChartjsPlot */}
<canvas
ref={canvasRef}
style={{
width: '100%',
height: isFullscreen ? '100%' : 'calc(100% - 60px)',
display: 'block',
borderRadius: isFullscreen ? '0' : '6px',
touchAction: 'none',
WebkitUserSelect: 'none',
userSelect: 'none'
}}
/>
{/* Data points counter */}
<Box
position="absolute"
top={2}
right={2}
bg="rgba(0,0,0,0.75)"
color="white"
px={2}
py={1}
borderRadius="md"
p={2}
fontSize="xs"
fontWeight="medium"
shadow="sm"
zIndex={5}
>
<canvas
ref={canvasRef}
style={{
width: '100%',
height: '100%',
display: 'block'
}}
/>
📊 {dataPointsCount} points
</Box>
{/* Chart Info */}
<HStack mt={2} fontSize="xs" color={textColor} justify="space-between">
<Text>
Session: {session?.name || 'Unknown'}
</Text>
<Text>
Variables: {session?.variables?.length || 0}
</Text>
{currentTimeRange && (
{/* Chart Info - Hide in fullscreen to save space */}
{!isFullscreen && (
<HStack mt={2} fontSize="xs" color={textColor} justify="space-between">
<Text>
Range: {new Date(currentTimeRange.start).toLocaleDateString()} - {new Date(currentTimeRange.end).toLocaleDateString()}
Session: {session?.name || 'Unknown'}
</Text>
)}
</HStack>
<Text>
Variables: {session?.variables?.length || 0}
</Text>
{currentTimeRange && (
<Text>
Range: {new Date(currentTimeRange.start).toLocaleDateString()} - {new Date(currentTimeRange.end).toLocaleDateString()}
</Text>
)}
</HStack>
)}
</Box>
);
};

View File

@ -37,6 +37,7 @@ import {
ModalHeader,
ModalCloseButton,
ModalBody,
ModalFooter,
useDisclosure,
Input,
Spinner,
@ -45,7 +46,7 @@ import {
Progress,
Tooltip
} from '@chakra-ui/react'
import { SettingsIcon, RepeatIcon, ViewIcon, DeleteIcon, TimeIcon, CalendarIcon } from '@chakra-ui/icons'
import { SettingsIcon, RepeatIcon, ViewIcon, DeleteIcon, TimeIcon, CalendarIcon, ViewOffIcon } from '@chakra-ui/icons'
import ChartjsHistoricalPlot from './ChartjsHistoricalPlot.jsx'
import TimePointSelector from './TimePointSelector.jsx'
import DataAvailabilityBar from './DataAvailabilityBar.jsx'
@ -168,6 +169,10 @@ export default function PlotHistoricalSession({
const toast = useToast()
const { isOpen: isConfigModalOpen, onOpen: onConfigModalOpen, onClose: onConfigModalClose } = useDisclosure()
const { isOpen: isFullscreen, onOpen: openFullscreen, onClose: closeFullscreen } = useDisclosure()
// NEW: Simple plot mode toggle
const [isSimplePlot, setIsSimplePlot] = useState(false)
// Keep track of the last loaded data range for optimization
const [loadedDataRange, setLoadedDataRange] = useState(null)
@ -547,6 +552,25 @@ export default function PlotHistoricalSession({
colorScheme={showDataPreview ? 'blue' : 'gray'}
/>
</Tooltip>
<Tooltip label="Simple Plot Mode">
<IconButton
icon={<ViewOffIcon />}
size="sm"
variant="ghost"
onClick={() => setIsSimplePlot(!isSimplePlot)}
colorScheme={isSimplePlot ? 'blue' : 'gray'}
/>
</Tooltip>
<Tooltip label="Fullscreen">
<Button
size="sm"
variant="ghost"
leftIcon={<ViewIcon />}
onClick={openFullscreen}
>
Fullscreen
</Button>
</Tooltip>
<Tooltip label="Remove Plot">
<IconButton
icon={<DeleteIcon />}
@ -649,6 +673,7 @@ export default function PlotHistoricalSession({
onZoomToTimeRange={handleZoomToTimeRange}
onPanToTimeRange={handlePanToTimeRange}
height="400px"
isSimplePlot={isSimplePlot}
/>
{/* Loading overlay */}
@ -801,6 +826,96 @@ export default function PlotHistoricalSession({
</Box>
</ModalContent>
</Modal>
{/* Fullscreen Modal */}
<Modal isOpen={isFullscreen} onClose={closeFullscreen} size="full">
<ModalOverlay bg="blackAlpha.800" />
<ModalContent bg={useColorModeValue('white', 'gray.700')} m={0} borderRadius={0} h="100vh">
<ModalHeader>
<HStack>
<Text>📈 {session.name} - Fullscreen Mode</Text>
<Spacer />
<Text fontSize="sm" color={useColorModeValue('gray.600', 'gray.300')}>
Zoom: Drag to select area | Pan: Shift + Drag | Double-click to reset
</Text>
</HStack>
</ModalHeader>
<ModalCloseButton size="lg" />
<ModalBody p={4} h="calc(100vh - 80px)" display="flex" flexDirection="column">
<Box flex="1" w="100%" minH={0}>
<ChartjsHistoricalPlot
key={`${session.id}-fullscreen`}
session={session}
historicalData={stableHistoricalData}
timeRange={derivedTimeRange}
config={stableConfig}
onZoomToTimeRange={handleZoomToTimeRange}
onPanToTimeRange={handlePanToTimeRange}
height="100%"
isSimplePlot={isSimplePlot}
isFullscreen={true}
/>
</Box>
{/* Time Navigation Controls - Following realtime pattern */}
<VStack spacing={3} mt={4}>
{/* Time slider in compact form */}
{dateRange && (
<Box w="100%">
<TimePointSelector
minDate={dateRange.minDate}
maxDate={dateRange.maxDate}
initial={centralTime}
initialRangeSeconds={timeRangeSeconds}
stepMinutes={1}
dataSegments={dataSegments}
onTimeChange={handleTimePointChange}
/>
</Box>
)}
{/* Control buttons row - Following realtime pattern */}
<HStack spacing={2} justify="center" wrap="wrap">
<Button
size="sm"
variant="outline"
leftIcon={isSimplePlot ? <ViewIcon /> : <ViewOffIcon />}
onClick={() => setIsSimplePlot(!isSimplePlot)}
colorScheme={isSimplePlot ? 'blue' : 'gray'}
>
{isSimplePlot ? 'Detailed' : 'Simple'}
</Button>
<Button
size="sm"
variant="outline"
leftIcon={<RepeatIcon />}
onClick={() => loadHistoricalData(true)}
isLoading={isLoading}
>
Reload
</Button>
<Button
size="sm"
variant="outline"
leftIcon={<SettingsIcon />}
onClick={onConfigModalOpen}
>
Settings
</Button>
{/* Time range info */}
<Box px={3} py={1} bg={useColorModeValue('blue.50', 'blue.900')} borderRadius="md" fontSize="xs">
<HStack spacing={4}>
<Text><strong>Range:</strong> {timeRangeSeconds}s</Text>
<Text><strong>From:</strong> {formatCentralTimeInfo().start}</Text>
<Text><strong>To:</strong> {formatCentralTimeInfo().end}</Text>
</HStack>
</Box>
</HStack>
</VStack>
</ModalBody>
</ModalContent>
</Modal>
</Card>
)
}

View File

@ -3,12 +3,12 @@
"should_connect": true,
"should_stream": false,
"active_datasets": [
"DAR",
"Test",
"Fast",
"Test"
"DAR"
]
},
"auto_recovery_enabled": true,
"last_update": "2025-08-16T19:07:34.585967",
"last_update": "2025-08-16T20:26:17.367139",
"plotjuggler_path": "C:\\Program Files\\PlotJuggler\\plotjuggler.exe"
}