**Site index:** [https://2plot.dev/llms.txt](https://2plot.dev/llms.txt) — every page on this site, as Markdown.  
**Network index:** [https://2plot.ai/llms.txt](https://2plot.ai/llms.txt) — The 2plot network; start here to discover sibling sites.  
**Sibling sites:** 11 more in The 2plot network — listed in the site index above.  
**Sitemap:** https://2plot.dev/sitemap.xml  



`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](https://github.com/pip-install-python/dash_planet)

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

```bash
pip install dash-planet
```

---

### Quick Start

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



```python
# 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:

```python
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.



```python
# 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 Tier**                         | **Premium 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](https://plotly.pro/product/prod_SY2xOUihEmOKda)

To use premium features, provide your API key:

```python
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.

```python
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.

```python
@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.

```python
@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:**

```python
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:**

```python
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.

```python
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:**

- **`mass`**: Controls the "weight" of the animation (default: 1)
  - Lower values (0.5-1): Light, quick animations
  - Higher values (2-5): Heavy, slower animations

- **`tension`**: Controls spring stiffness (default: 500)
  - Lower values (100-300): Looser, more elastic
  - Higher values (500-1000): Tighter, snappier

- **`friction`**: Controls damping/resistance (default: 17)
  - Lower values (5-15): More bouncy, oscillating
  - Higher values (20-30): Smoother, more damped

---

### Styling and Appearance

**Component Styling:**

Apply custom styles to the container using the `style` prop:

```python
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:

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

**Satellite Orientation (Premium):**

Control how satellites rotate as they orbit:

- **`DEFAULT`**: No rotation, satellites maintain upright position
- **`INSIDE`**: Satellites face toward the center
- **`OUTSIDE`**: Satellites face away from the center
- **`READABLE`**: Satellites rotate to remain readable (top half upright, bottom half inverted)

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

---

### Browser Support

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

- Chrome (latest)
- Firefox (latest)
- Safari (latest)
- Edge (latest)

**Minimum Requirements:**
- `dash` ≥ 2.0.0
- `react` ≥ 18.3.1

---

### Component Properties

| Property           | Type        | Default      | Description                                                                                                     |
| :----------------- | :---------- | :----------- | :-------------------------------------------------------------------------------------------------------------- |
| **`id`**           | `string`    | **Required** | Unique identifier for the component used in Dash callbacks.                                                     |
| `centerContent`    | `node`      | `None`       | Content displayed in the center of the orbit (e.g., avatar, button, icon).                                      |
| `children`         | `node`      | `None`       | Satellite elements to be displayed in orbit around the center. Can be any valid Dash components.                |
| `open`             | `bool`      | `False`      | Controls visibility of satellite elements. Set to `True` to show satellites, `False` to hide.                   |
| `orbitRadius`      | `number`    | `120`        | Radius of the orbit in pixels. Determines how far satellites are from the center.                               |
| `rotation`         | `number`    | `0`          | Initial rotation angle of the orbital system in degrees. Use to offset starting positions.                      |
| `hideOrbit`        | `bool`      | `False`      | If `True`, hides the circular orbit line. Satellites still orbit, but the path is invisible.                    |
| `mass`             | `number`    | `1`          | Mass parameter for spring physics animation. Higher values create heavier, slower animations.                   |
| `tension`          | `number`    | `500`        | Spring tension parameter. Higher values create stiffer, faster animations.                                      |
| `friction`         | `number`    | `17`         | Spring friction parameter. Higher values create more damping and reduce bounce.                                 |
| `style`            | `dict`      | `{}`         | Standard CSS styles to apply to the component container.                                                        |
| `className`        | `string`    | `None`       | CSS class name to apply to the component container.                                                            |
| `n_clicks`         | `number`    | `0`          | Number of times the center element has been clicked. Updates automatically on click.                            |
| `apiKey`           | `string`    | `None`       | API key for unlocking premium features. Purchase at [plotly.pro](https://plotly.pro/product/prod_SY2xOUihEmOKda) |
| `dragablePlanet`   | `bool`      | `False`      | **(Premium)** Enable dragging of the center element. Requires valid `apiKey`.                                   |
| `dragableSatellites` | `bool`    | `False`      | **(Premium)** Enable dragging of satellite elements. Requires valid `apiKey`.                                   |
| `bounce`           | `bool`      | `False`      | **(Premium)** Enable bounce animation effect. Requires valid `apiKey`.                                          |
| `bounceDirection`  | `string`    | `"TOP"`      | **(Premium)** Direction of bounce animation. Options: `"TOP"`, `"BOTTOM"`, `"LEFT"`, `"RIGHT"`. Requires `apiKey`. |
| `satelliteOrientation` | `string` | `"DEFAULT"` | **(Premium)** How satellites rotate in orbit. Options: `"DEFAULT"`, `"INSIDE"`, `"OUTSIDE"`, `"READABLE"`. Requires `apiKey`. |
| `setProps`         | `func`      | (Dash Internal) | Callback function to update component properties.                                                            |
| `loading_state`    | `object`    | (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.
