Contents Menu Expand Light mode Dark mode Auto light/dark, in light mode Auto light/dark, in dark mode Skip to content
Dash EChartsX
Dash EChartsX

Guide

  • Usage guide
  • Example gallery
  • Live option playground
  • Migration from dash-echarts

Reference

  • Python API
  • TypeScript component props
Back to top
View this page

Example gallery¶

These complete Dash applications can be run from the repository root after installing the project with python -m pip install -e .. Code blocks include copy buttons; see the examples README for run commands and optional WebSocket setup.

Financial multi-axis chart¶

Candlesticks, trading volume, and MACD share linked category axes and data zoom.

examples/financial_multi_axis.py¶
"""Dash example: candlesticks, trading volume, and MACD on linked axes."""

from datetime import date, timedelta
from math import cos, sin

from dash import Dash, html

from dash_echartsx import DashEChartsX


def ema(values: list[float], period: int) -> list[float]:
    multiplier = 2 / (period + 1)
    result = [values[0]]
    for value in values[1:]:
        result.append((value - result[-1]) * multiplier + result[-1])
    return result


def market_data(count: int = 72) -> tuple[list[str], list[list[float]], list[int]]:
    dates: list[str] = []
    candles: list[list[float]] = []
    volumes: list[int] = []
    previous_close = 96.0
    day = date(2026, 1, 5)

    while len(dates) < count:
        if day.weekday() < 5:
            index = len(dates)
            close = 96 + index * 0.18 + sin(index / 4) * 3.2
            open_price = previous_close + cos(index / 3) * 0.8
            high = max(open_price, close) + 0.7 + (index % 4) * 0.15
            low = min(open_price, close) - 0.6 - (index % 3) * 0.12
            dates.append(day.strftime("%b %d"))
            candles.append(
                [
                    round(open_price, 2),
                    round(close, 2),
                    round(low, 2),
                    round(high, 2),
                ]
            )
            volumes.append(850_000 + (index % 11) * 92_000 + index * 1_700)
            previous_close = close
        day += timedelta(days=1)

    return dates, candles, volumes


def build_option() -> dict:
    dates, candles, volumes = market_data()
    closes = [candle[1] for candle in candles]
    fast = ema(closes, 12)
    slow = ema(closes, 26)
    dif = [fast_value - slow_value for fast_value, slow_value in zip(fast, slow)]
    dea = ema(dif, 9)
    macd = [(dif_value - dea_value) * 2 for dif_value, dea_value in zip(dif, dea)]

    volume_data = [
        {
            "value": volume,
            "itemStyle": {"color": "#e85d5d" if candle[1] >= candle[0] else "#27a889"},
        }
        for candle, volume in zip(candles, volumes)
    ]
    macd_data = [
        {
            "value": round(value, 3),
            "itemStyle": {"color": "#e85d5d" if value >= 0 else "#27a889"},
        }
        for value in macd
    ]

    return {
        "animation": False,
        "title": {"text": "Financial dashboard · Candlestick, volume & MACD"},
        "legend": {
            "top": 30,
            "data": ["Price", "Volume", "MACD", "DIF", "DEA"],
        },
        "tooltip": {"trigger": "axis", "axisPointer": {"type": "cross"}},
        "axisPointer": {"link": [{"xAxisIndex": "all"}]},
        "grid": [
            {"left": 64, "right": 28, "top": 82, "height": "48%"},
            {"left": 64, "right": 28, "top": "64%", "height": "13%"},
            {"left": 64, "right": 28, "top": "82%", "height": "12%"},
        ],
        "xAxis": [
            {"type": "category", "data": dates, "gridIndex": 0, "boundaryGap": True},
            {
                "type": "category",
                "data": dates,
                "gridIndex": 1,
                "axisLabel": {"show": False},
                "boundaryGap": True,
            },
            {
                "type": "category",
                "data": dates,
                "gridIndex": 2,
                "axisLabel": {"rotate": 30},
                "boundaryGap": True,
            },
        ],
        "yAxis": [
            {
                "type": "value",
                "gridIndex": 0,
                "scale": True,
                "splitLine": {"show": True},
            },
            {
                "type": "value",
                "gridIndex": 1,
                "scale": True,
                "splitNumber": 2,
                "splitLine": {"show": False},
            },
            {
                "type": "value",
                "gridIndex": 2,
                "scale": True,
                "splitNumber": 3,
                "splitLine": {"show": False},
            },
        ],
        "dataZoom": [
            {"type": "inside", "xAxisIndex": [0, 1, 2], "start": 55, "end": 100},
            {"type": "slider", "xAxisIndex": [0, 1, 2], "top": "96%", "height": 16},
        ],
        "series": [
            {
                "name": "Price",
                "type": "candlestick",
                "xAxisIndex": 0,
                "yAxisIndex": 0,
                "data": candles,
                "itemStyle": {
                    "color": "#e85d5d",
                    "color0": "#27a889",
                    "borderColor": "#e85d5d",
                    "borderColor0": "#27a889",
                },
            },
            {
                "name": "Volume",
                "type": "bar",
                "xAxisIndex": 1,
                "yAxisIndex": 1,
                "data": volume_data,
            },
            {
                "name": "MACD",
                "type": "bar",
                "xAxisIndex": 2,
                "yAxisIndex": 2,
                "data": macd_data,
            },
            {
                "name": "DIF",
                "type": "line",
                "xAxisIndex": 2,
                "yAxisIndex": 2,
                "data": dif,
                "showSymbol": False,
                "lineStyle": {"width": 1.5, "color": "#5470c6"},
            },
            {
                "name": "DEA",
                "type": "line",
                "xAxisIndex": 2,
                "yAxisIndex": 2,
                "data": dea,
                "showSymbol": False,
                "lineStyle": {"width": 1.5, "color": "#fac858"},
            },
        ],
    }


app = Dash(__name__)
app.layout = html.Main(
    [
        html.H1("Financial chart example"),
        html.P("Linked time axes keep price, volume, and MACD aligned while zooming."),
        DashEChartsX(
            id="financial-chart",
            option=build_option(),
            renderer="canvas",
            style={"width": "100%", "height": "82vh", "aspectRatio": "auto"},
        ),
    ],
    style={"width": "96%", "margin": "1rem auto", "fontFamily": "sans-serif"},
)


if __name__ == "__main__":
    app.run(debug=True)

Real-time streaming¶

The app uses dcc.Interval by default and can consume a JSON WebSocket feed when ECHARTSX_WEBSOCKET_URL is set.

examples/realtime_streaming.py¶
"""Interval-driven chart with an optional WebSocket JSON feed.

Set ECHARTSX_WEBSOCKET_URL to a feed that sends
{"timestamp": "...", "value": 12.3} messages. Without it, the app streams
deterministic sample values from dcc.Interval.
"""

import json
import math
import os
import queue
import threading
from datetime import datetime, timezone

from dash import Dash, Input, Output, State, dcc, html, no_update

from dash_echartsx import DashEChartsX

MAX_POINTS = 90
WEBSOCKET_URL = os.environ.get("ECHARTSX_WEBSOCKET_URL")
POINT_QUEUE: queue.Queue[list[object]] = queue.Queue(maxsize=MAX_POINTS * 2)
ERROR_QUEUE: queue.Queue[str] = queue.Queue(maxsize=1)
STOP_WORKER = threading.Event()


def put_latest(target: queue.Queue, value: object) -> None:
    try:
        target.put_nowait(value)
    except queue.Full:
        try:
            target.get_nowait()
        except queue.Empty:
            pass
        target.put_nowait(value)


def timestamp_label(value: object) -> str:
    if isinstance(value, (int, float)):
        seconds = value / 1000 if value > 10_000_000_000 else value
        return datetime.fromtimestamp(seconds, timezone.utc).strftime("%H:%M:%S")
    return str(value)


def start_websocket_worker(url: str) -> threading.Thread:
    try:
        from websockets.exceptions import WebSocketException
        from websockets.sync.client import connect
    except ImportError as error:
        raise RuntimeError(
            "Install the optional WebSocket client with `pip install websockets`."
        ) from error

    def receive() -> None:
        while not STOP_WORKER.is_set():
            try:
                with connect(url, open_timeout=5) as websocket:
                    for message in websocket:
                        payload = json.loads(message)
                        point = [
                            timestamp_label(payload["timestamp"]),
                            float(payload["value"]),
                        ]
                        if not math.isfinite(point[1]):
                            raise ValueError("WebSocket value must be finite.")
                        put_latest(POINT_QUEUE, point)
            except (
                OSError,
                TimeoutError,
                WebSocketException,
                ValueError,
                KeyError,
                TypeError,
            ) as error:
                if STOP_WORKER.is_set():
                    return
                put_latest(ERROR_QUEUE, str(error))
                print(f"WebSocket feed unavailable ({error}); retrying in 2 seconds.")
                STOP_WORKER.wait(2)

    worker = threading.Thread(target=receive, name="echarts-websocket", daemon=True)
    worker.start()
    return worker


def make_option(points: list[list[object]]) -> dict:
    return {
        "animation": False,
        "grid": {"left": 58, "right": 24, "top": 48, "bottom": 36},
        "tooltip": {"trigger": "axis"},
        "xAxis": {
            "type": "category",
            "boundaryGap": False,
            "data": [point[0] for point in points],
        },
        "yAxis": {"type": "value", "scale": True, "name": "value"},
        "series": [
            {
                "type": "line",
                "name": "Live value",
                "showSymbol": False,
                "smooth": 0.15,
                "lineStyle": {"width": 2},
                "areaStyle": {"opacity": 0.12},
                "data": [point[1] for point in points],
            }
        ],
    }


app = Dash(__name__)
app.layout = html.Main(
    [
        html.H1("Real-time streaming"),
        html.P(
            "The interval generates sample values by default. Set "
            "ECHARTSX_WEBSOCKET_URL to consume JSON points from a WebSocket feed."
        ),
        DashEChartsX(
            id="stream-chart",
            option=make_option([]),
            lazyUpdate=True,
            style={"width": "100%", "height": "65vh", "aspectRatio": "auto"},
        ),
        dcc.Interval(id="stream-interval", interval=1000, n_intervals=0),
        dcc.Store(id="stream-history", data=[]),
        html.P(id="stream-status", role="status"),
    ],
    style={"width": "94%", "margin": "1rem auto", "fontFamily": "sans-serif"},
)


@app.callback(
    Output("stream-chart", "option"),
    Output("stream-history", "data"),
    Output("stream-status", "children"),
    Input("stream-interval", "n_intervals"),
    State("stream-history", "data"),
)
def update_stream(
    tick: int, history: list[list[object]] | None
) -> tuple[dict, list[list[object]], str] | tuple[object, object, str]:
    points = list(history or [])
    incoming: list[list[object]] = []

    if WEBSOCKET_URL:
        while True:
            try:
                incoming.append(POINT_QUEUE.get_nowait())
            except queue.Empty:
                break
        try:
            feed_error = ERROR_QUEUE.get_nowait()
        except queue.Empty:
            feed_error = None
        if not incoming:
            status = (
                f"Waiting for WebSocket data from {WEBSOCKET_URL}."
                if not feed_error
                else f"WebSocket reconnecting: {feed_error}"
            )
            return no_update, no_update, status
        status = f"Receiving WebSocket data from {WEBSOCKET_URL}."
    else:
        now = datetime.now(timezone.utc).strftime("%H:%M:%S")
        value = 100 + 12 * math.sin(tick / 5) + 2 * math.cos(tick / 2)
        incoming.append([now, round(value, 2)])
        status = "Streaming generated sample data with dcc.Interval."

    points = (points + incoming)[-MAX_POINTS:]
    return make_option(points), points, status


if __name__ == "__main__":
    if WEBSOCKET_URL:
        start_websocket_worker(WEBSOCKET_URL)
    app.run(debug=True, use_reloader=False)

GeoJSON map and boundary interaction¶

The example loads a local GeoJSON file, registers it with maps, and reports the clicked boundary.

examples/geospatial_map.py¶
"""Dash example: load local GeoJSON and interact with region boundaries."""

import json
from pathlib import Path

from dash import Dash, Input, Output, html

from dash_echartsx import DashEChartsX

GEOJSON_PATH = Path(__file__).parent / "data" / "harbor_county.geojson"
with GEOJSON_PATH.open(encoding="utf-8") as geojson_file:
    HARBOR_COUNTY = json.load(geojson_file)

REGION_VALUES = [
    {"name": "North Harbor", "value": 74},
    {"name": "Old Town", "value": 52},
    {"name": "South Point", "value": 91},
]

app = Dash(__name__)
app.layout = html.Main(
    [
        html.H1("GeoJSON region explorer"),
        html.P("Select a region boundary to inspect its example index."),
        DashEChartsX(
            id="region-chart",
            maps=[{"name": "harbor-county", "geoJSON": HARBOR_COUNTY}],
            option={
                "tooltip": {"trigger": "item"},
                "visualMap": {
                    "min": 0,
                    "max": 100,
                    "left": "left",
                    "bottom": 24,
                    "text": ["High", "Low"],
                    "inRange": {"color": ["#e8f4f8", "#73b3d1", "#174a70"]},
                },
                "series": [
                    {
                        "name": "Harbor index",
                        "type": "map",
                        "map": "harbor-county",
                        "roam": True,
                        "selectedMode": "single",
                        "emphasis": {
                            "label": {"show": True, "fontWeight": "bold"},
                            "itemStyle": {"areaColor": "#f6c85f"},
                        },
                        "select": {
                            "label": {"show": True, "color": "#18212b"},
                            "itemStyle": {"areaColor": "#f6c85f"},
                        },
                        "data": REGION_VALUES,
                    }
                ],
            },
            style={"width": "100%", "height": "70vh", "aspectRatio": "auto"},
        ),
        html.P("Click a boundary to see its value.", id="region-detail", role="status"),
    ],
    style={"width": "94%", "margin": "1rem auto", "fontFamily": "sans-serif"},
)


@app.callback(Output("region-detail", "children"), Input("region-chart", "click_data"))
def describe_region(event: dict | None) -> str:
    if not event:
        return "Click a boundary to see its value."
    name = event.get("name", "Selected region")
    value = event.get("value")
    if value is None:
        return f"Selected {name}."
    return f"{name}: index {value}."


if __name__ == "__main__":
    app.run(debug=True)
examples/data/harbor_county.geojson¶
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "properties": {
        "name": "North Harbor"
      },
      "geometry": {
        "type": "Polygon",
        "coordinates": [
          [
            [-8, 4],
            [-2, 4],
            [-1, 9],
            [-7, 10],
            [-8, 4]
          ]
        ]
      }
    },
    {
      "type": "Feature",
      "properties": {
        "name": "Old Town"
      },
      "geometry": {
        "type": "Polygon",
        "coordinates": [
          [
            [-3, -2],
            [3, -2],
            [4, 3],
            [-2, 4],
            [-3, -2]
          ]
        ]
      }
    },
    {
      "type": "Feature",
      "properties": {
        "name": "South Point"
      },
      "geometry": {
        "type": "Polygon",
        "coordinates": [
          [
            [2, -8],
            [9, -7],
            [8, -1],
            [3, -2],
            [2, -8]
          ]
        ]
      }
    }
  ]
}

ECharts-GL globe and scatter¶

Both 3D charts load the optional GL bundle. The globe texture is embedded, so the app does not depend on an external image service.

examples/globe_and_scatter3d.py¶
"""Dash example: an ECharts-GL globe with a second 3D scatter plot."""

import base64

from dash import Dash, html

from dash_echartsx import DashEChartsX

TEXTURE_SVG = """\
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="512"
     viewBox="0 0 1024 512">
  <defs>
    <linearGradient id="ocean" x2="0" y2="1">
      <stop offset="0" stop-color="#153b64"/>
      <stop offset="1" stop-color="#07182f"/>
    </linearGradient>
  </defs>
  <rect width="1024" height="512" fill="url(#ocean)"/>
  <g fill="#58a675" stroke="#a6d49a" stroke-width="3">
    <path d="M103 93 151 56 204 65 222 99 195 120
      187 153 151 169 134 204 111 195 99 163 78 146 89 118Z"/>
    <path d="m203 216 33 15 25 45-10 53-23 53-20 65-24-15-7-55
      -21-39 13-50-8-38Z"/>
    <path d="m337 90 38-22 54 10 27 30-16 28-43 2-19 25-32-13
      -31 9-20-26 24-21Z"/>
    <path d="m394 177 45-21 46 25 18 49-20 48-10 66-32 72-24-13
      -14-59-24-38 1-64-20-43Z"/>
    <path d="m489 91 60-29 82 13 42 34-9 29-52 1-25 21-43-10
      -30 24-38-13-13-37Z"/>
    <path d="m585 181 57-20 57 24 24 43-21 39-42 4-25 32-44-9
      -28-44-7-40Z"/>
    <path d="m701 114 45-23 51 18 18 38-28 25-42-6-35 17-31-25Z"/>
    <path d="m800 301 46-16 41 19 14 36-31 31-47-8-28-27Z"/>
  </g>
  <g fill="none" stroke="#6a9fc3" stroke-opacity=".35">
    <path d="M0 128h1024M0 256h1024M0 384h1024"/>
    <path d="M256 0v512M512 0v512M768 0v512"/>
  </g>
</svg>
"""

TEXTURE_URL = "data:image/svg+xml;base64," + base64.b64encode(
    TEXTURE_SVG.encode("utf-8")
).decode("ascii")

GLOBE_OPTION = {
    "backgroundColor": "#07111f",
    "tooltip": {"trigger": "item"},
    "globe": {
        "baseTexture": TEXTURE_URL,
        "globeRadius": 75,
        "shading": "lambert",
        "environment": "#07111f",
        "viewControl": {
            "autoRotate": True,
            "autoRotateSpeed": 4,
            "distance": 180,
        },
        "light": {"main": {"intensity": 1.4, "shadow": True}},
    },
    "series": [
        {
            "name": "Observatories",
            "type": "scatter3D",
            "coordinateSystem": "globe",
            "symbolSize": 13,
            "itemStyle": {"color": "#ffd166", "opacity": 0.95},
            "data": [
                [-122.33, 47.61, 0.2],
                [-73.94, 40.67, 0.2],
                [0.12, 51.51, 0.2],
                [13.4, 52.52, 0.2],
                [139.69, 35.68, 0.2],
                [151.21, -33.87, 0.2],
            ],
        }
    ],
}

SCATTER_OPTION = {
    "backgroundColor": "#101827",
    "tooltip": {},
    "visualMap": {
        "dimension": 3,
        "min": 0,
        "max": 100,
        "inRange": {"color": ["#3b82f6", "#4ade80", "#facc15", "#fb7185"]},
        "textStyle": {"color": "#e5e7eb"},
    },
    "grid3D": {
        "boxWidth": 110,
        "boxDepth": 80,
        "viewControl": {"autoRotate": True, "autoRotateSpeed": 8},
    },
    "xAxis3D": {"name": "X"},
    "yAxis3D": {"name": "Y"},
    "zAxis3D": {"name": "Z"},
    "series": [
        {
            "type": "scatter3D",
            "symbolSize": 9,
            "data": [
                [x, y, (x * 7 + y * 11) % 35, (x * 13 + y * 17) % 101]
                for x, y in [
                    (1, 3),
                    (3, 7),
                    (5, 2),
                    (7, 8),
                    (9, 4),
                    (2, 9),
                    (6, 6),
                    (8, 1),
                    (4, 5),
                    (10, 10),
                    (11, 3),
                    (3, 11),
                ]
            ],
        }
    ],
}

app = Dash(__name__)
app.layout = html.Main(
    [
        html.H1("ECharts-GL: globe and 3D scatter"),
        html.P(
            "The globe uses an embedded texture; both charts work without "
            "external map or image downloads."
        ),
        DashEChartsX(
            id="globe-chart",
            enable_gl=True,
            option=GLOBE_OPTION,
            style={"width": "100%", "height": "55vh", "aspectRatio": "auto"},
        ),
        DashEChartsX(
            id="scatter3d-chart",
            enable_gl=True,
            option=SCATTER_OPTION,
            style={"width": "100%", "height": "55vh", "aspectRatio": "auto"},
        ),
    ],
    style={
        "width": "94%",
        "margin": "1rem auto",
        "fontFamily": "sans-serif",
        "background": "#0b1220",
        "color": "#f3f4f6",
    },
)


if __name__ == "__main__":
    app.run(debug=True)
Next
Live option playground
Previous
Usage guide
Copyright © 2026, dash-echartsx contributors
Made with Sphinx and @pradyunsg's Furo
On this page
  • Example gallery
    • Financial multi-axis chart
    • Real-time streaming
    • GeoJSON map and boundary interaction
    • ECharts-GL globe and scatter