Visualizations (@view)

Panda contracts can produce rich visualizations using the @view decorator. Views render natively in the block explorer, playground, and notebooks -- charts, SVGs, HTML widgets, and images display inline without any external tools.

Overview

The @view decorator marks a query method that returns visual content instead of raw data. The explorer detects the content type and renders it appropriately. Unlike @query, @view accepts a content_type parameter that tells the renderer what format to expect.

Here is a complete Vega-Lite scatter plot contract:

<!-- Source: contracts/views/vega_scatter.py -->
from panda import contract, call, view


@contract
class VegaScatter:
    class State:
        points: list = []
        title: str = "Scatter Plot"

    @call
    def add_point(self, ctx, x: float, y: float, label: str = "default"):
        self.state.points = self.state.points + [{"x": x, "y": y, "label": label}]

    @call
    def set_points(self, ctx, points: list):
        self.state.points = points

    @view(content_type="vega")
    def scatter(self):
        return {
            "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
            "title": self.state.title,
            "data": {"values": self.state.points},
            "mark": {"type": "point", "filled": True, "size": 60},
            "encoding": {
                "x": {"field": "x", "type": "quantitative"},
                "y": {"field": "y", "type": "quantitative"},
                "color": {"field": "label", "type": "nominal"},
            },
            "width": 400,
            "height": 300,
        }

Supported Content Types

TypeContent-TypeDescription
Vega-Liteapplication/vnd.vegalite+jsonInteractive charts (bar, line, scatter, heatmap, etc.)
SVGimage/svg+xmlVector graphics
HTMLtext/htmlArbitrary HTML content
JSONapplication/jsonStructured data
PNG/JPEGimage/png, image/jpegBase64-encoded images
Plain texttext/plainRendered as preformatted text

The explorer auto-detects the content type from the return value:

  • JSON objects with a $schema field containing "vega" are treated as Vega-Lite specs
  • Strings starting with <svg are treated as SVG
  • Strings starting with <! or <html are treated as HTML
  • Otherwise, the content type header from the RPC response is used

Vega-Lite Charts

Vega-Lite is the recommended way to create interactive charts. The VegaScatter contract above shows the pattern: return a Vega-Lite JSON spec from a @view(content_type="vega") method. The spec is a Python dict -- the renderer serializes it automatically.

Vega-Lite charts are interactive by default -- hover for tooltips, click to select, drag to zoom (depending on the spec).

SVG Graphics

Return raw SVG markup for custom vector graphics. Here is the BarChart contract that renders an SVG bar chart:

<!-- Source: contracts/views/bar_chart.py -->
from panda import contract, call, query, view


@contract
class BarChart:
    class State:
        data: list = [10, 25, 40, 30, 50]
        title: str = "Bar Chart"
        bar_color: str = "#2dd4bf"

    @call
    def set_data(self, ctx, data: list):
        self.state.data = data

    @call
    def set_title(self, ctx, title: str):
        self.state.title = title

    @view(content_type="svg")
    def chart(self):
        data = self.state.data
        max_val = max(data) if data else 1
        bar_w = 50
        gap = 10
        chart_w = len(data) * (bar_w + gap) + gap
        chart_h = 150
        color = self.state.bar_color

        bars = ""
        for i, val in enumerate(data):
            h = int((val / max_val) * (chart_h - 30))
            x = gap + i * (bar_w + gap)
            y = chart_h - h - 20
            bars += f'<rect x="{x}" y="{y}" width="{bar_w}" height="{h}" fill="{color}" rx="4"/>'
            bars += f'<text x="{x + bar_w // 2}" y="{chart_h - 5}" text-anchor="middle" font-size="12" fill="#888">{val}</text>'

        title_el = f'<text x="{chart_w // 2}" y="15" text-anchor="middle" font-size="14" font-weight="bold" fill="#fff">{self.state.title}</text>'
        return f'<svg width="{chart_w}" height="{chart_h}" xmlns="http://www.w3.org/2000/svg" style="background:#111">{title_el}{bars}</svg>'

    @query
    def get_data(self):
        return self.state.data

SVG content is sanitized before rendering -- script tags and event handlers are stripped for security.

HTML Widgets

Return HTML for rich interactive content. The Dashboard contract renders metric cards using styled HTML:

<!-- Source: contracts/views/dashboard.py -->
from panda import contract, call, view


@contract
class Dashboard:
    class State:
        metrics: dict = {"users": 0, "transactions": 0, "contracts": 0, "gas_used": 0}
        title: str = "Panda Dashboard"

    @call
    def update_metrics(self, ctx, **kwargs):
        metrics = dict(self.state.metrics)
        for k, v in kwargs.items():
            if k in metrics:
                metrics[k] = v
        self.state.metrics = metrics

    @view(content_type="html")
    def render(self):
        m = self.state.metrics
        cards = ""
        for key, val in m.items():
            label = key.replace("_", " ").title()
            cards += f"""
            <div style="background:#1a1a2e;border-radius:12px;padding:20px;min-width:150px;text-align:center">
                <div style="font-size:28px;font-weight:bold;color:#2dd4bf">{val:,}</div>
                <div style="font-size:13px;color:#888;margin-top:4px">{label}</div>
            </div>"""

        return f"""<!DOCTYPE html>
<html><head><style>
body {{ background:#0b0b0d; color:#fff; font-family:system-ui; margin:0; padding:24px }}
.grid {{ display:flex; gap:16px; flex-wrap:wrap; justify-content:center }}
h1 {{ text-align:center; font-size:22px; margin-bottom:24px; color:#2dd4bf }}
</style></head><body>
<h1>{self.state.title}</h1>
<div class="grid">{cards}</div>
</body></html>"""

HTML is rendered inside a sandboxed iframe for security.

Data Tables

The DataTable contract supports both HTML and JSON views of tabular data:

<!-- Source: contracts/views/data_table.py -->
from panda import contract, call, view


@contract
class DataTable:
    class State:
        columns: list = ["Name", "Value", "Type"]
        rows: list = []

    @call
    def add_row(self, ctx, row: list):
        self.state.rows = self.state.rows + [row]

    @call
    def set_data(self, ctx, columns: list, rows: list):
        self.state.columns = columns
        self.state.rows = rows

    @view(content_type="html")
    def table(self):
        cols = self.state.columns
        rows = self.state.rows

        headers = "".join(
            f"<th style='padding:8px 16px;text-align:left;border-bottom:2px solid #2dd4bf'>{c}</th>"
            for c in cols
        )
        body = ""
        for row in rows:
            cells = "".join(
                f"<td style='padding:8px 16px;border-bottom:1px solid #222'>{v}</td>" for v in row
            )
            body += f"<tr>{cells}</tr>"

        return f"""<!DOCTYPE html>
<html><head><style>
body {{ background:#0b0b0d; color:#e0e0e0; font-family:monospace; margin:0; padding:16px }}
table {{ border-collapse:collapse; width:100% }}
</style></head><body>
<table><thead><tr>{headers}</tr></thead><tbody>{body}</tbody></table>
</body></html>"""

    @view(content_type="json")
    def as_json(self):
        return {
            "columns": self.state.columns,
            "rows": self.state.rows,
            "total_rows": len(self.state.rows),
        }

Note how DataTable exposes two views: table returns styled HTML, while as_json returns structured JSON for programmatic consumption.

Viewing Visualizations

From the Block Explorer

Navigate to any contract page and look for methods marked with the @view badge. Click Render to see the visualization inline.

From the Playground

In the Contract Playground, @view methods appear in the Interact panel with a render button. The visualization displays directly below the method form.

From Notebooks

There are three ways to view visualizations in Notebooks:

1. Using the Contract class:

dashboard = Contract("0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18")
await dashboard.view("render")

2. Using magic commands:

%panda view 0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18 scatter
%panda view 0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18 render

3. Building charts locally from contract data:

You can query contract state and create visualizations entirely in-browser with matplotlib:

import micropip
await micropip.install("matplotlib")
import matplotlib
matplotlib.use("AGG")
import matplotlib.pyplot as plt
import io, base64

# Fetch data from a contract
chart = Contract("0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18")
state = await chart.get_state()

# Create chart locally
fig, ax = plt.subplots(figsize=(8, 4))
ax.bar(range(len(state["data"])), state["data"], color="#2dd4bf")
ax.set_title("Bar Chart from Contract State")

# Render as inline image
buf = io.BytesIO()
fig.savefig(buf, format="png", dpi=100, bbox_inches="tight")
buf.seek(0)
print(f"<img src='data:image/png;base64,{base64.b64encode(buf.read()).decode()}'/>")
plt.close(fig)

Complete Workflow Example

Here is a full example using the real VegaScatter contract: deploy it, add data, then visualize.

Step 1: Deploy from a Notebook

%panda deploy 0 {"title": "Training Clusters"}

Note the contract address from the output (e.g., 0xABC...123).

Step 2: Add Some Data Points

%panda call 0xABC...123 add_point {"x": 1.0, "y": 2.5, "label": "cluster_a"}
%panda call 0xABC...123 add_point {"x": 3.0, "y": 1.2, "label": "cluster_b"}
%panda call 0xABC...123 add_point {"x": 2.0, "y": 3.8, "label": "cluster_a"}
%panda call 0xABC...123 add_point {"x": 4.5, "y": 0.9, "label": "cluster_b"}
%panda call 0xABC...123 add_point {"x": 1.5, "y": 3.1, "label": "cluster_a"}

Step 3: Render the Chart

%panda view 0xABC...123 scatter

The Vega-Lite scatter plot renders inline with colored points grouped by label.

Step 4: Analyze in Python

scatter = Contract("0xABC...123")
# The VegaScatter contract doesn't have a query for raw points,
# but you can query state directly
state = await scatter.get_state()
points = state["points"]
print(f"Total points: {len(points)}")
for p in points:
    print(f"  ({p['x']}, {p['y']}) -> {p['label']}")

Troubleshooting

"address X is not a Panda contract"

This means no Panda contract is deployed at the given address. This can happen if:

  • The address is wrong -- double-check it
  • The contract was deployed on a different network (e.g., local devnet vs mainnet)
  • The deploy transaction failed silently (check the tx receipt status)

Verify a contract exists with %panda state <address> in a notebook.

View returns empty or plain text

Make sure the @view method returns a properly formatted value:

  • For Vega-Lite: use @view(content_type="vega") and return a dict with a $schema field
  • For SVG: use @view(content_type="svg") and return a string starting with <svg
  • For HTML: use @view(content_type="html") and return a string starting with <!DOCTYPE or <html

Chart not interactive

Vega-Lite interactivity depends on the spec. Add selection or params to your spec for click/hover/drag interactions. See the Vega-Lite docs.

Next Steps