ConfidenceCone
A forecast chart with a widening prediction band (the confidence cone).
Usage
import { ConfidenceCone } from '@acronis-platform/ui-react';
import type { ChartConfig } from '@acronis-platform/ui-react';ConfidenceCone plots a projection with explicit uncertainty as one metric
in one color: a solid line + filled area over the known/actual period, a dashed
line over the forecast, and a shaded band (the "cone") between a lower and upper
bound that widens with the horizon. A dashed divider and a subtle shaded region
set the forecast off from the actuals (toggle with showForecastRegion). Point
the field-name props at your columns (actualKey, forecastKey, lowerKey,
upperKey); the band is drawn behind the lines and kept out of the tooltip and
legend, and actual vs forecast differ by line style, not hue. Because the three
recharts series are one metric, the legend names it once — a single swatch under
the actual series' label — and the forecast key's color is re-pointed at the
actual series' color, so config can't paint the metric in two hues.
Metric colors come from the palette prop, not from config — see
Palettes. Each config entry maps a key to a
label and an optional tone that re-points it within the palette, resolved
into the --color-<key> custom properties the lines and areas paint from. The
actual area and the cone band reuse the metric's color at low opacity.
Pass series to plot several metrics against one shared axis, omit the bound
keys for a projection with no cone, and layer on referenceLine thresholds,
showDots and styleForecastTicks — each covered below.
Examples
Give the hand-off point both an actual and a forecast (with lower = upper
so the cone starts at a point) to make the two lines meet:
const data = [
{ month: 'Apr', actual: 130 },
{ month: 'May', actual: 141 },
{ month: 'Jun', actual: 150, forecast: 150, lower: 150, upper: 150 }, // hand-off
{ month: 'Jul', forecast: 162, lower: 150, upper: 176 },
{ month: 'Aug', forecast: 173, lower: 154, upper: 196 },
];
const config = {
actual: { label: 'Actual' },
forecast: { label: 'Forecast' },
} satisfies ChartConfig;
<ConfidenceCone
config={config}
data={data}
xKey="month"
actualKey="actual"
forecastKey="forecast"
lowerKey="lower"
upperKey="upper"
className="h-[320px] w-[560px]"
/>;Add axis titles and a y-axis unit suffix with xAxisLabel / yAxisLabel /
yUnit (the x-axis is categorical). The titles inherit the theme token:
<ConfidenceCone
config={config}
data={data}
xKey="month"
actualKey="actual"
forecastKey="forecast"
lowerKey="lower"
upperKey="upper"
xAxisLabel="Month"
yAxisLabel="Storage"
yUnit="GB"
/>Several metrics on one axis
Pass series to plot more than one metric against the same axis. Each entry
names its own actual / forecast / bound columns and takes its hue from
config[actualKey], so every metric gets an independent cone. Every cone is
drawn behind every line, and the legend names each metric once:
<ConfidenceCone
config={{
storage: { label: 'Storage' },
storageForecast: { label: 'Storage forecast' },
backups: { label: 'Backups' },
backupsForecast: { label: 'Backups forecast' },
}}
data={data}
xKey="month"
series={[
{
actualKey: 'storage',
forecastKey: 'storageForecast',
lowerKey: 'storageLower',
upperKey: 'storageUpper',
},
{
actualKey: 'backups',
forecastKey: 'backupsForecast',
lowerKey: 'backupsLower',
upperKey: 'backupsUpper',
},
]}
/>series supersedes the single-series actualKey / forecastKey / lowerKey /
upperKey shorthand — pass one form or the other, not both. One of the two is
required: the types reject a chart that names no columns at all.
Actual as a line
By default the observed period is an area: a solid line with a low-opacity
fill beneath it. Set actualType="line" to drop that fill and leave the cone as
the only shaded region — worth doing as soon as a chart carries more than one
metric, where overlapping fills muddy each other and the bands:
<ConfidenceCone
config={config}
data={data}
xKey="month"
actualKey="actual"
forecastKey="forecast"
lowerKey="lower"
upperKey="upper"
actualType="line"
/>Projection without a cone
Omit lowerKey and upperKey when a model gives a point estimate but no
interval: the actual line hands off to a bare dashed forecast, with no band. It
works per series, so a coned metric and a band-less one can share a chart:
<ConfidenceCone
config={config}
data={data}
xKey="month"
actualKey="actual"
forecastKey="forecast"
/>Thresholds, dots and forecast ticks
referenceLine draws a dashed horizontal threshold on the value axis — a
target, a capacity limit — with an optional caption at its right end; pass an
array to draw several. A threshold beyond the data max extends the domain so it
stays visible.
showDots marks each point: filled on the observed values, hollow on the
projection, so a point reads as measured or predicted without tracing the line
style. styleForecastTicks italicizes the X ticks over the projected period and
paints them in the first series' hue — useful when showForecastRegion is off
and the shaded region isn't there to mark the horizon:
<ConfidenceCone
config={config}
data={data}
xKey="month"
actualKey="actual"
forecastKey="forecast"
lowerKey="lower"
upperKey="upper"
referenceLine={{ value: 190, label: 'Capacity' }}
showDots
styleForecastTicks
/>Custom tooltip
Replace the tooltip with a configured ChartTooltipContent — imported from the
same library, so you never compose recharts yourself. The synthetic cone band is
already excluded from the payload:
import { ConfidenceCone, ChartTooltipContent } from '@acronis-platform/ui-react';
<ConfidenceCone
config={config}
data={data}
xKey="month"
actualKey="actual"
forecastKey="forecast"
lowerKey="lower"
upperKey="upper"
tooltipContent={
<ChartTooltipContent
labelFormatter={(label) => `${label} · projection`}
formatter={(value, name) => `${name}: ${value.toLocaleString()}`}
/>
}
/>;Format the tick values with xTickFormatter / yTickFormatter, or hide an axis
with showXAxis / showYAxis — the shared axis knobs described under
Formatting and hiding axes. This is
how an MRR axis reads as $146.5K:
import { ConfidenceCone, createTickFormatter } from '@acronis-platform/ui-react';
<ConfidenceCone
config={config}
data={data}
xKey="month"
actualKey="actual"
forecastKey="forecast"
lowerKey="lower"
upperKey="upper"
yTickFormatter={createTickFormatter({
style: 'currency', currency: 'USD', notation: 'compact', maximumFractionDigits: 1,
})}
/>;Animation
Charts render statically by default. Opt in to an entrance animation with
animate, and tune it with animationDuration / animationBegin /
animationEasing:
<ConfidenceCone config={config} data={data} animate animationDuration={800} animationEasing="ease-out" />animate honors prefers-reduced-motion: for a visitor who has asked their
system to reduce motion, the series render at their final geometry with no
animation (the same applies when rendering on the server).
Accessibility
Neither ConfidenceCone nor the shared ChartContainer adds a role or an
aria-* attribute of its own. What semantics the chart has come from recharts,
whose accessibility layer is on by default and is not switched off here: the
plot's <svg> renders role="application" with tabindex="0", so Tab reaches
it and focusing it opens the tooltip on the first category. The left/right arrow
keys step it along the categories. Enter toggles the tooltip rather than
pinning it, and since focus has already opened one, the first Enter closes it —
press Enter again, or an arrow key, to bring it back.
That surface has no accessible name, though, and the layer to fix that is this
one rather than recharts. recharts takes title, desc and role as
first-class chart props and writes them straight into the plot's <svg>;
ConfidenceCone passes none of them down, because it spreads the props it
doesn't consume onto its own root <div> and hands the recharts chart only the
data and margins it computes itself. The <title> and <desc> recharts always
emits therefore stay empty and a screen reader announces an unnamed application
region. The tooltip is not a live region either: ChartTooltipContent renders
plain markup with no role="status" or aria-live, so the values it reveals as
you arrow along are shown but never announced.
So name the chart from the outside: role and aria-label (or
aria-labelledby) land on that same root <div>. Choose the role before you
copy the snippet, though. role="img" names the chart and also makes its
subtree presentational, hiding the plot from assistive technology while it stays
in the tab order; that is the right trade when the picture is the whole message.
Where the keyboard tooltip should stay reachable, wrap the chart in a <figure>
with a <figcaption>, or pass role="group" with the same aria-label, and
leave the plot's own semantics alone.
<ConfidenceCone
role="img"
aria-label="Storage used, with a projection and its confidence interval"
config={config}
data={data}
xKey="month"
actualKey="actual"
forecastKey="forecast"
lowerKey="lower"
upperKey="upper"
/>A name is not a text alternative, and it matters more here than on a plain
series chart: the whole point of a cone is that the projected values are
uncertain, and none of that is in the picture for a reader who can't see it. Put
the numbers and the caveat in text as well — a caption naming the horizon and the
interval, a summary sentence, or the same rows in an adjacent
Table. The component's own visual distinctions are already
non-chromatic and worth keeping: actual and forecast differ by line style rather
than hue (solid versus dashed, one color per metric), showDots marks observed
points filled and projected ones hollow, and styleForecastTicks italicizes the
ticks over the projected period.
Direction (RTL)
Under dir="rtl" the chrome around the plot mirrors on its own — the legend, the
tooltip, and any readouts you compose alongside the chart are ordinary flow
layout built from logical CSS utilities. The plot area itself deliberately stays
left-to-right: recharts lays every mark out in physical SVG coordinates, so a
mirrored category axis would flip the reading order of the data without flipping
the geometry that encodes it — and here that order is also the direction of time,
with the forecast region and its widening cone reading toward the future. This
is what mainstream charting libraries do by default.
ChartContainer pins .recharts-surface to direction: ltr so that holds
whatever the page inherits, and the pin is load-bearing rather than a
formality: recharts anchors axis tick text with the direction-relative SVG
keywords text-anchor: start / end while placing each mark at a coordinate it
computed in physical space, so an inherited dir="rtl" mirrors the text away
from the geometry — end-anchored Y-axis ticks land inside the plot area,
rotated X-axis ticks drift, and a ReferenceLine label at the right edge
overflows the surface. Text anchored middle is direction-immune either way:
unrotated X-axis ticks and axis titles. Because the pin stops at the surface,
the tooltip and the legend — HTML outside it — still mirror.
Chart carries the measured detail.
API Reference
Prop
Type