aai agency logo
OG Components Docs

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.

Variance (Actual − Forecast)
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)

KeyLabelFormulaEditable params
flatFlatq(t) = qiqi
linearLinearq(t) = qi + slope · tqi, slope
exponentialExponentialq(t) = qi · e−Di · tqi, Di
harmonicHarmonicq(t) = qi / (1 + Di · t)qi, Di
hyperbolicHyperbolicq(t) = qi / (1 + b · Di · t)1/bqi, Di, b
stretchedExponentialStretched Expq(t) = qi · e−(Di · t)nqi, Di, n

Operational presets (Operations)

KeyLabelFormulaNotes
flowbackFlowbackq(t) = qi + slope · tSame math as linear, defaults to slope = +25
shutInShut-inq(t) = 0qi forced to zero. Triggers bisect-resumption logic.
constrainedConstrainedq(t) = qiPlateau. Surface or pipeline limited.
chokedChokedq(t) = qiPlateau. 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

bash
pnpm add @aai-agency/og-components

Chart 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):

tsx
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).

tsx
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.

tsx
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

PropTypeDefaultDescription
productionnumber[]-Actual production values aligned with `time`. Generates sample data if omitted.
timenumber[]-Time values (e.g. days or months from t=0). Defaults to 0..N-1 when omitted.
initialSegmentsSegment[]-Preloaded multi-segment forecast configuration. Each segment has id, tStart, equation, params (qi/di/b/slope), and optional qiAnchored / locked / color / note / tEnd.
initialAnnotationsAnnotation[]-Preloaded range annotations. Each has id, tStart, tEnd, type, optional label/description/color.
initialParamsPartial<HyperbolicParams>-Starting qi/di/b for the default single hyperbolic segment when initialSegments is omitted.
forecastHorizonnumber-Project the forecast out to this t value. Defaults to lastActualT + the actual data range.
unitstring"BBL/mo"Display unit on the y-axis label and tooltips.
unitsPerYearnumber-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.
startDateDate | string-Calendar date corresponding to t = 0. Enables the editor's days ↔ date toggle.
showVariancebooleantrueRender the variance sub-chart by default.
heightnumber300Production chart height in pixels.
varianceHeightnumber120Variance sub-chart height in pixels.
actualColorstring"#10b981"Stroke color for the historical-actuals line.
forecastColorstring-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:

tsx
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";