(
+ [],
+ );
+
+ const sync = {
+ highlightedAxis,
+ onHighlightedAxisChange: setHighlightedAxis,
+ };
+
+ return (
+
+ ({
+ width: '100%',
+ [`& .${axisClasses.tick}, & .${axisClasses.line}`]: {
+ stroke: theme.palette.divider,
+ },
+ [`& .${axisClasses.tickLabel}`]: {
+ fill: theme.palette.secondary,
+ },
+ })}
+ >
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+type AxisSyncProps = {
+ highlightedAxis: AxisItemIdentifier[];
+ onHighlightedAxisChange: (axisItems: AxisItemIdentifier[]) => void;
+};
+
+function ForecastChart(props: AxisSyncProps) {
+ return (
+
+ value.toLocaleDateString('en-US', {
+ month: 'short',
+ day: 'numeric',
+ hour: 'numeric',
+ }),
+ },
+ ]}
+ yAxis={[
+ {
+ id: 'temperature',
+ min: 6,
+ max: 26,
+ width: 50,
+ valueFormatter: (value: number) => `${value}°`,
+ },
+ {
+ id: 'precipitation',
+ label: 'Precipitation (mm)',
+ position: 'right',
+ min: 0,
+ max: 8,
+ width: 56,
+ },
+ ]}
+ series={weatherSeries}
+ height={310}
+ margin={{ top: 64, right: 24, bottom: 8, left: 36 }}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function WindChart(props: AxisSyncProps) {
+ return (
+
+ value.toLocaleDateString('en-US', {
+ month: 'short',
+ day: 'numeric',
+ hour: 'numeric',
+ }),
+ },
+ ]}
+ yAxis={[
+ {
+ id: 'wind',
+ position: 'right',
+ min: 0,
+ width: 56,
+ tickInterval: [0, 4, 8],
+ },
+ ]}
+ series={windSeries}
+ height={150}
+ margin={{ top: 8, right: 24, bottom: 32, left: 86 }}
+ sx={{ [`& [data-series=gust]`]: { strokeDasharray: '4 4' } }}
+ >
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function WeatherTooltip({ type }: { type: 'weather' | 'wind' }) {
+ const tooltipData = useAxesTooltip();
+
+ if (
+ !tooltipData ||
+ tooltipData.length === 0 ||
+ (type === 'weather' &&
+ !tooltipData[0].seriesItems.some((item) => item.seriesId === 'temperature')) ||
+ (type === 'wind' &&
+ !tooltipData[0].seriesItems.some((item) => item.seriesId === 'wind'))
+ ) {
+ return null;
+ }
+
+ const { dataIndex, axisFormattedValue } = tooltipData[0];
+ const item = forecast[dataIndex];
+
+ if (!item) {
+ return null;
+ }
+
+ const rows = [
+ {
+ label: 'Temperature',
+ color: colors.temperature,
+ mark: 'line',
+ value: `${item.temperature}°C`,
+ },
+ {
+ label: 'Precipitation',
+ color: colors.precipitation,
+ mark: 'square',
+ value: `${item.precipitation}mm`,
+ },
+ {
+ label: 'Max precip.',
+ color: colors.maxPrecipitation,
+ mark: 'square',
+ value: `${item.maxPrecipitation}mm`,
+ },
+ { label: 'Wind', color: colors.wind, mark: 'line', value: `${item.wind} m/s` },
+ {
+ label: 'Wind gust',
+ color: colors.windGust,
+ mark: 'line',
+ value: `${item.gust} m/s`,
+ },
+ ] as const;
+
+ return (
+
+
+ {axisFormattedValue}
+
+ {rows.map((row) => (
+
+
+
+
+ {row.label}
+
+
+ {row.value}
+
+ ))}
+
+
+
+ );
+}
+
+const colors = {
+ temperature: '#c21807',
+ precipitation: '#1565c0',
+ maxPrecipitation: '#64b5f6',
+ wind: '#7e22ce',
+ windGust: '#7c4dff',
+};
+
+const weatherSeries = [
+ {
+ id: 'precipitation',
+ type: 'bar',
+ dataKey: 'precipitation',
+ yAxisId: 'precipitation',
+ label: 'Precipitation',
+ color: colors.precipitation,
+ valueFormatter: (value: number | null) => (value !== null ? `${value}mm` : ''),
+ },
+ {
+ id: 'temperature',
+ type: 'line',
+ dataKey: 'temperature',
+ yAxisId: 'temperature',
+ label: 'Temperature',
+ color: colors.temperature,
+ showMark: false,
+ curve: 'natural',
+ valueFormatter: (value: number | null) => (value !== null ? `${value}°C` : ''),
+ },
+] as const;
+
+const windSeries = [
+ {
+ id: 'wind',
+ type: 'line',
+ dataKey: 'wind',
+ yAxisId: 'wind',
+ label: 'Wind',
+ color: colors.wind,
+ curve: 'linear',
+ valueFormatter: (value: number | null) => (value !== null ? `${value} m/s` : ''),
+ },
+ {
+ id: 'gust',
+ type: 'line',
+ dataKey: 'gust',
+ yAxisId: 'wind',
+ label: 'Wind gust',
+ color: colors.windGust,
+ curve: 'linear',
+ valueFormatter: (value: number | null) => (value !== null ? `${value} m/s` : ''),
+ },
+] as const;
diff --git a/docs/data/charts/composition/composition.md b/docs/data/charts/composition/composition.md
index 633e30e13b759..4191634a68eee 100644
--- a/docs/data/charts/composition/composition.md
+++ b/docs/data/charts/composition/composition.md
@@ -10,6 +10,16 @@ packageName: '@mui/x-charts'
Learn how to use composition to build advanced custom Charts.
+## Overview
+
+Composition lets you assemble charts from individual building blocks instead of a single preconfigured component.
+You can mix series types, stack custom SVG and WebGL layers, and reuse the chart's scales through hooks to render your own elements aligned with the plotted data.
+This unlocks visualizations that go beyond the standard chart components, while keeping a shared coordinate system, axes, tooltip, and interactions.
+
+The forecast below combines bars, lines, axes, a synced tooltip, and custom SVG layers in a single composed chart.
+
+{{"demo": "WeatherComposition.js"}}
+
The MUI X Charts components follow an architecture based on context providers: you can pass your series and axes definitions to specialized components that transform the data and make it available to its descendants.
These descendants can then be composed.
diff --git a/docs/data/charts/composition/weatherCompositionComponents.js b/docs/data/charts/composition/weatherCompositionComponents.js
new file mode 100644
index 0000000000000..2ece0f26200fe
--- /dev/null
+++ b/docs/data/charts/composition/weatherCompositionComponents.js
@@ -0,0 +1,190 @@
+import * as React from 'react';
+import { useTheme } from '@mui/material/styles';
+import Stack from '@mui/material/Stack';
+import Box from '@mui/material/Box';
+import Typography from '@mui/material/Typography';
+import { useDrawingArea, useXScale, useYScale } from '@mui/x-charts/hooks';
+import { forecast } from '../dataset/weatherForecast';
+
+export function WeatherIcon({ type }) {
+ const isRain = type === 'rain';
+ const isMoon = type === 'moon-cloud';
+ const isSun = type === 'partly-cloudy';
+
+ return (
+
+ {isSun && (
+
+
+
+
+ )}
+ {isMoon && (
+
+ )}
+
+ {isRain && (
+
+ )}
+
+ );
+}
+
+export function DayAndTimeHeader() {
+ const xScale = useXScale();
+ const { top, height } = useDrawingArea();
+ const theme = useTheme();
+
+ // Get the start/end time value grouped per day.
+ const days = React.useMemo(() => {
+ return xScale.domain().reduce((acc, date) => {
+ if (
+ acc.length === 0 ||
+ date.getDate() !== acc[acc.length - 1].start.getDate()
+ ) {
+ return [...acc, { start: date, end: date }];
+ }
+ return [
+ ...acc.slice(0, acc.length - 1),
+ { start: acc[acc.length - 1].start, end: date },
+ ];
+ }, []);
+ }, [xScale]);
+
+ return (
+
+ {days.map(({ start, end }, dayIndex) => {
+ const endDay =
+ xScale(end) + xScale.step() - (xScale.step() - xScale.bandwidth()) / 2;
+ const middleDay = (xScale(end) + xScale(start)) / 2;
+
+ const labelX = dayIndex === 0 ? endDay : middleDay;
+ const showLine = dayIndex !== days.length - 1;
+
+ return (
+
+
+ {start.toLocaleDateString('en-US', {
+ weekday: 'short',
+ day: 'numeric',
+ month: 'short',
+ })}
+
+ {showLine && (
+
+ )}
+
+ );
+ })}
+ {forecast.map((item) => {
+ const x = (xScale(item.time) ?? 0) + xScale.bandwidth() / 2;
+ return (
+
+ {item.time.toLocaleTimeString('en-US', { hour: 'numeric' })}
+
+ );
+ })}
+
+ );
+}
+
+export function WeatherMarkers() {
+ const xScale = useXScale();
+ const yScale = useYScale('temperature');
+
+ return (
+
+ {forecast.map((item) => {
+ const x = (xScale(item.time) ?? 0) + xScale.bandwidth() / 2;
+ const y = (yScale(item.temperature) ?? 0) - 26;
+
+ return (
+
+
+
+ );
+ })}
+
+ );
+}
+
+export function MaxPrecipitationBars() {
+ const xScale = useXScale();
+ const yScale = useYScale('precipitation');
+ const zero = yScale(0) ?? 0;
+ const barWidth = xScale.bandwidth() * 0.56;
+
+ return (
+
+
+
+
+
+
+
+ {forecast.map((item) => {
+ const valueY = yScale(item.maxPrecipitation) ?? zero;
+ const x = (xScale(item.time) ?? 0) + (xScale.bandwidth() - barWidth) / 2;
+ return (
+
+ );
+ })}
+
+ );
+}
+
+export function LegendItem({ color, label, dashed = false, hatch = false }) {
+ return (
+
+
+ {label}
+
+ );
+}
diff --git a/docs/data/charts/composition/weatherCompositionComponents.tsx b/docs/data/charts/composition/weatherCompositionComponents.tsx
new file mode 100644
index 0000000000000..ba5346233d5b1
--- /dev/null
+++ b/docs/data/charts/composition/weatherCompositionComponents.tsx
@@ -0,0 +1,203 @@
+import * as React from 'react';
+import { useTheme } from '@mui/material/styles';
+import Stack from '@mui/material/Stack';
+import Box from '@mui/material/Box';
+import Typography from '@mui/material/Typography';
+import { useDrawingArea, useXScale, useYScale } from '@mui/x-charts/hooks';
+import { forecast, type IconType } from '../dataset/weatherForecast';
+
+export function WeatherIcon({ type }: { type: IconType }) {
+ const isRain = type === 'rain';
+ const isMoon = type === 'moon-cloud';
+ const isSun = type === 'partly-cloudy';
+
+ return (
+
+ {isSun && (
+
+
+
+
+ )}
+ {isMoon && (
+
+ )}
+
+ {isRain && (
+
+ )}
+
+ );
+}
+
+export function DayAndTimeHeader() {
+ const xScale = useXScale<'band'>();
+ const { top, height } = useDrawingArea();
+ const theme = useTheme();
+
+ // Get the start/end time value grouped per day.
+ const days = React.useMemo(() => {
+ return (xScale.domain() as Date[]).reduce(
+ (acc: { start: Date; end: Date }[], date: Date) => {
+ if (
+ acc.length === 0 ||
+ date.getDate() !== acc[acc.length - 1].start.getDate()
+ ) {
+ return [...acc, { start: date, end: date }];
+ }
+ return [
+ ...acc.slice(0, acc.length - 1),
+ { start: acc[acc.length - 1].start, end: date },
+ ];
+ },
+ [] as { start: Date; end: Date }[],
+ );
+ }, [xScale]);
+
+ return (
+
+ {days.map(({ start, end }, dayIndex: number) => {
+ const endDay =
+ xScale(end)! + xScale.step() - (xScale.step() - xScale.bandwidth()) / 2;
+ const middleDay = (xScale(end)! + xScale(start)!) / 2;
+
+ const labelX = dayIndex === 0 ? endDay : middleDay;
+ const showLine = dayIndex !== days.length - 1;
+
+ return (
+
+
+ {start.toLocaleDateString('en-US', {
+ weekday: 'short',
+ day: 'numeric',
+ month: 'short',
+ })}
+
+ {showLine && (
+
+ )}
+
+ );
+ })}
+ {forecast.map((item) => {
+ const x = (xScale(item.time) ?? 0) + xScale.bandwidth() / 2;
+ return (
+
+ {item.time.toLocaleTimeString('en-US', { hour: 'numeric' })}
+
+ );
+ })}
+
+ );
+}
+
+export function WeatherMarkers() {
+ const xScale = useXScale<'band'>();
+ const yScale = useYScale<'linear'>('temperature');
+
+ return (
+
+ {forecast.map((item) => {
+ const x = (xScale(item.time) ?? 0) + xScale.bandwidth() / 2;
+ const y = (yScale(item.temperature) ?? 0) - 26;
+
+ return (
+
+
+
+ );
+ })}
+
+ );
+}
+
+export function MaxPrecipitationBars() {
+ const xScale = useXScale<'band'>();
+ const yScale = useYScale<'linear'>('precipitation');
+ const zero = yScale(0) ?? 0;
+ const barWidth = xScale.bandwidth() * 0.56;
+
+ return (
+
+
+
+
+
+
+
+ {forecast.map((item) => {
+ const valueY = yScale(item.maxPrecipitation) ?? zero;
+ const x = (xScale(item.time) ?? 0) + (xScale.bandwidth() - barWidth) / 2;
+ return (
+
+ );
+ })}
+
+ );
+}
+
+export function LegendItem({
+ color,
+ label,
+ dashed = false,
+ hatch = false,
+}: {
+ color: string;
+ label: string;
+ dashed?: boolean;
+ hatch?: boolean;
+}) {
+ return (
+
+
+ {label}
+
+ );
+}
diff --git a/docs/data/charts/dataset/weatherForecast.js b/docs/data/charts/dataset/weatherForecast.js
new file mode 100644
index 0000000000000..243dffd6f8be0
--- /dev/null
+++ b/docs/data/charts/dataset/weatherForecast.js
@@ -0,0 +1,122 @@
+export const forecast = [
+ {
+ time: new Date(2026, 5, 8, 20, 0),
+ temperature: 20,
+ precipitation: 0,
+ maxPrecipitation: 0,
+ wind: 2.6,
+ gust: 6,
+ icon: 'cloud',
+ windDirection: 35,
+ },
+ {
+ time: new Date(2026, 5, 8, 22, 0),
+ temperature: 17,
+ precipitation: 0,
+ maxPrecipitation: 0,
+ wind: 2,
+ gust: 4,
+ icon: 'moon-cloud',
+ windDirection: 50,
+ },
+ {
+ time: new Date(2026, 5, 9, 0, 0),
+ temperature: 15,
+ precipitation: 0,
+ maxPrecipitation: 0,
+ wind: 2.4,
+ gust: 5,
+ icon: 'moon-cloud',
+ windDirection: 30,
+ },
+ {
+ time: new Date(2026, 5, 9, 2, 0),
+ temperature: 14,
+ precipitation: 0,
+ maxPrecipitation: 0,
+ wind: 2.8,
+ gust: 5,
+ icon: 'cloud',
+ windDirection: 10,
+ },
+ {
+ time: new Date(2026, 5, 9, 4, 0),
+ temperature: 15,
+ precipitation: 0.4,
+ maxPrecipitation: 1.2,
+ wind: 3.2,
+ gust: 6,
+ icon: 'rain',
+ windDirection: -15,
+ },
+ {
+ time: new Date(2026, 5, 9, 6, 0),
+ temperature: 15,
+ precipitation: 1.8,
+ maxPrecipitation: 3.2,
+ wind: 4.1,
+ gust: 7,
+ icon: 'rain',
+ windDirection: -45,
+ },
+ {
+ time: new Date(2026, 5, 9, 8, 0),
+ temperature: 16,
+ precipitation: 3.9,
+ maxPrecipitation: 5.4,
+ wind: 5.5,
+ gust: 9,
+ icon: 'rain',
+ windDirection: -70,
+ },
+ {
+ time: new Date(2026, 5, 9, 10, 0),
+ temperature: 19,
+ precipitation: 2.8,
+ maxPrecipitation: 4.7,
+ wind: 3.8,
+ gust: 8,
+ icon: 'rain',
+ windDirection: -95,
+ },
+ {
+ time: new Date(2026, 5, 9, 12, 0),
+ temperature: 21,
+ precipitation: 1.2,
+ maxPrecipitation: 3.1,
+ wind: 4.4,
+ gust: 7,
+ icon: 'partly-cloudy',
+ windDirection: -35,
+ },
+ {
+ time: new Date(2026, 5, 9, 14, 0),
+ temperature: 19,
+ precipitation: 0.3,
+ maxPrecipitation: 1.4,
+ wind: 4.6,
+ gust: 7,
+ icon: 'partly-cloudy',
+ windDirection: 15,
+ },
+ {
+ time: new Date(2026, 5, 9, 16, 0),
+ temperature: 16,
+ precipitation: 0,
+ maxPrecipitation: 0.5,
+ wind: 4.7,
+ gust: 6,
+ icon: 'cloud',
+ windDirection: 35,
+ },
+ {
+ time: new Date(2026, 5, 9, 18, 0),
+ temperature: 15,
+ precipitation: 0,
+ maxPrecipitation: 0,
+ wind: 4.4,
+ gust: 6,
+ icon: 'cloud',
+ windDirection: 20,
+ },
+];
diff --git a/docs/data/charts/dataset/weatherForecast.ts b/docs/data/charts/dataset/weatherForecast.ts
new file mode 100644
index 0000000000000..a916085f4284c
--- /dev/null
+++ b/docs/data/charts/dataset/weatherForecast.ts
@@ -0,0 +1,135 @@
+export type IconType = 'cloud' | 'moon-cloud' | 'rain' | 'partly-cloudy';
+
+type Forecast = {
+ time: Date;
+ temperature: number;
+ precipitation: number;
+ maxPrecipitation: number;
+ wind: number;
+ gust: number;
+ icon: IconType;
+ windDirection: number;
+};
+
+export const forecast: Forecast[] = [
+ {
+ time: new Date(2026, 5, 8, 20, 0),
+ temperature: 20,
+ precipitation: 0,
+ maxPrecipitation: 0,
+ wind: 2.6,
+ gust: 6,
+ icon: 'cloud',
+ windDirection: 35,
+ },
+ {
+ time: new Date(2026, 5, 8, 22, 0),
+ temperature: 17,
+ precipitation: 0,
+ maxPrecipitation: 0,
+ wind: 2,
+ gust: 4,
+ icon: 'moon-cloud',
+ windDirection: 50,
+ },
+ {
+ time: new Date(2026, 5, 9, 0, 0),
+ temperature: 15,
+ precipitation: 0,
+ maxPrecipitation: 0,
+ wind: 2.4,
+ gust: 5,
+ icon: 'moon-cloud',
+ windDirection: 30,
+ },
+ {
+ time: new Date(2026, 5, 9, 2, 0),
+ temperature: 14,
+ precipitation: 0,
+ maxPrecipitation: 0,
+ wind: 2.8,
+ gust: 5,
+ icon: 'cloud',
+ windDirection: 10,
+ },
+ {
+ time: new Date(2026, 5, 9, 4, 0),
+ temperature: 15,
+ precipitation: 0.4,
+ maxPrecipitation: 1.2,
+ wind: 3.2,
+ gust: 6,
+ icon: 'rain',
+ windDirection: -15,
+ },
+ {
+ time: new Date(2026, 5, 9, 6, 0),
+ temperature: 15,
+ precipitation: 1.8,
+ maxPrecipitation: 3.2,
+ wind: 4.1,
+ gust: 7,
+ icon: 'rain',
+ windDirection: -45,
+ },
+ {
+ time: new Date(2026, 5, 9, 8, 0),
+ temperature: 16,
+ precipitation: 3.9,
+ maxPrecipitation: 5.4,
+ wind: 5.5,
+ gust: 9,
+ icon: 'rain',
+ windDirection: -70,
+ },
+ {
+ time: new Date(2026, 5, 9, 10, 0),
+ temperature: 19,
+ precipitation: 2.8,
+ maxPrecipitation: 4.7,
+ wind: 3.8,
+ gust: 8,
+ icon: 'rain',
+ windDirection: -95,
+ },
+ {
+ time: new Date(2026, 5, 9, 12, 0),
+ temperature: 21,
+ precipitation: 1.2,
+ maxPrecipitation: 3.1,
+ wind: 4.4,
+ gust: 7,
+ icon: 'partly-cloudy',
+ windDirection: -35,
+ },
+ {
+ time: new Date(2026, 5, 9, 14, 0),
+ temperature: 19,
+ precipitation: 0.3,
+ maxPrecipitation: 1.4,
+ wind: 4.6,
+ gust: 7,
+ icon: 'partly-cloudy',
+ windDirection: 15,
+ },
+ {
+ time: new Date(2026, 5, 9, 16, 0),
+ temperature: 16,
+ precipitation: 0,
+ maxPrecipitation: 0.5,
+ wind: 4.7,
+ gust: 6,
+ icon: 'cloud',
+ windDirection: 35,
+ },
+ {
+ time: new Date(2026, 5, 9, 18, 0),
+ temperature: 15,
+ precipitation: 0,
+ maxPrecipitation: 0,
+ wind: 4.4,
+ gust: 6,
+ icon: 'cloud',
+ windDirection: 20,
+ },
+];