Install
The reference implementation is a single zero-dependency package: validator, curve math, SVG renderer, React wrapper, headless export, adapters, and CLI.
Publishing to the npm registry is deferred while 0.x settles (tracked in
sh4t/open-lec#5).
Until then, install straight from GitHub:
npm install github:sh4t/open-lec.
Quick start
Fetch a document, validate it, render it. The renderer draws into any container and
returns a handle with update() and
destroy().
import { render, validate } from "open-lec";
const doc = await fetch("/my-lec.json").then((r) => r.json());
const result = validate(doc);
if (result.valid) {
render(document.querySelector("#chart"), doc, { logX: true });
} else {
console.error(result.errors); // [{ path, message }, …]
}
Your first document
Four points and a currency make a conforming curve. Points sample the exceedance function P(annual loss ≥ loss).
Paste it into the playground and break it on purpose — reorder the losses, push a probability above an earlier one — to watch the validator name the exact path and rule.
Document structure
An open-lec document is a single UTF-8 JSON object with three required members and one optional one.
| field | type | req. | description |
|---|---|---|---|
| openlec | string | ✓ | Spec version, "0.2". 0.1 documents remain valid. |
| metadata | object | ✓ | Document-level metadata; currency is required. |
| scenarios | Scenario[] | ✓ | One or more curves. Array order is presentation order. |
| referenceLines | RefLine[] | — | Document-level guide lines on either axis. |
Readers must ignore unrecognized keys, and writers should namespace extensions
under x- prefixed keys at any level
("x-acme": {…}). The validator warns — never errors —
on unknown keys outside that namespace.
Metadata
| field | type | req. | description |
|---|---|---|---|
| currency | string | ✓ | ISO 4217 code. Every loss in the document is in this currency's major unit. |
| title | string | — | Human-readable title; renderers use it for the chart's accessible name. |
| created | string | — | RFC 3339 timestamp of generation. |
| horizon | object | — | { value > 0, unit: year | quarter | month }. Default 1 year. No conversion semantics exist — a 1-quarter curve is not a 1-year curve ÷ 4. |
| generator | object | — | { name, version? } of the emitting tool. |
| methodology | string | — | Free text or tag, e.g. "FAIR / Monte Carlo". |
| iterations | integer | — | Simulation iteration count (≥ 1), if applicable. |
Scenarios
A scenario is one curve — the current state, a projected state after a treatment
plan, or one member of a portfolio comparison. Comparing a
baseline against one or more
treatment scenarios is the canonical board visual.
| field | type | req. | description |
|---|---|---|---|
| id | string | ✓ | Unique in the document; matches [A-Za-z0-9_-]+. |
| label | string | — | Display name; renderers fall back to id. |
| role | string | — | baseline | treatment | comparison — a presentation hint only. |
| curve | Curve | ✓ | The exceedance curve (below). |
| bands | Band[] | — | Uncertainty bands (below). |
| statistics | object | — | Precomputed summaries — informative only. |
| annotations | object[] | — | { loss, label } markers pinned to the curve. |
| color | string | — | #rrggbb rendering hint; renderers may override. |
Curves & interpolation
A curve is at least two samples of the exceedance function, under four semantic rules the validator enforces:
lossis finite and ≥ 0, in the metadata currency;pis finite, in [0, 1];- losses are sorted strictly ascending — no duplicates;
pis non-increasing — an exceedance function cannot rise.
Curves may declare how to interpolate between knots:
| profile | meaning | use when |
|---|---|---|
| linear | p linear in loss (default) | evenly sampled curves, linear axes |
| log-loss | p linear in ln(loss); every loss must be > 0 | heavy-tailed curves on log-spaced grids — segments render straight on a log axis, so the drawn chord, the interpolation math, and the hover crosshair all agree exactly |
Below the first point the curve extends horizontally at p₀; above the last, at p_N. From the curve alone, compliant tooling derives exceedance probability at any loss, loss at any probability (VaR-style), return periods, and the expected annual loss — the exact per-segment integral under the declared profile, a lower bound when the final p is still > 0.
Uncertainty bands
Bands express an interval around the curve at a stated level, and 0.2 lets them say what kind of uncertainty they carry — the two kinds make very differently sized bands:
| kind | expresses | behaves |
|---|---|---|
| sampling | Monte Carlo estimation error (e.g. a Wilson interval on each p̂) | shrinks as iterations grow |
| parameter | epistemic uncertainty about model inputs | does not shrink with iterations |
| mixed | both at once | — |
Quantiles of the raw 0/1 exceedance indicator are not a band — they collapse to [0, 1] almost everywhere. The validator warns when every band point spans the full interval, because the band carries no information.
Reference lines
Document-level guide lines put governance context in the picture: risk tolerance, insurance attachment and limit, a 1-in-N probability line.
"referenceLines": [
{ "axis": "loss", "value": 5000000, "label": "Risk tolerance" },
{ "axis": "probability", "value": 0.05, "label": "1-in-20" }
]
Statistics
Optional precomputed summaries — mean,
median, percentiles,
sampleCount, observed extremes — so simple consumers can
print headline numbers without math. Readers must never trust them over the curve;
the reference tooling always re-derives.
Conformance & versioning
- A conforming document validates against the JSON Schema and satisfies the semantic rules — the reference validator checks both, and an agreement test suite keeps the two layers from drifting apart.
- A conforming reader ignores unknown keys, interpolates per the declared profile, and treats statistics as informative.
- Versions are
major.minor; 0.x minors are additive, so readers of 0.x accept any 0.y ≥ x by ignoring unknown fields.
JavaScript API
Everything is importable from the package root; adapters and headless export live in subpath exports.
| export | from | description |
|---|---|---|
| validate(doc) | open-lec | Full structural + semantic validation → { valid, errors, warnings }, each with a JSONPath-style path. |
| render(el, doc, opts?) | open-lec | Mount the interactive SVG chart → { svg, update(doc), destroy() }. |
| probabilityAt(points, loss, interp?) | open-lec | P(annual loss ≥ loss), interpolated per the profile. |
| lossAt(points, p, interp?) | open-lec | Loss at an exceedance probability — the VaR-style inverse. |
| returnPeriod(p) | open-lec | 1 / p horizons. |
| expectedAnnualLoss(points, interp?) | open-lec | ∫ P(X ≥ x) dx — exact under the declared profile. |
| summarize(points, interp?) | open-lec | EAL plus 1-in-2 / 1-in-10 / 1-in-100 losses in one call. |
| renderToSvgString(doc, opts?) | open-lec/headless | Standalone SVG string, no browser — same drawing code as render(). |
| scenarioFromSamples(samples, opts?) | open-lec/adapters | Raw per-iteration losses → curve + Wilson band + statistics. |
| makeDocument(scenarios, opts?) | open-lec/adapters | Wrap adapter output in a complete document. |
Renderer options
| option | default | description |
|---|---|---|
| width · height | 720 · 420 | ViewBox size; the SVG scales responsively to its container. |
| logX | true | Log₁₀ loss axis with 1–2–5 ticks; linear otherwise. |
| showBands | true | Draw uncertainty bands at 14% opacity. |
| showLegend | true | Legend when the document has 2+ scenarios. |
| interactive | true | Hover crosshair with per-scenario probabilities and return periods. |
| densify | false | Since 0.2.2 — subsample segments to draw the declared interpolation profile exactly when the axis scale doesn't match it. Off by default: curves draw as chords in axis space, which is exact whenever profile and axis agree (log-loss ↔ log axis). This site passes true so pasted mismatched documents keep the crosshair on the line. |
| palette | 6 colors | Categorical colors by scenario index; per-scenario color hints win. |
| theme | neutral | text, grid, axis, reference, tooltip colors, font — this site feeds it Hearth tokens and re-renders on theme switch. |
| margin | 24/24/52/64 | Plot margins, merged over the defaults. |
React
import OpenLEC from "open-lec/react";
<OpenLEC
doc={doc}
options={{ logX: true }}
onValidation={(result) => setErrors(result.errors)}
/>
The component validates on every doc change, renders only valid documents, updates in place, and cleans up listeners on unmount.
CLI
| command | description |
|---|---|
| openlec validate <files…> | Validate one or more documents; exit 0 only if all conform. Errors carry exact paths. |
| openlec stats <file> [--json] | Derived EAL and 1-in-2/10/100 losses per scenario; --json for pipelines. |
| openlec from riskquant <csv> | Netflix riskquant scenario CSV → document, re-running its Poisson × lognormal model with a seeded Monte Carlo. --aggregate adds a portfolio sum. |
| openlec from csv <file> | loss,p rows → curve; or a samples column → curve + band. |
| openlec from samples <file> | Raw per-iteration losses (JSON array or one-column CSV) → curve + Wilson band + statistics. |
| openlec render <file> [-o out.svg] | Standalone SVG, no browser. --width, --height, --linear, --no-bands. |
Every command reads - as stdin, so adapters, stats, and
rendering compose:
JSON Schema
For toolchains that speak draft 2020-12, the schema captures the structural half of conformance (the semantic rules — monotonicity, ordering, band coherence — need the validator).
Served from this site at /schema/openlec.schema.json,
canonical in the repository at schema/openlec.schema.json.