Composite Chart (0.0.8)
CompositeChart demos layering scatter and line series on one surface: trend overlays, reference lines, multi-axis charts and Pro zoom with slider preview.
Overview
CompositeChart demos layering scatter and line series on one surface: trend overlays, reference lines, multi-axis charts and Pro zoom with slider preview.
CompositeChart layers multiple chart types — type: 'scatter' and type: 'line' series — on a single surface using the MUI X Charts composition API (ChartDataProviderPro + ChartsSurface + individual plot components). Basic layering is Community (free); zoom/pan, slider preview, and the toolbar are Pro features that require a licenseKey.
It ships a custom axis tooltip that shows both line and scatter data at the hovered x-position (scatter matches by proximity, auto-computed from axis data spacing), and epoch-ms values are converted to Date objects automatically on scaleType: 'time' axes.
Basic scatter + line overlay
from dash_mui_charts import CompositeChart
CompositeChart(
series=[
{'type': 'scatter', 'id': 'readings', 'label': 'Sensor Readings',
'data': [{'x': 0, 'y': 18.5, 'id': 0}], 'markerSize': 4},
{'type': 'line', 'id': 'trend', 'label': 'Trend Line',
'data': [20.0, 22.1], # positional values
'curve': 'natural', 'showMark': False},
],
xAxis=[{'data': x_values, 'scaleType': 'linear'}],
)
Zoom with slider preview (Pro)
CompositeChart(
licenseKey=MUI_PRO_LICENSE,
series=[
{'type': 'line', 'id': 'baseline', 'data': values, 'area': True},
{'type': 'scatter', 'id': 'anomalies', 'data': scatter_points,
'markerSize': 6,
'preview': {'markerSize': 2}, # marker size in the slider preview
'highlightScope': {'highlight': 'item'}},
],
xAxis=[{
'data': timestamps,
'scaleType': 'time', # epoch ms auto-converted to Date objects
'zoom': {'slider': {'enabled': True, 'preview': True}},
}],
initialZoom=[{'axisId': 'time-axis', 'start': 0, 'end': 30}],
)
Multi-axis
CompositeChart(
series=[
{'type': 'scatter', 'yAxisId': 'left-axis'},
{'type': 'line', 'yAxisId': 'right-axis'},
],
yAxis=[
{'id': 'left-axis', 'position': 'left'},
{'id': 'right-axis', 'position': 'right'},
],
)
Reference lines
CompositeChart(
referenceLines=[
{'y': 28, 'label': 'Upper Limit', 'lineStyle': {'stroke': 'red'}},
{'y': 16, 'label': 'Lower Limit', 'lineStyle': {'stroke': 'red'}},
],
)
Related pages
/composite— this demo (overlays, reference lines, multi-axis, Pro zoom)/composite-v120— v1.2.0 axis tooltip fix, highlightedAxis, cross-chart sync/composite-render-bp— rendering best practices for large time ranges/scatter— standalone ScatterChart
The examples on this page read a MUI X Pro license key from the MUI_PRO_API_KEY environment variable and pass it as licenseKey. Without it the charts render with the unlicensed watermark.
Live examples
# File: docs/composite/demo.py
"""Composite Chart demo — rendered on /composite via `.. exec::`.
Ported verbatim from the pre-migration pages/composite.py (same ids, same callbacks).
"""
import os
import json
import math
import random
import dash_mantine_components as dmc
from dash import html, callback, Input, Output
from dash_mui_charts import CompositeChart
# Get license key from environment
MUI_LICENSE_KEY = os.environ.get('MUI_PRO_API_KEY', '')
# ---------------------------------------------------------------------------
# Generate datasets
# ---------------------------------------------------------------------------
random.seed(99)
# Dataset 1: Scatter + Trend Line (hourly temperature over 3 days)
# Scatter: individual sensor readings with noise
hourly_readings = []
for i in range(72): # 3 days * 24 hours
temp = 20 + 8 * math.sin(2 * math.pi * (i - 6) / 24) + random.gauss(0, 2)
hourly_readings.append({'x': i, 'y': round(temp, 1), 'id': i})
# Line: smoothed expected curve (no noise)
expected_temps = [round(20 + 8 * math.sin(2 * math.pi * (i - 6) / 24), 1) for i in range(72)]
# X-axis data for line positioning
hours_3days = list(range(72))
# Dataset 2: Scatter + Line + Reference (temperature monitoring)
hours_in_week = list(range(168))
sensor_readings = [
{'x': h, 'y': round(22 + 3 * math.sin(2 * math.pi * h / 24) + random.gauss(0, 1.5), 1), 'id': h}
for h in hours_in_week
]
hourly_average = [
round(22 + 3 * math.sin(2 * math.pi * h / 24), 1)
for h in hours_in_week
]
# Dataset 3: Multi-axis (pressure vs temperature)
pressure_scatter = [
{'x': random.uniform(10, 35), 'y': round(900 + random.gauss(0, 30), 1), 'id': i}
for i in range(60)
]
# Sort for line plot
pressure_sorted_x = sorted(set(round(p['x']) for p in pressure_scatter))
pressure_trend = [round(1050 - 5 * x + random.gauss(0, 5), 1) for x in range(10, 36)]
temp_range = list(range(10, 36))
# Dataset 4: Time series with scatter overlay (for zoom demo)
random.seed(42)
ts_hours = 24 * 30 # 30 days
timestamps = [int((1704067200 + h * 3600) * 1000) for h in range(ts_hours)] # epoch ms from Jan 1 2024
baseline = [round(50 + 20 * math.sin(2 * math.pi * h / 168) + random.gauss(0, 3), 1) for h in range(ts_hours)]
# Anomaly scatter points - unusual readings, split into red/yellow groups randomly
anomalies_red = []
anomalies_yellow = []
for i in range(ts_hours):
if random.random() < 0.02: # 2% chance of anomaly
point = {
'x': timestamps[i],
'y': round(baseline[i] + random.choice([-1, 1]) * random.uniform(15, 30), 1),
}
if random.random() < 0.5:
point['id'] = len(anomalies_red)
anomalies_red.append(point)
else:
point['id'] = len(anomalies_yellow)
anomalies_yellow.append(point)
# Common styles
section_style = {'marginBottom': '50px'}
description_style = {'color': 'var(--mantine-color-dimmed)', 'marginBottom': '15px'}
code_style = {
'backgroundColor': 'var(--mantine-color-default)',
'padding': '15px',
'borderRadius': '5px',
'whiteSpace': 'pre-wrap',
'fontSize': '12px',
'overflow': 'auto',
}
pro_badge = html.Span(
"PRO",
style={
'backgroundColor': '#ff9800',
'color': 'white',
'padding': '2px 8px',
'borderRadius': '4px',
'fontSize': '10px',
'fontWeight': 'bold',
'marginLeft': '10px',
'verticalAlign': 'middle',
}
)
new_badge = html.Span(
"0.0.8",
style={
'backgroundColor': '#2e7d32',
'color': 'white',
'padding': '2px 8px',
'borderRadius': '4px',
'fontSize': '10px',
'fontWeight': 'bold',
'marginLeft': '8px',
'verticalAlign': 'middle',
}
)
component = html.Div([
# ==========================================================================
# 1. Scatter + Trend Line
# ==========================================================================
html.Div([
html.H2("Scatter + Trend Line"),
html.P(
"Individual sensor readings (scatter) overlaid with the expected temperature "
"curve (line). The scatter points show actual variance while the line shows "
"the theoretical model. Hover to see both data layers.",
style=description_style
),
CompositeChart(
id='composite-trend',
licenseKey=MUI_LICENSE_KEY,
height=400,
series=[
{
'type': 'scatter',
'id': 'readings',
'label': 'Sensor Readings',
'data': hourly_readings,
'color': '#90caf9',
'markerSize': 4,
},
{
'type': 'line',
'id': 'expected-curve',
'label': 'Expected Curve',
'data': expected_temps,
'color': '#1565c0',
'curve': 'natural',
'showMark': False,
},
],
xAxis=[{
'data': hours_3days,
'scaleType': 'linear',
'label': 'Hour',
'tickNumber': 12,
'min': 0,
'max': 71,
}],
yAxis=[{
'label': 'Temperature (\u00B0C)',
'width': 55,
'domainLimit': 'nice',
}],
grid={'horizontal': True},
voronoiMaxRadius=20,
margin={'left': 65, 'right': 20, 'top': 20, 'bottom': 50},
),
html.Details([
html.Summary("View Code", style={'cursor': 'pointer', 'marginTop': '10px'}),
dmc.CodeHighlight(code="""CompositeChart(
series=[
{
'type': 'scatter', # Scatter for actual readings
'label': 'Sensor Readings',
'data': [{'x': 0, 'y': 18.5, 'id': 0}, ...],
'markerSize': 4,
},
{
'type': 'line', # Line for expected curve
'label': 'Expected Curve',
'data': [20.0, 22.1, ...], # positional values
'curve': 'natural',
'showMark': False,
},
],
xAxis=[{
'data': [0, 1, 2, ...], # linear axis for both types
'scaleType': 'linear',
}],
)""", language="python"),
]),
], style=section_style),
html.Hr(),
# ==========================================================================
# 2. Scatter + Line + Reference Lines
# ==========================================================================
html.Div([
html.H2("Scatter + Line + Reference Lines"),
html.P(
"Temperature sensor readings (scatter) with expected curve (line) and "
"upper/lower threshold reference lines. Points outside thresholds indicate issues.",
style=description_style
),
CompositeChart(
id='composite-reference',
licenseKey=MUI_LICENSE_KEY,
height=400,
series=[
{
'type': 'scatter',
'id': 'readings',
'label': 'Sensor Readings',
'data': sensor_readings,
'color': '#42a5f5',
'markerSize': 2,
},
{
'type': 'line',
'id': 'expected',
'label': 'Expected Curve',
'data': hourly_average,
'color': '#ff7043',
'curve': 'natural',
'showMark': False,
},
],
xAxis=[{
'label': 'Hour of Week',
'tickLabelStyle': {'fontSize': 11},
}],
yAxis=[{
'label': 'Temperature (\u00B0C)',
'width': 50,
'domainLimit': 'nice',
}],
referenceLines=[
{
'y': 28,
'label': 'Upper Limit',
'lineStyle': {'stroke': '#e53935', 'strokeDasharray': '5 5', 'strokeWidth': 2},
'labelStyle': {'fill': '#e53935', 'fontWeight': 'bold'},
'labelAlign': 'end',
},
{
'y': 16,
'label': 'Lower Limit',
'lineStyle': {'stroke': '#e53935', 'strokeDasharray': '5 5', 'strokeWidth': 2},
'labelStyle': {'fill': '#e53935', 'fontWeight': 'bold'},
'labelAlign': 'end',
},
],
grid={'horizontal': True},
voronoiMaxRadius=15,
margin={'left': 60, 'right': 20, 'top': 20, 'bottom': 50},
),
html.Details([
html.Summary("View Code", style={'cursor': 'pointer', 'marginTop': '10px'}),
dmc.CodeHighlight(code="""CompositeChart(
series=[
{'type': 'scatter', 'label': 'Readings', 'data': [...], 'markerSize': 2},
{'type': 'line', 'label': 'Expected', 'data': [...], 'curve': 'natural'},
],
referenceLines=[
{'y': 28, 'label': 'Upper Limit',
'lineStyle': {'stroke': '#e53935', 'strokeDasharray': '5 5'}},
{'y': 16, 'label': 'Lower Limit',
'lineStyle': {'stroke': '#e53935', 'strokeDasharray': '5 5'}},
],
)""", language="python"),
]),
], style=section_style),
html.Hr(),
# ==========================================================================
# 3. Multi-Axis Composite
# ==========================================================================
html.Div([
html.H2("Multi-Axis Composite"),
html.P(
"Scatter data on the left axis and a line trend on the right axis. "
"Useful for showing correlated variables with different scales.",
style=description_style
),
CompositeChart(
id='composite-multiaxis',
licenseKey=MUI_LICENSE_KEY,
height=400,
series=[
{
'type': 'scatter',
'id': 'pressure-scatter',
'label': 'Pressure Readings',
'data': pressure_scatter,
'color': '#5c6bc0',
'markerSize': 4,
'yAxisId': 'pressure-axis',
'highlightScope': {'highlight': 'item', 'fade': 'global'},
},
{
'type': 'line',
'id': 'pressure-trend',
'label': 'Pressure Model',
'data': pressure_trend,
'color': '#ef5350',
'curve': 'natural',
'showMark': False,
'yAxisId': 'model-axis',
},
],
xAxis=[{
'data': temp_range,
'scaleType': 'linear',
'label': 'Temperature (\u00B0C)',
'min': 8,
'max': 37,
}],
yAxis=[
{
'id': 'pressure-axis',
'label': 'Pressure (hPa)',
'position': 'left',
'width': 60,
'labelStyle': {'fill': '#5c6bc0'},
'tickLabelStyle': {'fontSize': 11},
'domainLimit': 'nice',
},
{
'id': 'model-axis',
'label': 'Modeled Pressure (hPa)',
'position': 'right',
'width': 60,
'labelStyle': {'fill': '#ef5350'},
'tickLabelStyle': {'fontSize': 11},
'domainLimit': 'nice',
},
],
grid={'horizontal': True},
voronoiMaxRadius=30,
margin={'left': 70, 'right': 70, 'top': 20, 'bottom': 50},
),
html.Details([
html.Summary("View Code", style={'cursor': 'pointer', 'marginTop': '10px'}),
dmc.CodeHighlight(code="""CompositeChart(
series=[
{'type': 'scatter', 'yAxisId': 'pressure-axis', ...},
{'type': 'line', 'yAxisId': 'model-axis', ...},
],
yAxis=[
{'id': 'pressure-axis', 'position': 'left', ...},
{'id': 'model-axis', 'position': 'right', ...},
],
)""", language="python"),
]),
], style=section_style),
html.Hr(),
# ==========================================================================
# 4. Zoom-Enabled Composite (Pro)
# ==========================================================================
html.Div([
html.H2([
"Zoom-Enabled Composite",
pro_badge,
]),
html.P(
"Time series baseline with scatter anomaly overlay. Zoom and pan to "
"investigate anomalies. The tooltip shows both Baseline and Anomaly "
"data when hovering near an anomaly point. Use the sliders to adjust "
"chart and preview marker sizes.",
style=description_style
),
html.Div([
html.Div([
html.Label("Chart Marker Size", style={
'fontWeight': 500, 'marginBottom': '5px', 'display': 'block',
}),
dmc.Slider(
id='anomaly-marker-slider',
value=6,
min=2,
max=20,
step=1,
marks=[
{'value': 2, 'label': '2'},
{'value': 6, 'label': '6'},
{'value': 10, 'label': '10'},
{'value': 15, 'label': '15'},
{'value': 20, 'label': '20'},
],
style={'maxWidth': '400px'},
),
], style={'flex': '1'}),
html.Div([
html.Label("Preview Marker Size", style={
'fontWeight': 500, 'marginBottom': '5px', 'display': 'block',
}),
dmc.Slider(
id='preview-marker-slider',
value=2,
min=1,
max=10,
step=1,
marks=[
{'value': 1, 'label': '1'},
{'value': 2, 'label': '2'},
{'value': 5, 'label': '5'},
{'value': 10, 'label': '10'},
],
style={'maxWidth': '400px'},
),
], style={'flex': '1'}),
], style={'display': 'flex', 'gap': '40px', 'marginBottom': '20px'}),
CompositeChart(
id='composite-zoom',
licenseKey=MUI_LICENSE_KEY,
height=450,
series=[
{
'type': 'line',
'id': 'baseline',
'label': 'Baseline',
'data': baseline,
'color': '#66bb6a',
'curve': 'natural',
'showMark': False,
'area': True,
},
{
'type': 'scatter',
'id': 'anomalies-red',
'label': 'Anomalies (Critical)',
'data': anomalies_red,
'color': '#e53935',
'markerSize': 6,
'highlightScope': {'highlight': 'item'},
},
{
'type': 'scatter',
'id': 'anomalies-yellow',
'label': 'Anomalies (Warning)',
'data': anomalies_yellow,
'color': '#fdd835',
'markerSize': 6,
'highlightScope': {'highlight': 'item'},
},
],
xAxis=[{
'id': 'time-axis',
'data': timestamps,
'scaleType': 'time',
'label': 'Date',
'tickMinStep': 3600 * 1000 * 24,
'tickLabelStyle': {
'angle': 35,
'fontSize': 11,
'textAnchor': 'start',
},
'height': 60,
'zoom': {
'minSpan': 5,
'panning': True,
'filterMode': 'discard',
'slider': {
'enabled': True,
'preview': True,
},
},
}],
yAxis=[{
'label': 'Value',
'width': 50,
'domainLimit': 'nice',
}],
grid={'horizontal': True},
voronoiMaxRadius=20,
margin={'left': 60, 'right': 20, 'top': 20, 'bottom': 90},
initialZoom=[{'axisId': 'time-axis', 'start': 0, 'end': 30}],
),
html.H4("Click Data:", style={'marginTop': '15px'}),
html.Pre(
id='composite-zoom-output',
children="Click on a point to see event data",
style=code_style,
),
html.Details([
html.Summary("View Code", style={'cursor': 'pointer', 'marginTop': '10px'}),
dmc.CodeHighlight(code="""CompositeChart(
licenseKey=MUI_LICENSE_KEY,
series=[
{
'type': 'line',
'label': 'Baseline',
'data': baseline,
'area': True,
'curve': 'natural',
},
{
'type': 'scatter',
'label': 'Anomalies',
'data': [{'x': timestamp, 'y': value, 'id': i}, ...],
'markerSize': 6, # adjustable via DMC slider
},
],
xAxis=[{
'data': timestamps,
'scaleType': 'time',
'zoom': {
'slider': {'enabled': True, 'preview': True},
},
}],
initialZoom=[{'axisId': 'time-axis', 'start': 0, 'end': 30}],
)""", language="python"),
]),
], style=section_style),
])
@callback(
Output('composite-zoom', 'series'),
Input('anomaly-marker-slider', 'value'),
Input('preview-marker-slider', 'value'),
)
def update_anomaly_marker_size(marker_size, preview_size):
ms = marker_size or 6
ps = preview_size or 2
return [
{
'type': 'line',
'id': 'baseline',
'label': 'Baseline',
'data': baseline,
'color': '#66bb6a',
'curve': 'natural',
'showMark': False,
'area': True,
},
{
'type': 'scatter',
'id': 'anomalies-red',
'label': 'Anomalies (Critical)',
'data': anomalies_red,
'color': '#e53935',
'markerSize': ms,
'preview': {'markerSize': ps},
'highlightScope': {'highlight': 'item'},
},
{
'type': 'scatter',
'id': 'anomalies-yellow',
'label': 'Anomalies (Warning)',
'data': anomalies_yellow,
'color': '#fdd835',
'markerSize': ms,
'preview': {'markerSize': ps},
'highlightScope': {'highlight': 'item'},
},
]
@callback(
Output('composite-zoom-output', 'children'),
Input('composite-zoom', 'clickData'),
prevent_initial_call=True
)
def display_composite_click(click_data):
if click_data:
return json.dumps(click_data, indent=2)
return "Click on a point to see event data"
:defaultExpanded: false :withExpandedButton: true
Source: /composite
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:
- /composite/llms.txt — LLM-friendly documentation
- /sitemap.xml
- /robots.txt