diff --git a/projects/js-packages/charts/changelog/add-time-series-forecast-chart b/projects/js-packages/charts/changelog/add-time-series-forecast-chart
new file mode 100644
index 000000000000..f1aef3b57aae
--- /dev/null
+++ b/projects/js-packages/charts/changelog/add-time-series-forecast-chart
@@ -0,0 +1,4 @@
+Significance: minor
+Type: added
+
+Add TimeSeriesForecastChart component for visualizing time series data with forecast periods.
diff --git a/projects/js-packages/charts/package.json b/projects/js-packages/charts/package.json
index 87cb3ef37518..dff135fddab1 100644
--- a/projects/js-packages/charts/package.json
+++ b/projects/js-packages/charts/package.json
@@ -95,6 +95,12 @@
},
"./sparkline/style.css": "./dist/charts/sparkline/index.css",
"./style.css": "./dist/index.css",
+ "./time-series-forecast-chart": {
+ "jetpack:src": "./src/charts/time-series-forecast-chart/index.ts",
+ "import": "./dist/charts/time-series-forecast-chart/index.js",
+ "require": "./dist/charts/time-series-forecast-chart/index.cjs"
+ },
+ "./time-series-forecast-chart/style.css": "./dist/charts/time-series-forecast-chart/index.css",
"./tooltip": {
"jetpack:src": "./src/components/tooltip/index.ts",
"import": "./dist/components/tooltip/index.js",
@@ -171,6 +177,9 @@
"providers": [
"./dist/providers/index.d.ts"
],
+ "time-series-forecast-chart": [
+ "./dist/charts/time-series-forecast-chart/index.d.ts"
+ ],
"tooltip": [
"./dist/components/tooltip/index.d.ts"
],
diff --git a/projects/js-packages/charts/src/charts/time-series-forecast-chart/index.ts b/projects/js-packages/charts/src/charts/time-series-forecast-chart/index.ts
new file mode 100644
index 000000000000..ca7f81724ba5
--- /dev/null
+++ b/projects/js-packages/charts/src/charts/time-series-forecast-chart/index.ts
@@ -0,0 +1,12 @@
+export {
+ default as TimeSeriesForecastChart,
+ TimeSeriesForecastChartUnresponsive,
+} from './time-series-forecast-chart';
+
+export type {
+ TimeSeriesForecastChartProps,
+ TimeSeriesForecastAccessors,
+ SeriesKeys,
+ Accessor,
+ ForecastTooltipParams,
+} from './types';
diff --git a/projects/js-packages/charts/src/charts/time-series-forecast-chart/private/forecast-divider.tsx b/projects/js-packages/charts/src/charts/time-series-forecast-chart/private/forecast-divider.tsx
new file mode 100644
index 000000000000..0e7c71edd34e
--- /dev/null
+++ b/projects/js-packages/charts/src/charts/time-series-forecast-chart/private/forecast-divider.tsx
@@ -0,0 +1,57 @@
+import { DataContext } from '@visx/xychart';
+import { useContext } from 'react';
+import type { FC } from 'react';
+
+interface ForecastDividerProps {
+ /**
+ * The date at which the forecast begins
+ */
+ forecastStart: Date;
+ /**
+ * Color of the divider line. Default: '#888888'
+ */
+ color?: string;
+}
+
+/**
+ * Renders a vertical dashed line at the forecast start position.
+ * Uses DataContext from visx/xychart to access scales.
+ *
+ * @param root0 - Props object
+ * @param root0.forecastStart - The date at which the forecast begins
+ * @param root0.color - Color of the divider line
+ * @return SVG line element or null if scales are not available
+ */
+export const ForecastDivider: FC< ForecastDividerProps > = ( {
+ forecastStart,
+ color = '#888888',
+} ) => {
+ const context = useContext( DataContext );
+
+ if ( ! context?.xScale ) {
+ return null;
+ }
+
+ const { xScale, innerHeight } = context;
+
+ // Get x position from scale
+ const xScaleFn = xScale as ( value: Date ) => number | undefined;
+ const x = xScaleFn( forecastStart );
+
+ if ( x === undefined || ! Number.isFinite( x ) ) {
+ return null;
+ }
+
+ return (
+
+ );
+};
diff --git a/projects/js-packages/charts/src/charts/time-series-forecast-chart/private/index.ts b/projects/js-packages/charts/src/charts/time-series-forecast-chart/private/index.ts
new file mode 100644
index 000000000000..f846e8e2085b
--- /dev/null
+++ b/projects/js-packages/charts/src/charts/time-series-forecast-chart/private/index.ts
@@ -0,0 +1,2 @@
+export { useForecastData } from './use-forecast-data';
+export { ForecastDivider } from './forecast-divider';
diff --git a/projects/js-packages/charts/src/charts/time-series-forecast-chart/private/use-forecast-data.ts b/projects/js-packages/charts/src/charts/time-series-forecast-chart/private/use-forecast-data.ts
new file mode 100644
index 000000000000..8baeab643175
--- /dev/null
+++ b/projects/js-packages/charts/src/charts/time-series-forecast-chart/private/use-forecast-data.ts
@@ -0,0 +1,115 @@
+import { useMemo } from 'react';
+import type { TimeSeriesForecastAccessors, TransformedForecastPoint, BandPoint } from '../types';
+
+interface UseForecastDataOptions< D > {
+ data: readonly D[];
+ accessors: TimeSeriesForecastAccessors< D >;
+ forecastStart: Date;
+}
+
+interface UseForecastDataResult {
+ /** All transformed points (no duplicates) - used for tooltip */
+ allPoints: TransformedForecastPoint[];
+ /** Historical points (before forecastStart) - for visual rendering */
+ historical: TransformedForecastPoint[];
+ /** Forecast points (at or after forecastStart) - for visual rendering */
+ forecast: TransformedForecastPoint[];
+ /** Band data for uncertainty region */
+ bandData: BandPoint[];
+ yDomain: [ number, number ];
+ xDomain: [ Date, Date ];
+}
+
+/**
+ * Safe number extraction - returns null for non-finite numbers
+ *
+ * @param n - The value to extract as a number
+ * @return The number if valid and finite, otherwise null
+ */
+const asNumber = ( n: number | null | undefined ): number | null => {
+ return typeof n === 'number' && Number.isFinite( n ) ? n : null;
+};
+
+/**
+ * Hook to transform and split forecast data for rendering
+ *
+ * Handles:
+ * - Transforming generic data via accessors
+ * - Splitting data into historical and forecast portions
+ * - Creating band data for uncertainty regions
+ * - Computing x and y domains
+ *
+ * @param root0 - Options object
+ * @param root0.data - Array of data points
+ * @param root0.accessors - Accessor functions to extract values from data
+ * @param root0.forecastStart - Date at which forecast begins
+ * @return Transformed data split into historical and forecast portions
+ */
+export function useForecastData< D >( {
+ data,
+ accessors,
+ forecastStart,
+}: UseForecastDataOptions< D > ): UseForecastDataResult {
+ return useMemo( () => {
+ if ( data.length === 0 ) {
+ const now = new Date();
+ return {
+ allPoints: [],
+ historical: [],
+ forecast: [],
+ bandData: [],
+ yDomain: [ 0, 100 ] as [ number, number ],
+ xDomain: [ now, now ] as [ Date, Date ],
+ };
+ }
+
+ // 1. Transform all points via accessors
+ const transformed: TransformedForecastPoint[] = data.map( ( d, i ) => ( {
+ date: accessors.x( d, i ),
+ value: accessors.y( d, i ),
+ lower: accessors.yLower ? asNumber( accessors.yLower( d, i ) ) : null,
+ upper: accessors.yUpper ? asNumber( accessors.yUpper( d, i ) ) : null,
+ originalIndex: i,
+ } ) );
+
+ // 2. Sort by date for rendering stability
+ transformed.sort( ( a, b ) => a.date.getTime() - b.date.getTime() );
+
+ // 3. Split by forecastStart
+ // Both series include the transition point so the lines connect seamlessly visually
+ const forecastStartTime = forecastStart.getTime();
+ const historical = transformed.filter( p => p.date.getTime() <= forecastStartTime );
+ const forecast = transformed.filter( p => p.date.getTime() >= forecastStartTime );
+
+ // 4. Band data: only points with BOTH valid lower AND upper
+ const bandData: BandPoint[] = forecast
+ .filter(
+ ( p ): p is TransformedForecastPoint & { lower: number; upper: number } =>
+ p.lower !== null && p.upper !== null
+ )
+ .map( p => ( { date: p.date, lower: p.lower, upper: p.upper } ) );
+
+ // 5. Compute y domain including bounds
+ const allValues = transformed.map( p => p.value );
+ const allLowers = transformed.map( p => p.lower ).filter( ( v ): v is number => v !== null );
+ const allUppers = transformed.map( p => p.upper ).filter( ( v ): v is number => v !== null );
+
+ // Use concat instead of spread to avoid stack overflow on large arrays
+ const allYValues = allValues.concat( allLowers, allUppers );
+ const minY = allYValues.reduce( ( min, val ) => Math.min( min, val ), Infinity );
+ const maxY = allYValues.reduce( ( max, val ) => Math.max( max, val ), -Infinity );
+
+ // Add some padding to y domain (ensure minimum padding to avoid zero-height scales)
+ const yPadding = Math.max( ( maxY - minY ) * 0.1, 1 );
+ const yDomain: [ number, number ] = [ minY - yPadding, maxY + yPadding ];
+
+ // 6. Compute x domain (use reduce to avoid stack overflow on large arrays)
+ const allDateTimes = transformed.map( p => p.date.getTime() );
+ const xDomain: [ Date, Date ] = [
+ new Date( allDateTimes.reduce( ( min, val ) => Math.min( min, val ), Infinity ) ),
+ new Date( allDateTimes.reduce( ( max, val ) => Math.max( max, val ), -Infinity ) ),
+ ];
+
+ return { allPoints: transformed, historical, forecast, bandData, yDomain, xDomain };
+ }, [ data, accessors, forecastStart ] );
+}
diff --git a/projects/js-packages/charts/src/charts/time-series-forecast-chart/stories/config.tsx b/projects/js-packages/charts/src/charts/time-series-forecast-chart/stories/config.tsx
new file mode 100644
index 000000000000..e67e9177a4fd
--- /dev/null
+++ b/projects/js-packages/charts/src/charts/time-series-forecast-chart/stories/config.tsx
@@ -0,0 +1,156 @@
+import {
+ chartDecorator,
+ sharedChartArgTypes,
+ ChartStoryArgs,
+} from '../../../stories/chart-decorator';
+import { legendArgTypes } from '../../../stories/legend-config';
+import { sharedThemeArgs, themeArgTypes } from '../../../stories/theme-config';
+import TimeSeriesForecastChart from '../time-series-forecast-chart';
+import type { TimeSeriesForecastChartProps } from '../types';
+import type { Meta } from '@storybook/react';
+
+/**
+ * Sample forecast data point type
+ */
+export type SampleForecastDatum = {
+ date: Date;
+ value: number;
+ lower?: number;
+ upper?: number;
+};
+
+/**
+ * Generate sample forecast data for storybook demonstrations.
+ *
+ * @param historicalDays - Number of historical days to generate
+ * @param forecastDays - Number of forecast days to generate
+ * @param baseValue - Starting value for the data
+ * @param trend - Daily trend increment
+ * @param volatility - Random noise amplitude
+ * @return Array of sample forecast data points
+ */
+export const generateForecastData = (
+ historicalDays: number = 30,
+ forecastDays: number = 14,
+ baseValue: number = 100,
+ trend: number = 0.5,
+ volatility: number = 10
+): SampleForecastDatum[] => {
+ const data: SampleForecastDatum[] = [];
+ const startDate = new Date();
+ startDate.setDate( startDate.getDate() - historicalDays );
+
+ // Generate historical data (no bounds)
+ for ( let i = 0; i < historicalDays; i++ ) {
+ const date = new Date( startDate );
+ date.setDate( date.getDate() + i );
+ const noise = ( Math.random() - 0.5 ) * volatility * 2;
+ const value = baseValue + trend * i + noise;
+ data.push( { date, value: Math.max( 0, value ) } );
+ }
+
+ // Generate forecast data (with bounds)
+ const lastHistoricalValue = data[ data.length - 1 ].value;
+ for ( let i = 0; i < forecastDays; i++ ) {
+ const date = new Date( startDate );
+ date.setDate( date.getDate() + historicalDays + i );
+ const projectedValue = lastHistoricalValue + trend * ( i + 1 );
+
+ // Uncertainty grows with time
+ const uncertaintySpread = volatility * ( 1 + i * 0.2 );
+ const lower = projectedValue - uncertaintySpread;
+ const upper = projectedValue + uncertaintySpread;
+
+ data.push( {
+ date,
+ value: projectedValue,
+ lower: Math.max( 0, lower ),
+ upper,
+ } );
+ }
+
+ return data;
+};
+
+/**
+ * Fixed sample data for consistent story rendering
+ * Note: The last historical point (2024-02-19) is also the first forecast point
+ * to ensure the lines connect seamlessly at the transition.
+ */
+export const sampleForecastData: SampleForecastDatum[] = [
+ // Historical data (no bounds)
+ { date: new Date( '2024-01-01' ), value: 100 },
+ { date: new Date( '2024-01-08' ), value: 108 },
+ { date: new Date( '2024-01-15' ), value: 115 },
+ { date: new Date( '2024-01-22' ), value: 112 },
+ { date: new Date( '2024-01-29' ), value: 125 },
+ { date: new Date( '2024-02-05' ), value: 130 },
+ { date: new Date( '2024-02-12' ), value: 128 },
+ // Forecast data (with bounds) - starts at last historical point for seamless connection
+ { date: new Date( '2024-02-19' ), value: 135, lower: 135, upper: 135 }, // Transition point (no spread yet)
+ { date: new Date( '2024-02-26' ), value: 142, lower: 132, upper: 152 },
+ { date: new Date( '2024-03-04' ), value: 148, lower: 135, upper: 161 },
+ { date: new Date( '2024-03-11' ), value: 155, lower: 138, upper: 172 },
+ { date: new Date( '2024-03-18' ), value: 162, lower: 140, upper: 184 },
+];
+
+/**
+ * Sample accessors for the default data type
+ */
+export const sampleAccessors = {
+ x: ( d: SampleForecastDatum ) => d.date,
+ y: ( d: SampleForecastDatum ) => d.value,
+ yLower: ( d: SampleForecastDatum ) => d.lower,
+ yUpper: ( d: SampleForecastDatum ) => d.upper,
+};
+
+/**
+ * Forecast start date - the transition point where forecast begins
+ */
+export const sampleForecastStart = new Date( '2024-02-19' );
+
+type StoryArgs = ChartStoryArgs< TimeSeriesForecastChartProps< SampleForecastDatum > >;
+
+export const timeSeriesForecastChartMetaArgs: Meta< StoryArgs > = {
+ title: 'JS Packages/Charts Library/Charts/Time Series Forecast Chart',
+ component: TimeSeriesForecastChart,
+ parameters: {
+ layout: 'centered',
+ },
+ decorators: [ chartDecorator ],
+ argTypes: {
+ ...legendArgTypes,
+ ...themeArgTypes,
+ ...sharedChartArgTypes,
+ withTooltips: {
+ control: 'boolean',
+ description: 'Show tooltips on hover',
+ table: { category: 'Tooltip' },
+ },
+ animation: {
+ control: 'boolean',
+ description: 'Enable chart animation',
+ table: { category: 'Visual Style' },
+ },
+ gridVisibility: {
+ control: { type: 'select' },
+ options: [ 'x', 'y', 'xy', 'none' ],
+ description: 'Grid visibility',
+ table: { category: 'Visual Style' },
+ },
+ },
+};
+
+export const timeSeriesForecastChartStoryArgs = {
+ ...sharedThemeArgs,
+ data: sampleForecastData,
+ accessors: sampleAccessors,
+ forecastStart: sampleForecastStart,
+ withTooltips: true,
+ showLegend: false,
+ animation: false,
+ gridVisibility: 'y' as const,
+ maxWidth: 800,
+ aspectRatio: 0.5,
+ resizeDebounceTime: 300,
+};
diff --git a/projects/js-packages/charts/src/charts/time-series-forecast-chart/stories/index.stories.tsx b/projects/js-packages/charts/src/charts/time-series-forecast-chart/stories/index.stories.tsx
new file mode 100644
index 000000000000..e7d683a3e461
--- /dev/null
+++ b/projects/js-packages/charts/src/charts/time-series-forecast-chart/stories/index.stories.tsx
@@ -0,0 +1,453 @@
+import { formatNumber } from '@automattic/number-formatters';
+import { ChartStoryArgs } from '../../../stories';
+import TimeSeriesForecastChart from '../time-series-forecast-chart';
+import {
+ timeSeriesForecastChartMetaArgs,
+ timeSeriesForecastChartStoryArgs,
+ sampleForecastData,
+ sampleAccessors,
+ sampleForecastStart,
+ generateForecastData,
+ type SampleForecastDatum,
+} from './config';
+import type { TimeSeriesForecastChartProps } from '../types';
+import type { Meta, StoryFn, StoryObj } from '@storybook/react';
+
+type StoryArgs = ChartStoryArgs< TimeSeriesForecastChartProps< SampleForecastDatum > >;
+
+const meta: Meta< StoryArgs > = {
+ ...timeSeriesForecastChartMetaArgs,
+ title: 'JS Packages/Charts Library/Charts/Time Series Forecast Chart',
+};
+
+export default meta;
+
+const Template: StoryFn< typeof TimeSeriesForecastChart > = args => {
+ return ;
+};
+
+/**
+ * Default story showing historical data with forecast and uncertainty bands
+ */
+export const Default: StoryObj< StoryArgs > = Template.bind( {} );
+Default.args = {
+ ...timeSeriesForecastChartStoryArgs,
+};
+
+Default.parameters = {
+ docs: {
+ description: {
+ story:
+ 'The TimeSeriesForecastChart displays historical data as a solid line and forecast data as a dashed line. ' +
+ 'The shaded area represents the uncertainty band between lower and upper bounds. ' +
+ 'A vertical dashed line marks the forecast start date.',
+ },
+ },
+};
+
+/**
+ * With Legend enabled
+ */
+export const WithLegend: StoryObj< StoryArgs > = Template.bind( {} );
+WithLegend.args = {
+ ...timeSeriesForecastChartStoryArgs,
+ showLegend: true,
+ legendPosition: 'bottom',
+};
+
+WithLegend.parameters = {
+ docs: {
+ description: {
+ story:
+ 'The chart with legend showing series labels. The forecast series shows a dashed line indicator.',
+ },
+ },
+};
+
+/**
+ * With Animation
+ */
+export const WithAnimation: StoryObj< StoryArgs > = Template.bind( {} );
+WithAnimation.args = {
+ ...timeSeriesForecastChartStoryArgs,
+ animation: true,
+};
+
+WithAnimation.parameters = {
+ docs: {
+ description: {
+ story:
+ 'The chart with animation enabled. Animation respects the prefers-reduced-motion setting.',
+ },
+ },
+};
+
+/**
+ * Custom Series Labels
+ */
+export const CustomSeriesLabels: StoryObj< StoryArgs > = Template.bind( {} );
+CustomSeriesLabels.args = {
+ ...timeSeriesForecastChartStoryArgs,
+ showLegend: true,
+ seriesKeys: {
+ historical: 'Actual Revenue',
+ forecast: 'Projected Revenue',
+ band: 'Confidence Interval',
+ },
+};
+
+CustomSeriesLabels.parameters = {
+ docs: {
+ description: {
+ story:
+ 'Customize the series labels shown in the legend and tooltip using the seriesKeys prop.',
+ },
+ },
+};
+
+/**
+ * Custom Tooltip
+ */
+export const CustomTooltip: StoryObj< StoryArgs > = Template.bind( {} );
+CustomTooltip.args = {
+ ...timeSeriesForecastChartStoryArgs,
+ renderTooltip: ( { x, y, yLower, yUpper, isForecast } ) => (
+
+
+ { x.toLocaleDateString( 'en-US', { month: 'short', day: 'numeric', year: 'numeric' } ) }
+
+
Value: ${ formatNumber( y ) }
+ { isForecast && yLower !== undefined && yUpper !== undefined && (
+
+ Range: ${ formatNumber( yLower ) } - ${ formatNumber( yUpper ) }
+
+ ) }
+ { isForecast && (
+
+ Forecast
+
+ ) }
+
+ ),
+};
+
+CustomTooltip.parameters = {
+ docs: {
+ description: {
+ story:
+ 'Use the renderTooltip prop to provide a custom tooltip renderer. ' +
+ 'The tooltip receives the original datum, x/y values, bounds (if available), and whether the point is in the forecast region.',
+ },
+ },
+};
+
+/**
+ * Fixed Dimensions
+ */
+export const FixedDimensions: StoryObj< StoryArgs > = Template.bind( {} );
+FixedDimensions.args = {
+ ...timeSeriesForecastChartStoryArgs,
+ width: 600,
+ height: 300,
+};
+
+FixedDimensions.parameters = {
+ docs: {
+ description: {
+ story:
+ 'The chart with fixed dimensions instead of responsive behavior. Use width and height props directly.',
+ },
+ },
+};
+
+/**
+ * Custom Y Domain
+ */
+export const CustomYDomain: StoryObj< StoryArgs > = Template.bind( {} );
+CustomYDomain.args = {
+ ...timeSeriesForecastChartStoryArgs,
+ yDomain: [ 0, 200 ],
+};
+
+CustomYDomain.parameters = {
+ docs: {
+ description: {
+ story: 'Override the auto-calculated Y domain with a fixed range using the yDomain prop.',
+ },
+ },
+};
+
+/**
+ * Custom Tick Formatters
+ */
+export const CustomTickFormatters: StoryObj< StoryArgs > = Template.bind( {} );
+CustomTickFormatters.args = {
+ ...timeSeriesForecastChartStoryArgs,
+ xTickFormat: ( d: Date ) => d.toLocaleDateString( 'en-US', { month: 'short', day: 'numeric' } ),
+ yTickFormat: ( n: number ) => `$${ n }`,
+};
+
+CustomTickFormatters.parameters = {
+ docs: {
+ description: {
+ story: 'Customize axis tick labels using xTickFormat and yTickFormat props.',
+ },
+ },
+};
+
+/**
+ * Historical Only (no forecast data)
+ */
+export const HistoricalOnly: StoryObj< StoryArgs > = Template.bind( {} );
+HistoricalOnly.args = {
+ ...timeSeriesForecastChartStoryArgs,
+ data: sampleForecastData.slice( 0, 7 ), // Only historical points (before transition)
+ forecastStart: new Date( '2024-12-31' ), // Far in the future
+};
+
+HistoricalOnly.parameters = {
+ docs: {
+ description: {
+ story:
+ 'When forecastStart is after all data points, only the historical line is shown (no divider or band).',
+ },
+ },
+};
+
+/**
+ * Forecast Only (no historical data)
+ */
+export const ForecastOnly: StoryObj< StoryArgs > = Template.bind( {} );
+ForecastOnly.args = {
+ ...timeSeriesForecastChartStoryArgs,
+ data: sampleForecastData.slice( 7 ), // Only forecast points (from transition onward)
+ forecastStart: new Date( '2024-01-01' ), // Before all data
+};
+
+ForecastOnly.parameters = {
+ docs: {
+ description: {
+ story:
+ 'When forecastStart is before all data points, only the forecast line and band are shown (no divider).',
+ },
+ },
+};
+
+/**
+ * Different Grid Visibility Options
+ */
+export const GridVisibilityOptions: StoryObj< StoryArgs > = {
+ render: () => (
+
+
+
Y Grid (default)
+
+
+
+
X Grid
+
+
+
+
Both (XY)
+
+
+
+
No Grid
+
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story: 'Different grid visibility options: y (default), x, xy, or none.',
+ },
+ },
+ },
+};
+
+/**
+ * Dynamic/Generated Data
+ */
+export const DynamicData: StoryObj< StoryArgs > = {
+ render: () => {
+ const data = generateForecastData( 30, 14, 100, 1.5, 15 );
+ const forecastStart = new Date();
+
+ return (
+
+ );
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Example with dynamically generated data. The generateForecastData helper creates realistic time series with configurable parameters for trend, volatility, and forecast length.',
+ },
+ },
+ },
+};
+
+/**
+ * Generic Data Type Example
+ */
+type CustomDatum = {
+ timestamp: number;
+ measurement: number;
+ confidenceLow?: number;
+ confidenceHigh?: number;
+};
+
+const customData: CustomDatum[] = [
+ { timestamp: Date.parse( '2024-01-01' ), measurement: 50 },
+ { timestamp: Date.parse( '2024-01-15' ), measurement: 55 },
+ // Transition point - shared by both series
+ { timestamp: Date.parse( '2024-02-01' ), measurement: 60, confidenceLow: 60, confidenceHigh: 60 },
+ { timestamp: Date.parse( '2024-02-15' ), measurement: 65, confidenceLow: 58, confidenceHigh: 72 },
+ { timestamp: Date.parse( '2024-03-01' ), measurement: 70, confidenceLow: 60, confidenceHigh: 80 },
+];
+
+const customAccessors = {
+ x: ( d: CustomDatum ) => new Date( d.timestamp ),
+ y: ( d: CustomDatum ) => d.measurement,
+ yLower: ( d: CustomDatum ) => d.confidenceLow,
+ yUpper: ( d: CustomDatum ) => d.confidenceHigh,
+};
+
+export const GenericDataType: StoryObj< StoryArgs > = {
+ render: () => (
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'The chart supports generic data types via accessor functions. ' +
+ 'This example uses a custom datum shape with timestamp (number) instead of Date, ' +
+ 'and differently named fields for values and bounds.',
+ },
+ },
+ },
+};
+
+/**
+ * Empty Data State
+ */
+export const EmptyData: StoryObj< StoryArgs > = Template.bind( {} );
+EmptyData.args = {
+ ...timeSeriesForecastChartStoryArgs,
+ data: [],
+};
+
+EmptyData.parameters = {
+ docs: {
+ description: {
+ story: 'When no data is provided, the chart displays a "No data available" message.',
+ },
+ },
+};
+
+/**
+ * Partial Bounds (some forecast points missing bounds)
+ */
+const partialBoundsData: SampleForecastDatum[] = [
+ { date: new Date( '2024-01-01' ), value: 100 },
+ { date: new Date( '2024-01-15' ), value: 110 },
+ // Transition point - shared by both series (tight bounds at start)
+ { date: new Date( '2024-02-01' ), value: 120, lower: 120, upper: 120 },
+ // Forecast with bounds
+ { date: new Date( '2024-02-15' ), value: 130, lower: 120, upper: 140 },
+ // Forecast WITHOUT bounds (band won't include this point)
+ { date: new Date( '2024-03-01' ), value: 140 },
+ // Forecast with bounds again
+ { date: new Date( '2024-03-15' ), value: 150, lower: 135, upper: 165 },
+];
+
+export const PartialBounds: StoryObj< StoryArgs > = Template.bind( {} );
+PartialBounds.args = {
+ ...timeSeriesForecastChartStoryArgs,
+ data: partialBoundsData,
+ forecastStart: new Date( '2024-02-01' ),
+};
+
+PartialBounds.parameters = {
+ docs: {
+ description: {
+ story:
+ 'The uncertainty band only renders for forecast points that have both lower AND upper bounds. ' +
+ 'Points with missing bounds are still shown on the forecast line but excluded from the band.',
+ },
+ },
+};
+
+/**
+ * Legend at Top
+ */
+export const LegendAtTop: StoryObj< StoryArgs > = Template.bind( {} );
+LegendAtTop.args = {
+ ...timeSeriesForecastChartStoryArgs,
+ showLegend: true,
+ legendPosition: 'top',
+};
+
+LegendAtTop.parameters = {
+ docs: {
+ description: {
+ story: 'The legend positioned above the chart using legendPosition="top".',
+ },
+ },
+};
diff --git a/projects/js-packages/charts/src/charts/time-series-forecast-chart/time-series-forecast-chart.module.scss b/projects/js-packages/charts/src/charts/time-series-forecast-chart/time-series-forecast-chart.module.scss
new file mode 100644
index 000000000000..7d0ee592e931
--- /dev/null
+++ b/projects/js-packages/charts/src/charts/time-series-forecast-chart/time-series-forecast-chart.module.scss
@@ -0,0 +1,60 @@
+.time-series-forecast-chart {
+ position: relative;
+
+ &__svg-wrapper {
+ flex: 1;
+ min-height: 0; // Required for flex shrinking
+ }
+
+ &--animated {
+
+ path {
+ transform-origin: 0 95%;
+ transform: scaleY(0);
+ animation: rise 1s ease-out forwards;
+ }
+ }
+
+ svg {
+ overflow: visible;
+ }
+
+ &__tooltip {
+ background: var(--charts-tooltip-background, #fff);
+ color: var(--charts-tooltip-color, inherit);
+ padding: 0.5rem;
+ border-radius: 4px;
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
+ }
+
+ &__tooltip-header {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-weight: 700;
+ padding-block-end: 8px;
+ }
+
+ &__tooltip-row {
+ display: flex;
+ justify-content: space-between;
+ padding-block: 4px;
+ gap: 16px;
+ }
+
+ &__forecast-badge {
+ font-size: 0.75rem;
+ font-weight: 500;
+ padding-block: 2px;
+ padding-inline: 6px;
+ border-radius: 3px;
+ background: var(--charts-forecast-badge-background, rgba(0, 0, 0, 0.1));
+ }
+}
+
+@keyframes rise {
+
+ to {
+ transform: scaleY(1);
+ }
+}
diff --git a/projects/js-packages/charts/src/charts/time-series-forecast-chart/time-series-forecast-chart.tsx b/projects/js-packages/charts/src/charts/time-series-forecast-chart/time-series-forecast-chart.tsx
new file mode 100644
index 000000000000..d294b643dd48
--- /dev/null
+++ b/projects/js-packages/charts/src/charts/time-series-forecast-chart/time-series-forecast-chart.tsx
@@ -0,0 +1,488 @@
+import { formatNumberCompact, formatNumber } from '@automattic/number-formatters';
+import { curveMonotoneX } from '@visx/curve';
+import { XYChart, AreaSeries, LineSeries, Grid, Axis, Tooltip } from '@visx/xychart';
+import { __ } from '@wordpress/i18n';
+import { Stack } from '@wordpress/ui';
+import clsx from 'clsx';
+import { useContext, useMemo, useCallback } from 'react';
+import { Legend } from '../../components/legend';
+import { useXYChartTheme, useElementHeight, usePrefersReducedMotion } from '../../hooks';
+import {
+ GlobalChartsProvider,
+ GlobalChartsContext,
+ useChartId,
+ useGlobalChartsContext,
+ useGlobalChartsTheme,
+} from '../../providers';
+import { withResponsive } from '../private/with-responsive';
+import { ForecastDivider, useForecastData } from './private';
+import styles from './time-series-forecast-chart.module.scss';
+import type {
+ TimeSeriesForecastChartProps,
+ TransformedForecastPoint,
+ BandPoint,
+ SeriesKeys,
+} from './types';
+import type { BaseLegendItem } from '../../components/legend';
+import type { Optional } from '../../types';
+import type { TickFormatter } from '@visx/axis';
+import type { RenderTooltipParams } from '@visx/xychart/lib/components/Tooltip';
+import type { FC } from 'react';
+
+const DEFAULT_MARGIN = { top: 20, right: 20, bottom: 40, left: 50 };
+
+const DEFAULT_SERIES_KEYS: Required< SeriesKeys > = {
+ historical: 'Historical',
+ forecast: 'Forecast',
+ band: 'Uncertainty',
+};
+
+const formatDateTick = ( d: Date ) => {
+ return d.toLocaleDateString( undefined, {
+ month: 'short',
+ day: 'numeric',
+ } );
+};
+
+/**
+ * Internal component that renders the TimeSeriesForecastChart
+ *
+ * @param root0 - Component props
+ * @param root0.data - Array of data points to display
+ * @param root0.accessors - Accessor functions to extract values from data
+ * @param root0.forecastStart - Date at which forecast begins
+ * @param root0.height - Chart height in pixels
+ * @param root0.width - Chart width in pixels
+ * @param root0.margin - Chart margin configuration
+ * @param root0.yDomain - Optional fixed y-axis domain
+ * @param root0.xTickFormat - Formatter function for x-axis ticks
+ * @param root0.yTickFormat - Formatter function for y-axis ticks
+ * @param root0.withTooltips - Whether to show tooltips on hover
+ * @param root0.seriesKeys - Custom labels for series in legend/tooltip
+ * @param root0.renderTooltip - Custom tooltip renderer function
+ * @param root0.className - Additional CSS class name
+ * @param root0.chartId - Unique chart identifier
+ * @param root0.showLegend - Whether to show the legend
+ * @param root0.legendPosition - Position of the legend (top or bottom)
+ * @param root0.animation - Whether to enable chart animations
+ * @param root0.gridVisibility - Which grid lines to show (x, y, xy, or none)
+ * @param root0.gap - Gap between chart elements using WP design tokens
+ * @param root0.bandFillOpacity - Opacity of the uncertainty band fill
+ * @return The rendered chart component
+ */
+function TimeSeriesForecastChartInternal< D >( {
+ data,
+ accessors,
+ forecastStart,
+ height,
+ width = 600,
+ margin,
+ yDomain: providedYDomain,
+ xTickFormat = formatDateTick,
+ yTickFormat,
+ withTooltips = true,
+ seriesKeys: providedSeriesKeys,
+ renderTooltip,
+ className,
+ chartId: providedChartId,
+ showLegend = false,
+ legendPosition = 'bottom',
+ animation = false,
+ gridVisibility = 'y',
+ gap = 'md',
+ bandFillOpacity = 0.2,
+}: TimeSeriesForecastChartProps< D > ) {
+ const providerTheme = useGlobalChartsTheme();
+ const { getElementStyles } = useGlobalChartsContext();
+ const chartId = useChartId( providedChartId );
+ const [ svgWrapperRef, svgWrapperHeight ] = useElementHeight< HTMLDivElement >();
+ const prefersReducedMotion = usePrefersReducedMotion();
+
+ // Merge series keys with defaults
+ const seriesKeys = useMemo(
+ () => ( {
+ ...DEFAULT_SERIES_KEYS,
+ ...providedSeriesKeys,
+ } ),
+ [ providedSeriesKeys ]
+ );
+
+ // Transform and split data
+ const {
+ allPoints,
+ historical,
+ forecast,
+ bandData,
+ yDomain: computedYDomain,
+ xDomain,
+ } = useForecastData( {
+ data,
+ accessors,
+ forecastStart,
+ } );
+
+ // Use provided y domain or computed one
+ const yDomain = providedYDomain ?? computedYDomain;
+
+ // Get theme colors and styles
+ const { color: primaryColor } = getElementStyles( {
+ index: 0,
+ data: { group: 'primary', label: seriesKeys.historical, data: [] },
+ } );
+ const { color: bandColor } = getElementStyles( {
+ index: 1,
+ data: { group: 'primary', label: seriesKeys.band, data: [] },
+ } );
+ const { lineStyles: forecastLineStyles, shapeStyles: forecastShapeStyles } = getElementStyles( {
+ index: 1,
+ data: { group: 'primary', label: seriesKeys.forecast, data: [], options: {} },
+ } );
+
+ // Create mock series data for theme hook
+ const mockSeriesData = useMemo(
+ () => [
+ { label: seriesKeys.historical, data: [] },
+ { label: seriesKeys.forecast, data: [] },
+ ],
+ [ seriesKeys ]
+ );
+ const theme = useXYChartTheme( mockSeriesData );
+
+ // Computed margin
+ const computedMargin = useMemo(
+ () => ( {
+ ...DEFAULT_MARGIN,
+ ...margin,
+ } ),
+ [ margin ]
+ );
+
+ // Use the measured SVG wrapper height, falling back to the passed height if provided.
+ // When there's a legend, we must wait for measurement because
+ // the legend takes space and the svg-wrapper height will be less than the total height.
+ const chartHeight = svgWrapperHeight > 0 ? svgWrapperHeight : height;
+ const isWaitingForMeasurement = showLegend ? svgWrapperHeight === 0 : ! chartHeight;
+
+ // Create legend items
+ const legendItems = useMemo< BaseLegendItem[] >( () => {
+ const items: BaseLegendItem[] = [];
+
+ if ( historical.length > 0 ) {
+ items.push( {
+ label: seriesKeys.historical,
+ value: '',
+ color: primaryColor,
+ } );
+ }
+
+ if ( forecast.length > 0 ) {
+ items.push( {
+ label: seriesKeys.forecast,
+ value: '',
+ color: primaryColor,
+ shapeStyle: { strokeDasharray: '5 5', ...forecastShapeStyles },
+ } );
+ }
+
+ return items;
+ }, [ historical.length, forecast.length, seriesKeys, primaryColor, forecastShapeStyles ] );
+
+ // Accessors for transformed points (memoized to avoid recreating on each render)
+ const xAccessor = useCallback( ( p: TransformedForecastPoint | BandPoint ) => p.date, [] );
+ const yAccessor = useCallback( ( p: TransformedForecastPoint ) => p.value, [] );
+ const bandUpperAccessor = useCallback( ( p: BandPoint ) => p.upper, [] );
+ const bandLowerAccessor = useCallback( ( p: BandPoint ) => p.lower, [] );
+
+ // Default tooltip renderer (memoized to avoid recreating on each render)
+ const defaultRenderTooltip = useCallback(
+ ( params: RenderTooltipParams< TransformedForecastPoint > ) => {
+ const { tooltipData } = params;
+ const nearestDatum = tooltipData?.nearestDatum?.datum;
+
+ if ( ! nearestDatum ) {
+ return null;
+ }
+
+ const isForecast = nearestDatum.date.getTime() >= forecastStart.getTime();
+ const hasBounds = nearestDatum.lower !== null && nearestDatum.upper !== null;
+
+ return (
+
+
+ { xTickFormat( nearestDatum.date ) }
+ { isForecast && (
+
+ { seriesKeys.forecast }
+
+ ) }
+
+
+ { __( 'Value', 'jetpack-charts' ) }:
+ { formatNumber( nearestDatum.value ) }
+
+ { isForecast && hasBounds && (
+
+ { __( 'Range', 'jetpack-charts' ) }:
+
+ { formatNumber( nearestDatum.lower as number ) } -{ ' ' }
+ { formatNumber( nearestDatum.upper as number ) }
+
+
+ ) }
+
+ );
+ },
+ [ forecastStart, seriesKeys.forecast, xTickFormat ]
+ );
+
+ // Custom tooltip renderer that wraps user-provided function
+ const tooltipRenderer = useMemo( () => {
+ if ( renderTooltip ) {
+ return ( params: RenderTooltipParams< TransformedForecastPoint > ) => {
+ const nearestDatum = params.tooltipData?.nearestDatum?.datum;
+ if ( ! nearestDatum ) return null;
+
+ // originalIndex refers to the position in the original data array (before sorting),
+ // so this correctly retrieves the user's original datum for custom tooltip rendering
+ const originalDatum = data[ nearestDatum.originalIndex ];
+ const isForecast = nearestDatum.date.getTime() >= forecastStart.getTime();
+
+ return renderTooltip( {
+ nearest: originalDatum,
+ x: nearestDatum.date,
+ y: nearestDatum.value,
+ yLower: nearestDatum.lower ?? undefined,
+ yUpper: nearestDatum.upper ?? undefined,
+ isForecast,
+ } );
+ };
+ }
+ return defaultRenderTooltip;
+ }, [ renderTooltip, data, forecastStart, defaultRenderTooltip ] );
+
+ // Y-axis tick formatter
+ const yAxisTickFormat = useMemo( () => {
+ if ( yTickFormat ) {
+ return yTickFormat as TickFormatter< number >;
+ }
+ return formatNumberCompact as TickFormatter< number >;
+ }, [ yTickFormat ] );
+
+ // X-axis tick formatter - use directly without wrapper
+ const xAxisTickFormat = xTickFormat;
+
+ // Handle empty data
+ if ( data.length === 0 ) {
+ return (
+
+ { __( 'No data available', 'jetpack-charts' ) }
+
+ );
+ }
+
+ // Determine if we should show the divider
+ const showDivider = forecast.length > 0 && historical.length > 0;
+
+ // Determine grid settings
+ const showXGrid = gridVisibility === 'x' || gridVisibility === 'xy';
+ const showYGrid = gridVisibility === 'y' || gridVisibility === 'xy';
+
+ const legendElement = showLegend && (
+
+ );
+
+ return (
+
+ { legendPosition === 'top' && legendElement }
+
+
+ { ! isWaitingForMeasurement && (
+
+ { gridVisibility !== 'none' && (
+
+ ) }
+
+
+
+
+ { /* Uncertainty band - rendered first (behind lines), no pointer events */ }
+ { bandData.length > 0 && (
+
+ ) }
+
+ { /* Historical line - solid, visual only */ }
+ { historical.length > 0 && (
+
+ ) }
+
+ { /* Forecast line - dashed, visual only */ }
+ { forecast.length > 0 && (
+
+ ) }
+
+ { /* Invisible line for tooltip - handles all pointer events */ }
+
+
+ { /* Vertical divider at forecast start */ }
+ { showDivider && (
+
+ ) }
+
+ { /* Tooltip */ }
+ { withTooltips && (
+
+ ) }
+
+ ) }
+
+
+ { legendPosition === 'bottom' && legendElement }
+
+ );
+}
+
+/**
+ * TimeSeriesForecastChart with GlobalChartsProvider wrapper.
+ * Automatically wraps with provider if not already in one.
+ *
+ * @param props - Chart component props
+ * @return The rendered chart component with provider context
+ */
+function TimeSeriesForecastChartWithProvider< D >(
+ props: TimeSeriesForecastChartProps< D >
+): JSX.Element {
+ const existingContext = useContext( GlobalChartsContext );
+
+ if ( existingContext ) {
+ return ;
+ }
+
+ return (
+
+
+
+ );
+}
+
+TimeSeriesForecastChartWithProvider.displayName = 'TimeSeriesForecastChart';
+
+/**
+ * TimeSeriesForecastChart - Displays historical data and forecasts with uncertainty bands
+ *
+ * Features:
+ * - Historical series rendered as a solid line
+ * - Forecast series rendered as a dashed line
+ * - Uncertainty band (shaded area) between upper/lower bounds
+ * - Vertical divider at the forecast start date
+ * - Generic data type support via accessors
+ */
+const TimeSeriesForecastChart = withResponsive(
+ TimeSeriesForecastChartWithProvider as FC< TimeSeriesForecastChartProps< unknown > >
+) as < D >(
+ props: Optional< TimeSeriesForecastChartProps< D >, 'width' | 'height' > & {
+ maxWidth?: number;
+ aspectRatio?: number;
+ resizeDebounceTime?: number;
+ }
+) => JSX.Element;
+
+/**
+ * Non-responsive version of TimeSeriesForecastChart
+ * Requires explicit width and height props
+ */
+const TimeSeriesForecastChartUnresponsive = TimeSeriesForecastChartWithProvider as < D >(
+ props: TimeSeriesForecastChartProps< D >
+) => JSX.Element;
+
+export { TimeSeriesForecastChart as default, TimeSeriesForecastChartUnresponsive };
diff --git a/projects/js-packages/charts/src/charts/time-series-forecast-chart/types.ts b/projects/js-packages/charts/src/charts/time-series-forecast-chart/types.ts
new file mode 100644
index 000000000000..58cd693366c3
--- /dev/null
+++ b/projects/js-packages/charts/src/charts/time-series-forecast-chart/types.ts
@@ -0,0 +1,191 @@
+import type { GapSize } from '@wordpress/theme';
+import type { ReactNode } from 'react';
+
+/**
+ * Generic accessor type for extracting values from datum
+ */
+export type Accessor< D, R > = ( d: D, index: number ) => R;
+
+/**
+ * Accessors for extracting values from generic datum
+ */
+export type TimeSeriesForecastAccessors< D > = {
+ /**
+ * Accessor function to extract the x-axis date value from each datum
+ */
+ x: Accessor< D, Date >;
+ /**
+ * Accessor function to extract the y-axis numeric value from each datum
+ */
+ y: Accessor< D, number >;
+ /**
+ * Optional accessor function to extract the lower bound of the forecast uncertainty.
+ * The uncertainty band is only rendered for points where BOTH yLower and yUpper return
+ * valid numbers. If bounds are not provided, the forecast line will still be shown
+ * without an uncertainty band.
+ */
+ yLower?: Accessor< D, number | null | undefined >;
+ /**
+ * Optional accessor function to extract the upper bound of the forecast uncertainty.
+ * The uncertainty band is only rendered for points where BOTH yLower and yUpper return
+ * valid numbers. If bounds are not provided, the forecast line will still be shown
+ * without an uncertainty band.
+ */
+ yUpper?: Accessor< D, number | null | undefined >;
+};
+
+/**
+ * Series key labels for legend/tooltip
+ */
+export type SeriesKeys = {
+ /**
+ * Label for the historical series. Default: 'Historical'
+ */
+ historical?: string;
+ /**
+ * Label for the forecast series. Default: 'Forecast'
+ */
+ forecast?: string;
+ /**
+ * Label for the uncertainty band. Default: 'Uncertainty'
+ */
+ band?: string;
+};
+
+/**
+ * Internal transformed point (used after accessor extraction)
+ */
+export type TransformedForecastPoint = {
+ date: Date;
+ value: number;
+ lower: number | null;
+ upper: number | null;
+ originalIndex: number;
+};
+
+/**
+ * Band point with guaranteed bounds
+ */
+export type BandPoint = {
+ date: Date;
+ lower: number;
+ upper: number;
+};
+
+/**
+ * Tooltip params passed to custom tooltip renderer
+ */
+export interface ForecastTooltipParams< D > {
+ /**
+ * The nearest datum to the cursor
+ */
+ nearest: D;
+ /**
+ * The x-axis date value
+ */
+ x: Date;
+ /**
+ * The y-axis numeric value
+ */
+ y: number;
+ /**
+ * The lower bound of the forecast uncertainty (if available)
+ */
+ yLower?: number;
+ /**
+ * The upper bound of the forecast uncertainty (if available)
+ */
+ yUpper?: number;
+ /**
+ * Whether this point is in the forecast region
+ */
+ isForecast: boolean;
+}
+
+/**
+ * Main component props (generic over datum type D)
+ */
+export interface TimeSeriesForecastChartProps< D > {
+ /**
+ * Array of data points to display in the chart
+ */
+ data: readonly D[];
+ /**
+ * Accessor functions to extract values from each datum
+ */
+ accessors: TimeSeriesForecastAccessors< D >;
+ /**
+ * The date at which the forecast begins
+ */
+ forecastStart: Date;
+ /**
+ * Height of the chart in pixels
+ */
+ height: number;
+ /**
+ * Width of the chart in pixels
+ */
+ width?: number;
+ /**
+ * Chart margins
+ */
+ margin?: { top?: number; right?: number; bottom?: number; left?: number };
+ /**
+ * Fixed y-axis domain [min, max]
+ */
+ yDomain?: [ number, number ];
+ /**
+ * Custom formatter for x-axis tick labels
+ */
+ xTickFormat?: ( d: Date ) => string;
+ /**
+ * Custom formatter for y-axis tick labels
+ */
+ yTickFormat?: ( n: number ) => string;
+ /**
+ * Whether to show tooltips on hover. Default: true
+ */
+ withTooltips?: boolean;
+ /**
+ * Custom labels for series in legend and tooltip
+ */
+ seriesKeys?: SeriesKeys;
+ /**
+ * Custom tooltip renderer
+ */
+ renderTooltip?: ( params: ForecastTooltipParams< D > ) => ReactNode;
+ /**
+ * Additional CSS class name for the chart container
+ */
+ className?: string;
+ /**
+ * Optional unique identifier for the chart
+ */
+ chartId?: string;
+ /**
+ * Whether to show the legend. Default: false
+ */
+ showLegend?: boolean;
+ /**
+ * Legend position. Default: 'bottom'
+ */
+ legendPosition?: 'top' | 'bottom';
+ /**
+ * Whether to animate the chart on initial render. Default: false
+ */
+ animation?: boolean;
+ /**
+ * Grid visibility. Default: 'y'
+ */
+ gridVisibility?: 'x' | 'y' | 'xy' | 'none';
+ /**
+ * Gap between chart elements (SVG, legend).
+ * Uses WordPress design system tokens.
+ * @default 'md'
+ */
+ gap?: GapSize;
+ /**
+ * Opacity of the uncertainty band fill. Default: 0.2
+ */
+ bandFillOpacity?: number;
+}
diff --git a/projects/js-packages/charts/src/index.ts b/projects/js-packages/charts/src/index.ts
index cb5991f8aa9c..e951e8b48943 100644
--- a/projects/js-packages/charts/src/index.ts
+++ b/projects/js-packages/charts/src/index.ts
@@ -8,6 +8,10 @@ export { LineChart, LineChartUnresponsive } from './charts/line-chart';
export { PieChart, PieChartUnresponsive } from './charts/pie-chart';
export { PieSemiCircleChart, PieSemiCircleChartUnresponsive } from './charts/pie-semi-circle-chart';
export { Sparkline, SparklineUnresponsive } from './charts/sparkline';
+export {
+ TimeSeriesForecastChart,
+ TimeSeriesForecastChartUnresponsive,
+} from './charts/time-series-forecast-chart';
// Components
export { BaseTooltip } from './components/tooltip';
@@ -39,6 +43,10 @@ export type {
export type { GeoChartProps, GeoRegion, GeoResolution } from './charts/geo-chart';
export type { LegendValueDisplay, BaseLegendItem } from './components/legend';
export type { TrendIndicatorProps, TrendDirection } from './components/trend-indicator';
+export type {
+ TimeSeriesForecastChartProps,
+ TimeSeriesForecastAccessors,
+} from './charts/time-series-forecast-chart';
export type { LineStyles, GridStyles, EventHandlerParams } from '@visx/xychart';
export type {
GoogleDataTableColumn,