Every Pannellum scene option passes through the tour prop — plus the lookAt fly-to pattern for event-driven camera moves

Scene Configuration

Every Pannellum scene option passes through the tour prop — plus the lookAt fly-to pattern for event-driven camera moves


How configuration flows

A scene dictionary inside tour is passed to Pannellum untouched — anything in the Pannellum configuration reference works, even keys this page doesn't mention. Two rules of thumb:

tour config. Outputting a new tour re-initializes the viewer — since 0.3.0 the container is transparent, so a rebuild dips to your page background instead of flashing white.

imperative, animated, no rebuild. That's the fly-to section below.

Configuration playground

Live controls re-outputting the tour — switch the initial-view preset and set autoRotate for idle cinematics:

# File: docs/scenes/scene_config_example.py

import dash_mantine_components as dmc
from dash import Input, Output, callback, html

from dash_pannellum import DashPannellum

VIEW_PRESETS = {
    "ridge": {"label": "Sunrise ridge", "pitch": 2, "yaw": 5, "hfov": 90},
    "plateau": {"label": "Across the plateau", "pitch": -8, "yaw": 135, "hfov": 110},
    "zenith": {"label": "Straight up", "pitch": 55, "yaw": 0, "hfov": 100},
}


def build_tour(preset_key, auto_rotate):
    view = VIEW_PRESETS[preset_key]
    return {
        "default": {"firstScene": "toco"},
        "scenes": {
            "toco": {
                # Identity shown in the viewer's info bar
                "title": "Cerro Toco",
                "author": "Pip Install Python",
                # The panorama itself
                "type": "equirectangular",
                "panorama": "https://pannellum.org/images/cerro-toco-0.jpg",
                "autoLoad": True,
                # Initial camera — pitch/yaw/hfov are per-scene config
                "pitch": view["pitch"],
                "yaw": view["yaw"],
                "hfov": view["hfov"],
                # Zoom limits the user can't escape
                "minHfov": 60,
                "maxHfov": 120,
                # Idle cinematics: degrees/second, negative pans the other way
                "autoRotate": auto_rotate or None,
            }
        },
    }


component = html.Div(
    [
        DashPannellum(
            id="sc-pano",
            tour=build_tour("ridge", 0),
            autoLoad=True,
            width="100%",
            height="420px",
        ),
        dmc.Group(
            [
                dmc.Select(
                    id="sc-preset",
                    label="Initial view preset",
                    value="ridge",
                    data=[
                        {"value": k, "label": v["label"]}
                        for k, v in VIEW_PRESETS.items()
                    ],
                    w=220,
                    size="sm",
                ),
                dmc.Stack(
                    [
                        dmc.Text("autoRotate (°/s)", size="sm", fw=500),
                        dmc.Slider(
                            id="sc-rotate",
                            value=0,
                            min=-8,
                            max=8,
                            step=1,
                            w=240,
                            marks=[
                                {"value": -8, "label": "-8"},
                                {"value": 0, "label": "off"},
                                {"value": 8, "label": "8"},
                            ],
                        ),
                    ],
                    gap=4,
                ),
            ],
            mt="md",
            gap="xl",
            align="flex-end",
        ),
    ]
)


@callback(
    Output("sc-pano", "tour"),
    Input("sc-preset", "value"),
    Input("sc-rotate", "value"),
    prevent_initial_call=True,
)
def reconfigure(preset_key, auto_rotate):
    # Config props are LIVE: a new tour re-initializes the viewer with the
    # new scene settings. (For camera-only moves, use `lookAt` instead —
    # see the fly-to example below.)
    return build_tour(preset_key, auto_rotate)

:defaultExpanded: false :withExpandedButton: true

The scene config vocabulary

KeyTypeWhat it does
panoramastrEquirectangular image URL (or data URI)
panoramaCanvasIdstr0.3.0 — bind the scene to a live <canvas> instead (see Joystick Arena)
title / authorstrShown in the viewer's info bar
pitch / yaw / hfovnumberInitial camera
minHfov / maxHfovnumberZoom limits
minPitch / maxPitchnumberClamp vertical look (e.g. hide a tripod)
minYaw / maxYawnumberClamp horizontal look (partial panoramas)
autoRotatenumberIdle rotation in °/s; sign sets direction
autoRotateInactivityDelaynumberms of inactivity before rotation resumes
previewstrImage shown before load when autoLoad=False
vaov / vOffsetnumberVertical angle of view for partial panoramas
backgroundColor[r,g,b]Fill color around partial panoramas
hotSpotslistScene-switch and info hotspots (Virtual Tours)
sceneFadeDurationnumberCrossfade ms on scene change (tour default block)

Fly-to: event-driven camera triggers

The pattern you want for "something happened — look over there": keep a dictionary of named targets (pitch/yaw/hfov per place), and have any trigger — a button, a clicked hotspot, an alert from a websocket or an Interval poll — output a lookAt with a duration. Pannellum eases pitch, yaw and zoom together, which reads exactly like a map flyTo:

# File: docs/scenes/flyto_example.py

import random

import dash_mantine_components as dmc
from dash import Input, Output, callback, ctx, html, no_update

from dash_pannellum import DashPannellum

# Named places in the scene. A fly-to is just a lookAt with a duration:
# pitch/yaw aim the camera, hfov zooms it, animated eases the whole move.
TARGETS = {
    "telescope-array": {
        "label": "📡 Telescope array",
        "pitch": -1.2,
        "yaw": 122.0,
        "hfov": 55,
        "story": "Dust on the east dishes — camera dispatched to the array.",
    },
    "snowy-peaks": {
        "label": "🏔 Snowy peaks",
        "pitch": 1.5,
        "yaw": 60.0,
        "hfov": 60,
        "story": "Weather alert on the ridge — checking the snow line.",
    },
    "western-ridge": {
        "label": "🌅 Western ridge",
        "pitch": 0.5,
        "yaw": -120.0,
        "hfov": 70,
        "story": "Motion detected on the western approach.",
    },
}


def fly_to(name, animated=1200):
    target = TARGETS[name]
    return {
        "pitch": target["pitch"],
        "yaw": target["yaw"],
        "hfov": target["hfov"],
        "animated": animated,
    }


component = html.Div(
    [
        DashPannellum(
            id="fly-pano",
            tour={
                "default": {"firstScene": "alma"},
                "scenes": {
                    "alma": {
                        "title": "ALMA Observatory",
                        "type": "equirectangular",
                        "panorama": "https://pannellum.org/images/alma.jpg",
                        "autoLoad": True,
                        "yaw": 117,
                        "pitch": -3,
                        "hfov": 100,
                    }
                },
            },
            # The same targets double as clickable hotspots — clicking one
            # also flies the camera to it (see the second callback).
            callbackHotspots={
                "alma": [
                    {
                        "pitch": t["pitch"],
                        "yaw": t["yaw"],
                        "type": "info",
                        "text": t["label"],
                        "name": name,
                    }
                    for name, t in TARGETS.items()
                ]
            },
            autoLoad=True,
            compass=True,
            width="100%",
            height="420px",
        ),
        dmc.Group(
            [
                *[
                    dmc.Button(
                        t["label"],
                        id={"type": "fly-btn", "target": name},
                        variant="light",
                        size="xs",
                    )
                    for name, t in TARGETS.items()
                ],
                dmc.Button(
                    "🚨 Simulate event",
                    id="fly-event",
                    color="red",
                    variant="filled",
                    size="xs",
                ),
            ],
            mt="md",
            gap="xs",
            wrap="wrap",
        ),
        dmc.Alert(
            "Click a target button, a ⓘ hotspot in the scene, or simulate an "
            "event — the camera flies to the spot.",
            id="fly-status",
            color="gray",
            mt="sm",
        ),
        dmc.Code("camera idle", id="fly-readout", mt="xs"),
    ]
)


@callback(
    Output("fly-pano", "lookAt"),
    Output("fly-status", "children"),
    Output("fly-status", "color"),
    Input({"type": "fly-btn", "target": "telescope-array"}, "n_clicks"),
    Input({"type": "fly-btn", "target": "snowy-peaks"}, "n_clicks"),
    Input({"type": "fly-btn", "target": "western-ridge"}, "n_clicks"),
    Input("fly-event", "n_clicks"),
    Input("fly-pano", "lastClickedHotspot"),
    prevent_initial_call=True,
)
def dispatch_camera(*_):
    trigger = ctx.triggered_id

    if trigger == "fly-event":
        # The "external event" pattern: ANY server-side trigger (a websocket
        # message, an Interval poll, a queue consumer…) can output a lookAt
        # to point the panorama at the area of interest.
        name = random.choice(list(TARGETS))
        return fly_to(name, animated=1500), TARGETS[name]["story"], "red"

    if trigger == "fly-pano":
        name = ctx.inputs["fly-pano.lastClickedHotspot"]
        if name not in TARGETS:
            return no_update, no_update, no_update
        return (
            fly_to(name, animated=900),
            f"Centered on {TARGETS[name]['label']}.",
            "teal",
        )

    name = trigger["target"]
    return fly_to(name), f"Flying to {TARGETS[name]['label']}…", "blue"


@callback(
    Output("fly-readout", "children"),
    Input("fly-pano", "pitch"),
    Input("fly-pano", "yaw"),
    Input("fly-pano", "hfov"),
)
def show_camera(pitch, yaw, hfov):
    return (
        f"pitch {pitch or 0:+.1f}° · yaw {yaw or 0:+.1f}° · "
        f"hfov {hfov or 0:.0f}° (zoom)"
    )

:defaultExpanded: false :withExpandedButton: true

The three triggers in the example, all landing in one callback:

  1. Buttons — pattern-matching IDs, one per target.
  2. The scene itself — clicking a ⓘ callback hotspot flies to and zooms

into that target (lastClickedHotspotlookAt).

  1. A simulated external event — the 🚨 button stands in for any

server-side trigger (queue message, sensor threshold, websocket push): the callback picks the area of interest and dispatches the camera.

Target coordinates are authored the same way as hotspots: turn on showCenterDot=True, stream pitch/yaw (and now hfov) into a readout, aim, copy. The workflow is demonstrated on the Callback Hotspots page.

When to rebuild vs when to fly

You want to…UseCost
Point/zoom the cameralookAtnone — animated in place
Jump to another sceneloadScene (+ preloadScenes)none — instant if preloaded
Move through a world continuouslypanoramaCanvasId + dynamicUpdatea canvas redraw
Change the world's configurationre-output toura rebuild — treat it as a real transition

Source: /components/scenes

Note for AI agents: This is the static, prerendered view of an interactive Dash application served because we detected a non-JS user agent. Full prose docs: