Agent Skill
2/7/2026

panel-architecture

Best practices for building Panel visualization apps using param.Parameterized classes, reactive binding with pn.bind(), and URL state sync.

A
allenneuraldynamics
0GitHub Stars
1Views
npx skills add AllenNeuralDynamics/aind-analysis-framework-viz

SKILL.md

Namepanel-architecture
DescriptionBest practices for building Panel visualization apps using param.Parameterized classes, reactive binding with pn.bind(), and URL state sync.

name: panel-architecture description: Best practices for building Panel visualization apps using param.Parameterized classes, reactive binding with pn.bind(), and URL state sync.

Panel Application Architecture

Core Patterns

1. DataHolder for Reactive State

import param
import pandas as pd

class DataHolder(param.Parameterized):
    """Central state container that components watch."""
    selected_id = param.String(default="")
    filtered_df = param.DataFrame()
    is_loaded = param.Boolean(default=False)

2. Reactive Binding with pn.bind()

# Function called when parameter changes
display = pn.bind(
    self.render_content,
    record_id=self.data_holder.param.selected_id,
    df=self.data_holder.param.filtered_df,
)
pn.Column(display)

3. Component Pattern

class MyComponent:
    def __init__(self, data_holder, config):
        self.data_holder = data_holder
        self.config = config

    def create(self):
        """Return Panel viewable. Called once or reactively."""
        return pn.bind(self._render, df=self.data_holder.param.filtered_df)

    def _render(self, df):
        # Create and return widget/pane
        ...

URL State Synchronization

When to Use Each Pattern

ScenarioUseWhy
Widget created oncelocation.sync()Simple, bidirectional
Widget in reactive renderOne-way syncAvoids race conditions
Multiple syncs updating URLOne-way syncPrevents widget reversion

Native Sync (for static widgets)

def _sync_url_state(self):
    location = pn.state.location
    location.sync(self.my_widget, {'value': 'param_name'})

One-Way Sync (for reactive widgets)

from urllib.parse import parse_qs

def get_url_param(name, default=None):
    query = pn.state.location.search or ""
    params = parse_qs(query.lstrip("?"))
    return params.get(name, [default])[0]

def update_url_param(name, value):
    pn.state.location.update_query(**{name: value})

# Usage
class MyComponent:
    def __init__(self):
        self._initial = get_url_param("my_param")  # Read once

    def create(self):
        widget = pn.widgets.Select(value=self._initial, ...)
        widget.param.watch(
            lambda e: update_url_param("my_param", e.new), "value"
        )
        return widget

Auto-Load on Page Load

def main_layout(self):
    # ... create layout ...

    def _autoload():
        if get_url_param("project"):
            self.load_data()
    pn.state.onload(_autoload)

    return template

Layout Patterns

Tabulator for DataFrames

table = pn.widgets.Tabulator(
    df,
    selectable=1,
    frozen_columns=["id"],
    header_filters=True,
    height=400,
    sizing_mode="stretch_width",
)

def on_selection(event):
    if event.new:
        data_holder.selected_id = str(df.iloc[event.new[0]]["_id"])

table.param.watch(on_selection, "selection")

Bokeh Scatter with Hover Image

from bokeh.plotting import figure
from bokeh.models import ColumnDataSource, HoverTool

source = ColumnDataSource(df)
p = figure(tools="pan,wheel_zoom,reset,tap")
scatter = p.scatter(x="x", y="y", source=source)

tooltips = '<img src="@image_url{safe}" width="400">'
p.add_tools(HoverTool(tooltips=tooltips, renderers=[scatter]))

Best Practices

  1. Use pn.bind() over callbacks - cleaner, more efficient
  2. Throttle expensive updates: slider.param.value_throttled
  3. Update data sources, don't recreate widgets
  4. Cache data loading: @pn.cache(ttl=3600)

Project Structure

code/
├── app.py              # Entry point
├── config/             # Configuration (models.py, projects.py)
├── data/               # Data loaders with caching
├── core/               # State management (DataHolder)
├── components/         # UI components
└── utils/              # Helpers (url_state.py)

Running

# Development
panel serve code/app.py --dev --show

# Production
panel serve code/app.py --allow-websocket-origin=* --port 7860
Skills Info
Original Name:panel-architectureAuthor:allenneuraldynamics