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):
    pass

Icon Names#

Icons use Lucide icon names:

IconName
Boxbox
Settingssettings
Radioradio
Chartchart-line
Filefile
Folderfolder
Searchsearch
Playplay

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"}