data

package
v0.9.0 Latest Latest
Warning

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

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

Documentation

Overview

Package data is figure's data layer: columnar, batch-oriented access to a table of values.

The interface returns whole typed columns, never one value at a time. Scalar access is the single easiest way to make a plotting library slow, and a columnar shape is also what lets a []float64-backed source be borrowed instead of copied.

Index

Constants

This section is empty.

Variables

View Source
var ErrColumnCount = fmt.Errorf("figure/data: wrong number of values")

ErrColumnCount reports an Append whose value count does not match the stream's columns.

View Source
var ErrNoKeyColumn = errors.New("figure/data: no such key column")

ErrNoKeyColumn reports a key column neither table has, or one of a type that cannot name a row.

Functions

func AnyNull

func AnyNull(mask []bool) bool

AnyNull reports whether mask marks any row at all.

It is what lets a reader decide between the borrowed column and a copy: a mask that marks nothing changes no value, so there is nothing to write and the caller's slice is handed on untouched.

func Float64Column

func Float64Column(src Source, name string) ([]float64, bool)

Float64Column returns a column as numbers: Column.Numbers of the named column.

func FormatNumber

func FormatNumber(v float64) string

FormatNumber is how a numeric value is spelled when it is used as a category name. Both faceting and a categorical axis go through it, so a panel key and an axis tick for the same number are the same string.

func GroupBy

func GroupBy(src Source, col string) (keys []string, rows [][]int, ok bool)

GroupBy splits src into groups by the values of a column, returning the distinct values in first-appearance order and the row numbers of each.

The column may be textual, numeric or temporal; whichever it is, the group key is its formatted label, so a facet over a numeric column gets one panel per distinct number rather than a continuous axis. ok is false if src has no such column.

First-appearance order rather than sorted order is deliberate: it is the one ordering that is stable under every column type and lets a caller control panel order by ordering its rows.

func Int64Column

func Int64Column(src Source, name string) ([]int64, bool)

Int64Column returns an exact integer column, or ok == false for a column of any other kind. A caller wanting a number rather than an identifier asks Float64Column, which converts.

func IsNull

func IsNull(mask []bool, i int) bool

IsNull reports whether row i of mask is absent, tolerating a nil or short mask.

A mask is as long as the column it describes, so the bounds test is not defensive padding: Rows gathers a mask along with the column it belongs to, and a caller composing sources by hand may hand over one that stops early. A row past the end has a value, because that is what the column says. A negative row is not a row at all — an Alignment spells "this end has no such row" that way — and is not null either; the caller has already been told there is nothing there.

func Label

func Label(src Source, col string, row int) (string, bool)

Label reads one cell of a column as the text that row says there.

It is Labels for a single row, and it does not build a column to answer. That is the whole reason it exists: Labels spells a numeric or temporal column by allocating a []string as long as the table, which is the right shape for faceting — done once, for every row — and the wrong shape for a pointer, which asks about one row on every move. A hover over a chart of a million rows should not allocate a million strings to name one of them.

The spellings are Labels', so a key, a facet panel key and a categorical tick for the same value are the same string.

ok is false when the source is nil, the column is not there, the column is of a kind this build does not know, or row is outside the table.

func Labels

func Labels(src Source, col string) ([]string, bool)

Labels reads a column as one text label per row, whatever its type.

It is the shared spelling of "what does this row say in that column" — faceting groups by it, and a categorical axis encodes by it, so the two agree about what counts as one category.

func NullMask

func NullMask(src Source, name string) (null []bool, ok bool)

NullMask reports which of a column's rows are absent, or ok == false when the column is not there or has nothing to say.

It is the shared spelling of the question, the way Labels is for "what does this row say in that column". ok is false for a column whose mask marks nothing, which is what keeps the zero-copy path in Float64Columns intact: a reader asks first and copies only when the answer is yes, so a table without nulls costs exactly what it did.

func Origins

func Origins(src Source) []int

Origins returns the mapping from src's rows to the rows of the table it was cut from, or nil if src is not a cut of anything.

One level, not the whole chain: faceting cuts once, and a caller that has composed cuts of cuts knows it has and can compose the mappings the same way.

func StringColumn

func StringColumn(src Source, name string) ([]string, bool)

StringColumn returns a category column, or ok == false for a column of any other kind. A caller that wants any column *as* text asks Labels, which spells every kind.

func TimeColumn

func TimeColumn(src Source, name string) ([]time.Time, bool)

TimeColumn returns a temporal column, or ok == false for a column of any other kind.

Types

type Alignment

type Alignment struct {
	// Keys are the distinct keys of both tables, in the order above.
	Keys []string
	// A and B are, for each key, the row carrying it in each table, or -1 for
	// a key that table does not have.
	A, B []int
}

Alignment is two tables lined up by a key column: one entry per distinct key, saying which row of each table carries it.

It is the join a transition is built on, and it is separate from Tween so that a caller can ask what changed without interpolating anything.

Enter, update and exit

This is D3's data join, and deliberately the same three words, because the vocabulary is the useful part and there is nothing to be gained by inventing a fourth name for it. A key in both tables is an **update** — the same thing, somewhere else. A key only the end state has is an **enter**. A key only the start state has is an **exit**. Alignment.Updated, Alignment.Entered and Alignment.Exited are those three lists.

Where figure differs from D3 is what happens next, and it is worth being clear about because the vocabulary invites the assumption. In D3 the three selections are things you *attach behaviour to*: enter gets its own append and its own transition, exit gets a transition that ends in remove. Here they are three readings of one table. Every key is a row of the blend for the whole transition — see the section below — so an entering row is not something that arrives partway through, it is a row that spends the transition travelling from wherever EnterFrom put it. There is no enter selection to hang a different animation on, and no remove: an exiting row is still drawn at f == 1, sitting at its ExitTo.

That is a smaller vocabulary than D3's on purpose. What it buys is that the frame's structure never changes, which is what keeps an animation off the full-repaint path.

Key order

The key order is first appearance in a, then the keys only b has, in first appearance in b. It is never map iteration order: a chart whose panels are built on separate goroutines has to draw what a serial one drew, so nothing here may depend on how a map felt like enumerating itself. See docs/adr/0012-parallel-panels.md.

func Align

func Align(a, b Source, col string) (Alignment, error)

Align lines up two tables by a key column.

A key that appears more than once in a table takes its first row and the rest are ignored. That is a choice rather than an error: a duplicate key is a caller saying two rows are the same thing, and refusing the whole transition over it would be refusing to draw a chart that draws perfectly well. What it costs is that the second row does not move, which is visible and diagnosable, where a refusal at the top of an animation is neither.

func (Alignment) Entered

func (al Alignment) Entered() []string

Entered lists the keys only b has: the rows a transition brings in.

func (Alignment) Exited

func (al Alignment) Exited() []string

Exited lists the keys only a has: the rows a transition takes away.

func (Alignment) Len

func (al Alignment) Len() int

Len reports how many keys the alignment holds, which is the row count of the table a Tween over it produces.

func (Alignment) Updated

func (al Alignment) Updated() []string

Updated lists the keys both tables have: the rows that are the same thing in a different place, and the only ones anything is interpolated for.

type Column

type Column struct {
	// Kind says which of the value slices below is the column's own. The
	// others are nil.
	Kind Kind

	// Floats holds the values of a [KindFloat64] column.
	Floats []float64
	// Ints holds the values of a [KindInt64] column.
	Ints []int64
	// Strings holds the values of a [KindString] column.
	Strings []string
	// Times holds the values of a [KindTime] column.
	Times []time.Time

	// Nulls is one flag per row, true where the column has no value, or nil
	// for a column with nothing absent.
	//
	// It is a field rather than the optional interface it used to be because
	// absence belongs to the column: a Source that wraps another — a facet's
	// cut, a stream's snapshot, a transition's blend — carries it along with
	// the values instead of having to remember a second method. Forgetting
	// that method silently un-marked the missing rows, which is a chart
	// drawing something nobody measured.
	//
	// A numeric column may use it or use NaN, and the two spellings agree: a
	// NaN and a marked row are both missing and neither outranks the other. A
	// text or temporal column has no NaN to be missing with — "" is a string
	// somebody may have measured and the zero time is the year 1 — so for
	// those it is the only way to say it. See
	// [ADR 0034](../docs/adr/0034-null-values.md).
	Nulls []bool

	// Text spells one row, and is what makes a column readable without
	// knowing its kind: a label, a facet's strip, a legend entry, the key
	// [github.com/timzifer/figure/geom.KeyBy] identifies a row by.
	//
	// It is optional. A nil Text means [Column.Spell] derives the spelling
	// from the values, which is what every column in this package does. A
	// Source whose values are richer than the slice it can offer fills it in:
	// a column of physical quantities plots as float64 positions and spells
	// as "2.5 bar", and figure needs to know nothing about units to draw and
	// label it.
	//
	// It is a function rather than a slice of strings because most columns
	// are never spelled. Materialising a million labels for a scatter nobody
	// hovers over is a cost with no reader.
	//
	// It does not survive the JSON spec: a document records the values, and a
	// Source rebuilt from one spells them the default way.
	Text func(i int) string
}

Column is one column of a table: the values, which rows are absent, and how a row reads as text.

Why one type rather than one method per kind

Source used to answer three questions — is this column numeric, temporal, categorical — and a fourth kind meant a fourth optional interface, a fourth branch at every reader, and a fourth thing every wrapping Source had to remember to forward. Two in this package had already forgotten to forward the third. One accessor returning a growable struct makes a new kind a field and a case in this package's own helpers, and leaves every Source that does not produce it untouched. ADR 0061 is the record.

What is borrowed

The slices are lent, not given: a reader must not write to one, and a Source hands out its own memory rather than copying. That is what keeps Float64Columns zero-copy.

func ColumnOf

func ColumnOf(src Source, name string) (Column, bool)

ColumnOf returns a column of src, or the zero Column when src is nil, the name is empty or the column is not there.

It is the nil-tolerant spelling every reader in figure uses, so that "no source" and "no such column" are one answer rather than two guards at each call.

func (Column) Len

func (c Column) Len() int

Len reports the number of rows in the column.

func (Column) Numbers

func (c Column) Numbers() ([]float64, bool)

Numbers reports the column as float64, which is what a position on an axis is made of.

A KindFloat64 column is handed back borrowed. A KindInt64 column is converted, which allocates and which loses exactness above 2^53 — that is the honest trade and it is the right way round: a pixel cannot show the difference, and the exactness that matters is in Column.Spell, where an identifier is compared. Any other kind reports ok == false; a category and an instant reach an axis through github.com/timzifer/figure/scale, not through here.

func (Column) Spell

func (c Column) Spell(i int) string

Spell reports what row i reads as.

It is Column.Text where the Source supplied one, and otherwise the default spelling of the kind: an exact integer as digits, a number as Go's shortest round-tripping form, an instant as RFC 3339, a category as itself. A row outside the column, and a kind this build does not know, spell as "".

Two rows with the same value spell the same, which is what lets a spelling be an identity: Labels groups by it and a categorical axis encodes by it, so the two agree about what counts as one category.

type Kind

type Kind uint8

Kind says what a Column holds.

The set grows at the end. A reader that switches on it needs a default, and the default that is always right is Column.Spell: every column of every kind can say what a row reads as, so a kind added later is labelled rather than lost. See ADR 0061.

const (
	// KindFloat64 is a measured number, in [Column.Floats]. A missing value
	// is NaN or a marked row; see [Column.Nulls].
	KindFloat64 Kind = iota
	// KindInt64 is an exact integer, in [Column.Ints]. It exists because a
	// float64 stops counting exactly above 2^53, and an identifier that
	// collides there is a chart that draws two rows as one.
	KindInt64
	// KindString is a category name, in [Column.Strings].
	KindString
	// KindTime is an instant, in [Column.Times].
	KindTime
)

The column kinds.

type Source

type Source interface {
	// Len reports the number of rows. Every column has this length.
	Len() int

	// Columns lists the available column names. The order is stable across
	// calls on the same Source.
	Columns() []string

	// Column returns a column by name. ok is false if the column does not
	// exist.
	Column(name string) (Column, bool)
}

Source exposes columnar, batch access to a table.

Implementations return read-only views: the caller must not mutate a returned slice, and figure never does. An implementation that already holds its data as a Go slice should return that slice directly rather than copying.

Stability

Source is implemented outside this module, so it never gains a method, and its one column accessor takes and returns growable values rather than a list of typed alternatives. A kind of column this package does not yet know is a Kind and a field on Column, not another method here and not an optional interface beside it. See ADR 0060 and ADR 0061.

func Float64Columns

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

Float64Columns builds a Source over the given numeric columns.

The slices are borrowed, not copied: the returned Source aliases the caller's memory, and mutating it afterwards mutates what figure will plot. All columns must have the same length; Float64Columns panics otherwise, because a ragged table is a programming error rather than a runtime condition.

func Rows

func Rows(src Source, idx []int) Source

Rows returns a Source over the rows of src named by idx, in the order given.

It is how faceting cuts one table into panels: a facet reads the column it splits on, groups the row numbers, and hands each group to Rows. Out-of-range indices are dropped rather than panicking, because they come from a grouping pass rather than from the caller.

The result materialises the rows it is asked for. That is a copy — the zero-copy promise in Float64Columns is about the whole-column path, and a gathered subset has no contiguous slice to borrow. Columns are gathered lazily, so a table with forty columns and a chart that reads three copies three.

type Stream

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

Stream is a table a producer appends to while a renderer draws.

It is deliberately not a Source. A Source is read column by column, over several calls, and a table being appended to between two of them is a table that disagrees with itself — so the only way to draw a Stream is to freeze it:

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

go func() {
    for sample := range samples {
        st.Append(scale.Nanos(sample.At), sample.Value)
    }
}()

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

Stream.Source hands back a Source that reads the most recent snapshot, so a layer is built once rather than per frame. Stream.Snapshot is what moves it forward.

Two buffers, one copy

Snapshot copies the live rows into a buffer the renderer is not reading and swaps the two. That is one copy per frame — not per row, and not per column read — and the buffers are reused, so a steady stream costs no allocations after the first frame at each size.

The one rule

Append may be called from any goroutine, at any time. Snapshot may not be called while a render is in flight: it is the swap, and swapping the table out from under a half-drawn chart is the race this type exists to remove. Producer appends, renderer snapshots and draws, in that order.

Columns

A Stream carries numeric columns only. A timestamp is its Unix nanoseconds — github.com/timzifer/figure/scale.Nanos converts one, and github.com/timzifer/figure/scale.Time maps that domain — so a time series needs no second column type and no per-row allocation to carry one.

func NewStream

func NewStream(cols ...string) *Stream

NewStream returns an empty stream over the named numeric columns.

The order of the names is the order Stream.Append takes values in. It panics on no columns or a duplicate name, which are programming errors rather than runtime conditions.

func (*Stream) Append

func (s *Stream) Append(vals ...float64) error

Append adds one row. The values are positional, in the order NewStream was given.

It is safe to call from any goroutine, and it allocates nothing in the steady state: the row is copied into buffers that are already the right size.

func (*Stream) AppendTime

func (s *Stream) AppendTime(t time.Time, vals ...float64) error

AppendTime is Stream.Append with the first column given as a timestamp, which is what the first column of a live chart almost always is. The timestamp becomes its Unix nanoseconds, the domain a github.com/timzifer/figure/scale.Time axis maps.

func (*Stream) Columns

func (s *Stream) Columns() []string

Columns lists the stream's columns, in append order.

func (*Stream) Len

func (s *Stream) Len() int

Len reports how many rows are live. It is the producer's view, which may be ahead of what the last snapshot froze.

func (*Stream) Reset

func (s *Stream) Reset()

Reset empties the stream, keeping its buffers. The snapshot a renderer is holding is untouched until the next Stream.Snapshot.

func (*Stream) Snapshot

func (s *Stream) Snapshot() Source

Snapshot freezes the live rows and returns a Source over them.

The Source Stream.Source returned reads the same frozen rows, so a layer built once draws whatever the last Snapshot froze.

The returned Source is valid until the next call to Snapshot, which reuses its memory. Call it between frames, never during one.

func (*Stream) Source

func (s *Stream) Source() Source

Source returns a Source over the stream's most recent snapshot.

It is stable: the same value reads every frame, and what it reads changes only when Stream.Snapshot is called. That is what lets a layer be built once, before the first row has even arrived.

func (*Stream) Window

func (s *Stream) Window(n int) *Stream

Window caps the stream at the last n rows, dropping the oldest as new ones arrive. It returns s so the call can be chained onto NewStream.

A window is what makes a stream a *stream* rather than a log: a live chart shows the last few thousand samples, and keeping every sample since the process started is a memory leak with a plot attached. Zero means unbounded, which is the default.

Setting a window smaller than the rows already held drops the oldest of them, because that is what the window means.

type Subset

type Subset interface {
	// SourceRows returns, for each of this source's rows, the row it came from
	// in the source it selects from. The result is read-only.
	SourceRows() []int
}

Subset is implemented by a Source that is a selection of another source's rows, so that a row number can be traced back to the table it came from.

It exists because faceting makes such a source without the caller ever seeing it: github.com/timzifer/figure.Plot.Facet cuts each layer down to its panel's rows with Rows, and a row number relative to that cut is a row number in a table nobody holds. A geom reporting where its rows landed resolves them through this first, so what comes out is a row of the table that was handed in.

It is an optional interface. A Source that is not a selection of another does not implement it, and its rows are already its own.

type Table

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

Table is a Source that mixes numeric, exact-integer, temporal and categorical columns.

It is the general-purpose implementation: use it when a chart plots time or a category against values, which is the common case for the Time and Ordinal scales.

func NewTable

func NewTable() *Table

NewTable returns an empty Table.

func (*Table) Column

func (t *Table) Column(name string) (Column, bool)

Column returns a column by name.

func (*Table) Columns

func (t *Table) Columns() []string

Columns lists the column names in insertion order.

func (*Table) Float64

func (t *Table) Float64(name string, v []float64) *Table

Float64 adds a numeric column, borrowing the slice. It returns t so calls can be chained. It panics if the column length disagrees with columns already added, or if the name is already taken.

func (*Table) Int64

func (t *Table) Int64(name string, v []int64) *Table

Int64 adds an exact integer column, borrowing the slice. It returns t so calls can be chained. It panics if the column length disagrees with columns already added, or if the name is already taken.

Use it for a column that counts or identifies: a row id, an order number, an epoch in a unit of its own. It plots exactly as a numeric column does — the values are converted for the axis — and it *labels* exactly, which is the difference. A float64 stops counting at 2^53, and past that two ids become one row.

func (*Table) Len

func (t *Table) Len() int

Len reports the number of rows.

func (*Table) String

func (t *Table) String(name string, v []string) *Table

String adds a categorical column, borrowing the slice. It returns t so calls can be chained. It panics if the column length disagrees with columns already added, or if the name is already taken.

Plot such a column against a [scale.Ordinal] axis; a continuous scale has no position for a category name and a geom says so rather than guessing one.

func (*Table) Time

func (t *Table) Time(name string, v []time.Time) *Table

Time adds a temporal column, borrowing the slice. It returns t so calls can be chained. It panics if the column length disagrees with columns already added, or if the name is already taken.

func (*Table) WithNulls

func (t *Table) WithNulls(name string, null []bool) *Table

WithNulls marks rows of an existing column as absent, borrowing the mask. It returns t so calls can be chained. It panics if the column does not exist or the mask is not one flag per row.

A text or temporal column needs this because it has no NaN to be missing with: "" is a string somebody may have measured and the zero time is an instant, so absence has to be said beside the values rather than inside them. A numeric column may use it too, and the two spellings agree — a NaN and a marked row are both missing, and neither outranks the other.

A mask that marks nothing is not stored: a reader sees a column with no nulls either way, and would otherwise copy a column to change none of it.

func (*Table) WithText

func (t *Table) WithText(name string, text func(i int) string) *Table

WithText gives a column a spelling of its own, borrowing the function. It returns t so calls can be chained. It panics if the column does not exist.

It is how a caller whose values mean more than their type says so: a column of exact integers that are really order numbers, a numeric column whose rows carry a unit. See Column.Text.

type Tween

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

Tween is a table that reads as a blend of two others.

It is a Source whose *contents* change rather than a Source per frame: the columns are allocated once, at the joined row count, and rewritten in place by Tween.At. Build the layer over Tween.Source once, and a transition then costs no allocations per frame and none per row — the same promise Stream makes, for the same reason and by the same means.

What interpolates and what does not

Numeric and temporal columns interpolate. A time is its Unix nanoseconds, which is the domain scale.Time already maps, so a time axis needs nothing special.

String columns do not. They take the end state's value where the key is in it and the start's otherwise, because a string is a name rather than a quantity: a sankey's from and to are which nodes an edge joins, and half of "ingest" is not a node.

Text

The rule above decides two different-looking things, and only one of them is a limitation.

**A label that is a string snaps.** A [geom.Text] layer over a string column changes from one word to the other at the moment the column does, with nothing in between. There is no cross-fade and no character-level morph: both would need a per-row opacity or a second draw of the same label, and opacity is a property of a layer rather than of a row — the IR change docs/adr/0007-per-mark-colour.md exists to refuse. What *can* move is where the label is, because that comes from its position columns, so a label travelling to a new place while its text changes once is available and is usually what was wanted.

**A label that is a number counts.** A text layer reads its column through Labels, in Train, on every frame — so a numeric column bound to [geom.TextBy] is re-spelled from whatever the blend currently holds, and the label counts from one value to the other by itself. That is the animation people mean by "animated text" nine times in ten, and it needs nothing but a numeric column.

It does need Round. FormatNumber spells a float at full precision, so a third of the way from 0 to 100 reads "33.300000000000004" — arithmetic rather than a number. Round("n", 0) makes it 33.

The same rule reaches further than labels, and this is the part worth knowing before reaching for a transition at all: **anything a geom decides from a string column decides it abruptly.** A bar's slot on a categorical axis, a discrete colour class, a GroupBy membership — those snap at the moment the column changes rather than sliding, because the thing they are deciding from has no halfway. A chart that needs a bar to *slide* between categories has to say where it is going in a numeric column, which is what Hold's doc is about from the other direction.

The row set is the union, and it is fixed

A key in either table is a row of the blend for the whole of it. An entering row exists at f == 0 — sitting wherever EnterFrom put it — and an exiting one still exists at f == 1.

That is not a detail. It is what keeps the *structure* of the frame identical from one frame to the next, which is the condition ir.Damage needs to report that two frames are comparable. A blend whose row count changed mid-flight would make every frame a full repaint, and nothing about the picture would look wrong.

It refuses at construction

A column one table has and the other does not, or one numeric on one side and textual on the other, is an error from NewTween rather than a surprise three frames into an animation. A transition is built before any frame runs, so there is somewhere honest to put the failure.

func NewTween

func NewTween(a, b Source, key string, opts ...TweenOption) (*Tween, error)

NewTween lines up two tables by a key column and returns the blend between them, positioned at f == 0.

func (*Tween) Alignment

func (t *Tween) Alignment() Alignment

Alignment reports the join the blend was built on.

func (*Tween) At

func (t *Tween) At(f float64)

At sets the blend fraction, clamped to [0, 1], and rewrites the columns in place. It allocates nothing.

func (*Tween) Fraction

func (t *Tween) Fraction() float64

Fraction reports where the blend currently sits.

func (*Tween) Rows

func (t *Tween) Rows() int

Rows reports the blend's row count, which is the number of distinct keys and does not change.

func (*Tween) Source

func (t *Tween) Source() Source

Source returns the blended table.

It is stable for the life of the Tween: hand it to a layer once, and every Tween.At afterwards changes what that layer draws without the chart being rebuilt. That is the same arrangement Stream.Source has, and it is what makes a transition cost a frame rather than a frame and a rebuild.

type TweenOption

type TweenOption func(*tweenConfig)

TweenOption configures a Tween.

func EnterFrom

func EnterFrom(col string, v float64) TweenOption

EnterFrom is the value a numeric column takes, at f == 0, for a row only the end state has — so a bar grows out of its baseline instead of appearing at full height.

A column with no EnterFrom holds its end value, which is a row that arrives by not moving. That is the default because it needs no configuration and is never wrong; growing from somewhere is the choice, and only the caller knows where "nothing yet" is for their data.

There is no fade, and that is a limit rather than an oversight: opacity is a property of a layer and not of a row, and adding a per-row opacity channel would be a change to the IR that ADR 0007 exists to refuse. A value in data space is the honest substitute.

func ExitTo

func ExitTo(col string, v float64) TweenOption

ExitTo is EnterFrom for a row only the start state has: where it goes on its way out. A column with no ExitTo holds its start value.

func Hold

func Hold(cols ...string) TweenOption

Hold names numeric columns that snap at the halfway point rather than interpolating.

It is for a number that is a name: an identifier, a category encoded as an integer, an axis slot. Halfway between category 2 and category 5 is category 3.5, which is not a category — so a column like that wants the abruptness a string column gets for free.

func Round

func Round(col string, digits int) TweenOption

Round quantises a column to a number of decimal places as it blends, so that what comes out is a number somebody would write down.

It exists for the label that counts up. A text layer reads its column through Labels, which spells a float at full precision — so a value interpolated a third of the way from 0 to 100 is drawn as "33.300000000000004", which is arithmetic rather than a number. Rounding it to zero places makes the label read 33, and the count is the animation anybody wanted from it.

It is not formatting. FormatNumber is deliberately shared by a facet panel key, a categorical tick and a text label, so that one number is spelled one way everywhere; what this changes is the *value*, before anything spells it. That means a column bound to a position as well as to a label will move in steps, which is usually a reason to blend the position from a column of its own.

Negative digits round to tens, hundreds and so on, the way math.Round scaled would: Round("n", -3) counts in thousands.

Jump to

Keyboard shortcuts

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