Customize Panels
Create custom panels for the WiTwin Studio interface
Overview#
Panels are dockable UI containers in WiTwin Studio. You can create custom panels to add new functionality, display custom data, or provide specialized tools.
Basic Panel#
Use the @panel decorator to create a custom panel:
from witwin.panels import Panel, panel
@panel(name="My Panel")
class MyPanel(Panel):
def __init__(self):
super().__init__()
self.data = []
def get_ui(self):
return {
"type": "container",
"children": [
{"type": "label", "text": "My Custom Panel"},
{"type": "button", "label": "Click Me", "action": "on_click"},
]
}
def on_click(self):
return {"status": "clicked"}Panel Decorator Options#
@panel(
name="Display Name", # Panel title in UI (required)
icon="box", # Lucide icon name
default_position="right", # Initial dock position: left, right, bottom
default_width=300, # Initial width in pixels
default_height=200 # Initial height in pixels
)
class MyPanel(Panel):
passIcon Names#
Icons use Lucide icon names:
| Icon | Name |
|---|---|
| Box | box |
| Settings | settings |
| Radio | radio |
| Chart | chart-line |
| File | file |
| Folder | folder |
| Search | search |
| Play | play |
UI Definition#
The get_ui() method returns a JSON structure defining the panel's interface:
def get_ui(self):
return {
"type": "container",
"children": [
# UI elements here
]
}UI Elements#
Label
{"type": "label", "text": "Display text"}Button
{
"type": "button",
"label": "Button Text",
"action": "method_name" # Method to call on click
}Input
{
"type": "input",
"placeholder": "Enter value...",
"value": "default",
"action": "on_input_change" # Method receives new value
}Checkbox
{
"type": "checkbox",
"label": "Enable feature",
"checked": True,
"action": "on_toggle"
}Select/Dropdown
{
"type": "select",
"options": ["Option 1", "Option 2", "Option 3"],
"value": "Option 1",
"action": "on_select"
}Slider
{
"type": "slider",
"min": 0,
"max": 100,
"value": 50,
"step": 1,
"action": "on_slider_change"
}Container (Layout)
{
"type": "container",
"direction": "vertical", # or "horizontal"
"children": [...]
}Spacer
{"type": "spacer", "height": 16}Divider
{"type": "divider"}Action Methods#
Methods referenced in action fields are called when the user interacts:
@panel(name="Interactive Panel")
class InteractivePanel(Panel):
def __init__(self):
super().__init__()
self.count = 0
def get_ui(self):
return {
"type": "container",
"children": [
{"type": "label", "text": f"Count: {self.count}"},
{"type": "button", "label": "Increment", "action": "increment"},
{"type": "button", "label": "Reset", "action": "reset"},
]
}
def increment(self):
self.count += 1
return {"refresh": True} # Refresh panel UI
def reset(self):
self.count = 0
return {"refresh": True}Return Values#
Action methods can return:
# Refresh the panel UI
return {"refresh": True}
# Return data
return {"data": {"key": "value"}}
# Show status message
return {"status": "Operation complete"}
# Combination
return {
"refresh": True,
"status": "Saved successfully"
}Accessing Studio APIs#
Panels can use all WiTwin APIs:
from witwin.panels import Panel, panel
from witwin import Console, Results, Notifications
@panel(name="Analysis Panel", icon="chart-line")
class AnalysisPanel(Panel):
def get_ui(self):
return {
"type": "container",
"children": [
{"type": "button", "label": "Run Analysis", "action": "run_analysis"},
{"type": "button", "label": "Export Results", "action": "export"},
]
}
def run_analysis(self):
Console.log("Starting analysis...")
Notifications.progress("analysis", "Analysis", 0)
# Perform analysis
data = self.analyze()
# Show results
Results.plot(data['x'], data['y'], title="Analysis Results")
Results.commit(message="Analysis complete")
Notifications.progress("analysis", "Analysis", 100, "Complete")
Console.log("Analysis finished")
return {"status": "Analysis complete"}
def export(self):
Notifications.info("Export", "Results exported successfully")
return {"status": "Exported"}
def analyze(self):
import numpy as np
x = np.linspace(0, 10, 100)
y = np.sin(x)
return {"x": x.tolist(), "y": y.tolist()}Panel State#
Maintain state in instance variables:
@panel(name="Stateful Panel")
class StatefulPanel(Panel):
def __init__(self):
super().__init__()
self.items = []
self.selected_index = -1
self.filter_text = ""
def get_ui(self):
filtered_items = [
item for item in self.items
if self.filter_text.lower() in item.lower()
]
children = [
{
"type": "input",
"placeholder": "Filter...",
"value": self.filter_text,
"action": "on_filter"
},
{"type": "divider"},
]
for i, item in enumerate(filtered_items):
children.append({
"type": "button",
"label": item,
"action": f"select_{i}"
})
return {"type": "container", "children": children}
def on_filter(self, value):
self.filter_text = value
return {"refresh": True}
def add_item(self, name):
self.items.append(name)
return {"refresh": True}Complete Example#
from witwin.panels import Panel, panel
from witwin import Console, Results, Notifications
import numpy as np
@panel(
name="Signal Generator",
icon="radio",
default_position="right",
default_width=320
)
class SignalGeneratorPanel(Panel):
def __init__(self):
super().__init__()
self.frequency = 1.0
self.amplitude = 1.0
self.wave_type = "sine"
self.duration = 1.0
self.samples = 1000
def get_ui(self):
return {
"type": "container",
"children": [
{"type": "label", "text": "Signal Generator"},
{"type": "divider"},
{"type": "label", "text": "Wave Type"},
{
"type": "select",
"options": ["sine", "square", "sawtooth", "noise"],
"value": self.wave_type,
"action": "set_wave_type"
},
{"type": "spacer", "height": 8},
{"type": "label", "text": f"Frequency: {self.frequency:.1f} Hz"},
{
"type": "slider",
"min": 0.1,
"max": 100,
"value": self.frequency,
"step": 0.1,
"action": "set_frequency"
},
{"type": "label", "text": f"Amplitude: {self.amplitude:.2f}"},
{
"type": "slider",
"min": 0,
"max": 2,
"value": self.amplitude,
"step": 0.01,
"action": "set_amplitude"
},
{"type": "spacer", "height": 16},
{"type": "divider"},
{
"type": "container",
"direction": "horizontal",
"children": [
{"type": "button", "label": "Generate", "action": "generate"},
{"type": "button", "label": "Clear", "action": "clear"},
]
}
]
}
def set_wave_type(self, value):
self.wave_type = value
return {"refresh": True}
def set_frequency(self, value):
self.frequency = float(value)
return {"refresh": True}
def set_amplitude(self, value):
self.amplitude = float(value)
return {"refresh": True}
def generate(self):
Console.log(f"Generating {self.wave_type} wave...")
t = np.linspace(0, self.duration, self.samples)
if self.wave_type == "sine":
signal = self.amplitude * np.sin(2 * np.pi * self.frequency * t)
elif self.wave_type == "square":
signal = self.amplitude * np.sign(np.sin(2 * np.pi * self.frequency * t))
elif self.wave_type == "sawtooth":
signal = self.amplitude * (2 * (t * self.frequency % 1) - 1)
else: # noise
signal = self.amplitude * np.random.randn(self.samples)
Results.clear()
Results.plot(t.tolist(), signal.tolist(), title=f"{self.wave_type.title()} Wave")
Results.commit(message=f"{self.wave_type} f={self.frequency}Hz")
Notifications.success("Signal Generator", f"Generated {self.wave_type} wave")
return {"status": "Signal generated"}
def clear(self):
Results.clear()
return {"status": "Cleared"}