Decline Curve
Interactive piecewise decline-curve editor for production forecasting. Multi-segment forecasts that chain C0-continuously, drag-to-reshape, right-click insert with bisect-resumption, range annotations with Δ stats, Save/Discard draft flow, and a side panel list-then-editor navigation.
Preview
900 days of synthetic Bakken-style production. Flowback ramp, hyperbolic decline, 40-day workover, exponential post-workover, harmonic terminal. Click Actions ▼ in the chart toolbar to enter Forecast or Annotate mode.
import { DeclineCurve } from "@aai-agency/og-components";
import {
sampleDeclineCurveProduction,
sampleDeclineCurveSegments,
sampleDeclineCurveAnnotations,
} from "@aai-agency/og-components/sample-data";
<DeclineCurve
production={sampleDeclineCurveProduction.values}
time={sampleDeclineCurveProduction.time}
initialSegments={sampleDeclineCurveSegments}
initialAnnotations={sampleDeclineCurveAnnotations}
timeUnit="day"
unit="BBL/day"
unitsPerYear={365}
startDate="2024-01-01"
/>Overview
A production chart with a multi-segment forecast line on top of the actuals. Each segment chains C0-continuously to the prior segment's end value (set qiAnchored: true to override). Ten supported equations split across base math (flat, linear, exponential, harmonic, hyperbolic, stretched-exponential) and operational presets (flowback, shut-in, constrained, choked).
Range annotations sit on top of the chart for operational events (workovers, shut-ins, ESP fails, frac jobs). Each annotation gets Δ stats (avg actual, avg forecast, Δ%, total variance) inside its range, and the variance sub-chart can recolor by annotation, by sign, or hide entirely.
By default the chart is read-only. The user opts into Forecast (drag-to-edit) or Annotate (draw regions) mode via the toolbar Actions menu. The Segments and Annotations toolbar buttons each open a side panel with a list view of every item; clicking a row opens an editor for that one with a draft buffer (Save / Discard) and a confirmation prompt if the user navigates away with unsaved changes.
Equations
All ten equations evaluate q(t) = … where t is time since the segment's tStart. Every equation reads qi (initial production rate, the y-intercept of the curve). The segment editor only shows inputs for the params each equation actually consumes.
Base math (Decline)
| Key | Label | Formula | Editable params |
|---|---|---|---|
| flat | Flat | q(t) = qi | qi |
| linear | Linear | q(t) = qi + slope · t | qi, slope |
| exponential | Exponential | q(t) = qi · e−Di · t | qi, Di |
| harmonic | Harmonic | q(t) = qi / (1 + Di · t) | qi, Di |
| hyperbolic | Hyperbolic | q(t) = qi / (1 + b · Di · t)1/b | qi, Di, b |
| stretchedExponential | Stretched Exp | q(t) = qi · e−(Di · t)n | qi, Di, n |
Operational presets (Operations)
| Key | Label | Formula | Notes |
|---|---|---|---|
| flowback | Flowback | q(t) = qi + slope · t | Same math as linear, defaults to slope = +25 |
| shutIn | Shut-in | q(t) = 0 | qi forced to zero. Triggers bisect-resumption logic. |
| constrained | Constrained | q(t) = qi | Plateau. Surface or pipeline limited. |
| choked | Choked | q(t) = qi | Plateau. Operator-throttled (same math as constrained). |
Notation: qi is the standard O&G letter for initial production rate (the y-intercept of every decline curve in the family). linear is exactly y = mx + b with m = slope and b = qi. stretchedExponential uses n in the formula, but the data field is params.b (the same slot hyperbolic uses for its decline exponent).
Installation
pnpm add @aai-agency/og-componentsChart styles ship inside the package's styles.css (which you import once from your app entry alongside Tailwind v4). No separate uPlot import needed.
Usage
Drop-in demo with the bundled sample dataset (a 900-day Bakken-style well):
import { DeclineCurve } from "@aai-agency/og-components";
import {
sampleDeclineCurveProduction,
sampleDeclineCurveSegments,
sampleDeclineCurveAnnotations,
} from "@aai-agency/og-components/sample-data";
// Drop-in demo with a 900-day Bakken-style well: flowback ramp →
// hyperbolic decline → 40-day workover → exponential post-workover →
// harmonic terminal. Segments and annotations match the production
// data so the chart looks right out of the box.
export function ForecastEditor() {
return (
<DeclineCurve
production={sampleDeclineCurveProduction.values}
time={sampleDeclineCurveProduction.time}
initialSegments={sampleDeclineCurveSegments}
initialAnnotations={sampleDeclineCurveAnnotations}
timeUnit="day"
unit="BBL/day"
unitsPerYear={365}
startDate="2024-01-01"
onSegmentsChange={(segments) => console.log("Updated:", segments)}
/>
);
}Custom segments
Stitch a multi-phase forecast yourself. Segments chain C0-continuously by default; qiAnchored: true overrides the chain (used here on shut-in and the post-workover restart).
import type { Segment } from "@aai-agency/og-components";
// Stitch your own forecast. Segments chain C0-continuously: each
// segment's qi defaults to the prior segment's evaluated end value.
// Set qiAnchored: true to override the chain with an explicit qi.
const initialSegments: Segment[] = [
// Flowback ramp (well cleaning up)
{
id: "flowback",
tStart: 0,
equation: "flowback",
params: { qi: 200, di: 0, b: 0, slope: 15 },
},
// Primary hyperbolic decline
{
id: "primary",
tStart: 20,
equation: "hyperbolic",
params: { qi: 0, di: 0.006, b: 1.0, slope: 0 },
},
// 40-day workover shut-in
{
id: "shutin",
tStart: 350,
equation: "shutIn",
params: { qi: 0, di: 0, b: 0, slope: 0 },
qiAnchored: true,
},
// Post-workover exponential
{
id: "post-workover",
tStart: 390,
equation: "exponential",
params: { qi: 180, di: 0.003, b: 0, slope: 0 },
qiAnchored: true,
},
];Annotations
Range overlays for operational events. The annotation type drives the default color and label; descriptions show in the editor and the chart tooltip.
import type { Annotation } from "@aai-agency/og-components";
// Time-range overlays for operational events. The chart computes
// Δ stats inside each range (avg actual, avg forecast, Δ%, total
// variance) and recolors the variance fill by annotation if you
// switch the variance mode in the gear menu.
const initialAnnotations: Annotation[] = [
{
id: "ann-flowback",
tStart: 0,
tEnd: 20,
type: "flowback",
label: "Flowback",
description: "Initial flowback period, well cleaning up.",
},
{
id: "ann-workover",
tStart: 350,
tEnd: 390,
type: "shutInWorkover",
label: "Workover",
description: "40-day workover. ESP replacement + rod pump install.",
},
];Props
| Prop | Type | Default | Description |
|---|---|---|---|
production | number[] | - | Actual production values aligned with `time`. Generates sample data if omitted. |
time | number[] | - | Time values (e.g. days or months from t=0). Defaults to 0..N-1 when omitted. |
initialSegments | Segment[] | - | Preloaded multi-segment forecast configuration. Each segment has id, tStart, equation, params (qi/di/b/slope), and optional qiAnchored / locked / color / note / tEnd. |
initialAnnotations | Annotation[] | - | Preloaded range annotations. Each has id, tStart, tEnd, type, optional label/description/color. |
initialParams | Partial<HyperbolicParams> | - | Starting qi/di/b for the default single hyperbolic segment when initialSegments is omitted. |
forecastHorizon | number | - | Project the forecast out to this t value. Defaults to lastActualT + the actual data range. |
unit | string | "BBL/mo" | Display unit on the y-axis label and tooltips. |
unitsPerYear | number | - | 12 for monthly data, 365 for daily. Enables a friendly 'N years' suffix in editor inputs. |
timeUnit | "day" | "month" | "year" | "month" | What one t unit means on the calendar. Drives the date-axis when startDate is supplied. |
startDate | Date | string | - | Calendar date corresponding to t = 0. Enables the editor's days ↔ date toggle. |
showVariance | boolean | true | Render the variance sub-chart by default. |
height | number | 300 | Production chart height in pixels. |
varianceHeight | number | 120 | Variance sub-chart height in pixels. |
actualColor | string | "#10b981" | Stroke color for the historical-actuals line. |
forecastColor | string | - | Fallback color for the forecast cursor in edit mode and the default segment color when a segment provides none. |
onSegmentsChange | (segments: Segment[]) => void | - | Fires after every segment commit (drag, edit, insert, delete). |
onSave | (segments: Segment[]) => void | - | Alias for onSegmentsChange. Both fire on the same trigger. |
onAnnotationsChange | (annotations: Annotation[]) => void | - | Fires whenever the annotation list or any annotation changes. |
Sample data
The package ships with a 900-day Bakken-style well dataset matching the segments + annotations in the playground demo. Use it to prototype quickly without synthesizing your own data:
import {
sampleDeclineCurveProduction, // { time: number[], values: number[] }
sampleDeclineCurveSegments, // 5-segment forecast (Segment[])
sampleDeclineCurveAnnotations, // Flowback + Workover (Annotation[])
generateSampleDeclineCurveProduction, // (totalDays, seed). Re-roll the noise.
} from "@aai-agency/og-components/sample-data";