diff --git a/docs/data/charts/composition/WeatherComposition.js b/docs/data/charts/composition/WeatherComposition.js new file mode 100644 index 0000000000000..26f18b8c62ca8 --- /dev/null +++ b/docs/data/charts/composition/WeatherComposition.js @@ -0,0 +1,297 @@ +import * as React from 'react'; +import Stack from '@mui/material/Stack'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import { ChartsContainer } from '@mui/x-charts/ChartsContainer'; +import { BarPlot } from '@mui/x-charts/BarChart'; +import { LineHighlightPlot, LinePlot, MarkPlot } from '@mui/x-charts/LineChart'; +import { ChartsAxisHighlight } from '@mui/x-charts/ChartsAxisHighlight'; +import { ChartsYAxis } from '@mui/x-charts/ChartsYAxis'; +import { ChartsGrid } from '@mui/x-charts/ChartsGrid'; +import { + ChartsTooltipContainer, + ChartsTooltipPaper, + ChartsTooltipTable, + ChartsTooltipRow, + ChartsTooltipCell, + useAxesTooltip, +} from '@mui/x-charts/ChartsTooltip'; +import { ChartsLabelMark } from '@mui/x-charts/ChartsLabel'; +import { axisClasses } from '@mui/x-charts/ChartsAxis'; + +import { forecast } from '../dataset/weatherForecast'; +import { + DayAndTimeHeader, + WeatherMarkers, + MaxPrecipitationBars, + LegendItem, +} from './weatherCompositionComponents'; + +export default function WeatherComposition() { + const [highlightedAxis, setHighlightedAxis] = React.useState([]); + + const sync = { + highlightedAxis, + onHighlightedAxisChange: setHighlightedAxis, + }; + + return ( + + ({ + width: '100%', + [`& .${axisClasses.tick}, & .${axisClasses.line}`]: { + stroke: theme.palette.divider, + }, + [`& .${axisClasses.tickLabel}`]: { + fill: theme.palette.secondary, + }, + })} + > + + + + + + + + + + + + ); +} + +function ForecastChart(props) { + return ( + + value.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + hour: 'numeric', + }), + }, + ]} + yAxis={[ + { + id: 'temperature', + min: 6, + max: 26, + width: 50, + valueFormatter: (value) => `${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) { + 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 }) { + 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`, + }, + ]; + + 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) => (value !== null ? `${value}mm` : ''), + }, + { + id: 'temperature', + type: 'line', + dataKey: 'temperature', + yAxisId: 'temperature', + label: 'Temperature', + color: colors.temperature, + showMark: false, + curve: 'natural', + valueFormatter: (value) => (value !== null ? `${value}°C` : ''), + }, +]; + +const windSeries = [ + { + id: 'wind', + type: 'line', + dataKey: 'wind', + yAxisId: 'wind', + label: 'Wind', + color: colors.wind, + curve: 'linear', + valueFormatter: (value) => (value !== null ? `${value} m/s` : ''), + }, + { + id: 'gust', + type: 'line', + dataKey: 'gust', + yAxisId: 'wind', + label: 'Wind gust', + color: colors.windGust, + curve: 'linear', + valueFormatter: (value) => (value !== null ? `${value} m/s` : ''), + }, +]; diff --git a/docs/data/charts/composition/WeatherComposition.tsx b/docs/data/charts/composition/WeatherComposition.tsx new file mode 100644 index 0000000000000..d8ddfd43d936d --- /dev/null +++ b/docs/data/charts/composition/WeatherComposition.tsx @@ -0,0 +1,306 @@ +import * as React from 'react'; +import Stack from '@mui/material/Stack'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import { ChartsContainer } from '@mui/x-charts/ChartsContainer'; +import { BarPlot } from '@mui/x-charts/BarChart'; +import { LineHighlightPlot, LinePlot, MarkPlot } from '@mui/x-charts/LineChart'; +import { ChartsAxisHighlight } from '@mui/x-charts/ChartsAxisHighlight'; +import { ChartsYAxis } from '@mui/x-charts/ChartsYAxis'; +import { ChartsGrid } from '@mui/x-charts/ChartsGrid'; +import { + ChartsTooltipContainer, + ChartsTooltipPaper, + ChartsTooltipTable, + ChartsTooltipRow, + ChartsTooltipCell, + useAxesTooltip, +} from '@mui/x-charts/ChartsTooltip'; +import { ChartsLabelMark } from '@mui/x-charts/ChartsLabel'; +import { axisClasses } from '@mui/x-charts/ChartsAxis'; +import { AxisItemIdentifier } from '@mui/x-charts/models'; +import { forecast } from '../dataset/weatherForecast'; +import { + DayAndTimeHeader, + WeatherMarkers, + MaxPrecipitationBars, + LegendItem, +} from './weatherCompositionComponents'; + +export default function WeatherComposition() { + const [highlightedAxis, setHighlightedAxis] = React.useState( + [], + ); + + 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 ( + + ); +} + +export function MaxPrecipitationBars() { + const xScale = useXScale(); + const yScale = useYScale('precipitation'); + const zero = yScale(0) ?? 0; + const barWidth = xScale.bandwidth() * 0.56; + + 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 ( + + ); +} + +export function MaxPrecipitationBars() { + const xScale = useXScale<'band'>(); + const yScale = useYScale<'linear'>('precipitation'); + const zero = yScale(0) ?? 0; + const barWidth = xScale.bandwidth() * 0.56; + + 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, + }, +];