RFDT Solver and WiTwin Simulator are undergoing internal testing as we prepare for public release. The functions are limited. Sign up to receive updates.

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