Charts of arbitrary (x, y) data, in every language - #77
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #63.
plottakes 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.A domain on each axis, points that carry their own x, and three marks:
line,scatterandbar.plot,graph,areaGraphandmultiGraphare untouched and mean exactly what they meant — the 2699 lines added towidgets.jsonare 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
chartnode and COBOL aCHART/CHARTPTpair, which both COBOL bridges render identically. The widget gallery now has 30, still one runnable example per language per widget.Verified
bun testandnode --test, typecheck cleanports/bindings/tests/check.pydiffclean)next buildfor the site🤖 Generated with Claude Code
https://claude.ai/code/session_017Df2FNu5DhinMV2soRz3cy