Skip to content

Charts of arbitrary (x, y) data, in every language - #77

Merged
ralyodio merged 2 commits into
mainfrom
feat/chart
Sep 8, 2026
Merged

Charts of arbitrary (x, y) data, in every language#77
ralyodio merged 2 commits into
mainfrom
feat/chart

Conversation

@ralyodio

@ralyodio ralyodio commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Closes #63.

plot takes an array of numbers and puts one sample per column, so the x axis is the array index. That is exactly right for a history buffer and wrong for everything else: two series of different lengths silently render at different horizontal scales, a gap in the data is indistinguishable from a shorter series, and there is no way at all to say where on the x axis a point belongs.

ui.chart({
  series: [{ points: [[0, 1], [2.5, 4], [9, 3]], type: "scatter" }],
  x: { min: 0, max: 10, ticks: 3, format: v => `${v}s` },
  y: { min: 0, max: 5 },
});

A domain on each axis, points that carry their own x, and three marks: line, scatter and bar. plot, graph, areaGraph and multiGraph are untouched and mean exactly what they meant — the 2699 lines added to widgets.json are all new cases, and not one existing fixture moved.

Three things worth knowing

Points are drawn in the order given, not sorted by x. Sorting would be the obvious tidy-up and it would quietly make a loop, or a path that doubles back, impossible to draw.

A fill interpolates along the line rather than sampling the points that land in each column. Sampling is what the history-buffer fill does, and it is fine there because every column has a sample; with arbitrary x values most columns have none, and the area came out striped. There is a test for it.

A flat series has a zero-width domain, which cannot be mapped: every point lands in the same place and the division blows up. It gets room around itself instead of being collapsed onto a line, and a fixture pins that.

Reach

Six ports, and each matched the reference on all eight chart fixtures first time. The Ruby, PHP and Perl bridges gained a chart node and COBOL a CHART/CHARTPT pair, which both COBOL bridges render identically. The widget gallery now has 30, still one runnable example per language per widget.

Verified

  • 298 TypeScript tests under both bun test and node --test, typecheck clean
  • 69 widget scenes matching the reference in Rust, Go, Python, Zig and C++ (was 61)
  • 11/11 ctest, the bindings ABI test, and ports/bindings/tests/check.py
  • all eleven widget galleries run, including Ruby, PHP and Perl against a locally built bridge
  • both COBOL bridges render the new verbs identically (diff clean)
  • next build for the site

🤖 Generated with Claude Code

https://claude.ai/code/session_017Df2FNu5DhinMV2soRz3cy

ralyodio and others added 2 commits September 8, 2026 22:39
hqtui took over the whole screen or it did not run. `alternateScreen: false`
wrote to the main screen but still cleared it and still owned every row, so
the one shape it could not build was the common one: an installer, a build,
a deploy watcher -- a few live rows pinned low, finished work scrolling away
above them, and a readable transcript left behind at the end.

  const app = await createApp({ viewport: { mode: "inline", height: 3 } });
  app.insertBefore(1, ui => ui.text("compiled in 1.2s"));

`fullscreen` is the default and nothing about it changes. `inline` reserves a
strip in the normal flow of the terminal and never clears anything. `fixed`
takes a rectangle of a screen something else is driving.

The hard part is that an inline strip has no coordinates it can trust. It
does not know which screen row it started on, and the terminal scrolls it
upwards without saying so. It could ask -- ratatui does, with a cursor
position report -- but the answer is stale the moment anything scrolls. So
the anchor here is never a number: it is a saved cursor, re-saved whenever
the strip moves, and the encoder gained a relative addressing mode that walks
from it rather than jumping to absolute rows.

Two things that cost real debugging:

DECRC is a pop, not a peek. Restore twice against one save and some terminals
send the cursor home instead -- the top of the user's screen, where the strip
then redraws itself over their shell. A frame restores many times, once per
row it cannot track the cursor through. Every restore now re-arms its save, so
the sequence means the same thing on the stack terminals and the single-slot
ones. There is a test asserting exactly that, because a single frame looks
perfectly fine either way and only the fourth one is wrong.

DECRC restores the pen along with the position, so the encoder's model of the
current colour was quietly wrong after every jump. Anchors are saved with the
pen reset, and the encoder admits it knows nothing about the pen after a
restore.

Verified in a real pty against a VT emulator, not just by reading the escapes
back: on an eight-row terminal the finished lines land in scrollback in order,
the strip stays put at the bottom, and the prompt returns below the last frame
with the shell's earlier output untouched.

TypeScript only, as the issue proposes -- each port has its own terminal and
the semantics are worth settling in one place first.

Closes #61

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Df2FNu5DhinMV2soRz3cy
`plot` takes an array of numbers and puts one sample per column, so the x axis
is the array index. That is exactly right for a history buffer and wrong for
everything else: two series of different lengths silently render at different
horizontal scales, a gap in the data is indistinguishable from a shorter
series, and there is no way at all to say where on the x axis a point belongs.

  ui.chart({
    series: [{ points: [[0, 1], [2.5, 4], [9, 3]], type: "scatter" }],
    x: { min: 0, max: 10, ticks: 3, format: v => `${v}s` },
    y: { min: 0, max: 5 },
  });

A domain on each axis, points that carry their own x, and three marks: line,
scatter and bar. `plot`, `graph`, `areaGraph` and `multiGraph` are untouched
and mean exactly what they meant -- the 2699 lines added to widgets.json are
all new cases, and not one existing fixture moved.

Three things worth knowing.

Points are drawn in the order given rather than sorted by x. Sorting would be
the obvious tidy-up and it would quietly make a loop or a path that doubles
back impossible to draw.

A fill interpolates along the line rather than sampling the points that land
in each column. Sampling is what the history-buffer fill does and it is fine
there, because every column has a sample; with arbitrary x values most columns
have none, and the area came out striped. There is a test for it.

A flat series -- every point at the same height -- has a domain of zero width,
which cannot be mapped: every point lands in the same place and the division
blows up. It gets room around itself instead of being collapsed onto a line,
and a fixture pins that.

Six ports, and each matched the reference on all eight chart fixtures. The
Ruby, PHP and Perl bridges gained a chart node and COBOL a CHART/CHARTPT pair,
which both COBOL bridges render identically. The widget gallery now has 30,
still one runnable example per language per widget.

Verified: 298 TS tests under bun and node; 69 widget scenes matching the
reference in Rust, Go, Python, Zig and C++; 11/11 ctest; the bindings ABI test
and check.py; all eleven galleries; both COBOL bridges diff clean; the site
builds.

Closes #63

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Df2FNu5DhinMV2soRz3cy
@ralyodio
ralyodio merged commit 6bb29eb into main Sep 8, 2026
18 checks passed
@ralyodio
ralyodio deleted the feat/chart branch September 8, 2026 23:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Charts: plot arbitrary (x, y) data, not just y-by-index

1 participant