feat: Enhance plot configuration with visual style options
- Added line tension, stepped lines, point radius, and point hover radius to plot definitions. - Updated plot variables to include labels for better clarity. - Modified plot definitions schema to accommodate new visual style properties. - Enhanced UI schema to support new configuration options in the settings panel. - Improved ChartjsPlot component to utilize new visual style properties for rendering. - Implemented refresh functionality for plot configuration in PlotRealtimeSession. - Updated VariableSelectorWidget to remove unnecessary required attribute. - Adjusted system state to reflect changes in active datasets and last update timestamp.
This commit is contained in:
parent
d0d675d804
commit
087a9458ce
10641
application_events.json
10641
application_events.json
File diff suppressed because it is too large
Load Diff
|
@ -2,20 +2,17 @@
|
|||
"plots": [
|
||||
{
|
||||
"id": "plot_1",
|
||||
"line_tension": 0,
|
||||
"name": "UR29",
|
||||
"time_window": 25,
|
||||
"point_hover_radius": 4,
|
||||
"point_radius": 1,
|
||||
"stepped": true,
|
||||
"time_window": 20,
|
||||
"trigger_enabled": false,
|
||||
"trigger_on_true": true,
|
||||
"trigger_variable": null,
|
||||
"y_max": null,
|
||||
"y_min": null
|
||||
},
|
||||
{
|
||||
"id": "Brix",
|
||||
"name": "Brix",
|
||||
"time_window": 60,
|
||||
"trigger_enabled": false,
|
||||
"trigger_on_true": true
|
||||
}
|
||||
]
|
||||
}
|
|
@ -4,14 +4,20 @@
|
|||
"plot_id": "plot_1",
|
||||
"variables": [
|
||||
{
|
||||
"variable_name": "UR29_Brix",
|
||||
"label": "Brix",
|
||||
"color": "#3498db",
|
||||
"enabled": true,
|
||||
"variable_name": "UR29_Brix"
|
||||
"line_width": 2,
|
||||
"y_axis": "left",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"variable_name": "UR29_ma",
|
||||
"label": "ma",
|
||||
"color": "#e74c3c",
|
||||
"enabled": true,
|
||||
"variable_name": "UR29_ma"
|
||||
"line_width": 2,
|
||||
"y_axis": "left",
|
||||
"enabled": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -54,6 +54,36 @@
|
|||
"type": "boolean",
|
||||
"title": "Trigger on True",
|
||||
"default": true
|
||||
},
|
||||
"line_tension": {
|
||||
"type": "number",
|
||||
"title": "Line Tension",
|
||||
"description": "Bezier curve tension (0 = straight lines, 0.4 = smooth curves)",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"default": 0.4
|
||||
},
|
||||
"stepped": {
|
||||
"type": "boolean",
|
||||
"title": "Stepped Lines",
|
||||
"description": "Enable stepped line style",
|
||||
"default": false
|
||||
},
|
||||
"point_radius": {
|
||||
"type": "number",
|
||||
"title": "Point Radius",
|
||||
"description": "Size of data points",
|
||||
"minimum": 0,
|
||||
"maximum": 10,
|
||||
"default": 1
|
||||
},
|
||||
"point_hover_radius": {
|
||||
"type": "number",
|
||||
"title": "Point Hover Radius",
|
||||
"description": "Size of data points when hovering",
|
||||
"minimum": 0,
|
||||
"maximum": 15,
|
||||
"default": 4
|
||||
}
|
||||
},
|
||||
"required": ["id", "name", "time_window"]
|
||||
|
|
|
@ -15,7 +15,11 @@
|
|||
"y_max",
|
||||
"trigger_variable",
|
||||
"trigger_enabled",
|
||||
"trigger_on_true"
|
||||
"trigger_on_true",
|
||||
"line_tension",
|
||||
"stepped",
|
||||
"point_radius",
|
||||
"point_hover_radius"
|
||||
],
|
||||
"ui:layout": [
|
||||
[
|
||||
|
@ -55,6 +59,24 @@
|
|||
"name": "trigger_on_true",
|
||||
"width": 12
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"name": "line_tension",
|
||||
"width": 3
|
||||
},
|
||||
{
|
||||
"name": "stepped",
|
||||
"width": 3
|
||||
},
|
||||
{
|
||||
"name": "point_radius",
|
||||
"width": 3
|
||||
},
|
||||
{
|
||||
"name": "point_hover_radius",
|
||||
"width": 3
|
||||
}
|
||||
]
|
||||
],
|
||||
"id": {
|
||||
|
@ -95,6 +117,22 @@
|
|||
"trigger_on_true": {
|
||||
"ui:widget": "checkbox",
|
||||
"ui:help": "🔄 Trigger when variable becomes true (vs false)"
|
||||
},
|
||||
"line_tension": {
|
||||
"ui:widget": "updown",
|
||||
"ui:help": "📈 Line smoothness: 0=straight lines, 0.4=smooth curves"
|
||||
},
|
||||
"stepped": {
|
||||
"ui:widget": "checkbox",
|
||||
"ui:help": "📊 Enable stepped line style instead of curves"
|
||||
},
|
||||
"point_radius": {
|
||||
"ui:widget": "updown",
|
||||
"ui:help": "🔴 Size of data points (0-10)"
|
||||
},
|
||||
"point_hover_radius": {
|
||||
"ui:widget": "updown",
|
||||
"ui:help": "🎯 Size of points when hovering (0-15)"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -22,6 +22,7 @@ const ChartjsPlot = ({ session, height = '400px' }) => {
|
|||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [dataPointsCount, setDataPointsCount] = useState(0);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const resolvedConfigRef = useRef(null);
|
||||
|
||||
const bgColor = useColorModeValue('white', 'gray.800');
|
||||
|
@ -62,6 +63,7 @@ const ChartjsPlot = ({ session, height = '400px' }) => {
|
|||
.filter(varConfig => varConfig.enabled !== false && varConfig.variable_name)
|
||||
.map((varConfig, index) => ({
|
||||
name: varConfig.variable_name,
|
||||
label: varConfig.label || varConfig.variable_name, // Use display label if available
|
||||
color: varConfig.color || getColor(varConfig.variable_name, index),
|
||||
enabled: varConfig.enabled !== false
|
||||
}));
|
||||
|
@ -69,6 +71,7 @@ const ChartjsPlot = ({ session, height = '400px' }) => {
|
|||
// Handle simple array of strings
|
||||
return variables.map((variable, index) => ({
|
||||
name: variable,
|
||||
label: variable, // For simple strings, name and label are the same
|
||||
color: getColor(variable, index),
|
||||
enabled: true
|
||||
}));
|
||||
|
@ -79,6 +82,7 @@ const ChartjsPlot = ({ session, height = '400px' }) => {
|
|||
.filter(([id, config]) => config.enabled !== false && config.variable_name)
|
||||
.map(([id, config]) => ({
|
||||
name: config.variable_name,
|
||||
label: config.label || config.variable_name, // Use display label if available
|
||||
color: config.color || getColor(config.variable_name),
|
||||
enabled: config.enabled !== false
|
||||
}));
|
||||
|
@ -137,10 +141,21 @@ const ChartjsPlot = ({ session, height = '400px' }) => {
|
|||
|
||||
const ctx = canvasRef.current.getContext('2d');
|
||||
|
||||
// Destroy existing chart
|
||||
// Destroy existing chart more safely
|
||||
if (chartRef.current) {
|
||||
chartRef.current.destroy();
|
||||
chartRef.current = null;
|
||||
try {
|
||||
// Pause streaming before destroying to avoid dangling references
|
||||
const rt = chartRef.current.options?.scales?.x?.realtime;
|
||||
if (rt) {
|
||||
rt.pause = true;
|
||||
chartRef.current.update('none');
|
||||
}
|
||||
chartRef.current.destroy();
|
||||
} catch (destroyError) {
|
||||
console.warn('⚠️ Error destroying previous chart:', destroyError);
|
||||
} finally {
|
||||
chartRef.current = null;
|
||||
}
|
||||
}
|
||||
|
||||
const config = cfg;
|
||||
|
@ -148,17 +163,25 @@ const ChartjsPlot = ({ session, height = '400px' }) => {
|
|||
|
||||
const datasets = enabledVariables.map((variableInfo, index) => {
|
||||
const color = variableInfo.color || getColor(variableInfo.name, index);
|
||||
|
||||
// Get style configuration with defaults
|
||||
const lineTension = (typeof config.line_tension === 'number') ? config.line_tension : 0.4;
|
||||
const stepped = config.stepped === true;
|
||||
const pointRadius = (typeof config.point_radius === 'number') ? config.point_radius : 1;
|
||||
const pointHoverRadius = (typeof config.point_hover_radius === 'number') ? config.point_hover_radius : 4;
|
||||
|
||||
return {
|
||||
label: variableInfo.name,
|
||||
label: variableInfo.label, // Use display label for chart legend
|
||||
data: [],
|
||||
borderColor: color,
|
||||
backgroundColor: color + '20',
|
||||
borderWidth: 2,
|
||||
fill: false,
|
||||
spanGaps: true,
|
||||
pointRadius: 1,
|
||||
pointHoverRadius: 4,
|
||||
tension: 0.4
|
||||
pointRadius: pointRadius,
|
||||
pointHoverRadius: pointHoverRadius,
|
||||
tension: lineTension,
|
||||
stepped: stepped
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -500,6 +523,86 @@ const ChartjsPlot = ({ session, height = '400px' }) => {
|
|||
setDataPointsCount(0);
|
||||
}, []);
|
||||
|
||||
// Update configuration directly (for real-time style changes)
|
||||
const updateConfig = useCallback(async (newConfig) => {
|
||||
try {
|
||||
console.log(`🔄 Updating configuration for plot session ${session?.session_id}...`);
|
||||
|
||||
const oldConfig = resolvedConfigRef.current;
|
||||
resolvedConfigRef.current = { ...oldConfig, ...newConfig };
|
||||
|
||||
// Check if chart recreation is needed
|
||||
const needsRecreation = !oldConfig ||
|
||||
oldConfig.line_tension !== newConfig.line_tension ||
|
||||
oldConfig.stepped !== newConfig.stepped ||
|
||||
oldConfig.point_radius !== newConfig.point_radius ||
|
||||
oldConfig.point_hover_radius !== newConfig.point_hover_radius ||
|
||||
oldConfig.time_window !== newConfig.time_window ||
|
||||
oldConfig.y_min !== newConfig.y_min ||
|
||||
oldConfig.y_max !== newConfig.y_max;
|
||||
|
||||
if (needsRecreation) {
|
||||
console.log(`🔄 Chart needs recreation due to configuration changes`);
|
||||
await createStreamingChart();
|
||||
} else {
|
||||
console.log(`✅ No chart recreation needed, configuration updated`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ Error updating configuration for plot session ${session?.session_id}:`, error);
|
||||
setError(error.message);
|
||||
}
|
||||
}, [session?.session_id, createStreamingChart]);
|
||||
|
||||
// Refresh configuration from server and recreate chart
|
||||
const refreshConfiguration = useCallback(async () => {
|
||||
if (!session?.session_id) return;
|
||||
|
||||
setIsRefreshing(true);
|
||||
try {
|
||||
console.log(`🔄 Refreshing configuration for plot session ${session.session_id}...`);
|
||||
|
||||
// Fetch latest session configuration from server
|
||||
const response = await fetch(`/api/plots/${session.session_id}/config`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch session configuration: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const updatedSession = await response.json();
|
||||
|
||||
// Update the resolved config with the latest configuration
|
||||
if (updatedSession.success && updatedSession.config) {
|
||||
const oldConfig = resolvedConfigRef.current;
|
||||
resolvedConfigRef.current = updatedSession.config;
|
||||
console.log(`✅ Configuration refreshed for plot session ${session.session_id}`);
|
||||
|
||||
// Only recreate the chart if there are significant changes
|
||||
const needsRecreation = !oldConfig ||
|
||||
JSON.stringify(oldConfig.variables) !== JSON.stringify(updatedSession.config.variables) ||
|
||||
oldConfig.time_window !== updatedSession.config.time_window ||
|
||||
oldConfig.y_min !== updatedSession.config.y_min ||
|
||||
oldConfig.y_max !== updatedSession.config.y_max ||
|
||||
oldConfig.line_tension !== updatedSession.config.line_tension ||
|
||||
oldConfig.stepped !== updatedSession.config.stepped ||
|
||||
oldConfig.point_radius !== updatedSession.config.point_radius ||
|
||||
oldConfig.point_hover_radius !== updatedSession.config.point_hover_radius;
|
||||
|
||||
if (needsRecreation) {
|
||||
console.log(`🔄 Chart needs recreation due to configuration changes`);
|
||||
await createStreamingChart();
|
||||
} else {
|
||||
console.log(`✅ No chart recreation needed, configuration is the same`);
|
||||
}
|
||||
} else {
|
||||
throw new Error('Invalid response format or configuration not found');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ Error refreshing configuration for plot session ${session.session_id}:`, error);
|
||||
setError(error.message);
|
||||
} finally {
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
}, [session?.session_id, createStreamingChart]);
|
||||
|
||||
// Not using useImperativeHandle since we're exposing functions through props callback
|
||||
|
||||
// Also expose control functions through props for easier access
|
||||
|
@ -511,10 +614,12 @@ const ChartjsPlot = ({ session, height = '400px' }) => {
|
|||
session.onChartReady({
|
||||
pauseStreaming,
|
||||
resumeStreaming,
|
||||
clearChart
|
||||
clearChart,
|
||||
refreshConfiguration,
|
||||
updateConfig
|
||||
});
|
||||
}
|
||||
}, [pauseStreaming, resumeStreaming, clearChart, session?.session_id, session?.onChartReady]);
|
||||
}, [pauseStreaming, resumeStreaming, clearChart, refreshConfiguration, updateConfig, session?.session_id, session?.onChartReady]);
|
||||
|
||||
// Update chart when session status changes
|
||||
useEffect(() => {
|
||||
|
@ -554,9 +659,13 @@ const ChartjsPlot = ({ session, height = '400px' }) => {
|
|||
chartRef.current.destroy();
|
||||
chartRef.current = null;
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Chart cleanup error:', error);
|
||||
}
|
||||
|
||||
if (sessionDataRef.current.manualInterval) {
|
||||
clearInterval(sessionDataRef.current.manualInterval);
|
||||
sessionDataRef.current.manualInterval = null;
|
||||
}
|
||||
};
|
||||
}, [session?.session_id]);
|
||||
|
@ -621,6 +730,7 @@ const ChartjsPlot = ({ session, height = '400px' }) => {
|
|||
userSelect: 'none'
|
||||
}}
|
||||
/>
|
||||
{/* Points counter */}
|
||||
<Box
|
||||
position="absolute"
|
||||
top={2}
|
||||
|
|
|
@ -25,9 +25,14 @@ import {
|
|||
Grid,
|
||||
GridItem,
|
||||
Flex,
|
||||
useToast
|
||||
useToast,
|
||||
Checkbox,
|
||||
Slider,
|
||||
SliderTrack,
|
||||
SliderFilledTrack,
|
||||
SliderThumb
|
||||
} from '@chakra-ui/react'
|
||||
import { SettingsIcon } from '@chakra-ui/icons'
|
||||
import { SettingsIcon, RepeatIcon } from '@chakra-ui/icons'
|
||||
import ChartjsPlot from './ChartjsPlot.jsx'
|
||||
import * as api from '../services/api'
|
||||
|
||||
|
@ -50,13 +55,19 @@ export default function PlotRealtimeSession({
|
|||
})
|
||||
|
||||
const [showSettings, setShowSettings] = useState(false)
|
||||
const [isRefreshing, setIsRefreshing] = useState(false)
|
||||
const [localConfig, setLocalConfig] = useState({
|
||||
time_window: plotDefinition.time_window || 60,
|
||||
y_min: plotDefinition.y_min,
|
||||
y_max: plotDefinition.y_max,
|
||||
trigger_enabled: plotDefinition.trigger_enabled || false,
|
||||
trigger_variable: plotDefinition.trigger_variable,
|
||||
trigger_on_true: plotDefinition.trigger_on_true || true
|
||||
trigger_on_true: plotDefinition.trigger_on_true || true,
|
||||
// Visual style properties
|
||||
line_tension: plotDefinition.line_tension !== undefined ? plotDefinition.line_tension : 0.4,
|
||||
stepped: plotDefinition.stepped || false,
|
||||
point_radius: plotDefinition.point_radius !== undefined ? plotDefinition.point_radius : 1,
|
||||
point_hover_radius: plotDefinition.point_hover_radius !== undefined ? plotDefinition.point_hover_radius : 4
|
||||
})
|
||||
|
||||
const chartControlsRef = useRef(null)
|
||||
|
@ -254,11 +265,51 @@ export default function PlotRealtimeSession({
|
|||
y_max: plotDefinition.y_max,
|
||||
trigger_enabled: plotDefinition.trigger_enabled || false,
|
||||
trigger_variable: plotDefinition.trigger_variable,
|
||||
trigger_on_true: plotDefinition.trigger_on_true || true
|
||||
trigger_on_true: plotDefinition.trigger_on_true || true,
|
||||
// Reset visual style properties to defaults
|
||||
line_tension: plotDefinition.line_tension !== undefined ? plotDefinition.line_tension : 0.4,
|
||||
stepped: plotDefinition.stepped || false,
|
||||
point_radius: plotDefinition.point_radius !== undefined ? plotDefinition.point_radius : 1,
|
||||
point_hover_radius: plotDefinition.point_hover_radius !== undefined ? plotDefinition.point_hover_radius : 4
|
||||
})
|
||||
setShowSettings(false)
|
||||
}
|
||||
|
||||
// Refresh plot configuration and recreate chart
|
||||
const refreshPlotConfiguration = useCallback(async () => {
|
||||
setIsRefreshing(true)
|
||||
try {
|
||||
console.log(`🔄 Refreshing configuration for plot ${plotDefinition.id}...`)
|
||||
|
||||
// Trigger chart configuration refresh if available
|
||||
if (chartControlsRef.current?.refreshConfiguration) {
|
||||
await chartControlsRef.current.refreshConfiguration()
|
||||
}
|
||||
|
||||
// Also refresh session status
|
||||
await refreshSessionStatus()
|
||||
|
||||
toast({
|
||||
title: '🔄 Configuration refreshed',
|
||||
description: 'Plot configuration and variables have been updated',
|
||||
status: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
|
||||
console.log(`✅ Configuration refreshed for plot ${plotDefinition.id}`)
|
||||
} catch (error) {
|
||||
console.error(`❌ Error refreshing configuration for plot ${plotDefinition.id}:`, error)
|
||||
toast({
|
||||
title: '❌ Failed to refresh configuration',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
duration: 3000
|
||||
})
|
||||
} finally {
|
||||
setIsRefreshing(false)
|
||||
}
|
||||
}, [plotDefinition.id, refreshSessionStatus, toast])
|
||||
|
||||
// Auto-refresh session status
|
||||
useEffect(() => {
|
||||
// Try to get session status first, if it fails, create the session
|
||||
|
@ -292,6 +343,15 @@ export default function PlotRealtimeSession({
|
|||
</Box>
|
||||
<Spacer />
|
||||
<HStack>
|
||||
<IconButton
|
||||
icon={<RepeatIcon />}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
aria-label="Refresh Configuration"
|
||||
onClick={refreshPlotConfiguration}
|
||||
isLoading={isRefreshing}
|
||||
title="Refresh plot configuration and variables"
|
||||
/>
|
||||
<IconButton
|
||||
icon={<SettingsIcon />}
|
||||
size="sm"
|
||||
|
@ -315,83 +375,189 @@ export default function PlotRealtimeSession({
|
|||
{/* Settings Panel */}
|
||||
{showSettings && (
|
||||
<Box mb={4} p={4} bg={useColorModeValue('gray.50', 'gray.600')} borderRadius="md">
|
||||
<Grid templateColumns="repeat(auto-fit, minmax(200px, 1fr))" gap={4}>
|
||||
<GridItem>
|
||||
<FormControl>
|
||||
<FormLabel fontSize="sm">Time Window (seconds)</FormLabel>
|
||||
<NumberInput
|
||||
value={localConfig.time_window}
|
||||
onChange={(valueString) => setLocalConfig(prev => ({
|
||||
...prev,
|
||||
time_window: parseInt(valueString) || 60
|
||||
}))}
|
||||
min={10}
|
||||
max={3600}
|
||||
size="sm"
|
||||
>
|
||||
<NumberInputField />
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper />
|
||||
<NumberDecrementStepper />
|
||||
</NumberInputStepper>
|
||||
</NumberInput>
|
||||
</FormControl>
|
||||
</GridItem>
|
||||
|
||||
<GridItem>
|
||||
<FormControl>
|
||||
<FormLabel fontSize="sm">Y Min (auto if empty)</FormLabel>
|
||||
<NumberInput
|
||||
value={localConfig.y_min || ''}
|
||||
onChange={(valueString) => setLocalConfig(prev => ({
|
||||
...prev,
|
||||
y_min: valueString === '' ? null : parseFloat(valueString)
|
||||
}))}
|
||||
size="sm"
|
||||
>
|
||||
<NumberInputField placeholder="Auto" />
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper />
|
||||
<NumberDecrementStepper />
|
||||
</NumberInputStepper>
|
||||
</NumberInput>
|
||||
</FormControl>
|
||||
</GridItem>
|
||||
|
||||
<GridItem>
|
||||
<FormControl>
|
||||
<FormLabel fontSize="sm">Y Max (auto if empty)</FormLabel>
|
||||
<NumberInput
|
||||
value={localConfig.y_max || ''}
|
||||
onChange={(valueString) => setLocalConfig(prev => ({
|
||||
...prev,
|
||||
y_max: valueString === '' ? null : parseFloat(valueString)
|
||||
}))}
|
||||
size="sm"
|
||||
>
|
||||
<NumberInputField placeholder="Auto" />
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper />
|
||||
<NumberDecrementStepper />
|
||||
</NumberInputStepper>
|
||||
</NumberInput>
|
||||
</FormControl>
|
||||
</GridItem>
|
||||
|
||||
<GridItem>
|
||||
<FormControl>
|
||||
<FormLabel fontSize="sm">Enable Trigger</FormLabel>
|
||||
<Switch
|
||||
isChecked={localConfig.trigger_enabled}
|
||||
onChange={(e) => setLocalConfig(prev => ({
|
||||
...prev,
|
||||
trigger_enabled: e.target.checked
|
||||
}))}
|
||||
size="sm"
|
||||
/>
|
||||
</FormControl>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
<VStack spacing={4} align="stretch">
|
||||
{/* Basic Plot Settings */}
|
||||
<Box>
|
||||
<Heading size="sm" mb={3}>📊 Plot Configuration</Heading>
|
||||
<Grid templateColumns="repeat(auto-fit, minmax(200px, 1fr))" gap={4}>
|
||||
<GridItem>
|
||||
<FormControl>
|
||||
<FormLabel fontSize="sm">Time Window (seconds)</FormLabel>
|
||||
<NumberInput
|
||||
value={localConfig.time_window}
|
||||
onChange={(valueString) => setLocalConfig(prev => ({
|
||||
...prev,
|
||||
time_window: parseInt(valueString) || 60
|
||||
}))}
|
||||
min={10}
|
||||
max={3600}
|
||||
size="sm"
|
||||
>
|
||||
<NumberInputField />
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper />
|
||||
<NumberDecrementStepper />
|
||||
</NumberInputStepper>
|
||||
</NumberInput>
|
||||
</FormControl>
|
||||
</GridItem>
|
||||
|
||||
<GridItem>
|
||||
<FormControl>
|
||||
<FormLabel fontSize="sm">Y Min (auto if empty)</FormLabel>
|
||||
<NumberInput
|
||||
value={localConfig.y_min || ''}
|
||||
onChange={(valueString) => setLocalConfig(prev => ({
|
||||
...prev,
|
||||
y_min: valueString === '' ? null : parseFloat(valueString)
|
||||
}))}
|
||||
size="sm"
|
||||
>
|
||||
<NumberInputField placeholder="Auto" />
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper />
|
||||
<NumberDecrementStepper />
|
||||
</NumberInputStepper>
|
||||
</NumberInput>
|
||||
</FormControl>
|
||||
</GridItem>
|
||||
|
||||
<GridItem>
|
||||
<FormControl>
|
||||
<FormLabel fontSize="sm">Y Max (auto if empty)</FormLabel>
|
||||
<NumberInput
|
||||
value={localConfig.y_max || ''}
|
||||
onChange={(valueString) => setLocalConfig(prev => ({
|
||||
...prev,
|
||||
y_max: valueString === '' ? null : parseFloat(valueString)
|
||||
}))}
|
||||
size="sm"
|
||||
>
|
||||
<NumberInputField placeholder="Auto" />
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper />
|
||||
<NumberDecrementStepper />
|
||||
</NumberInputStepper>
|
||||
</NumberInput>
|
||||
</FormControl>
|
||||
</GridItem>
|
||||
|
||||
<GridItem>
|
||||
<FormControl>
|
||||
<FormLabel fontSize="sm">Enable Trigger</FormLabel>
|
||||
<Switch
|
||||
isChecked={localConfig.trigger_enabled}
|
||||
onChange={(e) => setLocalConfig(prev => ({
|
||||
...prev,
|
||||
trigger_enabled: e.target.checked
|
||||
}))}
|
||||
size="sm"
|
||||
/>
|
||||
</FormControl>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
{/* Visual Style Settings */}
|
||||
<Box>
|
||||
<Heading size="sm" mb={3}>🎨 Visual Style</Heading>
|
||||
<Grid templateColumns="repeat(auto-fit, minmax(200px, 1fr))" gap={4}>
|
||||
<GridItem>
|
||||
<FormControl>
|
||||
<FormLabel fontSize="sm">Line Tension: {localConfig.line_tension}</FormLabel>
|
||||
<Text fontSize="xs" color="gray.500" mb={2}>
|
||||
0 = straight lines, 1 = smooth curves
|
||||
</Text>
|
||||
<Slider
|
||||
value={localConfig.line_tension}
|
||||
onChange={(value) => setLocalConfig(prev => ({
|
||||
...prev,
|
||||
line_tension: value
|
||||
}))}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.1}
|
||||
size="sm"
|
||||
>
|
||||
<SliderTrack>
|
||||
<SliderFilledTrack />
|
||||
</SliderTrack>
|
||||
<SliderThumb />
|
||||
</Slider>
|
||||
</FormControl>
|
||||
</GridItem>
|
||||
|
||||
<GridItem>
|
||||
<FormControl>
|
||||
<FormLabel fontSize="sm">Stepped Lines</FormLabel>
|
||||
<Text fontSize="xs" color="gray.500" mb={2}>
|
||||
Enable step-line style instead of curves
|
||||
</Text>
|
||||
<Checkbox
|
||||
isChecked={localConfig.stepped}
|
||||
onChange={(e) => setLocalConfig(prev => ({
|
||||
...prev,
|
||||
stepped: e.target.checked
|
||||
}))}
|
||||
size="sm"
|
||||
>
|
||||
Step Mode
|
||||
</Checkbox>
|
||||
</FormControl>
|
||||
</GridItem>
|
||||
|
||||
<GridItem>
|
||||
<FormControl>
|
||||
<FormLabel fontSize="sm">Point Size: {localConfig.point_radius}px</FormLabel>
|
||||
<Text fontSize="xs" color="gray.500" mb={2}>
|
||||
Size of data points (0 = hidden)
|
||||
</Text>
|
||||
<Slider
|
||||
value={localConfig.point_radius}
|
||||
onChange={(value) => setLocalConfig(prev => ({
|
||||
...prev,
|
||||
point_radius: value
|
||||
}))}
|
||||
min={0}
|
||||
max={10}
|
||||
step={0.5}
|
||||
size="sm"
|
||||
>
|
||||
<SliderTrack>
|
||||
<SliderFilledTrack />
|
||||
</SliderTrack>
|
||||
<SliderThumb />
|
||||
</Slider>
|
||||
</FormControl>
|
||||
</GridItem>
|
||||
|
||||
<GridItem>
|
||||
<FormControl>
|
||||
<FormLabel fontSize="sm">Hover Point Size: {localConfig.point_hover_radius}px</FormLabel>
|
||||
<Text fontSize="xs" color="gray.500" mb={2}>
|
||||
Size when hovering over points
|
||||
</Text>
|
||||
<Slider
|
||||
value={localConfig.point_hover_radius}
|
||||
onChange={(value) => setLocalConfig(prev => ({
|
||||
...prev,
|
||||
point_hover_radius: value
|
||||
}))}
|
||||
min={0}
|
||||
max={15}
|
||||
step={0.5}
|
||||
size="sm"
|
||||
>
|
||||
<SliderTrack>
|
||||
<SliderFilledTrack />
|
||||
</SliderTrack>
|
||||
<SliderThumb />
|
||||
</Slider>
|
||||
</FormControl>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
</Box>
|
||||
</VStack>
|
||||
|
||||
<Flex mt={4} gap={2}>
|
||||
<Button size="sm" colorScheme="blue" onClick={applyConfigChanges}>
|
||||
|
|
|
@ -243,7 +243,7 @@ export function VariableSelectorWidget(props) {
|
|||
}
|
||||
|
||||
return (
|
||||
<FormControl isRequired={required} isDisabled={disabled} isReadOnly={readonly} isInvalid={rawErrors.length > 0}>
|
||||
<FormControl isDisabled={disabled} isReadOnly={readonly} isInvalid={rawErrors.length > 0}>
|
||||
{label && <FormLabel htmlFor={id}>{label}</FormLabel>}
|
||||
|
||||
<VStack spacing={3} align="stretch">
|
||||
|
@ -309,6 +309,7 @@ export function VariableSelectorWidget(props) {
|
|||
_focus={{ borderColor: focusBorderColor }}
|
||||
bg={bgColor}
|
||||
isDisabled={disabled || readonly}
|
||||
isRequired={required}
|
||||
>
|
||||
<option value="">Select a variable...</option>
|
||||
{filteredVariables.map((variable, index) => (
|
||||
|
|
|
@ -3,11 +3,11 @@
|
|||
"should_connect": true,
|
||||
"should_stream": false,
|
||||
"active_datasets": [
|
||||
"DAR",
|
||||
"Test",
|
||||
"Fast",
|
||||
"DAR"
|
||||
"Fast"
|
||||
]
|
||||
},
|
||||
"auto_recovery_enabled": true,
|
||||
"last_update": "2025-08-14T12:05:58.306284"
|
||||
"last_update": "2025-08-14T13:09:58.767138"
|
||||
}
|
Loading…
Reference in New Issue