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

The content_type keyword accepts a friendly shorthand that maps to a MIME type:

content_type=MIME typeDescription
"vega"application/vnd.vega.v5+jsonInteractive Vega-Lite charts (bar, line, scatter, heatmap, etc.)
"svg"image/svg+xmlVector graphics
"html"text/htmlArbitrary HTML content
"json"application/jsonStructured data
"png"image/pngBase64-encoded image

The shorthand keys above are the values defined in the @view decorator's content-type map (pandavm/panda_sdk.py). Any other string you pass is used verbatim as the MIME type, so other image MIME types such as image/jpeg render through the explorer's image renderer as well. The renderer also displays text/plain content as preformatted text.

The explorer's Vega renderer matches on the substring vega in the content type, so any Vega/Vega-Lite MIME variant (application/vnd.vega.v5+json, application/vnd.vega-lite+json, etc.) renders the same way -- prefer the "vega" shorthand above for consistency.

When you omit content_type (a bare @view), the VM auto-detects it from the return value:

  • A dict with a $schema field containing "vega" becomes application/vnd.vega.v5+json
  • Any other dict becomes application/json
  • A string starting with <svg or <?xml becomes image/svg+xml
  • A string starting with < and ending with > becomes text/html
  • A string that parses as JSON becomes application/json
  • Any other string becomes text/plain

View output is capped at 1MB.

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 --args '{"title": "Training Clusters"}'

The deploy command takes the index of the code cell that holds the contract source (here cell 0). Note the contract address from the output (e.g., 0xABC...123).

Step 2: Add Some Data Points

%panda call 0xABC...123 add_point --args '{"x": 1.0, "y": 2.5, "label": "cluster_a"}'
%panda call 0xABC...123 add_point --args '{"x": 3.0, "y": 1.2, "label": "cluster_b"}'
%panda call 0xABC...123 add_point --args '{"x": 2.0, "y": 3.8, "label": "cluster_a"}'
%panda call 0xABC...123 add_point --args '{"x": 4.5, "y": 0.9, "label": "cluster_b"}'
%panda call 0xABC...123 add_point --args '{"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 or <?xml
  • For HTML: use @view(content_type="html") and return a string (auto-detect treats a string that starts with < and ends with > as 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