Planet

Interactive orbital menu component with circular satellite layout, spring animations, and premium features

dash-planet is a Dash component library that provides an interactive orbital menu system for creating engaging circular navigation interfaces. It displays content elements (satellites) in a circular orbit around a central element with smooth spring-based animations. The component offers both free and premium tiers, supporting basic orbital menus with up to 3 satellites for free, and unlimited satellites with advanced features like draggable elements, semicircle layouts, and enhanced animation controls with a premium API key.

Installation

Visit GitHub Repo

⭐️ Star this component on GitHub! Stay up to date on new releases and browse the codebase.

pip install dash-planet

Quick Start

Create a basic interactive orbital menu with a central avatar and satellite action buttons.

# File: docs/dash_planet/introduction.py

import dash
from dash import html, Input, Output, State, callback, dcc, ALL
from dash_planet import DashPlanet
from dash_iconify import DashIconify
import json
import dash_mantine_components as dmc
import os
from dotenv import load_dotenv
from pathlib import Path

env_path = Path('.') / '.env'
load_dotenv(env_path)

API_KEY = os.getenv("API_KEY")

# Set React version
# _dash_renderer._set_react_version("18.3.1")

# Get API URL from environment or use default
# app = dash.Dash(__name__, suppress_callback_exceptions=True)

# Enable CORS for development
# app.enable_dev_tools(
#     dev_tools_hot_reload=True,
#     dev_tools_props_check=True,
#     dev_tools_serve_dev_bundles=True,
#     dev_tools_prune_errors=False
# )

# Rest of your styles remain the same...
styles = {
    "container": {
        "width": "100%",
        "height": "100vh",
        "display": "flex",
        "flexDirection": "column",
        "alignItems": "center",
        "justifyContent": "center",
        "padding": "20px",
    },
    "header": {
        "textAlign": "center",
        "marginBottom": "20px",
        "width": "100%",
        "maxWidth": "600px",
    },
    "apiInput": {
        "width": "100%",
        "maxWidth": "400px",
        "padding": "8px",
        "marginBottom": "10px",
        "border": "1px solid #ccc",
        "borderRadius": "4px",
    },
    "description": {
        "fontSize": "14px",
        "color": "#666",
        "marginBottom": "20px",
        "textAlign": "center",
    },
    "demoArea": {
        "display": "flex",
        "width": "100%",
        "height": "500px",  # Fixed height for demo area
        "backgroundColor": "white",
        "justifyContent": "center",
        "alignItems": "center",
        "position": "relative",
    },
    "planet": {
        "height": "120px",
        "width": "120px",
        "borderRadius": "50%",
        "backgroundColor": "#1976d2",
        "display": "flex",
        "justifyContent": "center",
        "alignItems": "center",
        "color": "white",
        "cursor": "pointer",
        "transition": "all 0.3s",
        "position": "relative",
    },
    "satellite": {
        "height": "40px",
        "width": "40px",
        # 'borderRadius': '50%',
        # 'backgroundColor': '#ff4081',
        "display": "flex",
        "justifyContent": "center",
        "alignItems": "center",
        "color": "white",
        "cursor": "pointer",
        "zIndex": 1,
    },
    "gridColumn": {
        "height": "300px",
        "width": "100%",
        "display": "flex",
        "justifyContent": "center",
        "alignItems": "center",
        "position": "relative",
        "padding": "20px",
        "boxSizing": "border-box",
    },
}


def generate_satellites(count):
    """Generate satellite elements with icons"""
    icons = [
        "fxemoji:email",
        "noto:calendar",
        "emojione-v1:bar-chart",
        "emojione:gear",
        "flat-color-icons:file",
        "emojione-v1:open-folder",
        "twemoji:heart-on-fire",
        "flat-color-icons:home",
    ]
    return [
        html.Div(
            [
                DashIconify(
                    icon=icons[i % len(icons)], width=40, height=40, color="white"
                )
            ],
            style=styles["satellite"],
            id={"type": "satellite", "index": i},
        )
        for i in range(count)
    ]


forms_props = dmc.GridCol(
    children=[

        dmc.Paper(
            children=[
                dmc.Stack(
                    [
                        dmc.Text("Props Control Panel", size="xl", fw=700, ta="center"),
                        dmc.Grid(
                            [
                                dmc.GridCol(
                                    dmc.Stack(
                                        [
                                            # Physics Controls
                                            dmc.Text(
                                                "Physics", fw=700, size="sm", c="dimmed"
                                            ),
                                            dmc.Group(
                                                [
                                                    dmc.NumberInput(
                                                        id="mass-input",
                                                        label="Mass",
                                                        value=1,
                                                        min=1,
                                                        max=10,
                                                        step=0.5,
                                                        style={"width": 100},
                                                    ),
                                                    dmc.NumberInput(
                                                        id="tension-input",
                                                        label="Tension",
                                                        value=200,
                                                        min=100,
                                                        max=1000,
                                                        step=50,
                                                        style={"width": 100},
                                                    ),
                                                    dmc.NumberInput(
                                                        id="friction-input",
                                                        label="Friction",
                                                        value=32,
                                                        min=1,
                                                        max=50,
                                                        step=1,
                                                        style={"width": 100},
                                                    ),
                                                ]
                                            ),
                                        ]
                                    ),
                                    span={"xs": 12, "md": 6},
                                ),
                                dmc.GridCol(
                                    dmc.Stack(
                                        [
                                            # Orbit Controls
                                            dmc.Text(
                                                "Orbit",
                                                fw=700,
                                                size="sm",
                                                c="dimmed",
                                                mt="md",
                                            ),
                                            dmc.Group(
                                                [
                                                    dmc.NumberInput(
                                                        id="orbit-radius-input",
                                                        label="Radius",
                                                        value=80,
                                                        min=40,
                                                        max=200,
                                                        step=10,
                                                        style={"width": 100},
                                                    ),
                                                    dmc.NumberInput(
                                                        id="rotation-input",
                                                        label="Rotation",
                                                        value=0,
                                                        min=0,
                                                        max=360,
                                                        step=15,
                                                        style={"width": 100},
                                                    ),
                                                ]
                                            ),
                                        ]
                                    ),
                                    span={"xs": 12, "md": 6},
                                ),
                            ],
                            grow=True,
                        ),
                        dmc.Grid(
                            [
                                dmc.GridCol(
                                    dmc.Stack(
                                        [
                                            dmc.Text(
                                                "Rotation Animation (Works in production not in Docs)",
                                                fw=700,
                                                size="sm",
                                                c="dimmed",
                                                mt="md",
                                            ),
                                            dmc.Group(
                                                [
                                                    dmc.Switch(
                                                        id="animate-rotation-input",
                                                        label="Animate Rotation",
                                                        checked=False,
                                                    ),
                                                    dmc.NumberInput(
                                                        id="rotation-speed-input",
                                                        label="Speed",
                                                        value=2,
                                                        min=0.1,
                                                        max=10,
                                                        step=0.1,
                                                        style={"width": 100},
                                                    ),
                                                ]
                                            ),
                                        ]
                                    ),
                                    span={"xs": 12, "md": 6},
                                ),
                                dmc.GridCol(
                                    dmc.Stack(
                                        [
                                            # Animation Controls
                                            dmc.Text(
                                                "Animation",
                                                fw=700,
                                                size="sm",
                                                c="dimmed",
                                                mt="md",
                                            ),
                                            dmc.Group(
                                                [
                                                    dmc.Switch(
                                                        id="bounce-input",
                                                        label="Bounce",
                                                        checked=True,
                                                    ),
                                                    dmc.Switch(
                                                        id="hide-orbit-input",
                                                        label="Hide Orbit",
                                                        checked=True,
                                                    ),
                                                ]
                                            ),
                                        ]
                                    ),
                                    span={"xs": 12, "md": 6},
                                ),
                            ]
                        ),
                        dmc.Grid(
                            [
                                dmc.GridCol(
                                    # Satellite Orientation
                                    dmc.Select(
                                        id="satellite-orientation-input",
                                        label="Satellite Orientation",
                                        data=[
                                            {"value": "DEFAULT", "label": "Default"},
                                            {"value": "INSIDE", "label": "Inside"},
                                            {"value": "OUTSIDE", "label": "Outside"},
                                            {"value": "READABLE", "label": "Readable"},
                                        ],
                                        value="DEFAULT",
                                        style={"width": "100%"},
                                    ),
                                    span={"xs": 12, "md": 12},
                                )
                            ]
                        ),
                        dcc.Interval(
                            id="rotation-interval",
                            interval=50,  # 50ms = 20fps
                            disabled=True,
                        ),
                    ]
                ),
            ],
            p="md",
            shadow="sm",
            radius="md",
            withBorder=True,
            style={"maxWidth": "100%", "width": "100%"},
        )
    ],
    style={
        "height": "100%",
        "display": "flex",
        "justifyContent": "center",
        "alignItems": "center",
        "position": "relative",
        "overflow": "auto",
    },
    span={"base": 12, "xl": 4},
)


# Create layout
component = dmc.Box(
    [
        dmc.Stack(
            [
                html.H1("DashPlanet Demo", style={"marginBottom": "10px"}),
                html.P(
                    [
                        "Free tier includes up to 3 satellites. ",
                        "Enter an API key to unlock all features.",
                    ],
                    style=styles["description"],
                ),
                dmc.Group(
                    [
                        dcc.Input(
                            id="api-key-input",
                            type="text",
                            placeholder="Enter your API key to check if it works",
                            value="",
                            style={'display': 'none'}
                        ),
                        dmc.Switch(
                            id="use-env-api-key",
                            label="Use Environment API Key",
                            checked=False,
                        ),
                    ],
                    justify="center",
                ),
                html.Div(
                    id="api-key-status", style={"color": "#666", "marginBottom": "10px"}
                ),

            ],
            justify="center",
            align="center",
            gap="md",
        ),
        dmc.Space(h=20),
        dmc.Grid(
            children=[
                forms_props,
                dmc.GridCol(
                    dmc.Stack(
                        [
                            dmc.Space(h=50),
                            dmc.Box(
                                DashPlanet(
                                    id="demo-planet",
                                    centerContent=dmc.Indicator(
                                        dmc.Avatar(
                                            size="lg",
                                            radius="xl",
                                            src="https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/avatars/avatar-3.png",
                                        ),
                                        inline=True,
                                        offset=7,
                                        position="bottom-end",
                                        color="red",
                                        withBorder=True,
                                        size=16,
                                    ),
                                    open=True,
                                    orbitRadius=80,
                                    hideOrbit=True,
                                    bounce=True,
                                    bounceOnOpen=True,
                                    rotation=0,
                                    dragablePlanet=True,
                                    dragableSatellites=True,
                                    satelliteOrientation="DEFAULT",
                                    children=generate_satellites(8),
                                    mass=4,
                                    tension=500,
                                    friction=19,
                                    apiKey="",
                                ),
                                style={
                                    "width": "100%",
                                    "maxWidth": "500px",
                                    "margin": "0 auto",
                                    "display": "flex",
                                    "justifyContent": "center",
                                    "alignItems": "center",
                                    "minHeight": "300px",
                                }
                            ),
                            dmc.Space(h=150),
                            html.Div(
                                [
                                    # API validation status
                                    html.Div(id="validation-status"),
                                    dmc.Text(
                                        "Click a satellite to see its function",
                                        id="action-text",
                                        ta="center",
                                    ),
                                ]
                            ),
                        ],
                        mt="100px",
                    ),
                    span={"base": 12, "xl": 4},
                    style={
                        "height": "100%",
                        "display": "flex",
                        "justifyContent": "center",
                        "alignItems": "center",
                        "position": "relative",
                        "padding": "20px",
                        "boxSizing": "border-box",
                    },
                ),
                dmc.GridCol(
                    dmc.Card(
                        [
                            dmc.Text("Features", size="xl", fw=700, mb="md"),
                            dmc.Stack(
                                [
                                    dmc.Group(
                                        [
                                            DashIconify(icon="mdi:check", width=20),
                                            dmc.Text("Free Tier: Up to 3 satellites"),
                                        ],
                                        gap="xs",
                                    ),
                                    dmc.Group(
                                        [
                                            DashIconify(icon="mdi:star", width=20),
                                            dmc.Text("Premium: Unlimited satellites"),
                                        ],
                                        gap="xs",
                                    ),
                                    dmc.Group(
                                        [
                                            DashIconify(icon="fxemoji:crescentmoon", width=20),
                                            dmc.Text("Premium: Semicircle Menu layout"),
                                        ],
                                        gap="xs",
                                    ),
                                    dmc.Group(
                                        [
                                            DashIconify(icon="mdi:animation", width=20),
                                            dmc.Text("Premium: Enhanced animation controls"),
                                        ],
                                        gap="xs",
                                    ),
                                    dmc.Group(
                                        [
                                            DashIconify(icon="fluent-emoji:sparkling-heart", width=20),
                                            dmc.Text("Supports independent Dash Components development"),
                                        ],
                                        gap="xs",
                                    ),
                                    dmc.Divider(),
                                    dmc.HoverCard(
                                        withArrow=True,
                                        width=200,
                                        shadow="md",
                                        children=[
                                            dmc.HoverCardTarget(
                                                dmc.Group(
                                                    [
                                                        DashIconify(icon="cib:buy-me-a-coffee", width=20),
                                                        dmc.Anchor(
                                                            "Buy a DashPlanet API key",
                                                            href="https://plotly.pro/product/prod_SY2xOUihEmOKda",
                                                            target="_blank",
                                                            size="md",
                                                        ),
                                                    ],
                                                    gap="xs",
                                                )
                                            ),
                                            dmc.HoverCardDropdown(
                                                dmc.Image(
                                                    radius="md",
                                                    src="/assets/images/tippy.png",
                                                )
                                            ),
                                        ],
                                    ),
                                ],
                                gap="sm",
                            ),
                        ],
                        p="xl",
                        shadow="sm",
                        radius="md",
                        withBorder=True,
                        style={"width": "100%"}
                    ),
                    style={
                        "display": "flex",
                        "justifyContent": "center",
                        "alignItems": "center",
                        "position": "relative",
                        "padding": "20px",
                        "boxSizing": "border-box",
                    },
                    span={"base": 12, "xl": 4},
                ),
            ],
            gutter="xl",
        ),
    ]
)


# Update callbacks
@callback(
    [
        Output("demo-planet", "apiKey"),
        Output("api-key-input", "disabled"),
        Output("api-key-status", "children"),
        Output("api-key-status", "style"),
    ],
    [
        Input("api-key-input", "value"),
        Input("use-env-api-key", "checked")
    ],
    prevent_initial_call=True,
)
def update_api_key(api_key, use_env_key):
    """Update API key based on input or environment variable"""
    if use_env_key:
        return API_KEY, True, "Demo using a paid API key", {"color": "#4CAF50"}

    if not api_key:
        return None, False, "Using free tier", {"color": "#666"}

    return api_key, False, f"Using API key: {api_key[:8]}...", {"color": "#4CAF50"}


@callback(
    Output("demo-planet", "open"),
    Input("demo-planet", "n_clicks"),
    prevent_initial_call=True,
)
def toggle_planet(n_clicks):
    """Toggle planet open/closed state"""
    if n_clicks is None:
        return dash.no_update
    return n_clicks % 2 == 1


# Add this callback to handle satellite clicks
@callback(
    Output("action-text", "children"),
    Input({"type": "satellite", "index": ALL}, "n_clicks"),
    prevent_initial_call=True,
)
def handle_satellite_click(clicks):
    ctx = dash.callback_context
    if not ctx.triggered:
        return "Click a satellite to see its function"

    triggered_id = ctx.triggered[0]["prop_id"].split(".")[0]
    satellite_index = json.loads(triggered_id)["index"]

    # Map indices to actions
    actions = [
        "Compose new email",
        "Open calendar",
        "View analytics",
        "Open settings",
        "New document",
        "Browse files",
        "Favorite item",
        "Return home",
    ]

    return f"Selected: {actions[satellite_index % len(actions)]}"


# Add these callbacks after your existing callbacks:
@callback(
    Output("demo-planet", "mass"),
    Input("mass-input", "value"),
    prevent_initial_call=True,
)
def update_mass(value):
    return value


@callback(
    Output("demo-planet", "tension"),
    Input("tension-input", "value"),
    prevent_initial_call=True,
)
def update_tension(value):
    return value


@callback(
    Output("demo-planet", "friction"),
    Input("friction-input", "value"),
    prevent_initial_call=True,
)
def update_friction(value):
    return value


@callback(
    Output("demo-planet", "orbitRadius"),
    Input("orbit-radius-input", "value"),
    prevent_initial_call=True,
)
def update_orbit_radius(value):
    return value


@callback(
    Output("demo-planet", "rotation"),
    Input("rotation-input", "value"),
    prevent_initial_call=True,
)
def update_rotation(value):
    return value


@callback(
    Output("demo-planet", "bounce"),
    Input("bounce-input", "checked"),
    prevent_initial_call=True,
)
def update_bounce(checked):
    return checked


@callback(
    Output("demo-planet", "hideOrbit"),
    Input("hide-orbit-input", "checked"),
    prevent_initial_call=True,
)
def update_hide_orbit(checked):
    return checked


@callback(
    Output("demo-planet", "satelliteOrientation"),
    Input("satellite-orientation-input", "value"),
    prevent_initial_call=True,
)
def update_satellite_orientation(value):
    return value


@callback(
    [Output("rotation-interval", "disabled"), Output("rotation-input", "disabled")],
    Input("animate-rotation-input", "checked"),
)
def toggle_animation(animate):
    return not animate, animate


# Callback to update rotation based on the interval
@callback(
    Output("demo-planet", "rotation", allow_duplicate=True),
    [Input("rotation-interval", "n_intervals"), Input("rotation-speed-input", "value")],
    State("demo-planet", "rotation"),
    prevent_initial_call=True,
)
def update_rotation(n_intervals, speed, current_rotation):
    if current_rotation is None:
        current_rotation = 0
    # Calculate new rotation angle
    new_rotation = (current_rotation + speed) % 360
    return new_rotation


# Callback to handle manual rotation input
@callback(
    Output("rotation-input", "disabled", allow_duplicate=True),
    Input("animate-rotation-input", "checked"),
    prevent_initial_call=True,
)
def toggle_rotation_input(animate):
    return animate

Basic Code Example

Here's a minimal example to get started with DashPlanet:

from dash import Dash
from dash_planet import DashPlanet
import dash_mantine_components as dmc
from dash_iconify import DashIconify

app = Dash(__name__)

app.layout = DashPlanet(
    id='my-planet',
    centerContent=dmc.Avatar(
        size="lg",
        radius="xl",
        src="path/to/avatar.png"
    ),
    children=[
        dmc.ActionIcon(
            DashIconify(icon="clarity:settings-line", width=20, height=20),
            size="lg",
            variant="filled",
            id="action-icon-1",
        ),
        dmc.ActionIcon(
            DashIconify(icon="mdi:email", width=20, height=20),
            size="lg",
            variant="filled",
            id="action-icon-2",
        ),
        dmc.ActionIcon(
            DashIconify(icon="mdi:bell", width=20, height=20),
            size="lg",
            variant="filled",
            id="action-icon-3",
        ),
    ],
    orbitRadius=80,
    rotation=0,
)

if __name__ == '__main__':
    app.run_server(debug=True)

Semicircle Menu Layout

Premium feature that displays satellites in a semicircle layout instead of full circular orbit. Perfect for creating arc-shaped menus and navigation bars.

# File: docs/dash_planet/semicircle_example.py

import dash
from dash import html, Input, Output, State, callback, dcc
from dash_planet import DashPlanet
from dash_iconify import DashIconify
import dash_mantine_components as dmc
import os
from dotenv import load_dotenv
from pathlib import Path

env_path = Path('.') / '.env'
load_dotenv(env_path)

API_URL = os.getenv("API_URL")
API_KEY = os.getenv("API_KEY")

styles = {
    'root': {
        'display': 'flex',
        'flex': '1',
        'width': '100%',
        'justifyContent': 'center',
        'alignItems': 'center',
        'flexDirection': 'column',
        'position': 'relative',
        'gap': '20px'
    },
    'satellite': {
        'height': '40px',
        'width': '40px',
        'display': 'flex',
        'justifyContent': 'center',
        'alignItems': 'center',
        'color': 'white',
        'cursor': 'pointer',
        'zIndex': 1
    }
}


def generate_satellites(count, empty_divs=5):
    """Generate satellite elements with icons"""
    icons = [
        "fxemoji:email",
        "noto:calendar",
        "emojione-v1:bar-chart",
        "emojione:gear",
        "flat-color-icons:file",
        "emojione-v1:open-folder",
        "twemoji:heart-on-fire",
        "flat-color-icons:home",
    ]

    empty_div_style = {
        'width': '40px',
        'height': '40px',
        'transition': 'transform 0.3s ease-in-out'
    }

    return [
        html.Div([
            DashIconify(icon=icons[i % len(icons)], width=40, height=40, color="white")
        ], style=styles['satellite'], id={'type': 'satellite', 'index': i})
        for i in range(count)
    ] + [html.Div(style=empty_div_style) for _ in range(empty_divs)]


component = dmc.Box([
    dcc.Input(
        id='api-key-input',
        type='text',
        value='O3iEIQMkVzbbdgs-ZSfBotNt3WoLhqGjID0fMrhuN64',
        style={'display': 'none'}
    ),

    dmc.Grid([
    # Add form controls
    dmc.GridCol([
        dmc.Paper([
            dmc.Text("Menu Semicircle Controls", size="lg", fw=500, ta="center", mb="md"),
            dmc.NumberInput(
                id="menu-planet-empty-divs-input",
                label="Number of Empty Divs",
                value=5,
                min=0,
                max=10,
                step=1,
                mb="sm"
            ),
            dmc.NumberInput(
                id="menu-planet-rotation-input",
                label="Rotation (degrees)",
                value=0,
                min=0,
                max=360,
                step=45,
                mb="sm"
            ),
            dmc.NumberInput(
                id="menu-planet-orbit-radius-input",
                label="Orbit Radius",
                value=80,
                min=40,
                max=200,
                step=10,
                mb="sm"
            ),
        ], p="md", shadow="sm", radius="md", withBorder=True, style={"width": "100%"})
    ], span={'base': 12, 'lg': 5}),

    dmc.GridCol([
        dmc.Box(
            DashPlanet(
                id='menu-planet',
                centerContent=dmc.Indicator(
                    dmc.Avatar(
                        size="lg",
                        radius="xl",
                        src="https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/avatars/avatar-3.png",
                    ),
                    inline=True,
                    offset=7,
                    position="bottom-end",
                    color="red",
                    withBorder=True,
                    size=16,
                ),
                open=True,
                orbitRadius=80,
                hideOrbit=True,
                bounce=True,
                bounceOnOpen=True,
                rotation=90,
                dragablePlanet=True,
                dragableSatellites=True,
                satelliteOrientation='DEFAULT',
                children=generate_satellites(3),
                mass=4,
                tension=500,
                friction=19,
                apiKey=API_KEY
            ),
            style={
                "width": "100%",
                "maxWidth": "500px",
                "margin": "0 auto",
                "display": "flex",
                "justifyContent": "center",
                "alignItems": "center",
                "minHeight": "300px",
            }
        )
    ], style={
        'display': 'flex',
        'flex': '1',
        'width': '100%',
        'justifyContent': 'center',
        'alignItems': 'center',
        'flexDirection': 'column',
        'position': 'relative',
        'gap': '20px',
        'padding': '20px',
        'boxSizing': 'border-box'
    }, span={'base': 12, 'lg': 7}),
        ], gutter="md")
])


@callback(
    Output('menu-planet', 'children'),
    Output('menu-planet', 'rotation'),
    Input('menu-planet-empty-divs-input', 'value'),
    Input('menu-planet-rotation-input', 'value')
)
def update_satellites(empty_divs, rotation):
    return generate_satellites(3, empty_divs), rotation

@callback(
    Output("menu-planet", "orbitRadius"),
    Input("menu-planet-orbit-radius-input", "value"),
    prevent_initial_call=True,
)
def update_orbit_radius(value):
    return value

Free vs Premium Features

DashPlanet offers both free and premium tiers to suit different project needs.

Free TierPremium Features
✓ Up to 3 satellite elements in orbit🌟 Unlimited satellite elements
✓ Basic orbital animation🌙 Semicircle Menu layout
✓ Customizable orbit radius and rotation⚡ Enhanced animation controls
✓ Click-to-toggle functionality💎 Draggable satellites and center element
🎯 Bounce animations with directional control
🔄 Advanced satellite orientation options

Get Premium Access: Buy DashPlanet API Key

To use premium features, provide your API key:

DashPlanet(
    apiKey="your-api-key-here",
    # Premium features now available
    dragableSatellites=True,
    bounce=True,
    bounceDirection="TOP",
)

Working with Callbacks

Toggle Orbital Menu:

Control the visibility of satellite elements using the open property and n_clicks callback.

from dash import Input, Output, callback

@callback(
    Output("my-planet", "open"),
    Input("my-planet", "n_clicks")
)
def toggle_planet(n_clicks):
    """Toggle satellite visibility on center element click"""
    if n_clicks is None:
        return False
    return n_clicks % 2 == 1

Dynamic Rotation Control:

Update the orbital rotation angle dynamically based on user input.

@callback(
    Output("my-planet", "rotation"),
    Input("rotation-slider", "value")
)
def update_rotation(value):
    """Rotate the entire orbital system"""
    return value

Satellite Click Handling:

Respond to clicks on individual satellite elements by giving each satellite a unique ID.

@callback(
    Output("output-div", "children"),
    Input("satellite-1", "n_clicks"),
    Input("satellite-2", "n_clicks"),
    Input("satellite-3", "n_clicks"),
)
def handle_satellite_clicks(n1, n2, n3):
    """Handle clicks on different satellites"""
    ctx = callback_context
    if not ctx.triggered:
        return "Click a satellite"

    button_id = ctx.triggered[0]["prop_id"].split(".")[0]
    return f"Clicked: {button_id}"

Customizing Satellite Elements

Satellites can be any valid Dash component, allowing for rich, interactive menu items.

Using Icons:

from dash_iconify import DashIconify
from dash import html

satellites = [
    html.Div([
        DashIconify(
            icon="mdi:email",
            width=40,
            height=40,
            color="#3b82f6"
        )
    ], style={'width': '40px', 'height': '40px'})
    for _ in range(3)
]

Using Mantine Components:

import dash_mantine_components as dmc

satellites = [
    dmc.ActionIcon(
        dmc.ThemeIcon(DashIconify(icon="mdi:home")),
        size="lg",
        variant="filled",
        color="blue",
    ),
    dmc.ActionIcon(
        dmc.ThemeIcon(DashIconify(icon="mdi:settings")),
        size="lg",
        variant="filled",
        color="green",
    ),
]

Animation Controls

Fine-tune the spring physics animation using mass, tension, and friction properties.

DashPlanet(
    mass=4,           # Higher mass = slower, heavier animation
    tension=500,      # Higher tension = stiffer spring, faster animation
    friction=19,      # Higher friction = more damping, less bounce
)

Animation Parameter Guide:


Styling and Appearance

Component Styling:

Apply custom styles to the container using the style prop:

DashPlanet(
    style={
        'backgroundColor': '#f8f9fa',
        'borderRadius': '50%',
        'boxShadow': '0 4px 6px rgba(0, 0, 0, 0.1)',
        'padding': '20px',
    }
)

Hiding the Orbit Line:

Toggle the visibility of the orbital path line:

DashPlanet(
    hideOrbit=True,  # Hide the circular orbit line
)

Satellite Orientation (Premium):

Control how satellites rotate as they orbit:

DashPlanet(
    apiKey="your-key",
    satelliteOrientation="READABLE",
)

Browser Support

DashPlanet is compatible with modern browsers that support CSS transforms and React Spring animations:

Minimum Requirements:


Component Properties

PropertyTypeDefaultDescription
idstringRequiredUnique identifier for the component used in Dash callbacks.
centerContentnodeNoneContent displayed in the center of the orbit (e.g., avatar, button, icon).
childrennodeNoneSatellite elements to be displayed in orbit around the center. Can be any valid Dash components.
openboolFalseControls visibility of satellite elements. Set to True to show satellites, False to hide.
orbitRadiusnumber120Radius of the orbit in pixels. Determines how far satellites are from the center.
rotationnumber0Initial rotation angle of the orbital system in degrees. Use to offset starting positions.
hideOrbitboolFalseIf True, hides the circular orbit line. Satellites still orbit, but the path is invisible.
massnumber1Mass parameter for spring physics animation. Higher values create heavier, slower animations.
tensionnumber500Spring tension parameter. Higher values create stiffer, faster animations.
frictionnumber17Spring friction parameter. Higher values create more damping and reduce bounce.
styledict{}Standard CSS styles to apply to the component container.
classNamestringNoneCSS class name to apply to the component container.
n_clicksnumber0Number of times the center element has been clicked. Updates automatically on click.
apiKeystringNoneAPI key for unlocking premium features. Purchase at plotly.pro
dragablePlanetboolFalse(Premium) Enable dragging of the center element. Requires valid apiKey.
dragableSatellitesboolFalse(Premium) Enable dragging of satellite elements. Requires valid apiKey.
bounceboolFalse(Premium) Enable bounce animation effect. Requires valid apiKey.
bounceDirectionstring"TOP"(Premium) Direction of bounce animation. Options: "TOP", "BOTTOM", "LEFT", "RIGHT". Requires apiKey.
satelliteOrientationstring"DEFAULT"(Premium) How satellites rotate in orbit. Options: "DEFAULT", "INSIDE", "OUTSIDE", "READABLE". Requires apiKey.
setPropsfunc(Dash Internal)Callback function to update component properties.
loading_stateobject(Dash Internal)Object describing the loading state of the component or its props.

Note: Premium features marked with (Premium) require a valid API key. Free tier is limited to 3 satellite elements maximum.


Contributing

Contributions to dash-planet are welcome! Please refer to the project's issues on GitHub for any feature requests or bug reports.

License

This project is licensed under the MIT License.

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: