refract

package module
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Sep 4, 2026 License: MIT Imports: 15 Imported by: 0

README

A Go gopher holding a prism that splits a white beam into a spectrum of charts

refract

CI Coverage Go Reference

A grammar-driven plotting library for Go: one model, many backends, runs everywhere — built on the GoGPU stack.

Status: pre-alpha. This is milestone v0.5, "web and interactivity". The API is not stable; every release below v1.0.0 may contain breaking changes without a deprecation cycle. See CONCEPT.md for the design and the road ahead.

The name is the thesis: one beam enters a prism, a spectrum comes out. One chart specification enters refract, a spectrum of output formats comes out.

A damped sine over a time axis, dark theme


What it is

One declarative chart specification, rendered through interchangeable backends. The core is pure Go with no dependencies at all — not "no cgo", literally nothing outside the standard library — and emits both vector formats, SVG and PDF. Add one module and the same specification renders to PNG and JPEG through gogpu/gg, still with CGO_ENABLED=0.

Dependencies Output
github.com/timzifer/refract stdlib only SVG, PDF, browser canvas
github.com/timzifer/refract/backend/gg GoGPU (gg), x/image — zero CGO PNG, JPEG
github.com/timzifer/refract/arrow apache/arrow-go — zero CGO — (a data source)

The browser is in the core too, because it needs nothing to be: a canvas 2D context is reached through syscall/js, which is the standard library (ADR 0017). Raster, GPU and native-window rendering all live behind the same ir.Backend interface. The rest are milestones, not architecture changes.

Install

go get github.com/timzifer/refract               # core: SVG and PDF, stdlib only
go get github.com/timzifer/refract/backend/gg    # raster: PNG and JPEG
go get github.com/timzifer/refract/arrow         # optional: plot Arrow data

Go 1.25 or newer (why).

Quick start

package main

import (
	"log"
	"math"

	"github.com/timzifer/refract"
	"github.com/timzifer/refract/geom"
	"github.com/timzifer/refract/palette"
	"github.com/timzifer/refract/scale"
	"github.com/timzifer/refract/theme"
)

func main() {
	xs := make([]float64, 200)
	ys := make([]float64, 200)
	for i := range xs {
		xs[i] = float64(i) / 20
		ys[i] = math.Sin(xs[i])
	}

	p := refract.New(
		refract.Theme(theme.Dark),
		refract.Size(800, 400),
		refract.Title("Signal"),
		refract.YTitle("amplitude"),
	)
	p.X(scale.Linear(scale.Nice()))
	p.Y(scale.Linear(scale.Nice()))
	p.Add(geom.Line(
		refract.Float64Columns(map[string][]float64{"x": xs, "y": ys}),
		geom.X("x"), geom.Y("y"),
		geom.Color(palette.Blue),
		geom.Tension(0.4),
	))

	if err := p.Render(refract.SVG("signal.svg")); err != nil {
		log.Fatal(err)
	}
}

For PDF or raster, swap the target — nothing else changes:

err := p.Render(refract.PDF("signal.pdf"))          // still stdlib only

import ggbackend "github.com/timzifer/refract/backend/gg"
err := p.Render(ggbackend.PNG("signal.png"))

A runnable version is in examples/signal.

Every figure below is rendered by backend/gg/cmd/gallery and re-checked in CI, so a picture here cannot drift away from the code that produced it.

Three series with a legend Two groups of scattered points
A response time histogram A damped sine on a time axis
An estimate with a shaded interval A step chart of replica counts
Bars by region, coloured by value with a colourbar Latency distributions as boxplots
Two growth curves on a log axis A series read against thresholds and a shaded window
Throughput faceted into one panel per region Four subplots on one dark canvas
A quarter of a million samples drawn as a clean line A million points drawn as a density raster

What it does

  • Scales — linear, time, log, symlog and ordinal/categorical. Linear tick placement uses extended Wilkinson (Talbot, Lin & Hanrahan 2010), so axis labels come out round rather than merely evenly spaced. Time ticks step in calendar units. Log and symlog subdivide each decade with unlabelled minor ticks; symlog is linear near zero, so signed data spanning orders of magnitude is plottable at all.
  • GeomsLine (optionally tension-smoothed), Scatter (six marker shapes), Bar, Area (to a baseline, or a band between two series), Step (pre/mid/post), Boxplot (Tukey whiskers, type-7 quartiles, outliers).
  • Colour — a qualitative palette per chart, plus continuous colour scales: geom.ColorBy maps a column through a sequential or diverging ramp. Ramps interpolate in linear light, so a gradient has no dark band through its middle.
  • Missing data — one explicit policy per layer (gap, interpolate, error), covering both NaN/Inf and values a scale has no position for, such as zero on a log axis.
  • AnnotationsHLine, VLine, HBand, VBand, Segment, Region and Note. They take values rather than a data source, because there is no column behind "the SLO is 200ms", and they extend the axis so the threshold is in view even when the data is nowhere near it.
  • Small multiples and subplotsfacet.Wrap and facet.Grid split one plot by a column; refract.NewGrid puts different plots on one canvas. Both go through one constraint solver, so panels are the same size and their axes line up (ADR 0010).
  • Chart furniture — axes, grid, tick labels with collision avoidance, chart and axis titles, and a guide column carrying a legend and colourbars.
  • Themes — light and dark, built from a dozen tokens rather than fifty fields, with a colourblind-safe (Okabe-Ito) default palette and perceptually uniform sequential ramps (Viridis, Cividis, Magma). Theme.With edits one; theme.Register and theme.ByName resolve one from a config file.
  • Big data — a layer with more rows than the plot has pixels reduces itself before it draws: stat.LTTB for a line, min/max per pixel column for a staircase or a band, density binning to an image for a point cloud. It happens when the chart is drawn, never when the scales are trained, so the axes still report the data rather than the subset that survived (ADR 0011).
  • Parallel panels — a facet or a grid builds its panels on separate goroutines and replays them in panel order, so the output is byte-identical to a serial render (ADR 0012).
  • Data — columnar and batch-oriented, carrying numeric, time and categorical columns. A []float64-backed source is borrowed, never copied, and so is a null-free float64 column read straight out of an Apache Arrow record through the optional refract/arrow module.
  • InteractionPlot.On registers handlers for hover, click, zoom and pan; Plot.Live draws into a surface that can be redrawn; Live.Bind wires a DOM element to it. Hit-testing runs over the marks a render emitted rather than over a second copy of every geom's projection, and Live.TrackRows makes a hit name the source row behind the mark (ADR 0015).
  • Live datadata.Stream is appended to from any goroutine and frozen between frames, and a redraw repaints only what changed (ADR 0016).
  • A chart as JSON — a *Plot marshals to a Vega-Lite-shaped document and reads back as the same chart (ADR 0014).
  • Backends — three built-in emitters — SVG, PDF and a browser canvas — and the gg raster adapter.

Deliberately not here yet: the GPU tier and a native interactive window. Both are v0.6 in CONCEPT.md §14.

Categories, distributions and orders of magnitude

src := refract.NewTable().
    String("region", []string{"north", "south", "east", "west"}).
    Float64("sales", []float64{18, 42, 31, 25})

p := refract.New(refract.Size(700, 400), refract.Title("Sales by region"))
p.X(scale.Ordinal())                        // equal slots, one per category
p.Y(scale.Linear(scale.Nice(), scale.Zero()))
p.Add(geom.Bar(src,
    geom.X("region"), geom.Y("sales"),
    geom.ColorBy("sales", scale.Sequential(palette.Viridis)),
))

An ordinal axis is a band scale: it tells the bar how wide to be, rather than the bar guessing from the spacing of the data. The same applies to geom.Boxplot. For data that spans decades, swap in scale.Log(scale.LogNice()) — or scale.SymLog() when it also crosses zero.

A runnable version, together with a boxplot over the same kind of data, is in examples/categories.

Small multiples

p := refract.New(refract.Size(900, 520), refract.Title("Throughput by region"))
p.Add(
    geom.Line(src, geom.X("hour"), geom.Y("rps"), geom.Label("throughput")),
    geom.HLine(60, geom.Label("target")),          // no data: drawn on every panel
)
p.Facet(facet.Wrap("region", facet.Columns(3)))

Panels share their scales by default, which is what makes small multiples comparable at a glance. facet.FreeX, facet.FreeY and facet.Free give each panel its own — a deliberate choice, because a reader who does not notice the axes changed will read the panels as comparable when they are not.

For unrelated charts on one canvas, build a grid of plots instead:

g := refract.NewGrid(2, refract.GridSize(900, 560), refract.GridTitle("Fleet"))
g.Add(latency, throughput, errors, saturation)
err := g.Render(refract.PDF("overview.pdf"))

A runnable version of both, with annotations and PDF output, is in examples/dashboard.

A million rows

p := refract.New(refract.Size(800, 500), refract.Title("A million samples"))
p.Add(geom.Line(src, geom.X("i"), geom.Y("v")))   // nothing else needed

That renders in about 60 ms into under 30 kB of SVG. Drawing every row takes six times as long and produces 15 MB — of a picture that is 800 pixels wide, so the extra 999,000 vertices land on top of each other.

The layer sees how many rows it has against how wide the plot is and reduces itself accordingly: LTTB for a line, min/max per pixel column for a step or a band, a density raster for a scatter dense enough that its markers would bury one another. Override it per layer when the default is not what you want:

geom.Line(src, geom.X("i"), geom.Y("v"), geom.Decimate(geom.MinMax))    // keep every spike
geom.Line(src, geom.X("i"), geom.Y("v"), geom.Decimate(geom.NoDecimation)) // every row
geom.Scatter(src, geom.X("x"), geom.Y("y"), geom.Budget(4000))          // at most 4000 marks

The reduction happens when the chart is drawn, not when its scales are trained, so the axes are the data's either way — a spike survives the reduction and the axis still reaches it.

The same milestone made a redrawn chart cheap: everything sized by the data comes from a pool, so a steady-state frame over a million rows costs the same handful of allocations as one over a thousand. There is a test that fails if that stops being true.

A runnable version — two million samples with a spike and a dropout in them, and a million-point cloud — is in examples/bigdata.

Interactive, in a browser

The same model that renders SVG on a server draws on a <canvas>, with the pointer reporting what it is over:

//go:build js && wasm

p.On(refract.Hover, func(ev refract.Event) {
	if ev.Found {
		readout.Set("textContent", fmt.Sprintf("%s: %.2f, %.2f", ev.Series(), ev.Hit.X, ev.Hit.Y))
	}
})

live, err := p.Live(canvas.Element(el))   // a surface, redrawn
defer live.Close()
live.Draw()
defer live.Bind(el)()                     // pointer, wheel zoom, drag pan, double-click reset

Hit-testing works over the marks the render actually emitted, so it is right for every geom — including a decimated one, where the rows you can point at are exactly the rows on screen (ADR 0015). Zoom and pan are arithmetic on the scales, so the value under the pointer stays under the pointer on a log or a time axis as much as on a linear one.

Turn on row identity when a hit has to name a row rather than describe a point — highlighting the matching row of a table beside the chart is the case:

live.TrackRows(true)

p.On(refract.Hover, func(ev refract.Event) {
	if ev.Found && ev.Hit.Row >= 0 {
		highlightTableRow(ev.Hit.Row)   // a row of the table you handed in
	}
})

It is off by default and costs a position and a row number per mark; it does not cost per-frame allocations, and CI pins that. Decimation is not in the way — LTTB and min/max keep real rows — and neither is faceting, whose per-panel cuts are resolved back to the table you passed. A mark that no single row is behind — a boxplot's box, a density raster, an interpolated point across a gap — reports -1 rather than a plausible neighbour.

A runnable version is in examples/web:

GOOS=js GOARCH=wasm go build -o examples/web/chart.wasm ./examples/web
cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" examples/web/

Live data

A data.Stream is appended to from one goroutine and frozen for the renderer on another. It is deliberately not a Source: a table being appended to between two column reads is a table that disagrees with itself.

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

go func() {
	for s := range samples {
		st.Append(scale.Nanos(s.At), s.Value)   // any goroutine, any time
	}
}()

for range ticker.C {
	st.Snapshot()   // freeze what has arrived
	live.Draw()     // draw the frozen view
}

Each Draw compares the frame with the last one and repaints only where they differ; a frame identical to the last is not painted at all. A backend says it can do that by implementing ir.Partial, and one that cannot gets the whole frame as before (ADR 0016). Appending a row and freezing a view both allocate nothing in the steady state, and the benchmark gate keeps it that way.

See examples/stream.

A chart as JSON

doc, err := p.MarshalJSON()      // indented; json.Marshal(p) compacts it
q, err := refract.ParseJSON(doc) // and reads back as the same chart

The document is Vega-Lite-shaped: data.values, mark.type, encoding.x.field, scale.type, facet and resolve mean what they mean in Vega-Lite, so anyone who knows that vocabulary can read one. It is not a Vega-Lite subset and does not claim to be — refract has marks and options Vega-Lite has no name for, and naming them plainly beats smuggling them through a borrowed name. What is guaranteed is the round trip through refract, and there is a test per mark and per scale that renders both and compares the primitives (ADR 0014).

{
  "$schema": "https://github.com/timzifer/refract/spec/v0.5",
  "width": 640,
  "height": 400,
  "title": "Throughput",
  "data": {
    "values": [{"x": 0, "y": 2}],
    "format": {"parse": {"x": "number", "y": "number"}}
  },
  "encoding": {
    "x": {"type": "quantitative", "scale": {"type": "linear", "nice": true}},
    "y": {"type": "quantitative", "scale": {"type": "linear", "nice": true}}
  },
  "layer": [
    {
      "mark": {"type": "line", "color": "#0072b2"},
      "encoding": {"x": {"field": "x"}, "y": {"field": "y"}}
    }
  ],
  "config": {"theme": "light"}
}

(shown with the objects folded up; the real output puts every field on its own line)

Plotting Arrow data

import "github.com/timzifer/refract/arrow"

src := arrow.Source(rec)      // rec is an arrow.Record
p.Add(geom.Line(src, geom.X("t"), geom.Y("p99")))

A float64 column with no nulls is Arrow's own buffer — no copy, no conversion. Everything else (integers, float32, timestamps, dictionary-encoded strings) converts once on first use and is cached, so a record with forty columns and a chart that plots two pays for two. An Arrow null becomes NaN, which means the missing-data policy you already set covers it (ADR 0013).

How it fits together

   Your spec  ──►  Model  ──►  IR  ──►  Backend  ──►  output
   ─────────      ─────      ────      ───────       ──────
   geoms          scales     ~8        backend/svg     SVG
   scales         layout     drawing   backend/pdf     PDF
   theme          ticks      ops       backend/canvas  browser canvas
   facets         panels               backend/gg      PNG / JPEG
                                       (future)        GPU, native window

The ir.Backend interface is the seam. Geoms never touch a renderer; a renderer never knows what a scale is. That is what lets refract stand on a young, fast-moving graphics stack without being welded to it — the whole gg adapter is about 300 lines (why that matters).

Two things ride on that seam without widening it. A render can be watched, so that a pointer can be told which layer drew what it is over (ADR 0015); and two frames can be compared, so that a surface repaints only what moved (ADR 0016). Neither put an identity channel or a damage channel into the drawing interface every backend implements.

Documentation

  • CONCEPT.md — the design document: motivation, positioning, architecture, roadmap.
  • docs/adr — why the open questions were answered the way they were.
  • CONTRIBUTING.md — building a three-module repository, and how to regenerate golden files and figures.

License

MIT. The core links nothing; backend/gg links only permissively licensed code (gg is MIT, x/image is BSD-3-Clause). That is a requirement rather than a preference — refract must be embeddable by downstream projects under any license.

Documentation

Overview

Package refract turns one declarative chart specification into any output you need — SVG, PDF and a browser canvas today, raster through one more module, GPU and a native window through later ones — from the same model, with the same geometry.

The core module is pure Go and depends on nothing but the standard library. Both vector emitters are built in and need no rendering engine and no font stack, so a server that wants a chart as SVG or a report generator that wants one as PDF links nothing native and nothing young. The browser backend is built in for the same reason: a canvas 2D context is reached through syscall/js. Raster output lives in a separate module, github.com/timzifer/refract/backend/gg, which is still CGO-free.

Shape of the API

Build a plot, give it scales, add layers, render it to a target:

src := refract.Float64Columns(map[string][]float64{"t": times, "y": values})

p := refract.New(
    refract.Theme(theme.Dark),
    refract.Size(800, 500),
    refract.Title("Signal"),
)
p.X(scale.Time())
p.Y(scale.Linear(scale.Nice()))
p.Add(geom.Line(src, geom.X("t"), geom.Y("y"), geom.Color(palette.Blue)))

err := p.Render(refract.SVG("signal.svg"))

Scales cover linear, time, log, symlog and ordinal/categorical axes; geoms cover lines, scatters, bars, areas, steps and boxplots. A mark's colour can come from the data through scale.Sequential or scale.Diverging and geom.ColorBy, which contributes a colourbar beside the plot.

Annotations

geom.HLine, geom.VLine, geom.HBand, geom.VBand, geom.Segment, geom.Region and geom.Note add the marks that are not data — a threshold, a shaded window, a label pointing at what happened. They take values rather than a data source.

Many panels

Plot.Facet splits one plot into small multiples, one panel per value of a column; NewGrid puts several different plots on one canvas. Both lay their panels out with the same solver, so the axes line up either way.

p.Facet(facet.Wrap("region", facet.Columns(3)))

Interaction

Plot.On registers a handler for hover, click, zoom or pan, and Plot.Live draws the chart into a surface that can be redrawn, pointed at, panned and zoomed. Each redraw repaints only what changed, and a frame identical to the last is not painted at all. In a browser, [Live.Bind] wires a DOM element to all of it; see package backend/canvas.

p.On(refract.Hover, func(ev refract.Event) {
    if ev.Found {
        tooltip(ev.Series(), ev.Hit.X, ev.Hit.Y)
    }
})

live, err := p.Live(canvas.Element(el))

Live data

data.Stream is a table a producer appends to from one goroutine while the renderer draws a frozen snapshot on another.

A chart as JSON

A Plot marshals to a Vega-Lite-shaped document and reads back as the same chart — see Plot.Spec, ParseJSON and package spec.

Status

Pre-alpha. Every release below v1.0.0 may contain breaking changes without a deprecation cycle. See CONCEPT.md for the design and the roadmap.

Index

Constants

View Source
const (
	Hover = interact.Hover
	Leave = interact.Leave
	Click = interact.Click
	Zoom  = interact.Zoom
	Pan   = interact.Pan
)

The event kinds. See interact.EventKind.

Variables

View Source
var ErrEmptyGrid = errors.New("refract: grid has no plots")

ErrEmptyGrid reports a render of a grid with no plots in it.

View Source
var ErrNoLayers = errors.New("refract: plot has no layers and no scales")

ErrNoLayers reports a render of a plot with nothing in it. Rendering empty axes is a legitimate thing to want, so this is only returned when there is also no scale configured — that combination is always a mistake.

Functions

func NewTable

func NewTable() *data.Table

NewTable returns an empty table that can mix numeric and time columns. See data.NewTable.

Types

type Backend

type Backend = ir.Backend

Backend is a renderer. See package ir.

type Event added in v0.5.0

type Event = interact.Event

Event is one thing that happened to a chart. See interact.Event.

type EventKind added in v0.5.0

type EventKind = interact.EventKind

EventKind is what happened. See interact.EventKind.

type Grid added in v0.3.0

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

Grid renders several plots together in one image, with their axes aligned.

It is the other half of the multi-panel story: Plot.Facet splits one plot by a column, and a Grid puts different plots side by side. Both go through the same solver, so the panels line up either way.

g := refract.NewGrid(2, refract.GridSize(900, 600), refract.GridTitle("Fleet"))
g.Add(latency, throughput, errors, saturation)
err := g.Render(refract.SVG("fleet.svg"))

A member plot contributes its layers, its scales and its title, which becomes the label above its panel. The canvas is the grid's: its size, theme, chart title and axis titles are the ones used, and a member plot's own size, theme and axis titles are not. That is the price of one image — two panels cannot disagree about the colour of the paper they are printed on.

func NewGrid added in v0.3.0

func NewGrid(cols int, opts ...GridOption) *Grid

NewGrid creates a grid that flows plots into rows of cols panels.

func (*Grid) Add added in v0.3.0

func (g *Grid) Add(ps ...*Plot) *Grid

Add appends plots, filling the grid left to right and wrapping.

func (*Grid) At added in v0.3.0

func (g *Grid) At(row, col int, p *Plot) *Grid

At places a plot in a specific cell, replacing whatever was there. Cells left empty stay empty, which is how a grid is given a deliberate hole.

func (*Grid) Render added in v0.3.0

func (g *Grid) Render(t Target) (err error)

Render draws the grid into t.

type GridOption added in v0.3.0

type GridOption func(*Grid)

GridOption configures a Grid at construction.

func GridAxisTitles added in v0.3.0

func GridAxisTitles(x, y string) GridOption

GridAxisTitles labels the shared axes, once for the grid. Panels keep their own scales; these name what those scales measure.

func GridDPR added in v0.3.0

func GridDPR(r float64) GridOption

GridDPR sets the device pixel ratio. See DPR.

func GridLegend added in v0.3.0

func GridLegend(show bool) GridOption

GridLegend forces the legend on or off. By default it appears when any panel would have shown one.

func GridParallel added in v0.4.0

func GridParallel(on bool) GridOption

GridParallel controls whether the panels are built concurrently. See Parallel; a grid is the shape that benefits most, because its panels are different charts over different data.

func GridSize added in v0.3.0

func GridSize(w, h int) GridOption

GridSize sets the output size in device-independent pixels. The default is 900x600, which is a grid's worth rather than a single chart's.

func GridTheme added in v0.3.0

func GridTheme(t themepkg.Theme) GridOption

GridTheme sets the visual tokens for the whole grid.

func GridTitle added in v0.3.0

func GridTitle(s string) GridOption

GridTitle sets the title above the grid.

type Hit added in v0.5.0

type Hit = interact.Hit

Hit is the mark under a pointer. See interact.Hit.

type Live added in v0.5.0

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

Live is a chart drawn into a surface that can be redrawn, pointed at, panned and zoomed.

It is the interactive half of a plot: Plot.Render draws once into a file, Live draws over and over into something that stays open — a browser canvas, a window, a test.

live, err := p.Live(canvas.Element(el))
defer live.Close()
live.Draw()

// from the surface's event loop
live.Move(x, y)
live.Wheel(x, y, dy)

What a redraw costs

Each Draw records the frame, compares it with the last one and repaints only where the two differ — see ir.Damage. A backend that cannot repaint part of a frame gets the whole one, and a frame that is identical to the last is not painted at all.

The chart is built once

Live resolves the plot into panels when it is created, so that a zoom lands on scales that are still there next frame. Adding a layer, changing the facet or replacing a scale afterwards needs Live.Rebuild, which starts again from the plot as it now stands — and, like any fresh start, forgets where the view was zoomed to.

A Live is not safe for concurrent use.

func (*Live) Autoscale added in v0.5.0

func (l *Live) Autoscale() error

Autoscale releases every zoom and pan, so the axes come from the data again, and redraws. It is the "reset view" every interactive chart needs.

func (*Live) Click added in v0.5.0

func (l *Live) Click(x, y float64) Event

Click reports a click at a device position and fires Click.

func (*Live) Close added in v0.5.0

func (l *Live) Close() error

Close finalises the target. The last frame drawn is what it holds.

func (*Live) Draw added in v0.5.0

func (l *Live) Draw() error

Draw renders the current state of the plot.

It returns nil having painted nothing when the frame is identical to the last one, which is the common case for a pointer moving over a chart that is not being zoomed.

func (*Live) Index added in v0.5.0

func (l *Live) Index() *interact.Index

Index returns the hit index of the last frame, for a caller drawing its own tooltip or crosshair.

func (*Live) Leave added in v0.5.0

func (l *Live) Leave() Event

Leave reports the pointer leaving the surface and fires Leave.

func (*Live) Move added in v0.5.0

func (l *Live) Move(x, y float64) Event

Move reports the pointer at a device position and fires Hover.

The event carries the mark under the pointer, if there is one within interact.DefaultTolerance, and the panel the pointer is in, or -1 for a point in the margins.

The one move that fires something else is the move that leaves the last panel: that fires Leave instead, once, so that a tooltip opened on a hover has a matching event to close on. Moving around in the margins after that goes on firing Hover with no hit.

func (*Live) PanBy added in v0.5.0

func (l *Live) PanBy(dx, dy float64) error

PanBy moves the view by a device-space delta and redraws. It is what a drag does: the data follows the pointer, so dragging right shows earlier data.

func (*Live) Rebuild added in v0.5.0

func (l *Live) Rebuild() error

Rebuild resolves the plot again, picking up layers, scales or a facet added since the Live was created. It forgets any zoom on a facet's free axes, which belong to panels that no longer exist.

func (*Live) TrackRows added in v0.5.0

func (l *Live) TrackRows(on bool) *Live

TrackRows turns row identity on or off and returns l, so the call can be chained onto Plot.Live.

live, err := p.Live(canvas.Element(el))
live.TrackRows(true)
// ...
p.On(refract.Hover, func(ev refract.Event) {
    if ev.Found && ev.Hit.Row >= 0 {
        highlightTableRow(ev.Hit.Row)
    }
})

It is off by default because it is not free. With it on, every layer that can report its rows records where each one landed, and the hit index keeps a position and a row number per mark on top of the marks it already keeps — memory proportional to the marks on screen, which after decimation is thousands rather than millions, but not nothing. Without it, [Hit.Row] is -1 and a hit still reports the data values under the pointer.

It takes effect on the next Live.Draw.

Not every mark has a row to report. A boxplot's box aggregates many rows, a density raster is not a mark at all, an interpolated point across a gap was never measured, and a third-party geom that does not report its rows has none to report; all of those leave [Hit.Row] at -1 rather than guessing a nearby one.

func (*Live) Wheel added in v0.5.0

func (l *Live) Wheel(x, y, factor float64) error

Wheel zooms about a device position by factor, and redraws.

factor below 1 zooms in and above 1 zooms out: it multiplies the width of the view, so 0.8 shows four fifths of what was there. A wheel notch is usually turned into 0.9 or 1.1 by the surface.

Both axes zoom, about the pointer, so that the value under the cursor stays under the cursor. A scale that cannot be zoomed — an ordinal axis, where half a category is not a view of anything — is left alone, and a wheel over a chart with two such axes does nothing at all.

func (*Live) ZoomTo added in v0.5.0

func (l *Live) ZoomTo(r ir.Rect) error

ZoomTo zooms into a device-space rectangle — a rubber-band selection — and redraws.

type Option

type Option func(*Plot)

Option configures a Plot at construction.

func DPR

func DPR(r float64) Option

DPR sets the device pixel ratio. Backends that rasterize multiply the pixel buffer by it; coordinates stay in device-independent units either way. The default is 1.

func Legend

func Legend(show bool) Option

Legend forces the legend on or off. By default a legend appears once a plot has more than one layer: one series does not need to be told apart from anything.

func Parallel added in v0.4.0

func Parallel(on bool) Option

Parallel controls whether a multi-panel chart builds its panels concurrently. It is on by default and produces identical output either way: each panel is recorded on its own goroutine and the recordings are replayed in panel order.

Turn it off to keep a render on one goroutine — inside a benchmark that is measuring something else, or in a process that has already committed its cores elsewhere. It has no effect on a chart with a single panel, which has nothing to overlap.

func Size

func Size(w, h int) Option

Size sets the output size in device-independent pixels. The default is 800x500.

func Theme

func Theme(t themepkg.Theme) Option

Theme sets the visual tokens. The default is [theme.Light].

func Title

func Title(s string) Option

Title sets the chart title.

func XTitle

func XTitle(s string) Option

XTitle sets the horizontal axis title.

func YTitle

func YTitle(s string) Option

YTitle sets the vertical axis title.

type Plot

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

Plot is a chart specification: size, theme, scales and layers.

A Plot is not safe for concurrent modification. Rendering the same Plot twice is supported and produces the same result, provided the underlying data has not changed.

func FromSpec added in v0.5.0

func FromSpec(s spec.Spec) (*Plot, error)

FromSpec builds a plot from a document.

func New

func New(opts ...Option) *Plot

New creates a Plot.

func ParseJSON added in v0.5.0

func ParseJSON(b []byte) (*Plot, error)

ParseJSON builds a plot from a spec document.

p, err := refract.ParseJSON(b)
if err == nil {
    err = p.Render(refract.SVG("chart.svg"))
}

It is the whole web workflow in two calls: a browser or a config file hands over a chart, and the same model that a Go program builds by hand draws it.

func (*Plot) Add

func (p *Plot) Add(gs ...geom.Geom) *Plot

Add appends layers, drawn in the order given.

func (*Plot) Facet added in v0.3.0

func (p *Plot) Facet(s *facet.Spec) *Plot

Facet splits the plot into small multiples, one panel per value of a column. See facet.Wrap and facet.Grid.

p.Facet(facet.Wrap("region", facet.Columns(3)))

Passing nil turns faceting back off.

func (*Plot) Live added in v0.5.0

func (p *Plot) Live(t Target) (*Live, error)

Live opens t and returns a chart drawn into it.

The target stays open until Live.Close, which is what makes this different from Plot.Render: a Live draws frame after frame into one surface.

It wants a surface rather than a document. The SVG and PDF emitters build a document and write it whole, so drawing many frames into one collects every frame in the same file rather than replacing what was there — use Plot.Render for those, and a Live for a canvas, a window, or anything else that is repainted. A single Draw into a document target is exactly a Render, and is a reasonable way to export what an interactive chart currently shows.

func (*Plot) MarshalJSON added in v0.5.0

func (p *Plot) MarshalJSON() ([]byte, error)

MarshalJSON writes the plot as a spec document, so that a Plot can be handed straight to encoding/json.

Called directly it returns the indented form spec.Spec.Marshal produces: a chart is a thing people read and edit, and the compact form of one over a hundred rows is a single very long line. Called through json.Marshal it comes back compact, because that is what json.Marshal does to anything a Marshaler returns.

func (*Plot) On added in v0.5.0

func (p *Plot) On(kind EventKind, h func(Event)) *Plot

On registers a handler for an event kind.

p.On(refract.Hover, func(ev refract.Event) {
    if ev.Found {
        tooltip(ev.Series(), ev.Hit.X, ev.Hit.Y)
    }
})
p.On(refract.Zoom, func(ev refract.Event) { log.Println(ev.Rect) })

Handlers fire in registration order, from Live's input methods, on the goroutine that called one. Registering a handler does not by itself make a chart interactive — Plot.Live is what draws one into a surface that can report input.

func (*Plot) Render

func (p *Plot) Render(t Target) (err error)

Render draws the plot into t.

It opens the target, lowers the chart into the backend it returns, flushes, and closes the target — so a file target has a complete file on disk when Render returns nil.

func (*Plot) Spec added in v0.5.0

func (p *Plot) Spec() (spec.Spec, error)

Spec writes the plot down as a document that can be marshalled to JSON and read back with FromSpec.

It fails on a layer or a scale that cannot describe itself rather than writing a document that draws a different chart — see github.com/timzifer/refract/spec for what survives the trip and what cannot.

func (*Plot) UnmarshalJSON added in v0.5.0

func (p *Plot) UnmarshalJSON(b []byte) error

UnmarshalJSON replaces the plot's contents with the document in b.

func (*Plot) X

func (p *Plot) X(s scale.Scale) *Plot

X sets the horizontal scale. The default is scale.Linear with nicing.

func (*Plot) Y

func (p *Plot) Y(s scale.Scale) *Plot

Y sets the vertical scale. The default is scale.Linear with nicing.

type Source

type Source = data.Source

Source is a columnar data source. See package data.

func Float64Columns

func Float64Columns(cols map[string][]float64) Source

Float64Columns builds a Source over numeric columns, borrowing the slices. See data.Float64Columns.

type Target

type Target = ir.Target

Target is a render destination. See package ir.

func PDF added in v0.3.0

func PDF(path string, opts ...pdf.Option) Target

PDF returns a target writing a PDF document to the named file.

Like SVG, this is a zero-dependency path: the emitter is in backend/pdf and uses nothing but the standard library. The page is one PDF point per device-independent pixel, so a chart sized 800x500 is an 800x500pt page.

func PDFWriter added in v0.3.0

func PDFWriter(w io.Writer, opts ...pdf.Option) Target

PDFWriter returns a target writing a PDF document to w.

func SVG

func SVG(path string, opts ...svg.Option) Target

SVG returns a target writing an SVG document to the named file.

This is the zero-dependency path: it uses the built-in emitter in backend/svg and links no rendering engine.

func SVGWriter

func SVGWriter(w io.Writer, opts ...svg.Option) Target

SVGWriter returns a target writing an SVG document to w.

Directories

Path Synopsis
arrow module
backend
canvas
Package canvas draws a chart into a browser canvas.
Package canvas draws a chart into a browser canvas.
pdf
Package pdf renders a chart to PDF using nothing but the standard library.
Package pdf renders a chart to PDF using nothing but the standard library.
svg
Package svg is refract's built-in, zero-dependency SVG backend.
Package svg is refract's built-in, zero-dependency SVG backend.
gg module
gg/gpu module
window module
Package data is refract's data layer: columnar, batch-oriented access to a table of values.
Package data is refract's data layer: columnar, batch-oriented access to a table of values.
examples
bigdata command
Command bigdata renders two charts that could not be drawn mark for mark.
Command bigdata renders two charts that could not be drawn mark for mark.
categories command
Command categories renders the categorical chart from the README.
Command categories renders the categorical chart from the README.
dashboard command
Command dashboard renders the v0.3 additions: small multiples, annotations, a colourbar, a grid of subplots, and PDF output.
Command dashboard renders the v0.3 additions: small multiples, annotations, a colourbar, a grid of subplots, and PDF output.
signal command
Command signal renders the chart from CONCEPT.md §13.
Command signal renders the chart from CONCEPT.md §13.
stream command
Command stream draws a live chart over a growing series.
Command stream draws a live chart over a growing series.
web command
Command web draws an interactive chart in a browser.
Command web draws an interactive chart in a browser.
Package facet splits one chart into small multiples.
Package facet splits one chart into small multiples.
Package geom holds the visual marks a chart is made of.
Package geom holds the visual marks a chart is made of.
Package interact turns a rendered chart into something a pointer can ask questions of.
Package interact turns a rendered chart into something a pointer can ask questions of.
internal
fontmetrics
Package fontmetrics answers "how wide is this string" using nothing but the standard library.
Package fontmetrics answers "how wide is this string" using nothing but the standard library.
irtest
Package irtest provides a recording ir.Backend for tests.
Package irtest provides a recording ir.Backend for tests.
markers
Package markers builds the outline of a scatter marker.
Package markers builds the outline of a scatter marker.
svgdiff
Package svgdiff compares two SVG documents as drawings rather than as bytes.
Package svgdiff compares two SVG documents as drawings rather than as bytes.
Package ir defines refract's intermediate representation: a small, backend-agnostic scene description, plus the Backend interface every renderer implements.
Package ir defines refract's intermediate representation: a small, backend-agnostic scene description, plus the Backend interface every renderer implements.
Package layout decides where the plot area, titles and guides go.
Package layout decides where the plot area, titles and guides go.
Package palette provides colours and colour sequences for charts.
Package palette provides colours and colour sequences for charts.
Package render lowers a resolved chart into IR.
Package render lowers a resolved chart into IR.
Package scale maps data values onto visual positions and generates the ticks that label them.
Package scale maps data values onto visual positions and generates the ticks that label them.
Package spec writes a chart down as JSON and reads it back.
Package spec writes a chart down as JSON and reads it back.
Package stat aggregates data before it is drawn.
Package stat aggregates data before it is drawn.
Package theme holds the visual tokens a chart is drawn with: colours, fonts, sizes and spacings.
Package theme holds the visual tokens a chart is drawn with: colours, fonts, sizes and spacings.

Jump to

Keyboard shortcuts

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