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:
parent
3803cc92ae
commit
7738f1d241
12318
application_events.json
12318
application_events.json
File diff suppressed because it is too large
Load Diff
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
Loading…
Reference in New Issue