fynefigure

package module
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 22, 2026 License: MIT Imports: 7 Imported by: 0

README

fynefigure

CI Go Reference MIT

figure charts in a Fyne app.

p := figure.New(figure.Responsive(true), figure.Title("Signal"))
p.Add(geom.Line(src, geom.X("t"), geom.Y("signal")))

w.SetContent(chart.New(p, chart.Interactive(true)))

That is the whole of it. The widget hovers, drags to pan, zooms about the pointer, resets the view on a double click, follows its own size and the application's colours, and shows a tooltip for the mark under the pointer.

Leave out chart.Interactive(true) and it is a picture: it takes no pointer events at all, so a chart inside a scroll container, a list or a form scrolls with it instead of taking the wheel. c.SetInteractive(true) lets a reader at it later, and false takes the pointer back.

go get github.com/timzifer/fynefigure
git clone https://github.com/timzifer/fynefigure
cd fynefigure/cmd/demo && go run .

The demo is a module of its own — it opts into the GPU tier, which is nested and so outside the widget's module graph — so it is run from its directory rather than fetched with go run ...@latest. On Linux it draws through the CPU rasterizer: the tier and Fyne's cgo driver cannot share a binary there, which cmd/demo/gpu.go explains.

What is in here

Package
fynefigure an ir.Target that rasterizes a chart and shows it in a canvas.Raster
fynefigure/chart the widget: the plot, the pointer, the tooltip, the theme, the stream
fynefigure/orbit the widget for a scene in three dimensions: the cameras, the pointer that turns them

The split is figure's own, between backend/window and backend/window/show: one draws, the others steer. A backend must not know what a scale or a panel is, and everything in chart is about scales and panels.

Why it draws the way it does

Fyne's canvas has lines, rectangles, circles, regular polygons, arcs, text and images. It has no path API — no Bézier segments, no clipping, no affine transform, no stroke joins or dashes, no rotated text — and its own rasterizer is under internal/. A renderer built on those primitives could not draw a smoothed line, a polar chart, a clipped panel or a rotated axis label at all.

Writing a second rasterizer instead is what figure's ADR 0021 rejects: the day it disagrees with the first is the day a chart looks different on screen than in the file it exports. So this does what figure's native window does. It rasterizes with backend/gg — figure's one rasterizer — and presents the pixels.

Two things follow, and both are tested:

  • A chart on screen is the PNG beside it. p.Render(gg.PNG("chart.png")) and the widget produce identical pixels, because they are the same code.
  • Text is measured with the face it is drawn with, so labels are placed exactly rather than approximated.

Live.Draw compares the frame it recorded with the last one and paints nothing at all when they are the same, so a pointer moving over a chart nobody is zooming does not repaint the widget. What it does not do here is repaint only the part that changed — see below.

The widget

c := chart.New(p,
    chart.Interactive(true),               // take the pointer; off, it is a picture
    chart.MinSize(320, 200),
    chart.Detail(0.5),                     // soften while dragging; 1 is the default
    chart.FrameInterval(0),                // 0 is adaptive, <0 draws every event
    chart.Follow(true, false),             // x tracks the data, y stays put
    chart.TrackRows(true),                 // Hit.Row on every hover
    chart.DragMode(figure.DragSelects),   // drag marks out a rectangle
    chart.Brush(&figure.Brush{}),         // what that rectangle looks like
    chart.LegendToggle(true),              // click a legend row to hide a series
    chart.Overlay(&figure.Crosshair{}),   // paint over the finished chart
    chart.TooltipFormat(myFormat),         // or chart.Tooltip(false)
    chart.TooltipLook(myTooltipStyle),     // colours, padding, type
    chart.ThemeFont(true),                 // the app's typeface; see below
    chart.WheelScale(4),                   // zoom per notch
)

Events are figure's, not a second set of callbacks:

p.On(figure.Hover, func(ev figure.Event) {
    if ev.Found {
        status.SetText(ev.Series())
    }
})
Selecting, hiding, linking

figure wires none of these itself, and says why in its ADRs 0045 and 0047: a legend that always toggled, a crosshair nobody asked for and two charts that always moved together would each be wrong somewhere. The widget offers them as switches because for a widget they are the common case, and every one of them is off by default.

A drag can mark out a rectangle instead of panning. The rows under it arrive as ordinary figure events, one per layer, and the chart draws the band — figure paints nothing while one is being dragged out, because what the feedback looks like is the surface's business:

c := chart.New(p, chart.DragMode(figure.DragSelects))
c.Plot().On(figure.Select, func(ev figure.Event) {
    fmt.Println(ev.Hit.Series, len(ev.Rows))
})

Selecting turns row tracking on by itself: a selection reads rows out of the hit index, and an index that was not tracking them holds none.

A click on a legend row can hide the layer it stands for. The axes deliberately do not move — a toggle is a reading aid, and an axis that rescaled on every click would make the two readings incomparable:

c := chart.New(p, chart.LegendToggle(true))
c.HideLayer(1, true)   // or ToggleLayer, ShowAllLayers, LayerHidden

Two charts are linked by handing one's view to the other, one line each way. That does not loop: SetView is not a reader moving anything and reports nothing back.

left.OnViewChange(func(v figure.View) { right.SetView(v) })
right.OnViewChange(func(v figure.View) { left.SetView(v) })
Overlays

An overlay paints over a finished chart — a crosshair, rings around a set of points, a box of text. It is installed once and then moved, because it is a pointer to a struct whose fields a handler writes:

cross := &figure.Crosshair{}
c.Overlay(cross)
c.Plot().On(figure.Hover, func(ev figure.Event) {
    cross.At, cross.Show = ev.Hit.At, ev.Found
})

A hover redraws the chart while an overlay is installed and does not while one is not, so a chart with no use for one should not install a do-nothing overlay to keep the code uniform. The chart composes the caller's overlay with the band of a selection drag, so a chart with both shows both.

Transitions

A transition is figure's — a keyed join between two tables, blended in data space — and the clock is the host's. The widget is the clock:

tw, _ := data.NewTween(before, after, "lang", data.Hold("slot"))
tr, _ := c.Transition(tw)
c.Play(tr.Over(400*time.Millisecond).Ease(figure.EaseOut), func() {
    fmt.Println("arrived")
})

Frames are advanced by the wall clock on Fyne's goroutine, so a transition of four hundred milliseconds takes four hundred milliseconds on a machine that cannot draw sixty frames in that time: it skips the frames it cannot afford instead of running long. A transition being played belongs to the goroutine playing it — that is what the completion callback is for, and why nothing on it should be read from elsewhere while it runs.

Live data
st := data.NewStream("t", "y").Window(600)
p.Add(geom.Line(st.Source(), geom.X("t"), geom.Y("y")))

c := chart.New(p)
c.Stream(st)
c.Animate(50 * time.Millisecond)

The producer appends from wherever it likes; the chart freezes a snapshot between frames, so it never reads a stream half-written. From a goroutine of your own, ask for a frame with c.Redraw() — every other method here belongs to Fyne's goroutine.

Redraw and Refresh queue the frame rather than draw it, and a chart asked again before its turn draws once: a container refreshing its children and new data arriving in the same turn cost one frame. A hidden chart draws nothing — Fyne lays out hidden widgets too — and catches up when it is shown. To release a zoom only to set a view straight after, c.ResetView() does what Autoscale does without painting the released view or reporting it.

A sliding window needs two things a static chart does not, and chart.Follow does both for the x axis by default.

A scale is trained, and training accumulates: a domain grows and never shrinks, because a chart of a fixed table must not depend on the order its rows arrived in. A window whose oldest row leaves every frame is the other case, and an axis that remembers that row pins the left edge to the first sample the chart ever saw. So a followed axis is released before each frame and established again from the rows that are there.

A linear axis also nices by default — it rounds its domain outward to whole tick steps. That frames a still chart well and fights a moving one: the axis holds while the data slides under it, then jumps a whole tick and holds again, so the oldest samples visibly leave the chart before the axis admits they are gone. A followed axis is rebuilt without that rounding, once, keeping everything else about it.

A followed chart is not dragged or zoomed. That sounds like a restriction and is the opposite: a sliding window has nothing behind its tip, because the rows that scrolled off were dropped, so a pan that took the view back would strand the reader in front of data that no longer exists while the live data marched off the other side. It would also flicker — figure redraws from inside its own pan, so a gesture frame would show the dragged view and the next frame would snap back to the data. chart.FollowPause(true) is the other policy: the first gesture pauses following, the view stays where the reader put it, and the double click that resets the view hands the chart back. That is what a source with history rather than a window wants.

Pin the y axis with scale.Domain; one that rescales itself every frame makes two frames impossible to compare by eye.

Tooltips

A hover shows the series and the values under the pointer. What it says is chart.TooltipFormat, and a label with newlines in it is drawn as several lines:

chart.TooltipFormat(func(h figure.Hit) string {
    return fmt.Sprintf("%s
%.4g", h.Series, h.Y)
})

How it looks is chart.TooltipLook, and a caller who wants both per hover returns a chart.TooltipContent instead — from a function, or from any type implementing chart.Tooltipper, which is where formatting with state of its own belongs:

type reading struct{ unit string }

func (r reading) Tooltip(h figure.Hit) chart.TooltipContent {
    c := chart.TooltipContent{Text: fmt.Sprintf("%.1f %s", h.Y, r.unit)}
    if h.Y < 0 {
        c.Style.Text = color.NRGBA{R: 220, G: 60, B: 60, A: 255}
    }
    return c
}

c := chart.New(p, chart.TooltipWith(reading{unit: "°C"}))

Every field of a chart.TooltipStyle is optional: what a content leaves zero comes from chart.TooltipLook, and what that leaves zero comes from the Fyne theme.

The box is drawn by the rasterizer rather than by Fyne's text engine, and that is the point. Fyne substitutes U+FFFD for a character the theme font has no glyph for — see internal/painter/font.go — and a theme carrying its own font resource gets no fallback at all, so ≤ ≥ ∞ ± came out as � in the one place a chart puts numbers in front of a reader. Drawing the tooltip through the same rasterizer as the axis beside it gives it the same coverage and the same typeface, and brings multi-line labels with it.

A scene in three dimensions

figure v0.9 draws a chart whose x, y and z are all data — a surface over a grid, a trajectory through a volume, bars over two categoricals — in its package three. It deliberately ships no loop to turn one: a camera is a value, three.Orbit and three.Dolly are pure functions from one to another, and the pointer is the host's. orbit is that host's side for Fyne.

sc := three.NewScene(three.XTitle("x"), three.YTitle("y"), three.ZTitle("gain"))
sc.Add(three.Surface(src, geom.X("x"), geom.Y("y"), geom.Z("gain")))

p := three.New(three.Title("Response")).Scene(sc)
w.SetContent(orbit.New(p, orbit.Interactive(true)))

A drag takes hold of the scene and turns it — the side facing the reader follows the pointer, as in three.js, Blender and matplotlib — the wheel brings it closer, a double click puts it back at the camera its author chose. It follows its size and the application's colours the way the flat widget does, through the same rasterizer. Like the flat widget it is a picture until made interactive, and SetInteractive switches it either way later.

c := orbit.New(p,
    orbit.Interactive(true),   // take the pointer; off, it is a picture
    orbit.PerPixel(0.008),     // radians of turn per pixel of drag; <0 turns against it
    orbit.WheelScale(4),       // dolly per unit of Fyne's scroll
    orbit.Together(false),     // a drag turns the view it started in
    orbit.TrackRows(true),     // Hit.Row on every hover
)
c.OnHover(func(h interact.Hit, found bool) { /* h.Panel is the view */ })
c.OnCamera(func(view int, cam three.Camera) { /* a reader moved it */ })

A plot with several views — one scene seen as a three-quarter, a plan and two profiles — turns the view the gesture started in and leaves the others still, so a turned view can be compared with a fixed one. orbit.Together(true) turns them all by the same amount instead. Two widgets are linked the way two flat charts are, one line each way; SetCamera reports nothing back, so it does not loop:

left.OnCamera(func(i int, cam three.Camera) { _ = right.SetCamera(i, cam) })
right.OnCamera(func(i int, cam three.Camera) { _ = left.SetCamera(i, cam) })

A hover in a projected scene has no x and y to report — a device point in a turned cube does not resolve to a pair of values — so it says which view, which layer and, with rows tracked, which source row. That is figure's answer too, and the reason orbit has no tooltip of its own: what a row means is the caller's.

Pacing

Fyne's desktop driver drains the whole operating-system event queue in one pass, so a pointer moving at a hundred events a second delivers a hundred of them — and each one that pans or zooms costs a frame. At tens of milliseconds a frame that is four times the work there is time for, and the backlog does not drain: the chart falls further behind the cursor for as long as the drag lasts.

So a frame that would start before the last one has been paid for is not drawn. The newest position replaces it, and a trailing timer draws whatever is left over, so a drag that stops still lands where it stopped. A hundred drag events cost 60 ms of work instead of 2.6 seconds.

Nothing is lost by dropping a pan — figure pans by the distance from the last position it was told about, so the next one covers the whole way — and nothing by dropping a zoom, because the deltas are added up and applied together (a wheel factor is exp(delta/1000), and exp(a)*exp(b) is exp(a+b)). Hovering is never paced: it reads the hit index, draws nothing, and costs microseconds.

The interval is the last frame's own measured cost, so the pacing follows the machine, the chart's size and the amount of data rather than a guess. chart.FrameInterval pins it, or turns it off.

Level of detail

A rasterizer's work is per pixel; stretching a small picture over a large area is the graphics card's, and free. Fyne hands a canvas.Raster the pixel size it is about to paint but uploads whatever size it is given as the texture, and draws the quad at the widget's size either way — so a frame drawn at half the resolution is scaled up by the GPU rather than resampled on the processor.

So a chart can be drawn coarse while a reader is dragging or zooming it and properly once they stop. Per drag frame on a 900x500 chart, 23 ms becomes 7.4 ms at chart.Detail(0.5).

It is off by default, because it is visible: a chart in motion is soft until it is let go, and that is a trade a reader should be offered rather than given. Reach for it when a chart is large, the data heavy or the machine slow — and note that the GPU tier below makes the same frame cost 5 ms without softening anything, so try that first. A chart nobody is touching is always drawn at full resolution.

What a frame costs

Rasterizing is the whole of it, and it is not cheap. On a 900x480 chart, per frame, on a Ryzen 7 5800H:

empty chart, axes and titles only 20 ms
a 600-point line, whole frame 25 ms
panning a 4000-point line 45 ms
the same at half resolution, with chart.Detail(0.5) 21 ms
a sliding stream at half resolution 8 ms
a hover that finds a mark, no repaint 3 µs

So an interactive chart runs at twenty to thirty frames a second, and a hover costs nothing because it paints nothing. That is the rasterizer's price, and it is why the pacing above exists rather than being a nicety.

There is a GPU tier, in fynefigure/gpu, and it is worth having:

CPU GPU
a sliding stream, whole frame 25 ms 7 ms
panning a 4000-point line 45 ms 5 ms

One blank import turns it on:

import _ "github.com/timzifer/fynefigure/gpu"

A machine with no usable device falls back to the CPU rasterizer, and gpu.Enabled reports which way it went — figure's tier proves the accelerator puts ink in a buffer before keeping it, so that is an answer about drawing rather than about registration.

The two tiers do not share a coverage filler, so they do not agree pixel for pixel: the difference is the antialiasing on the edge of everything drawn, about one part in 255 on average. The package's parity test measures it.

Figure offers to tell a backend where a frame changed so that only that part is repainted. It is off here, because measured it is six times slower: the rasterizer clears the damaged box and clips every drawing call to it, and its clip is a mask rasterized across the surface and then sampled per pixel — on top of the clip a panel already puts there. A stream sliding left costs 29 ms a frame whole and 180 ms partial. fynefigure.DamageBudget turns it back on for a chart whose changes really are confined to a corner; go test -bench=Frame is the measurement.

Colours and type

The chart reads the application's background colour and picks figure's light or dark theme to match, in the page colour and the text size the Fyne theme asks for. chart.FollowTheme(false) turns that off.

The labels are drawn in figure's own fonts rather than the application's, and that is deliberate. Fyne renders text through a shaper that falls back: a character its theme font has no glyph for is drawn from another font, so the label appears. The rasterizer that draws a chart is handed one font and has no fallback, so the same character is drawn as nothing at all — silently, leaving a gap. Fyne's theme font is NotoSans-Regular, which has no glyph for ≤, ≥ or ∞, so a chart titled 30° ≤ x would keep the degree sign and lose the rest. chart.ThemeFont(true) matches the application's typeface for a chart whose labels are plain enough to carry it; the default also keeps the widget comparable pixel for pixel with an exported PNG.

Building

go build ./... && go vet ./... && go test ./...
gofmt -l .          # must print nothing
(cd cmd/demo && go build ./... && go vet ./... && go test ./...)
(cd cmd/demo && go run .)   # its own module; needs a display

The tests need no display: Fyne's software painter draws the widget, and what is asserted is what figure reports — the size it was laid out at, the domain after a zoom, the frames it painted — rather than how it looks.

On Linux the demo needs the headers Fyne's desktop driver is built against: libgl1-mesa-dev xorg-dev libxkbcommon-dev. The library packages themselves build with CGO_ENABLED=0.

See CONTRIBUTING.md for the layout, the benchmarks behind the defaults, and what to know before changing them.

Versions

fyne.io/fyne/v2 v2.7.3, github.com/timzifer/figure v0.9.0 and its raster backend at backend/gg/v0.9.0, pinned exactly — a release of this bridge is validated against one release of each and says which.

figure and its rasterizer are cgo-free; Fyne's desktop driver is not, so a program using this needs cgo the way any Fyne program does. Only the packages here and the tests build with CGO_ENABLED=0 — the widget does, the window it goes in does not.

Documentation

Overview

Package fynefigure draws figure charts into a Fyne canvas object.

t := fynefigure.New()
live, err := p.Live(t)
// ... live.Draw(); t.Present() ...
container.NewStack(t.Object())

It is an ir.Target like any other, and what it adds is that the pixels end up in a Fyne widget tree. The package next door, fynefigure/chart, is what joins one to a *figure.Plot and turns Fyne's mouse events into hovers, pans and zooms, and fynefigure/orbit does the same for a *three.Plot, turning its cameras — a backend must not know what a scale or a panel is, so steering is a separate package for the same reason figure keeps backend/window and backend/window/show apart.

Why a raster and not canvas objects

Fyne's canvas package draws lines, rectangles, circles, regular polygons, arcs, text and images. It has no path API: no Bézier segments, no clipping, no affine transform, no stroke joins or dashes, no rotated text. Its own rasterizer is under internal/ and cannot be imported. So an ir.Backend built on Fyne primitives could not draw a smoothed line, a polar chart, a clipped panel or a rotated axis label at all.

The alternative — a second rasterizer of figure's own, drawing into an image — is the thing figure's ADR 0021 rejects: the day it disagrees with the first is the day a chart looks different on screen than in the file it exports. So this package does what the native window does. It rasterizes with backend/gg, which is figure's one rasterizer, and presents the result. A chart in a Fyne widget is the same pixels as the PNG beside it, and text is measured with the face it is drawn with rather than approximated.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Frame

type Frame struct {
	// At is when it finished.
	At time.Time
	// Cost is how long it held the surface: the pointer handling, hit test and
	// layout the call did as well as the rasterizing, because that is the time
	// the chart was not available for the next event.
	Cost time.Duration
	// Painted is whether it put a new frame in the surface. Most pointer moves
	// over a chart nobody is zooming do not: figure paints nothing for a frame
	// identical to the last.
	Painted bool
	// Latency is how long after the pointer event it answers the frame was
	// finished, pacing included — zero for a frame that answers none. See
	// [Target.Input].
	Latency time.Duration
	// W and H are the frame's logical size and DPR the ratio its buffer is
	// scaled by. They are set only on a painted frame.
	W, H int
	DPR  float64
}

Frame is what one call through Target.Render cost.

type Option

type Option func(*config)

Option configures a Target.

func DamageBudget

func DamageBudget(f float32) Option

DamageBudget turns partial repaints back on, for frames whose damaged region covers no more than f of the surface. Zero, the default, repaints the whole frame every time.

figure works out where a frame changed and offers the rasterizer the chance to repaint only that. It sounds like a saving and here it is not: the rasterizer clears the damaged box and clips every drawing call to it, and its clip is a mask it rasterizes across the surface and then samples per pixel — on top of the clip a panel already puts there. Measured on a 900x480 chart, per frame:

                          whole frame   partial
a stream sliding left        29 ms       180 ms
a stream growing at its
  right-hand tip             30 ms       170 ms
panning a static chart       50 ms        50 ms

Six times slower where it does anything, and a wash where it does not. So the default is off, and this is here for a chart whose changes really are confined to a corner of it, and for the day the rasterizer's clip gets cheaper. There is a benchmark; measure before turning it on.

func Font

func Font(regular, bold, italic []byte) Option

Font replaces the rasterizer's embedded Go fonts with supplied TrueType or OpenType files. Pass bold or italic as nil to reuse regular for that style.

It is how a chart is drawn in the application's own typeface: package fynefigure/chart reads the faces off the Fyne theme and passes them here. Without it a chart uses the same fonts every other figure raster does, which is what makes its pixels comparable with an exported PNG.

func ScaleMode

func ScaleMode(m canvas.ImageScale) Option

ScaleMode sets how Fyne resamples the chart if it ever has to.

It normally does not: the raster is generated at exactly the pixel size the painter asks for. The exception is the one frame after a display's device pixel ratio changes, where the previous frame is stretched while the next is rasterized at the new ratio. The default is canvas.ImageScaleSmooth.

type Ref

type Ref struct {
	// Key is the row's identity, or "" for a layer that names no key column.
	Key string
	// View is the panel or view the reader picked in, or -1 when it came from
	// somewhere that was not a pointer.
	View int
	// Layer is which layer of it, and Row the source row in the table that
	// layer was given. Row is -1 when the hit reported none, which is what an
	// index that was not tracking rows reports.
	Layer, Row int
}

Ref is one row a reader picked: which row, and what it is called.

Two of them name the same row when both carry a non-empty Key and the keys are equal, and otherwise when their layer and row agree. The key is what crosses a table — github.com/timzifer/figure/geom.KeyBy names the column that identifies a row, so two charts drawn from two tables can agree about one measurement — and the layer and row are the fallback for a layer that names no key, where the only honest claim is about one table.

View is deliberately not part of identity. A figure with four cameras is one chart looked at four ways, so a row picked in the plan is the same row in the three-quarter view; View records where the reader was pointing when they picked it, which is worth keeping and is not what makes it that row.

func (Ref) Named

func (r Ref) Named() bool

Named reports whether the ref carries an identity that crosses tables.

func (Ref) Same

func (r Ref) Same(o Ref) bool

Same reports whether two refs name one row.

type Selection

type Selection []Ref

Selection is the set of rows a reader has picked, in the order they picked them.

It is a slice rather than a map because it is small, ordered and compared far more often than it is searched: a widget asks "is this the same selection as last frame" on every pointer move and "is this row in it" once per row it draws. The order is worth keeping — the first row picked is the one a caller showing a single reading shows.

The zero Selection is empty and usable.

func (Selection) Add

func (s Selection) Add(r Ref) Selection

Add returns the selection with a row added, or unchanged when it is already there. It does not modify the receiver, so a handler may keep the value it was handed.

func (Selection) Clone

func (s Selection) Clone() Selection

Clone returns a copy, for a caller keeping a selection past the call it arrived in.

func (Selection) Contains

func (s Selection) Contains(r Ref) bool

Contains reports whether the selection names a row.

func (Selection) Equal

func (s Selection) Equal(o Selection) bool

Equal reports whether two selections name the same rows in the same order.

It is what a widget asks before telling anybody that the selection changed, so that a click landing on the row that was already picked is not an event — and, with the no-echo rule the two widgets follow, what keeps two linked charts from telling each other about a selection for ever.

func (Selection) IndexOf

func (s Selection) IndexOf(r Ref) int

IndexOf reports where a row is in the selection, or -1.

func (Selection) Remove

func (s Selection) Remove(r Ref) Selection

Remove returns the selection without a row, or unchanged when it was not there.

func (Selection) Toggle

func (s Selection) Toggle(r Ref) Selection

Toggle adds a row that is not selected and removes one that is, which is what a click with a modifier held usually means.

type Target

type Target struct {
	// contains filtered or unexported fields
}

Target is a render destination that draws into memory and shows the result in a Fyne canvas object.

It is ggbackend.Surface with a Fyne front end. The surface keeps its pixels, so the backend it opens implements ir.Resizer and ir.Partial: a resized chart is repainted in place, and a live one repaints only where it changed. What this type adds is Target.Object, the object to put in a widget tree, and Target.Present, which shows the frame that was just drawn.

A Target draws one chart at a time and is not safe for concurrent use.

func New

func New(opts ...Option) *Target

New returns a target that has not been opened yet. Nothing is rasterized until a chart is drawn into it.

func (*Target) Close

func (t *Target) Close() error

Close releases the pixel buffer. The frame is not available afterwards.

Plot.Render closes the target it was given, so rendering a chart once into a Target leaves nothing to show — a chart in a widget is drawn through Plot.Live, which keeps its target open and redraws into it frame after frame. That is the difference between a document and a surface, and it is why this type is one.

func (*Target) Frames

func (t *Target) Frames() uint64

Frames reports how many frames have been painted into the surface.

It is not a frame number: figure paints nothing for a frame identical to the one before it, so this counts the frames that changed something. It is what a test asserts on, and what tells a caller whether the last draw did any work.

func (*Target) Image

func (t *Target) Image() image.Image

Image returns the pixels of the last frame, or nil before the first one.

The image is the rasterizer's own buffer, so it is valid until the next frame overwrites it. A caller keeping one past that must copy it. It is what a test compares and what an export of exactly what is on screen would read.

func (*Target) Input

func (t *Target) Input()

Input marks that a pointer event has arrived for the chart drawn here.

The next frame painted after it reports in Frame.Latency how long after the event it was finished: the wait a paced widget imposed on it as well as the drawing, which is the roundtrip a reader feels. A call through Render that handles the event and paints nothing answers it too, with no frame to report. Only the first event of a run is kept — the frame that answers it answers every event that arrived after it.

It must not be called from inside Render. It costs nothing when no OnFrame callback is registered, beyond the lock.

func (*Target) Object

func (t *Target) Object() fyne.CanvasObject

Object returns the canvas object the chart is shown in.

It is a canvas.Raster, so Fyne asks it for an image at the size it is about to paint, in *physical* pixels — which is the size the chart should be rasterized at, device pixel ratio included. The generator never draws: it hands back the frame that is already there. When the painter asks for a size the chart was not rasterized at — the frame after a window moves to a display with a different ratio — the previous frame is stretched for that one paint and the size is reported through Target.OnGeometry, so that whoever owns the chart can rasterize the next one correctly.

func (*Target) OnFrame

func (t *Target) OnFrame(fn func(Frame))

OnFrame registers a callback for every call through Target.Render, painted or not. It is how a caller measures what a chart costs to draw without wrapping every path that draws it.

It is called with the surface held, on whichever goroutine drew — Fyne's, for a widget — so a handler must only record: calling back into the target is a deadlock, and anything slow is time every frame pays. Pass nil to stop.

func (*Target) OnGeometry

func (t *Target) OnGeometry(fn func(widthPx, heightPx int))

OnGeometry registers a callback for the pixel size Fyne is painting at, when that is not the size the current frame was rasterized at.

It is called from the painter, which is the goroutine Fyne draws on, so a handler must not draw a chart from it — it should ask for one, and let the redraw happen in its turn.

func (*Target) Open

func (t *Target) Open(s ir.Surface) (ir.Backend, error)

Open prepares the target for a chart of the given size. It is ir.Target's half of the contract and is called by figure, not by a caller.

func (*Target) Present

func (t *Target) Present()

Present shows the frame that was last drawn.

It is what to call after Live.Draw, and it is cheap when nothing changed: Live paints nothing when a frame is identical to the last, and a frame nobody painted is not handed to Fyne. So a pointer moving over a chart that is not being zoomed costs a comparison of two integers rather than a texture upload.

The frame Fyne is handed comes out of the rasterizer's buffer, and the painter reads it while it uploads the texture. So the surface is held whenever it is drawn into or read from — see Target.Render — rather than resting on the drawing and the painting being the same goroutine, which they are on a desktop driver and are not under Fyne's test one.

func (*Target) Render

func (t *Target) Render(fn func() error) error

Render runs fn with the surface held, and is how a frame is drawn.

Everything that draws into the surface goes through here, so that a frame cannot be painted while Fyne is reading the last one. fn is whatever rasterizes — Live.Draw, Live.Resize, Live.Rescale — and its error is returned unchanged.

It must not be called from inside another Render, and fn must not reach back into the target: this is a plain lock, not a reentrant one.

func (*Target) SetFont

func (t *Target) SetFont(regular, bold, italic []byte) error

SetFont replaces the typeface the rasterizer draws labels with, keeping the canvas object the chart is already shown in.

A font is fixed when a rasterizer is made, so this makes a new one — which is why it closes the surface, and why the chart drawn into it has to be opened again afterwards. What it deliberately does not replace is Target.Object: a widget holds that object, and handing it a different one would leave it showing a chart nothing draws into any more.

Pass all three as nil to go back to the rasterizer's own fonts.

func (*Target) Size

func (t *Target) Size() (w, h int, dpr float64)

Size reports the logical size of the chart being drawn, in device-independent pixels, and the device pixel ratio its buffer is scaled by. It is zero before the first chart is opened.

Directories

Path Synopsis
Package chart shows a figure plot in a Fyne widget.
Package chart shows a figure plot in a Fyne widget.
internal
look
Package look reads what a Fyne theme asks of a chart: the page it sits on, the size of the text around it and the typeface that text is set in.
Package look reads what a Fyne theme asks of a chart: the page it sits on, the size of the text around it and the typeface that text is set in.
Package orbit shows a three-dimensional figure plot in a Fyne widget, and turns it under the pointer.
Package orbit shows a three-dimensional figure plot in a Fyne widget, and turns it under the pointer.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL