Skip to content

Follow figure's main branch: the charts it grew, and a fallback font - #6

Merged
timzifer merged 12 commits into
shared-selectionfrom
demo-horizon
Sep 22, 2026
Merged

timzifer merged 12 commits into
shared-selectionfrom
demo-horizon

Conversation

@timzifer

@timzifer timzifer commented Sep 14, 2026 •

Copy link
Copy Markdown
Owner

The branch started as one horizon chart and has followed figure's main branch
since. It is now the demo's catalogue caught up with what figure can draw,
plus the one widget change that catching up asked for.

The widget change

figure's backend/gg grew WithFallbackFont: fonts consulted, in order, for
a rune the chart's own font has no glyph for, with a ? written for a rune no
face can draw at all. That closes the gap chart.ThemeFont has carried a
warning about since it was written — Fyne's NotoSans-Regular has no ≤, ≥
or ∞, and a rasterizer handed one font drew nothing where the shaper Fyne
draws its own text with would have fallen back.

  • fynefigure.FallbackFont is the new option, and Target.SetFont takes the
    fallbacks with the typeface, so a chart that follows the application to a new
    font keeps them. The config holds the faces rather than the rasterizer's
    options: SetFont used to rebuild that list, and would now have dropped the
    fallback while replacing the font.
  • The chart widget passes look.Fallback() — the Go regular face, which is
    what every other figure raster is drawn in — whenever it follows the theme's
    typeface, and the tooltip beside it gets the same list.
  • The test draws the same title as "????" and requires the two to differ. A
    question mark is ink, so counting ink no longer tells a drawn symbol from a
    lost one.

The charts

Each entry is a chart type figure grew, because the demo is where one has to
appear or nobody sees it.

  • horizon — a series folded into bands of equal height, named by a
    colourbar instead of an axis, with the band height pinned rather than cut
    from the data
  • loci, and the tree opening on what is new
  • bars-extruded, weibull, survival, roc, correlogram, spectrogram,
    dendrogram-heatmap, radial-tree, spc
    — the raster mark, the probability
    scales, the statistical instruments and the tidy tree
  • smith-sphere, antenna-pattern, bloch, scatter3 — the spherical scene as
    three charts plus the 3D scatter
  • soil-texture, alloy-prism — the barycentric coord and the ternary prism
    over it, in a new Ternary group beside Polar and Smith. The prism is
    sampled over the whole simplex, so the liquidus is a surface rather than the
    corner a real alloy catalogue would crowd into.
  • ribbon — a descending approach as a band, which occludes and says which
    of two passes is in front
  • joint-orientations — two fracture sets over a scattered background,
    binned into cells of equal area: the chart a stereonet's contoured density is
    the flat form of

Three existing entries moved rather than being added to:

  • bloch draws two dozen states rather than two hundred and forty, which is
    where Frame.Arc shows: a coarse sweep joined by chords would cut through
    the ball.
  • dendrogram-heatmap puts the gene tree in a left track —
    geom.Orient(geom.Horizontal) with geom.Baseline(1) — so ADR 0053's
    headline chart is drawn whole.
  • bars-extruded names a fill and a colour, which outlines all three faces
    of an extruded mark.

cmd/demo/plots/data.go's clustering is now a function of the items and a
distance, because the genes are clustered by the same code read the other way
round, and its uniform deviate takes three rounds: two read side by side off
one affine round walk in step, which put the histogram's background on a curve
instead of over the ball.

Base branch

Targets shared-selection, not main: cmd/demo/plots/ does not exist on
main, so this work sits on top of that branch rather than beside it.

CI

Green, and it took three fixes that had nothing to do with the charts.

  • The requires named tags without the API the code calls. CI builds each
    module from its own go.mod, with no go.work, so the comment saying "builds
    through go.work until figure is released" was a red build. All three modules
    now name a commit of figure's main branch — the widget calls
    gg.WithFallbackFont, the demo draws the chart types after v0.11.0, and the
    GPU tier calls Available, Disable and Enable, which backend/gg/gpu has
    never released. Each go.mod says why it is not a tag and when it goes back
    to being one.
  • The Linux binary still carried the GPU tier. Its import moved behind
    !linux a while ago, but three files went on importing it for Enabled,
    Available, Enable, Disable and Close, so the link still ended in
    dlopen_stub: unhandled relocation for goffi_dlopen. CI never said so because
    module resolution failed first. The tier is now reached through six functions,
    the import lives in the one file Linux does not build, and tier_linux.go
    answers instead. GOOS=linux go list -deps went from eight matches on the
    tier and its foreign-function layer to none.
  • The demo's tests raced. The kitchen refreshes the panel from a ticker
    through fyne.Do; the test driver documents that it runs the function on
    whichever goroutine called it, so the ticker was a second goroutine in the
    widgets the test was driving. newKitchen takes the interval and the test
    passes zero.

Test plan

  • everything below was run with GOWORK=off, which is how CI builds it
  • root module: go build ./... && go vet ./... && go test ./... — passes,
    including the new font test and its negative control (with the fallback list
    emptied, the symbol titles render identically to "????" and the test fails)
  • cmd/demo: go build ./... && go vet ./... && go test ./... — passes;
    TestEveryEntryBuildsAndPaints paints every new entry in a Fyne test window,
    and TestCatalogueShape checks the new group's order and ids
  • gpu module: go build ./... && go vet ./...
  • gofmt -l . — clean in both modules
  • every new and changed entry was rendered to PNG and looked at

🤖 Generated with Claude Code

timzifer and others added 8 commits September 14, 2026 09:58
figure gained geom.Horizon, which folds a series into bands of equal height and
names them with a colourbar rather than with an axis. The demo is where a new
chart type has to appear or nobody sees it.

One entry under Groups & stacking: six hours of feeder load, folded into 30 kW
bands about the idle draw. The band height is pinned rather than cut from the
data, which is the discipline the mark exists for — 30 kW means 30 kW in the
next chart drawn this way. The strip is 220 pixels because that is where the
form starts paying for itself against the line chart of the same series.

Its id goes to the front of Newest, so the tree opens on it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HdSCXbhBhWBgUiCns9Abhf
geom.Horizon and Plot.Theme both landed in figure v0.11.0. With the require at
v0.10.0 this repository builds only through go.work, which is why CI could not
compile chart/chart.go: Plot.Theme did not exist in the core it resolved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HdSCXbhBhWBgUiCns9Abhf
Interactive lets a reader hover, drag, zoom and click; it no longer floats a
box over the marks as well. A chart that hands its hover to the application —
a readout, a crosshair, a linked table — wants the pointer without one.

Tooltip is now off by default and shows only on a chart that is both
interactive and asked for one. Chart.SetTooltip and Chart.Tooltip switch and
report it on a chart already on screen; the hover handlers are registered
either way so it can be turned on later.

The demo gets a Tooltips switch beside Interactive, on by default.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
figure's main branch has grown a raster mark, survival curves, trees,
probability scales, an oblique coord with extruded marks, and the statistical
instruments that are drawn with the marks that already exist. The demo is
where a new chart type has to appear or nobody sees it, so each gets an entry:

- bars-extruded: bars given a top and a side by coord.Oblique
- weibull: failure times on Weibull paper, a complementary log-log axis
- survival: Kaplan-Meier curves with their band, censor ticks and a
  numbers-at-risk track
- roc: two classifiers and the area under each
- correlogram: stat.ACF against the white-noise bound
- spectrogram: a short-time Fourier transform drawn as one raster, resampled
  by Max so a one-millisecond click survives the downscale
- dendrogram-heatmap: clustered samples, the tree in a track over the cells
- radial-tree: a tidy tree bent round a polar coord
- spc: an individuals chart with frozen limits and Nelson's run rules

The ports follow figure's own examples. Their ids go to the front of Newest.

These APIs are not in figure v0.11.0, which cmd/demo/go.mod requires, so the
demo builds only through go.work until figure is released again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
figure's PR #26 gave the three package a spherical scene and a scatter. The
spherical scene is a coordinate system rather than a chart type, so the demo
shows it three ways, each a port of figure's examples/sphere:

- smith-sphere: the Smith chart on the Riemann sphere, where a negative
  resistance runs into the southern hemisphere instead of off the page
- antenna-pattern: a broadside array's power pattern as a surface
- bloch: a detuned Rabi oscillation as one closed curve

and scatter3 is two batches that overlap on the floor and part only in the
third column, with the droplines that make a floating point's height readable.

The Bloch sphere's kets are written with ASCII brackets, as figure's example
writes them: the chart's typeface has no ⟩.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
figure's backend/gg grew WithFallbackFont: fonts consulted, in order, for a
rune the chart's own font has no glyph for, with a `?` written for a rune no
face can draw at all. That closes the gap chart.ThemeFont has carried a warning
about since it was written — Fyne's NotoSans-Regular has no ≤, ≥ or ∞, and a
rasterizer handed one font drew nothing where the shaper Fyne draws its own
text with would have fallen back.

- fynefigure.FallbackFont is the new option, and Target.SetFont takes the
  fallbacks with the typeface so a chart that follows the application to a new
  font keeps them. The config holds the faces rather than the rasterizer's
  options, because SetFont used to rebuild that list and would now drop the
  fallback while replacing the font.
- The chart widget passes look.Fallback() — the Go regular face, which is what
  every other figure raster is drawn in — whenever it follows the theme's
  typeface, and the tooltip beside it gets the same list.

The test draws the same title as "????" and requires the two to differ: a
question mark is ink, so counting ink no longer tells a drawn symbol from a
lost one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
figure's main branch has grown a barycentric coord and the prism over it, a
ribbon, a spherical histogram, an arc between two rows on a ball, an
orientation for a mark, and an outline an extruded mark keeps. The demo is
where a new chart type has to appear or nobody sees it, so each gets an entry
or takes over one that was already there:

- soil-texture: three parts of a mixture in a triangle, with the derived
  component named at its own corner because the third grid family has no
  labels yet. It opens the new Ternary group, beside Polar and Smith.
- alloy-prism: the same triangle as a scene's floor with the melting point up
  the fourth axis, sampled over the whole simplex so the liquidus is a surface
  rather than the corner a real alloy catalogue would crowd into
- ribbon: a descending approach as a band, which occludes and says which of
  two passes is in front
- joint-orientations: two fracture sets over a scattered background, binned
  into cells of equal area — the chart a stereonet's contoured density is the
  flat form of
- bloch: two dozen states rather than two hundred and forty, which is where
  the arc shows: a coarse sweep joined by chords would cut through the ball
- dendrogram-heatmap: the gene tree in a left track, geom.Horizontal with
  geom.Baseline(1), so the record's headline chart is drawn whole
- bars-extruded: a fill and a colour, which outlines all three faces

data.go's clustering is now a function of the items and a distance, because
the genes are clustered by the same code read the other way round, and its
uniform deviate takes three rounds: two read side by side off one affine round
walk in step, which put the histogram's background on a curve instead of over
the ball.

These APIs are not in figure v0.11.0, which cmd/demo/go.mod requires, so the
demo still builds only through go.work until figure is released again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The require directives name published tags, and the option the chart widget
now passes is not in one yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@timzifer timzifer changed the title demo: draw the horizon chart figure grew Follow figure's main branch: the charts it grew, and a fallback font Sep 16, 2026
timzifer and others added 3 commits September 16, 2026 09:02
CI builds each module from its own go.mod, with no go.work, so a require that
names a tag without the API the code calls is a red build rather than a note in
a comment. Three modules were in that state: the widget calls
gg.WithFallbackFont, the demo draws the chart types figure grew after v0.11.0,
and the GPU tier calls Available, Disable and Enable, which backend/gg/gpu has
never released because it is held out of releases while its replace is needed.

So the requires name a commit of figure's main branch. A pseudo-version is
what keeps a module buildable by anyone who fetches it before the next release,
and each go.mod says why it is not a tag and when it goes back to being one.

Verified with GOWORK=off in all three modules: build, vet, test and
govulncheck, plus the no-cgo build of the library alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The tier's import moved behind !linux in "Give the demo the GPU tier where a
binary can hold it", because on linux/amd64 with cgo on — which Fyne's desktop
driver requires — its foreign-function layer does not link. Three files went on
importing it anyway, for Enabled, Available, Enable, Disable and Close, so the
Linux binary still carried it and still failed to link:

    dlopen_stub: unhandled relocation for goffi_dlopen (SDYNIMPORT) (R_CALL)

CI never said so, because module resolution failed before the link did.

So the tier is reached through six functions, and the import lives in the one
file that is not built on Linux. tier_linux.go answers instead, with the
answers a machine with no usable device would give, and imports nothing — the
point is the import, not the return value. The panel says which of the two it
is: a build with no tier reads differently from a machine where no device
answered.

go mod tidy sees every platform, so the requirement on the tier stays.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…outine

The race detector fails the demo's tests, and the CI job that would have said
so never got past the build. The kitchen refreshes the panel's numbers from a
ticker four times a second through fyne.Do. Under the desktop driver that puts
the refresh on the goroutine the driver draws from; the test driver documents
that it "ignores the wait flag as our threading is simple" and runs the
function on whichever goroutine called it. So the ticker was a second
goroutine in the widgets the test was driving, and the two met in the panel's
labels.

newKitchen takes the interval, and the test passes zero, which leaves the
goroutine unstarted. The app passes panelRefresh and behaves as it did. The
test refreshes what it reads, which it already did.

Verified with `go test -race ./...` in all three modules.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… its new size (#7)

* Ring a picked row only in the panel it was picked in

figure numbers layers per panel, and a band beside the panel is a panel of
its own holding its own layers. A selection made by a click names a layer and
a row, and marks.locate asked every panel for that pair — so a row picked in a
gantt strip under a curve was also ringed at layer 0, row n of the curve's
panel and of every other band, which are different rows of different tables.

With bands present, a ref this chart made is now located only in the panel it
was picked in, and the key lookup reads the plot's layers for panel 0 alone:
a band's layers are not reachable from here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Let a chart limit what a click can pick

chart.SelectWhere takes a predicate over the hit. A click on a mark it turns
down is a click on nothing and clears the selection.

A caller could already clear an unwanted pick from its OnSelect handler, but
only outside the chart's lock, which is a frame later — and for that frame
the ring is drawn round a row the reader was never meant to pick. Deciding
before the selection is installed leaves nothing to flash.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Forget what the painter asked of a size the chart no longer has

The painter rounds a raster's pixels and the chart truncates its size, so at
any fractional width the painter asks for a pixel or so more than was drawn,
and painterGeometry records it. Nothing reads that until the next checkScale,
and the next one is often the resize — which divided the old size's pixels by
the new width. After a jump as large as a maximize that read as a fraction of
the real device pixel ratio, and the chart was rasterized at about the pixel
count it had before and stretched. A drag moves a few pixels at a time, which
is why it never showed there.

A resize now drops the recorded geometry, in chart and in orbit alike.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Let a chart hold its view against the pointer

chart.PanZoom(false) keeps drag, wheel and double click from moving the axes,
and leaves the rest of the pointer alone: hover, the tooltip, a click that
picks a row and a drag that selects all still work. SetPanZoom changes it on
a chart already on screen.

Interactive(false) was the only way to stop a pan, and it takes the tooltip
and the click with it. A list of charts that the program holds on one time
range wants those and must not let a reader drag one of them out of line.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Leave the wheel to the list when a chart may not zoom

The wheel moves from the pointer layer to a layer of its own, shown only on
an interactive chart with PanZoom on. Fyne looks for a Scrollable apart from
a Mouseable, so hiding the wheel layer keeps hover and clicks on the chart
and lets the wheel reach the scroll container around it.

Before, a chart with PanZoom(false) took the wheel and ignored it, and a list
of such charts could not be scrolled with the pointer over any of them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Give the wheel test the time a race-detected runner needs

TestOnGestureEndsAWheelOnceItIsStill sends two notches and expects one
gesture, which holds only while the gap between them is shorter than
wheelSettles. Under the race detector on CI the first notch took longer than
120ms to draw, the wheel settled in between, and OnGesture said
[true false true].

wheelSettles is now a variable a test can widen through export_test.go, and
this test widens it to a second; the wait for the gesture to end grows to
match. What the chart does outside tests is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@timzifer
timzifer merged commit 32c73a5 into shared-selection Sep 22, 2026
5 checks passed
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.

1 participant