Chart ingestion¶
Turn a chart printed in a datasheet (a pump curve, an efficiency map) into a
real (x, y) data series on the canvas, anchored to the page it came from.
This pipeline is the first worked example of an external OIP producer. ANCHOR writes no chart-specific code. It discovers a conforming tool, briefs the agent about it, and renders its output by token. The same path serves any future producer that follows the standard.
The pieces¶
| Repo | Role |
|---|---|
| graph-data-extractor | The producer. Traces chart lines into data. Ships an OIP manifest, a trace_series MCP tool, and a graph-tracer-trace CLI. Installs without a GUI. |
| OIP | The contract. v0.3 adds region producers (consumes), derived regions (derived_from), and recognised renders tokens (chart). |
| anchor (this repo) | The consumer. Discovers the producer, folds its agent skill, and renders a chart region with ChartPrimitive. |
The tracing engine fuses ridge detection, colour similarity, and patch embeddings, then connects a few seed waypoints into the full curve. The agent places those seeds from the image; a human can place them by clicking instead.
How it fits OIP¶
graph-data-extractor is an OIP region producer. It does not ingest a
source. It consumes a chart region another producer wrote (a chart image
with a png crop) and produces a chart_series region whose renders
hint is chart. The derived region keeps the parent chart's source_ref, so
the recovered series still points at the same page and bbox.
chart region (anchor_pdfs) chart_series region (graph-tracer)
kind: chart kind: chart_series
source_ref: pdf-page-bbox ──────▶ derived_from: <chart region id>
content.png: crop.png source_ref: <copied from parent>
content.data: { axes, series }
content.data is exactly the OIP chart token payload:
{
"x_label": "Q (m3/h)", "y_label": "H (m)",
"x_scale": "linear", "y_scale": "linear",
"series": [{ "label": "LKH-85", "points": [[0, 94], [150, 94], [400, 50]] }]
}
Setup¶
Install the producer and register it so any OIP consumer can find it:
This writes a manifest to ~/.config/oip/producers.d/graph-tracer.json.
ANCHOR discovers it (anchor extensions list) and folds its agent skill into
the composed briefing, so a connected agent learns when and how to use it.
For the agent to call the tracer, register its MCP server in the same harness that runs ANCHOR's:
The harness is the integration point. It holds both ANCHOR's tools and the producer's tools, so the agent orchestrates across them. ANCHOR does not spawn the producer itself.
The flow (agent-driven)¶
- Ingest the datasheet in ANCHOR. The PDF producer extracts regions; a chart
region has
kind: chart, and its crop is addressable as<page>/<region_id>.png. - Read the crop.
anchor crop <slug> <page>/<region_id>.png(MCPget_crop, HTTP/api/documents/<slug>/crops/<page>/<region_id>.png) renders it lazily from the bronze PDF on first request and returns the image path. The default 300 dpi is usually enough; for a thin curve pass--dpi 600(MCPdpiarg, HTTP?dpi=600) to re-render sharper.anchor page-imagetakes the same--dpiwhen you want the whole page. - Place calibration and seeds. Read two tick values per axis and their pixel positions; drop a few waypoints along each line. All in pixels.
- Trace. Call
graphtracer.trace_series(image_path, request). It returns{ axes, series }. - Place it. Call
anchor canvas_add_node(node_type="chart", data={ chart: <result>, source_ref: <parent chart region's source_ref> }). The node renders as a chart, with a chip that opens the source PDF at the page and bbox.
The node type registry is open, so chart needs no registration; the canvas
renders it through the chart token. A namespaced producer node type works
too: a graphtracer:chart_series node has no renderer of its own, so the
canvas resolves it through the renders: "chart" token its manifest declares
(exact registration first, then the token, then the default box). Provenance
is preserved end to end: the chart on the canvas points back at the page
region it was traced from.
The human-click path¶
The same chart token and the same ChartPrimitive serve a human-driven
trace. Instead of the agent placing pixels, the user clicks the calibration
ticks and waypoints on the chart region in the canvas, and the same
trace_series request is sent. This path needs a canvas point-collection
mode (planned); the data contract and renderer are already shared.
What is built, what is next¶
Built and verified:
- Producer: headless
trace_series/graph-tracer-trace, OIP manifest + installer, Qt-free core install. - Contract: OIP 0.3
consumes/derived_from/charttoken (RFC 0001). - Consumer: discovery + skill composition (no per-producer code), and the
chartrender token (ChartPrimitive) with source provenance. - Region crops on demand: gold ships no pre-rendered crops; the first read of
gold/<slug>/pages/<page>/<region_id>.pngrenders it from the bronze PDF (bbox + 3 pt margin, 300 dpi default,dpiup to 600) and caches it. Reachable on all three adapters (CLIanchor crop, MCPget_crop, HTTPGET /api/documents/{slug}/crops/{page}/{region_id}.png). - Durable persistence:
derive_regionstores newchart_seriesrecords inknowledge/<slug>/pages/, or that document generation's knowledge pages, inheriting the parent'ssource_refand recordingderived_from. Reachable on all three adapters (MCPderive_region, CLIanchor derive-region, HTTPPOST /api/documents/{slug}/derived-regions). Runanchor embed <slug>to make it searchable. This helper is generic: any region producer reuses it, not just the chart digitizer.
To store the series durably (so search_documents finds "LKH-85 max head"),
after tracing call derive_region(slug, <chart region id>, <chart_series
region>) instead of only placing a canvas node, then re-embed.
Use qualified parent locators such as p2/r1; bare IDs must be unique.
See Derived region provenance for source
inheritance, explicit override validation and generation behavior.
Next:
- Canvas point-collection for the human-click path.
- Auto-place a
chartcanvas node from a storedchart_series(today the agent adds the node; new derived data lives in knowledge).