geom

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: 16 Imported by: 0

Documentation

Overview

Package geom holds the visual marks a chart is made of.

A geom knows what its shape is and never how it is rendered: it reads columns, asks the scales where values go, and emits IR. It has no reference to a backend, no knowledge of SVG or raster, and no opinion about layout beyond the rectangle it is given.

Index

Constants

View Source
const DefaultHexRadius = 10

DefaultHexRadius is the circumradius of one hexbin cell in device units when the layer names none. Ten pixels puts a few hundred cells in a normal panel, which is enough to show structure and few enough that each hexagon still reads as a mark.

Variables

View Source
var ErrBothAxes = errors.New("figure/geom: an error bar names an interval on both axes; it runs along one")

ErrBothAxes reports a layer that named an interval on both axes. It is returned rather than drawn as a box because a mark that guessed which of the two the caller meant would draw a different chart depending on the order the options happened to be written in.

View Source
var ErrCategorical = errors.New("figure/geom: categorical column on a continuous scale")

ErrCategorical reports a text column mapped onto an axis that has no position for a name.

View Source
var ErrCyclic = errors.New("figure/geom: this mark needs an edge table with no cycle in it")

ErrCyclic reports an edge table that runs in a circle where it must not. A flow that returns to where it came from has no column to stand in, and a hierarchy that is its own ancestor has no root to be measured against.

View Source
var ErrNoColumn = errors.New("figure/geom: column not found")

ErrNoColumn reports a column named by an option that the source does not have. It is returned rather than panicking because the column name usually comes from user input or a config file.

View Source
var ErrNoInterval = errors.New("figure/geom: an error bar needs an interval: give it geom.Y2/geom.X2 or geom.ErrorBy/geom.ErrorXBy")

ErrNoInterval reports an error bar with no interval to draw. An interval is the mark, so a layer without one is a layer that named the wrong geom rather than one with nothing to say.

View Source
var ErrNotCategorical = fmt.Errorf("figure/geom: this mark needs a categorical axis")

ErrNotCategorical reports an axis that has to name categories and cannot.

View Source
var ErrNotContinuous = errors.New("figure/geom: this mark places its own layout and needs a continuous axis")

ErrNotContinuous reports a positional scale that cannot carry a layout.

A mark that places its own geometry needs an axis it can put a fraction on. An ordinal scale has slots rather than positions, so every node would land in slot zero — drawn on top of each other rather than refused, which is the failure this error exists to turn into a sentence.

View Source
var ErrRampOnPath = errors.New("figure/geom: continuous colour ramp on a path")

ErrRampOnPath reports a continuous colour ramp bound to a layer that draws a path rather than a mark.

A stroke carries one colour and no stops, so a path colours in stretches: it can say "this part is over the limit" and it cannot say "this part is slightly further over it than that part". The scales that cut a continuum into stretches are scale.Threshold, scale.Quantize and scale.Quantile; the mark that can paint a continuum is a point.

View Source
var ErrUnknownMark = fmt.Errorf("figure/geom: unknown mark")

ErrUnknownMark reports a Desc naming a mark this package does not have.

Functions

func KeyOf

func KeyOf(g Geom) string

KeyOf returns the column a layer identifies its rows by, or "" for a layer that named none.

It reads the layer's Desc, so a third-party mark that describes itself — which is what Register and the JSON round trip already require — answers this without implementing anything further.

func OnSecondaryX

func OnSecondaryX(g Geom) bool

OnSecondaryX is OnSecondaryY for the horizontal axis.

func OnSecondaryY

func OnSecondaryY(g Geom) bool

OnSecondaryY and OnSecondaryX report whether a layer draws against the chart's secondary vertical or horizontal axis.

They are asked through Describer rather than through a method on Geom, because Geom is implemented outside this package and never gains one — and because the binding is already part of what a layer says about itself, so the answer and the document agree by construction. A layer that cannot describe itself reads the primary axes, which is what every layer written before there were two of them means.

func Register

func Register(m Mark, build func(Desc) (Geom, error))

Register makes a mark this package does not define buildable by FromDesc, and therefore readable from a JSON spec.

The name is the one the mark's Describer reports, and the one a document carries as the mark's type. A name owned by this package — every Mark constant — is refused with a panic, because shadowing a built-in would change what every existing document means. Registering a name twice replaces the earlier builder, which is what a test or a hot reload wants.

Nothing iterates the registry, and lookups are guarded by a lock, so registering from an init function or from a goroutine is safe and registration order never changes what a chart draws.

func init() {
    geom.Register("lollipop", func(d geom.Desc) (geom.Geom, error) {
        if d.Source == nil {
            return nil, errors.New("a lollipop layer needs a data source")
        }
        return Lollipop(d.Source, d.Options()...), nil
    })
}

func SourceOf

func SourceOf(g Geom) (data.Source, bool)

SourceOf returns the data behind a layer, or ok == false for one that does not hold any — an annotation takes values rather than columns — or one defined outside this package that does not report it.

Types

type ColorGuide

type ColorGuide struct {
	// Label titles the bar. It defaults to the name of the coloured column.
	Label string
	// Scale is the trained colour scale the bar shows.
	Scale scale.ColorScale
}

ColorGuide is the guide a layer needs when its colour comes from a continuous scale: a colourbar, not a legend swatch.

A single swatch cannot represent a ramp — it would have to pick one colour out of a continuum and label it with a column name — so a layer using ColorBy contributes one of these instead of a LegendEntry.

func (ColorGuide) Key

func (g ColorGuide) Key() string

Key identifies a guide by what it looks like rather than by which scale object produced it.

Two layers sharing one colour scale must produce one colourbar, and so must two layers whose separate scales agree about the label, the domain and the ramp — those would be drawn identically, so showing both would only take room from the chart. Comparing the scales themselves is not available: a ColorScale is an interface, and an implementation is free to be a type that == panics on.

type Datum

type Datum struct {
	X0, Y0, X1, Y1 float64
}

Datum carries the values an annotation is placed by. A rule uses X0 or Y0 alone, a band a pair on one axis, a segment and a region all four.

type Decimation

type Decimation uint8

Decimation is how a layer reduces its rows before it draws them.

A plot area is a few hundred pixels wide. A column with a million rows in it therefore has thousands of rows per pixel column, and drawing all of them emits thousands of segments that land on the same handful of pixels: slower to build, slower to rasterize, larger as a file, and not one pixel different. Reducing first is not a compromise for big data, it is the accurate way to draw it.

Nothing here touches a scale's domain. A geom trains on every row and reduces only when it draws, so the axes report the data rather than the subset that survived.

const (
	// AutoDecimation lets the layer choose — see [Decimate] for what it picks.
	AutoDecimation Decimation = iota

	// NoDecimation draws every row. It is what to ask for when the chart is
	// evidence and a reader will zoom into the vector output.
	NoDecimation

	// LTTB keeps the rows that carry a line's shape, using
	// largest-triangle-three-buckets. See [stat.LTTB].
	LTTB

	// MinMax keeps the extremes of every pixel column, so a spike one sample
	// wide still reaches full height. See [stat.MinMax].
	MinMax

	// DensityRaster counts rows per cell and draws the counts as an image
	// instead of drawing marks. See [stat.Grid].
	DensityRaster
)

The reductions. AutoDecimation is the default: a geom knows what its mark is and can see how many rows there are against how wide the plot is, which is exactly the information the choice needs.

type Desc

type Desc struct {
	// Mark is what the layer draws.
	Mark Mark
	// Source is the layer's data. It is nil for an annotation, which takes
	// values rather than columns.
	Source data.Source

	// X, Y, X2 and Y2 name the columns mapped to the axes. ColorCol and
	// ColorScale are [ColorBy]'s two halves.
	X, Y, X2, Y2 string
	// Z names the column mapped to the depth axis. It is empty for every mark
	// this package defines — see [Z] — and carries the third channel of a
	// layer in [github.com/timzifer/figure/three].
	Z          string
	ColorCol   string
	ColorScale scale.ColorScale

	// Group names the series column, and Stack, Dodge, DodgePad, Order and
	// WidthCol are the position adjustment defined over it.
	//
	// Stack carries the adjustment the layer is actually using rather than the
	// one it was given — a grouped bar that was told nothing stacks, and a
	// Desc that said NoStack for one would describe a different chart — and
	// StackSet reports whether the layer chose it. The pair is [Dash] and
	// DashSet again, and for the same reason: NoStack is both the zero value
	// and an adjustment somebody may have asked for, and without the flag a
	// round trip through the spec would turn a grouped bar's default into a
	// pinned "do not stack".
	Group    string
	Stack    Stacking
	StackSet bool
	Dodge    bool
	DodgePad float64
	Order    Ordering
	WidthCol string

	// Key is the column that identifies a row across renders, from [KeyBy].
	// Nothing in this package reads it; it is carried so that the layer can be
	// written down and read back with the identity it was given.
	Key string

	// SizeCol and SizeScale are [SizeBy]'s two halves: the column each mark
	// takes its size from, and the scale that turns a value into a diameter.
	SizeCol   string
	SizeScale scale.SizeScale

	// Bins, BinLo and BinHi configure a [Histogram]: how many bins and over
	// what interval. Zero and an empty interval mean the layer chooses.
	Bins         int
	BinLo, BinHi float64
	// Bandwidth is the kernel width a [Violin] or a [Ridgeline] estimates with,
	// Span the fraction of the rows one local fit of a [Trend] sees, Smooth how
	// it fits, and Overlap how far a ridge rises. Each carries the value the
	// layer is actually using.
	Bandwidth float64
	Span      float64
	Smooth    Smoothing
	Overlap   float64

	// Explode is how far the layer's marks are broken out of the middle of the
	// coord, as a fraction of its outer radius, and ExplodeCol the column that
	// answers it per row. Zero and "" are a layer that stays where it is.
	Explode    float64
	ExplodeCol string

	// From and To name an edge's two ends, ID and Parent a hierarchy's, and
	// Value the magnitude of either. They are what a relational or
	// hierarchical mark reads instead of X and Y — see [From] and [ID] for why
	// the two pairs are spelled apart.
	From, To      string
	ID, ParentCol string
	ValueCol      string
	// Padding is the gap between the shapes such a layout places, as a
	// fraction of the plot, and Thickness how much of its slot a node fills.
	// Both are zero when the layer left them to the mark.
	Padding   float64
	Thickness float64

	// Label names the layer in the legend.
	Label string

	// Datum places an annotation, and Text is a note's text. Both are unused
	// by a layer that has a Source.
	Datum Datum
	Text  string

	// TextCol is the column a [Text] layer reads its labels from, and Elide
	// whether it truncates one that does not fit rather than dropping it.
	// Both are unused by a layer that draws no text.
	TextCol string
	Elide   bool
	// AvoidOverlap opts a text layer into panel-local collision avoidance.
	AvoidOverlap bool

	// The styling options, one field per [Option]. A nil Color or Fill means
	// the layer takes its colour from the palette.
	Color   *ir.Color
	Fill    *ir.Color
	Width   float32
	Dash    []float32
	DashSet bool
	Tension float64
	Missing Missing
	// Marker is the shape a scatter draws, and MarkerSet reports whether the
	// layer chose it. The pair is [Dash] and DashSet again, and for the same
	// reason: a circle is both the zero value and a shape somebody may have
	// asked for, and a theme's redundant encoding replaces the first but not
	// the second.
	Marker    ir.Marker
	MarkerSet bool
	// Closed reports a connected layer that joins its last mark back to its
	// first — the radar contour of [Closed].
	Closed bool
	// OnY2 and OnX2 report a layer bound to the chart's secondary vertical or
	// horizontal axis. They are independent: a layer may be on both. See
	// [OnY2] and [OnX2].
	OnY2     bool
	OnX2     bool
	Size     float32
	BarWidth float64
	Baseline float64
	Opacity  float64
	Steps    StepPos
	Whisker  float64
	Outliers bool
	// MidCol is the column an [ErrorBar] marks its measurement at, ErrorCol
	// and ErrorXCol the half-widths of a symmetric interval on each axis, and
	// Caps whether the ends carry a crossbar. Caps needs no companion flag
	// the way [Desc.DashSet] does: it defaults to true rather than to its zero
	// value, so a document that says nothing and one that says false are
	// already different.
	MidCol    string
	ErrorCol  string
	ErrorXCol string
	Caps      bool
	Decimate  Decimation
	Budget    int
	CellSize  float64
	FontSize  float64
	HAlign    ir.HAlign
	VAlign    ir.VAlign
	// AlignSet is whether the layer was told how to align its text. The start
	// of a run on the baseline is both the zero value and an alignment
	// somebody may have asked for, and a [Text] layer centres a label in its
	// box when nobody has — so without the flag a round trip would turn that
	// default into a pinned left edge, exactly as it would for DashSet.
	AlignSet bool
	Rotation float64
	Extend   bool

	// Extra is what a third-party mark's own options set — see [Extra]. It is
	// nil for a layer configured entirely from this package's options, and
	// what the JSON spec carries as the mark's own properties for a mark this
	// package did not define.
	Extra map[string]any
}

Desc is a layer reduced to what configures it: its mark, its data, the columns it reads and the options it was given.

It exists so that a chart can be written down and read back. A Geom is an interface with three methods and no way to ask it what it is, which is right for drawing and useless for serialization — so every layer in this package answers Describer, and FromDesc turns the answer back into a layer that draws the same marks.

A field left at its zero value means "not set", exactly as leaving the corresponding Option out does. The fields that have a non-zero default — BarWidth, Whisker, Outliers, Opacity, Extend — are filled in by Describe with the default the layer is actually using, so a Desc is complete rather than partial.

func Configure

func Configure(opts ...Option) Desc

Configure applies opts and reports what they set.

It is how a geom defined outside this package reads the shared option set. Option is a function over an unexported configuration, which is what keeps the set one namespace — an option a geom has no use for is accepted and ignored — and it is also what would keep a third-party mark from honouring X, Color or Label at all. So the configuration is handed out in the form that is already public: the Desc a layer writes itself down as, with no Mark and no Source, because the caller is about to supply both.

func Lollipop(src data.Source, opts ...geom.Option) geom.Geom {
    return &lollipop{src: src, cfg: geom.Configure(opts...)}
}

func (l *lollipop) Build(b ir.Backend, f geom.Frame) error {
    xs, _ := data.Float64Column(l.src, l.cfg.X)
    col := f.Theme.Palette.At(f.Index)
    if l.cfg.Color != nil {
        col = *l.cfg.Color
    }
    // …
}

The defaults are the ones every layer in this package starts from: a bar fills 0.8 of its slot, whiskers reach 1.5 IQR, outliers are shown, missing values leave a gap, an annotation extends its axis. A layer that returns this Desc from its own Describer — with Mark and Source filled in — is serialisable by the JSON spec like any built-in one, and a layer built by Register from that Desc is configured identically: Desc and Configure are inverses, the same way Describe and FromDesc are.

func Describe

func Describe(g Geom) (Desc, bool)

Describe reports g's configuration, or ok == false if g cannot describe itself.

func (Desc) Options

func (d Desc) Options() []Option

Options turns the description back into the option list that would have produced it — the inverse of Configure, and what a builder given to Register hands to its own constructor.

Every option is applied rather than only the ones that differ from a default: an option set to its default value is the default value, and filtering would only be a second place for the defaults to be written down.

type Describer

type Describer interface {
	// Describe returns the layer's configuration.
	Describe() Desc
}

Describer is implemented by a layer that can say what it is.

It is an optional interface, like Faceter and Guided: a third-party geom that does not implement it still draws, and is simply not serializable. That is a better failure than a half-written spec — see github.com/timzifer/figure/spec.

type Faceter

type Faceter interface {
	// Source returns the layer's data, so a facet can read the column it
	// splits on.
	Source() data.Source

	// Subset returns a copy of this layer restricted to the given rows. The
	// copy shares its configuration with the original, including any colour
	// scale — which is what makes one colour mean one thing across every
	// panel, and one colourbar enough to say so.
	Subset(rows []int) Geom
}

Faceter is implemented by a layer that can be split into panels.

Faceting is a data operation, and a geom is the only thing that knows which data it holds — so the split happens here rather than in the facet package reaching into a layer. A layer that does not implement Faceter is drawn unchanged in every panel, which is exactly right for an annotation: a threshold line belongs on all of them.

type Frame

type Frame struct {
	// Area is the plot rectangle in device space. Scales already map into it.
	Area ir.Rect
	// X and Y are the trained, ranged scales.
	X, Y scale.Scale
	// Coord decides what the interval a scale maps into means, and therefore
	// where a mapped pair lands. A nil Coord is [coord.Cartesian], so a Frame
	// built by code that never heard of coordinate systems draws the chart it
	// always drew — reach for [Frame.Coords] rather than the field.
	Coord coord.Coord
	// Theme supplies defaults the geom did not override.
	Theme theme.Theme
	// Index is the geom's position among the chart's layers, used to pick a
	// default colour from the palette.
	Index int

	// Rows, when non-nil, collects which source row is behind each mark the
	// geom draws. It is nil for an ordinary render and a geom must check it
	// before doing the bookkeeping — see [Rows] and [Frame.Marks].
	Rows Rows

	// Labels places opt-in text labels against earlier participating labels in
	// this panel. A nil placer preserves their original positions.
	Labels LabelPlacer
}

Frame is everything a geom needs to turn data into IR.

func (Frame) Coords

func (f Frame) Coords() coord.Coord

Coords is the frame's coordinate system, which is coord.Cartesian framed in the plot rectangle when it has none.

A geom asks for it once at the top of Build and uses what comes back: the answer never changes within one frame, and the nil check is not worth repeating per row. Framing the fallback rather than handing back a bare Cartesian is what makes a Frame built by hand — in a test, or by a caller driving a geom directly — behave as it always did: the coord it gets can answer coord.Coord.Extent, which is where a rule that spans the plot finds the far edge.

func (Frame) Marks

func (f Frame) Marks(m MarkRows)

Marks reports the rows behind a set of marks, if anyone asked. A geom calls it with the positions a row landed at, whatever it then draws through them.

type Geom

type Geom interface {
	// Train feeds the geom's data into the scales so they can establish their
	// domains. It runs before layout, because layout needs tick labels and
	// tick labels need a domain.
	Train(t Training) error

	// Build emits the geom's marks into b.
	Build(b ir.Backend, f Frame) error

	// Legend returns the entry this geom contributes, or ok == false if it
	// should not appear in the legend.
	Legend(f Frame) (LegendEntry, bool)
}

Geom is a layer of marks.

Stability

Geom is implemented outside this module, so it never gains a method: three is the whole contract. Everything else a layer can do is an optional interface beside it — Faceter, Guided, Sized, Legender, Describer — and a layer that implements none of them still draws. A layer defined outside this package reads the shared options through Configure, takes its own through Extra, and is written down and read back through Describer and Register.

func Arc

func Arc(src data.Source, opts ...Option) Geom

Arc draws an edge list as nodes on a rail with ribbons between them: an arc diagram.

geom.Arc(src, geom.From("a"), geom.To("b"), geom.Value("calls"))

The table is one row per edge — its two ends and its weight. Nodes are not declared anywhere: a node exists because a row mentioned it, and it takes a share of the rail proportional to the traffic through it. An edge naming no weight counts as one, so an unweighted graph draws without a Value column.

A chord diagram is this mark under a polar coord

The layout is a position along the rail and a height off it, in the unit square, and the coordinate stage decides what that means. Left Cartesian the nodes sit on a rail and the ribbons rise off it. Wrapped round a circle, with the rail moved to the rim, the ribbons cross the middle — which is a chord diagram:

p := figure.New(figure.Theme(bare), figure.Coord(coord.Polar()))
p.X(scale.Linear())  // the rail, swept round the circle
p.Y(scale.Linear())  // the height off it, read as the radius
p.Add(geom.Arc(src, geom.From("a"), geom.To("b"), geom.Value("calls"),
    geom.Baseline(1)))

Baseline is where the rail sits, and it is the whole difference between the two pictures: 0 — the default — puts it at the bottom of the plot with the ribbons above, and 1 puts it at the outer rim with them crossing the middle. It is coord.Polar and not coord.Pie, which sweeps the wrong axis.

Thickness is how deep the rail is, and Padding the gap between adjacent nodes on it. Both axes describe the unit square, so give the chart a theme with no grid, no axis lines and no ticks.

func Area

func Area(src data.Source, opts ...Option) Geom

Area fills the region between a series and a baseline, or — given Y2 — the band between two series.

The upper edge is stroked in the layer's full colour and the interior is filled with a faded version of it. A band drawn in one flat colour reads as a solid object; a band with a drawn edge reads as a series with uncertainty around it, which is what an area chart is for. Use Opacity to change the fill, Fill to set it outright.

Given GroupBy it draws one band per series, stacked: a stacked area chart. Stack chooses the baseline they are stacked about — StackFill for a 100 % chart, StackSilhouette for a ThemeRiver, StackWiggle for a streamgraph — and the fill is solid rather than faded, because the bands of a stack are read against each other rather than through each other.

func Bar

func Bar(src data.Source, opts ...Option) Geom

Bar draws a rectangle per row, from a baseline to the row's Y value.

Given GroupBy it draws one rectangle per row per series, stacked from the baseline up: a long table with a series column is a stacked bar chart, and Stack and Dodge are how it becomes a 100 % chart or a grouped one instead. See docs/adr/0019-position-adjustments.md.

The cross axis is the bar's width, and a row that names both of its edges with X and X2 gets exactly those rather than a share of the slot. In github.com/timzifer/figure/coord.Donut that axis is the radius, so the pair is a slice's inner and outer radius: a donut whose slices reach different distances is this layer with one more column, not another mark. Explode and ExplodeBy then break a slice out of the ring.

func Beeswarm

func Beeswarm(src data.Source, opts ...Option) Geom

Beeswarm draws one marker per row and moves it aside until it stops overlapping its neighbours: every observation of a distribution, none of them hidden.

It is the distribution plot that shows the rows themselves. A boxplot summarises and a violin smooths; both answer "what does this look like" and neither answers "how many are there, and where exactly". A swarm answers both — and it is honest about small samples in the way a density estimate is not, because a group of six observations draws six marks rather than a curve.

The X column is the grouping key, exactly as it is for Boxplot and Violin, so a swarm wants many rows per X value and usually a scale.Ordinal axis. Given GroupBy it draws one swarm per series within each slot, side by side.

Why the offsets are computed when it draws

A mark is moved aside until it clears its neighbours *by a marker's width*, which is a length in device units — so the arrangement depends on how wide the panel is and on how large the markers are, and it therefore belongs in Build. That is the same line ADR 0011 draws for decimation, and it has the same consequence: what the axis says does not change when the swarm rearranges, because the axis was trained on the observations.

A swarm that will not fit its slot is packed as tightly as the slot allows rather than spilling into the one beside it: a mark in the wrong slot is a mark attributed to the wrong category, which is worse than two marks touching.

func Boxplot

func Boxplot(src data.Source, opts ...Option) Geom

Boxplot summarises the distribution of the Y column within each distinct X value: a box spanning the interquartile range, a line at the median, whiskers reaching to the furthest observation within Whisker times the IQR, and a marker for every observation beyond them.

The X column is the grouping key, so a boxplot wants many rows per X value — typically a categorical column against a scale.Ordinal axis, which also gives every box the same width. On a continuous axis the width comes from the closest pair of groups, as it does for a bar.

Whiskers stop at an observation, never at the theoretical fence. A whisker drawn out to 1.5·IQR when the data stops well short of it would be claiming a reading that does not exist.

func ECDF

func ECDF(src data.Source, opts ...Option) Geom

ECDF draws the empirical cumulative distribution of the X column: a staircase rising from 0 to 1, one step per distinct observation.

It is the distribution plot with no parameter in it. A histogram picks bin edges and a violin picks a bandwidth, and both choices change the picture, so two of them drawn together compare two smoothing decisions as much as they compare two datasets. An ECDF has nothing to choose: the curve is the data. What it costs is the shape — a reader sees medians and tails clearly and bimodality hardly at all, which is why this and Violin are both here.

The Y axis is the layer's own, running 0 to 1, so the Y column is not read. Given GroupBy it draws one staircase per series, which is what the mark is most useful for: several distributions on one pair of axes, none of them hiding another.

func ErrorBar

func ErrorBar(src data.Source, opts ...Option) Geom

ErrorBar draws the interval a measurement is known to within: a rule between two bounds, a crossbar at each end, and a marker at the measurement itself when the row names one.

It is the mark figure drew fourteen others before: every chart of a mean, a forecast, a tolerance or a sampled quantity has one number and a claim about how well that number is known, and until this the second half had nowhere to go. A band through Area is the continuous version of the same statement and is right for a series; this is right for the categories and the handful of points where a band would be a shape drawn through three vertices.

The two spellings

The interval's ends are two columns, exactly as the band of an Area and the box of a Rect are:

geom.ErrorBar(src, geom.X("group"), geom.Y("lo"), geom.Y2("hi"), geom.Mid("mean"))

or one column of half-widths about the value, which is what a table of means and standard deviations already holds:

geom.ErrorBar(src, geom.X("group"), geom.Y("mean"), geom.ErrorBy("sd"))

The second marks the measurement without being asked, because with that spelling there always is one — the centre is the Y column. The first marks it only when Mid names it: a minimum and a maximum are not evidence of a mean, and drawing one would be inventing a reading.

Which way it runs

Along the axis whose two ends the encoding names, and nothing else decides it: Y2 or ErrorBy makes it vertical, X2 or ErrorXBy makes it horizontal. That is the rule Rect already follows about its edges, and it is why there is no orientation option to forget. A layer that names both pairs is a box rather than an interval, and says so rather than guessing.

What it composes with

GroupBy gives one colour per series and Dodge gives each series its own share of the slot, so a grouped error bar lines up over the grouped bars it annotates — the bar layer and this one take the same BarWidth and the same Dodge, and the caps come out half as wide as the bars by construction. ColorBy paints each row through a scale. Caps turns the crossbars off for the point-range look.

Under a polar coord the rule follows the radius and the caps become arcs, because every span goes through the coord rather than being drawn as two device points — which is what makes a radial error bar the same mark rather than a second one.

func FromDesc

func FromDesc(d Desc) (Geom, error)

FromDesc builds the layer d describes.

It is the inverse of Describe over every layer in this package: describing a layer and rebuilding it produces one that draws the same marks. A layer with data needs a Source; an annotation ignores one. A mark this package does not define is built by whoever registered it — see Register — and one nobody did is ErrUnknownMark.

func HBand

func HBand(y0, y1 float64, opts ...Option) Geom

HBand shades the horizontal strip between two Y values, across the whole plot. It is how a tolerance range or a target window is drawn.

func HLine

func HLine(y float64, opts ...Option) Geom

HLine draws a horizontal reference line across the plot at y.

func Hexbin

func Hexbin(src data.Source, opts ...Option) Geom

Hexbin counts the rows falling in each cell of a hexagonal lattice over the plot area and draws a hexagon per populated cell, shaded by its count.

It is the third answer to overplotting, beside decimation and the density raster: a scatter of a million rows says more about row order than about the data, because the last mark drawn wins. A hexbin says how many rows are there. It differs from the raster in what a cell *is* — a hexagon has six neighbours all the same distance away, where a square has four near and four far, so a cloud binned into squares grows faint crosses and diagonal seams that belong to the bins rather than to the data. And it differs in resolution: the raster paints a pixel per cell and this draws a mark per cell, so the cells are counted in the hundreds.

DensityCells sets the cell radius in device units; the default is DefaultHexRadius.

Why the lattice is in device space

A hexagon is only a hexagon if its six neighbours really are equidistant, and on screen is where that has to be true — a lattice laid out in data space and then mapped through the axes comes out stretched by whatever aspect ratio the panel happens to have. So the binning happens in Build, where the rectangle is known, exactly as the density raster's does.

The cost of that is one thing this layer deliberately does not have: a colourbar. The counts are not known until the plot rectangle is, and the guide column is measured before it — the same ordering ADR 0011 describes for decimation. So a hexbin shades from a faded version of its own colour to the full one, and says how many rows are behind a cell through a hit rather than through a key. Give it a ColorBy scale to shade through a ramp instead; the column named there is not read, because the quantity being coloured is the layer's own count.

func Histogram

func Histogram(src data.Source, opts ...Option) Geom

Histogram counts the X column into bins and draws a bar per bin.

It is the first geom whose Y axis is not in the data: the counts are the layer's own, so the axis is trained on them and the Y column is not read at all. That is the same arrangement Boxplot already has, where the quantiles rather than the observations decide the domain.

The bins are contiguous by construction, so the bars touch. That is not a missing gap: a histogram's bars are an unbroken division of the axis, and separating them would make it read as a bar chart of categories, which is a different claim about the data. BarWidth is therefore one of the options this layer ignores.

Bins sets the count and BinRange the interval. Left alone, the layer bins over the data's own extent with the Freedman–Diaconis rule, falling back to Sturges's where the column has no interquartile range to measure.

Groups

A histogram ignores GroupBy. Two distributions drawn as two overlapping histograms hide each other wherever they agree, which is exactly where the comparison is; Violin, Ridgeline and ECDF are the three marks that answer that question without overplotting, and each of them takes the series column.

func Icicle

func Icicle(src data.Source, opts ...Option) Geom

Icicle draws a hierarchy as bands: one per node, as wide as its share of the whole and as far out as it is deep.

geom.Icicle(src, geom.ID("path"), geom.Parent("under"), geom.Value("bytes"))

The table is one row per node — the name it is known by, the name of the node above it, and its magnitude. Usually only the leaves carry a number; an internal node's size is what is under it, which geom.Value explains.

A sunburst is this mark under a polar coord

The layout is a span across and a depth out, in the unit square, and the coordinate stage decides what that looks like. Left Cartesian it is an icicle, growing upward from a root along the bottom — the flame-graph orientation. Wrapped round a circle it is a sunburst, with the root at the middle:

p := figure.New(figure.Theme(bare), figure.Coord(coord.Polar()))
p.X(scale.Linear())  // the span, swept round the circle
p.Y(scale.Linear())  // the depth, read as the radius
p.Add(geom.Icicle(src, geom.ID("path"), geom.Parent("under"), geom.Value("bytes")))

It is coord.Polar and not coord.Pie: a pie sweeps the *Y* axis round, and this mark's Y is its depth. coord.Hole leaves the middle empty, which is what a sunburst that does not want to draw its root wants.

Both axes describe the unit square, which is nothing a reader needs to see: give the chart a theme with no grid, no axis lines and no ticks, exactly as a pie does.

func Line

func Line(src data.Source, opts ...Option) Geom

Line connects consecutive rows with a stroked path.

Given GroupBy it draws one path per series over a long table, each in its own colour and named in the legend — which is what a table of measurements with a series column plots as, and is one layer rather than N.

func Note

func Note(x, y float64, text string, opts ...Option) Geom

Note places a text label at a position in data space.

Alignment is about that position: the default puts the text's start on it. Use Align to hang the label off the other side of a line it is naming.

func QQ

func QQ(src data.Source, opts ...Option) Geom

QQ draws a normal quantile-quantile plot of the X column. The horizontal axis holds standard-normal theoretical quantiles; the vertical axis holds the ordered observations in their original units. Y is not read. A straight pattern indicates agreement up to location and scale; no fit or reference line is inferred. GroupBy draws one comparison per series.

The sample is summarised in Train and both axes learn the computed pairs. For another theoretical distribution, use stat.QQ with a quantile function and draw its pairs with Scatter. Quantiles are summaries, so this mark does not claim source-row identity. Missing Interpolate has the same meaning as Gap: missing observations are omitted before ranking.

func Rect

func Rect(src data.Source, opts ...Option) Geom

Rect draws one rectangle per row, occupying an arbitrary box in data space.

It is the mark Bar is not: a bar grows from a shared baseline to a value and therefore always touches the axis, while a rect is bounded on both axes by the row itself. That one difference is what a heatmap, a gantt chart, a candle, a waterfall step and a waffle cell all need, and it is why they are recipes over this mark rather than five more geoms — see docs/chart-types.md.

An edge the row does not name is the slot the axis implies: a band scale's own bandwidth, or the closest spacing in the data narrowed by BarWidth. So a heatmap over two categorical axes needs two columns and a colour, and nothing else:

geom.Rect(src, geom.X("day"), geom.Y("hour"),
    geom.ColorBy("calls", scale.Sequential(palette.Viridis)))

while a gantt bar, which knows where it starts and stops, names both:

geom.Rect(src, geom.X("start"), geom.X2("end"), geom.Y("task"))

X2 gives the far edge on the horizontal axis and Y2 on the vertical one. A ColorBy column paints each cell separately, through a ramp or through a qualitative palette; without one the whole layer is one colour.

Under a polar coord a cell is an annular sector, so Explode and ExplodeBy break one out of the middle exactly as they do a slice of a donut — the two marks that draw a sector are the two that can leave one.

Cells that share an edge are drawn as separate shapes and antialiased separately, so a shared edge can show as a hairline of the background. That is compositing rather than a gap in the data — the cells are exactly contiguous, and the golden files say so — and removing it would need a mesh primitive in the IR, which is the thing ADR 0007 exists to refuse. Use BarWidth or the axis's own padding for a gap that is meant to be seen.

func Region

func Region(x0, y0, x1, y1 float64, opts ...Option) Geom

Region shades an axis-aligned rectangle in data space, bounded on both axes. HBand and VBand are the cases where one axis is unbounded.

func Ridgeline

func Ridgeline(src data.Source, opts ...Option) Geom

Ridgeline draws the distribution of the X column within each category of the Y axis, as a density curve rising out of that category's slot.

It is the chart for "how did this distribution change" — one row per month, per cohort, per station — and it works because the ridges overlap: a grid of twenty little densities is twenty comparisons a reader has to carry between panels, and twenty overlapping ridges is one picture. Overlap sets how far they rise.

The Y column names the rows and wants a scale.Ordinal axis; the X column holds the observations. That is the transpose of Violin, and it is why the two are separate marks rather than one with a flag: a violin is a mark with a width and a ridgeline is a mark with a height, and neither reads as the other rotated.

The ridges are drawn from the top of the axis down, so that each one is painted over the one it rises into and the front of the picture is the row nearest the reader.

func Sankey

func Sankey(src data.Source, opts ...Option) Geom

Sankey draws an edge list as a flow: nodes in columns, and a band between them as thick as what it carries.

geom.Sankey(src, geom.From("stage"), geom.To("next"), geom.Value("units"))

The table is one row per link — where it comes from, where it goes and how much. Nodes are not declared anywhere: a node exists because a row mentioned it, and the order they are first mentioned in is the order they stack in. See From.

What it decides

A node stands one column past the deepest source that reaches it, so a flow reads left to right and a stage never sits before something feeding it. Its thickness is the greater of what enters and what leaves — a node that loses some of what it is given is as thick as the larger side, and the shortfall shows as the part of its edge no band leaves from.

Within a column, nodes keep the order their rows gave them and only their positions are relaxed, stat.SankeySweeps times, towards the middle of what they are joined to. Reordering them to reduce crossings would mean a sort per sweep, and a sort is where a layout stops being a pure function of its input and starts depending on how a tie was broken. Order is how a caller asks for a different one — by sorting its own rows.

Cycles

An edge list that runs in a circle is refused with ErrCyclic rather than drawn: a flow that returns to where it came from has no column to stand in, and picking one would draw an ordering nobody wrote.

Both axes describe the unit square, so give the chart a theme with no grid, no axis lines and no ticks.

func Scatter

func Scatter(src data.Source, opts ...Option) Geom

Scatter draws one marker per row.

Given GroupBy it draws one set of markers per series, each in its own colour and — where the theme asks for redundant encoding — its own shape.

Given SizeBy it draws a circle per row, sized by area from a column: the bubble chart. That is a channel rather than a second geom, and it is circles rather than markers because the IR carries one marker style per drawing call — see SizeBy.

func Segment

func Segment(x0, y0, x1, y1 float64, opts ...Option) Geom

Segment draws a straight line between two points in data space. Unlike HLine it does not span the plot, so it is what an arrow-less callout or a hand-placed trend line is made of.

func Step

func Step(src data.Source, opts ...Option) Geom

Step connects rows with horizontal and vertical segments instead of a straight line.

It is the honest shape for anything that holds a value and then changes it — a configuration, a queue depth, a price, a state machine. A plain line between two such samples draws a gradual transition that never happened. Use Steps to say where the change falls between the two rows.

func Text

func Text(src data.Source, opts ...Option) Geom

Text draws one label per row, read from a column.

It is what Note is not: a note places one literal string at one literal position, so labelling rows with it costs a layer per row — and since a plot only ever gains layers, a chart whose rows change has to be rebuilt from scratch, which takes the reader's zoom with it. A text layer reads the same data.Source every other mark reads and needs no rebuild when the rows change.

The label column is TextBy, and any column will do: a text column is used as it is, a numeric or temporal one is formatted the way a category name is.

Where the label goes follows from which channels the layer names.

Naming neither X2 nor Y2 puts the label at the row's point, laid out by Align exactly as a note is — the name beside a scatter point, the value above a bar:

geom.Text(src, geom.X("t"), geom.Y("v"), geom.TextBy("name"),
    geom.Align(ir.AlignCenter, ir.AlignBottom))

Naming either of them puts the label in the middle of the box the row spans, and that box is exactly the one Rect would draw for the same options — an edge the row does not name is the slot the axis implies, as it is there. So one set of options describes the rectangles and labels them:

opts := []geom.Option{geom.X("start"), geom.X2("end"), geom.Y("lo"), geom.Y2("hi"),
    geom.ColorBy("state", pal)}
p.Add(geom.Rect(src, opts...))
p.Add(geom.Text(src, append(opts, geom.TextBy("label"))...))

Two things follow from the layer knowing the box, and both are why this is a mark rather than a recipe over Note.

A label is measured with the font it will be drawn in, and one that overruns its box is dropped — an overrunning label reads as belonging to the neighbour. Elide truncates it instead. And the middle of the box is the middle of its *visible* part: a bar half scrolled off the edge carries its label in the middle of what is left rather than off-screen with the box's true centre.

A layer given ColorBy takes each label's ink from the fill that scale gives the row, dark on light and light on dark, so that a qualitative palette does not leave half its categories unreadable. Color overrides it.

AvoidOverlap opts into the renderer's panel-local label layout. Point labels may move; box labels remain anchored to their own box and are dropped if they collide. Without that option, neighbouring labels are not moved apart.

func Treemap

func Treemap(src data.Source, opts ...Option) Geom

Treemap draws a hierarchy as nested rectangles, each with an area proportional to its value.

geom.Treemap(src, geom.ID("path"), geom.Parent("under"), geom.Value("bytes"))

The table is one row per node — the name it is known by, the name of the node above it, and its magnitude. Usually only the leaves carry a number, and an internal node's size is what is under it; see Value.

What is drawn

The leaves, and only the leaves. An internal node's rectangle is exactly the union of its children's, so painting it would be painting underneath paint; what makes the nesting visible is Padding, which insets a node's box before its children are packed into it. A node carrying a value of its own beyond its children's keeps the remainder as empty room inside its box, which is what makes an unaccounted-for share something a reader can see.

Squarified, and against the panel

The packing is stat.Squarify: tiles are gathered into rows for as long as adding one improves the row's worst aspect ratio. It runs against the plot rectangle rather than against the unit square, because what it optimises is a shape *on screen* — packing a square and then stretching it into a wide panel would defeat the whole algorithm. That makes it the third stat to run in Build rather than in Train, beside Hexbin's lattice and Beeswarm's offsets, and for the same reason: its answer is a length the reader sees rather than a number the axis has to describe. See docs/adr/0028-distribution-stats.md.

Siblings are packed in the order their rows appear. Order with OrderValue is how to ask for the largest first, which gives the squarest tiles; source order is what keeps the picture stable as the numbers move.

Both axes describe the unit square and there is nothing in that for a reader to see: give the chart a theme with no grid, no axis lines and no ticks.

func Trend

func Trend(src data.Source, opts ...Option) Geom

Trend fits a smooth line through the X and Y columns and draws it.

It is the layer that goes on top of a scatter: the cloud says what was measured, the trend says what it is doing. Smooth chooses how — Loess, the default, fits a line through the neighbours of each abscissa and follows the data; LinearFit fits one straight line by least squares, which is the right mark when the claim being made is that the relationship *is* linear. Span sets how many neighbours a local fit sees.

Given GroupBy it fits one line per series, which is the comparison the mark is usually added for: two clouds with two trends through them.

Where the fitting happens

In Train, in data space — so the axis is trained on the fit as well as on the observations, and a curve that runs a little past the data is inside the plot rather than clipped at its edge. That is the opposite choice from decimation (ADR 0011), and for the same underlying reason: a reduction must not change what the axis says, and a *fit* is part of what the axis has to describe.

func VBand

func VBand(x0, x1 float64, opts ...Option) Geom

VBand shades the vertical strip between two X values, across the whole plot. It is how a maintenance window or a highlighted interval is drawn.

func VLine

func VLine(x float64, opts ...Option) Geom

VLine draws a vertical reference line across the plot at x.

func Violin

func Violin(src data.Source, opts ...Option) Geom

Violin draws the distribution of the Y column within each distinct X value, as a kernel density estimate mirrored about the slot's centre.

It is the boxplot's answer to the boxplot's own weakness. A box says where the quartiles are and nothing about the shape between them, so two very different distributions — one hump, or two — draw the same box. A violin draws the shape.

The X column is the grouping key, exactly as it is for Boxplot, so a violin wants many rows per X value and usually a scale.Ordinal axis. Given GroupBy as well it draws one violin per series within each slot, side by side: that is the comparison a grouped boxplot makes, with the shapes left in.

What the widths mean

Every estimate integrates to 1, and they are all drawn against the widest of them — so the violins are compared by shape and not by sample size, and one group with ten times the rows is not ten times as fat. Bandwidth pins the smoothing, which is what makes two groups strictly comparable; left alone, each group is smoothed by stat.Silverman's rule from its own spread.

type Guided

type Guided interface {
	// ColorGuide returns the colour guide this layer contributes, or
	// ok == false if it has none.
	ColorGuide() (ColorGuide, bool)
}

Guided is implemented by a layer that paints from a continuous colour scale.

It is an optional interface rather than a method on Geom: a layer that colours itself one colour has nothing to say here, and every third-party geom would otherwise have to write a stub returning false.

type LabelAvoider

type LabelAvoider interface {
	AvoidsLabels() bool
}

LabelAvoider optionally requests panel-local label placement. Geom's stable interface is unchanged; render allocates layout state only when requested.

type LabelPlacer

type LabelPlacer interface {
	PlaceLabel(run ir.TextRun, move bool) (at ir.Point, ok bool)
}

LabelPlacer is supplied by the renderer. A geom requests a position but neither solves layout nor changes drawing order. move is false for a label inside a box: moving that label could attribute it to a neighbouring row.

type LegendEntry

type LegendEntry struct {
	Label  string
	Color  ir.Color
	Kind   SwatchKind
	Marker ir.Marker
	Dash   []float32
	Width  float32
}

LegendEntry is a geom's contribution to the legend.

func Legends

func Legends(g Geom, f Frame) []LegendEntry

Legends returns the legend entries of a layer: its own list where it has one, and its single entry otherwise.

It is what render calls, so that the preference between the two interfaces is written down once.

func LegendsOr

func LegendsOr(g Geom, f Frame, many []LegendEntry) []LegendEntry

LegendsOr is a layer's own entries, or its single one where it has no series and no categories to name.

Every geom here that can contribute many entries can also contribute one — the same Bar draws a stack of five series or one plain row of bars — and this is the one place that decides which.

type Legender

type Legender interface {
	// Legends returns the entries this layer contributes, in legend order.
	//
	// It is the whole answer: a layer that implements it is not asked for
	// [Geom.Legend] as well, so a layer with one entry returns that one entry
	// here, and returning none means the layer stays out of the legend. The
	// geoms in this package answer it through [LegendsOr], which is that
	// fallback written once.
	Legends(f Frame) []LegendEntry
}

Legender is implemented by a layer that contributes more than one legend entry.

A pie has N slices inside one layer. So does a stacked bar, a grouped bar, a waffle and anything painted from a discrete colour scale — and Geom.Legend's single entry cannot name them. It is an *optional* interface rather than a second method on Geom because most geoms have exactly one entry to contribute and would otherwise all need a stub: the same argument Guided already made for colourbars, and the reason the extension API can still freeze at v1.0 with three methods on Geom. See docs/adr/0020-discrete-colour-and-multi-entry-legends.md.

type Mark

type Mark string

Mark names what a layer draws. It is the one word that decides which constructor built a layer, and the hinge a serialized chart turns on.

const (
	MarkLine    Mark = "line"
	MarkScatter Mark = "scatter"
	MarkBar     Mark = "bar"
	MarkArea    Mark = "area"
	MarkStep    Mark = "step"
	MarkBoxplot Mark = "boxplot"
	MarkRect    Mark = "rect"
	MarkText    Mark = "text"

	// The distribution marks. Each of them replaces the rows with a summary of
	// where they are, so each of them decides one of its own axes: a histogram
	// and a hexbin count, a violin and a ridgeline estimate a density, an ECDF
	// accumulates, a trend fits. See package
	// github.com/timzifer/figure/stat.
	MarkHistogram Mark = "histogram"
	MarkViolin    Mark = "violin"
	MarkRidgeline Mark = "ridgeline"
	MarkHexbin    Mark = "hexbin"
	MarkBeeswarm  Mark = "beeswarm"
	MarkECDF      Mark = "ecdf"
	MarkQQ        Mark = "qq"
	MarkTrend     Mark = "trend"

	// The relational and hierarchical marks. Each reads an edge table rather
	// than a pair of axes, and each places its own layout in the unit square —
	// which is what lets the coordinate stage decide what it looks like. An
	// icicle under a polar coord is a sunburst, and an arc diagram under one is
	// a chord diagram; neither is a mark of its own. See
	// docs/adr/0039-relational-layouts.md.
	MarkTreemap Mark = "treemap"
	MarkIcicle  Mark = "icicle"
	MarkSankey  Mark = "sankey"
	// MarkArc is the arc diagram, and it is spelled out rather than "arc"
	// because Vega-Lite's arc is a pie wedge — a document naming that would
	// round-trip into a mark this package cannot rebuild.
	MarkArc Mark = "arc-diagram"

	// MarkErrorBar is the interval mark: a rule between two bounds, with a cap
	// at each end and a marker at the measurement.
	MarkErrorBar Mark = "errorbar"

	MarkHLine   Mark = "hline"
	MarkVLine   Mark = "vline"
	MarkHBand   Mark = "hband"
	MarkVBand   Mark = "vband"
	MarkSegment Mark = "segment"
	MarkRegion  Mark = "region"
	MarkNote    Mark = "note"
)

The marks. These are figure's own names for its layers; the JSON spec translates them into its own vocabulary rather than the other way round, so that this package stays ignorant of any wire format.

type MarkRows

type MarkRows struct {
	// At are the device positions, and Rows the source row behind each: Rows[i]
	// is the row behind At[i], and a row of -1 marks a position that is not a
	// row. The two are parallel and the same length.
	//
	// Both slices are lent for the duration of the call, like everything else a
	// geom hands out — they come from a pool, and the next frame writes over
	// them.
	At   []ir.Point
	Rows []int
}

MarkRows is one report of which source row is behind which mark.

It is a struct rather than two parallel slices as parameters because a mark has more than a position and a row to say about itself — which layer drew it, what shape it took, how big it was — and a report that grows is a field here rather than a second interface beside Rows. ADR 0060 is the record.

type Missing

type Missing uint8

Missing is the policy for NaN and infinite values in a column.

const (
	Gap Missing = iota
	Interpolate
	Error
)

The missing-data policies. Gap is the default: a hole in the data should look like a hole, not like a straight line someone might read as real.

type Option

type Option func(*config)

Option configures a geom. Options are shared across geom constructors: an option a given geom has no use for is accepted and ignored, which keeps the API one namespace instead of six — including for a geom defined outside this package, which reads what an option set through Configure and adds a knob of its own through Extra.

func Align

func Align(h ir.HAlign, v ir.VAlign) Option

Align sets how a text annotation sits about its position. The default is the run's start on the point, on the baseline — except in a Text layer that labels a box, where a label with nothing said about it is centred in the box rather than hung off its left edge.

That is why the layer records having been told: the start of the run and the baseline are both the zero value and an alignment somebody may have asked for, exactly as a circle is for Shape.

func AvoidOverlap

func AvoidOverlap(on bool) Option

AvoidOverlap places Text labels in source order, trying the original anchor and eight nearby positions before dropping a label that still collides. Labels stay inside the panel rectangle. Box labels are never moved, only omitted. Only participating text layers avoid each other; marks and axis furniture are not obstacles. The default is false.

func Bandwidth

func Bandwidth(bw float64) Option

Bandwidth sets the kernel width a Violin or a Ridgeline estimates its density with, in the data's own units. The default, 0, lets github.com/timzifer/figure/stat.Silverman choose it from each group's own spread.

It is the one number that decides what a density looks like, so pinning it is how several groups are made comparable: a bandwidth chosen per group means each group is smoothed by a different amount, and a difference in shape can then be a difference in sample size rather than in distribution.

func BarWidth

func BarWidth(f float64) Option

BarWidth sets bar width as a fraction of the spacing between adjacent bars, in (0, 1]. The default is 0.8.

func Baseline

func Baseline(v float64) Option

Baseline sets the value bars and areas grow from. The default is 0.

func BinRange

func BinRange(lo, hi float64) Option

BinRange pins the interval a Histogram covers. The default is the extent of the data.

Pinning it is what makes two histograms comparable: bins chosen from each column's own extent put the same value in different places, and a reader comparing two panels is then comparing the axes rather than the data.

func Bins

func Bins(n int) Option

Bins sets how many bins a Histogram divides its column into. The default, 0, lets the layer choose: the Freedman-Diaconis rule where the data has an interquartile range to measure, and Sturges's rule where it does not.

func Budget

func Budget(n int) Option

Budget caps how many marks a reduced layer draws. The default is derived from the width of the plot area, which is the only number that decides how many marks are distinguishable.

It has no effect on a layer that is not reducing, and none on DensityRaster, whose resolution is the plot area itself.

func Caps

func Caps(show bool) Option

Caps turns the crossbars at the ends of an ErrorBar on or off. They are on by default.

Turning them off is the point-range look: a rule with a marker on it and nothing at the ends, which is what a chart with many intervals close together wants — caps that touch read as a grid. The cap is half as wide as a Bar of the same BarWidth would be, so an error bar drawn over a bar chart is narrower than the bar it annotates.

func Closed

func Closed(on bool) Option

Closed joins the last mark of a connected layer back to the first.

It is what turns a Line over an angular axis into a radar contour and an Area over one into a filled one: five axes drawn as an open line leave a gap between the last and the first, which is a hole in a shape that has none. It applies to Line, Area and Step; a layer whose marks are not connected ignores it.

It is an option rather than something coord.Polar decides, because whether a series wraps is a fact about the series and not about the transform: a radar covers every axis and closes, and a polar time series spiralling through three revolutions does not.

func Color

func Color(col ir.Color) Option

Color sets the mark colour, overriding the palette.

func ColorBy

func ColorBy(col string, s scale.ColorScale) Option

ColorBy maps a column through a colour scale, giving every mark its own colour. It applies to Scatter, Bar and Rect; geoms whose mark is one connected shape ignore it.

Which guide the layer then contributes follows from the kind of scale it was handed. A continuous scale — scale.Sequential, scale.Diverging — reads a numeric column and contributes a colourbar, because a single swatch cannot represent a continuum; see ColorGuide. A discrete one — scale.Qualitative — reads a column of categories and contributes one legend entry per category; see Legender.

The same scale also colours the series of a grouped layer, where the group label is the category. Sharing one across the layers of a chart, or across the panels of a facet, is what makes one colour mean one thing everywhere.

func Dash

func Dash(pattern ...float32) Option

Dash sets a dash pattern in device units. Dashing a series is redundant encoding: it keeps the chart readable in greyscale and for colourblind readers.

func Decimate

func Decimate(d Decimation) Option

Decimate sets how a layer reduces its rows before drawing.

The default, AutoDecimation, picks by mark and by size:

  • a line reduces with LTTB once it holds more than a few rows per pixel column, because a line is a shape and LTTB is the reduction that keeps one;
  • a step and a band reduce with MinMax, because their ink is their extremes — a staircase is its transitions and a band is its edges;
  • a scatter switches to a DensityRaster once its markers would cover the plot area several times over, because at that point the marks overplot and the picture says more about row order than about the data;
  • a bar or a boxplot never reduces: those marks already aggregate, and there is no row-level reduction that leaves a bar a bar.

A layer whose colour comes from a column is never reduced automatically: it draws one fact per mark, and a raster of counts would answer a different question. Ask for DensityRaster explicitly to override that.

func DensityCells

func DensityCells(px float64) Option

DensityCells sets the size of one cell in device pixels — the side of a density-raster cell, or the circumradius of a Hexbin's hexagon.

The default is 1 for a raster: one cell per pixel, which is as fine as the output can show, and larger cells trade resolution for a smaller image and a smoother picture. A hexbin's default is DefaultHexRadius instead, because its cells are marks rather than pixels and a lattice of one-pixel hexagons is a raster with extra steps.

func Dodge

func Dodge(padding float64) Option

Dodge places the groups of a slot side by side instead of stacking them, leaving padding of the slot blank between them — 0.1 is a tenth of the slot's width, and the default when Dodge is asked for with a padding outside [0, 1).

It is the other answer to the same question stacking answers, and a layer has one or the other: a dodged layer is not stacked, whatever Stack said. Comparing the parts is what dodging is for and comparing the totals is what stacking is for; a chart that needs both needs two layers.

func Elide

func Elide(on bool) Option

Elide lets a Text layer truncate a label that does not fit the box its row spans, ending it with an ellipsis.

Without it the label is dropped instead, which is the default because a truncated label is a claim about a row that the row does not quite make: a column of "Wareneingang", "Warenausgang" and "Wartung" elides to three labels that are hard to tell apart, and no label at all is honest about it. Turn it on where the first few characters are enough to identify the row.

It does nothing in point mode, where there is no box to overrun.

func ErrorBy

func ErrorBy(col string) Option

ErrorBy selects a column of half-widths, and makes an ErrorBar symmetric about its Y value: the interval runs from y−e to y+e.

It is the spelling a table usually has. A mean and a standard deviation, or a mean and a margin of error, are two columns; turning them into a low and a high column first is arithmetic the caller should not have to do to draw a chart of what they measured.

The measurement is marked, because with this spelling there always is one: the centre is the Y column, so a layer given a spread is a point with an interval around it rather than an interval alone.

func ErrorXBy

func ErrorXBy(col string) Option

ErrorXBy is ErrorBy along the horizontal axis: the interval runs from x−e to x+e about the X value, and the mark lies on its side.

Which axis an error bar runs along follows from the encoding and nothing else, exactly as a Rect's edges do: naming Y2 or ErrorBy puts the interval on the vertical axis, naming X2 or this puts it on the horizontal one. That is what a chart of measurements against categories on the Y axis needs, and it is why there is no orientation option.

func Explode

func Explode(f float64) Option

Explode breaks a layer's marks out of the middle of the coord, by the given fraction of its outer radius.

It is what pulls a slice out of a donut, and a tenth is about as far as one goes before the ring stops reading as a ring:

geom.Bar(src, geom.X("all"), geom.Y("share"), geom.GroupBy("browser"),
    geom.Explode(0.08))

Every mark of the layer moves, which is rarely what a chart means — one slice is usually the point. ExplodeBy is that, per row.

A coord with no middle to move away from ignores it: see github.com/timzifer/figure/coord.Exploder, which coord.Cartesian deliberately does not implement.

func ExplodeBy

func ExplodeBy(col string) Option

ExplodeBy reads the break-out per row from a column of fractions, so that one slice leaves the ring and the rest stay in it:

geom.Bar(src, geom.X("all"), geom.Y("share"), geom.GroupBy("browser"),
    geom.ExplodeBy("pull"))

A row whose value is zero or missing does not move. The column wins over a constant Explode where it has a value, which is how a layer says "this far, except here".

func Extend

func Extend(on bool) Option

Extend controls whether an annotation widens the axis domain to include itself. It is on by default: a threshold line the chart does not reach is still worth seeing, because "we are nowhere near the limit" is the answer the reader came for. Turn it off for an annotation that should appear only when the data reaches it.

func Extra

func Extra(key string, v any) Option

Extra sets an option this package does not define.

It is the one escape from a closed option type: a third-party mark that needs a knob of its own — a lollipop's stem width, a waffle's cell count — takes it here rather than through a second variadic list, so that its constructor reads exactly like a built-in one. The value travels through Desc.Extra and, for a mark that describes itself, through the JSON spec as a property on the mark object — which is why the key should be a name that reads well in a document, and why a value should be something encoding/json can write: a number, a string, a bool, or a list or map of those.

Every geom in this package accepts and ignores it, like any option it has no use for.

func Fill

func Fill(col ir.Color) Option

Fill sets a fill colour separately from the stroke colour, for geoms that have both.

func FontSize

func FontSize(pt float64) Option

FontSize sets the type size of a text annotation in device units. The default is the theme's label size.

func From

func From(col string) Option

From and To name the two ends of an edge, and Value what flows along it. The three are the whole encoding of a Sankey and an Arc: one row per edge, two names and a number.

geom.Sankey(src, geom.From("stage"), geom.To("next"), geom.Value("units"))

The names are read as they are written — github.com/timzifer/figure/data.Labels spells a numeric or temporal column the same way a categorical axis does — and a node is created the first time a row mentions it. That first mention is what fixes the order of everything downstream: the columns a sankey stacks, the arcs a chord diagram goes round in, and which colour of the palette each node takes. It is the order of the table and never the order of a map, so a chart whose panels are built on separate goroutines draws what a serial one drew — see docs/adr/0012-parallel-panels.md.

An edge naming a node only on one side still creates it: a stage nothing leaves is a sink, and dropping it would lose the end of the flow.

func GroupBy

func GroupBy(col string) Option

GroupBy splits a layer's rows into series by the values of a column.

One layer over a long table then draws one mark set per distinct value — N lines, N stacked segments, N bars in a slot — and contributes one legend entry per group rather than one for the layer. It is the prerequisite for every position adjustment: stacking is defined over the groups of a layer.

The column may hold text, numbers or instants; whichever it is, the group key is its formatted label, exactly as data.Labels spells it, so a group key and a categorical axis tick for the same value are the same string.

Colour comes from the layer's colour scale when it was given a discrete one — scale.Qualitative — and from the theme's palette otherwise. A faceted chart wants the scale: a palette index is per panel, and a panel missing one group would shift the colours of every group after it.

func ID

func ID(col string) Option

ID and Parent name a hierarchy: one row per node, the name it is known by and the name of the node above it. With Value they are the whole encoding of a Treemap and an Icicle.

geom.Treemap(src, geom.ID("path"), geom.Parent("under"), geom.Value("bytes"))

A row whose parent is empty, or names a node no row declares, is a root. There may be several, and they divide the whole between them.

They are spelled apart from From and To on purpose, although a hierarchy is an edge table too: a hierarchy's edge runs from the child to its parent and a flow's runs from source to target, so a document that called both "from" and "to" would read a treemap as a flow. The columns are the same shape; what they mean is not.

func KeyBy

func KeyBy(col string) Option

KeyBy names the column that identifies a row across renders, and across charts.

Nothing in this package reads it. A layer draws exactly what it drew before, and the column need not be one the mark plots — it is an answer to a question asked from outside: *which row is this, still*.

It exists because a row number is not an identity. Rows reports the row behind a mark, and that row is an index into the table as it stands for that frame; a table appended to, filtered or windowed renumbers its rows, and a row number carried across two frames of a stream names two different measurements. A key is a value the data already carries, so it survives whatever happens to the ordering.

Two things want one. A tooltip in one chart that highlights a flow in another needs a name for the thing under the pointer that the other chart also knows — see github.com/timzifer/figure.Event.Key. And a transition between two states of a table matches their rows by it, because "the same bar, moved" and "one bar gone and another arrived" are different pictures and only the data can say which this is.

The values are read with github.com/timzifer/figure/data.Label, so a numeric key is spelled the way a facet panel key and a categorical tick are spelled. Keys are not checked for uniqueness: a caller who names a column with duplicates gets duplicates, and Rows and the panel and layer indices are still there beside it.

func Label

func Label(s string) Option

Label names the series in the legend. It defaults to the Y column's name.

func Mid

func Mid(col string) Option

Mid selects the column an ErrorBar marks the measurement at, inside the interval it draws.

It is the third number an interval needs and the reason this mark has a channel of its own: Y and Y2 are the two ends, exactly as they are for the band an Area draws and the box a Rect draws, which leaves the measurement itself nowhere to go. A layer that names no Mid draws the interval alone, which is the honest picture when the interval is all that was measured — a min and a max are not evidence of a mean.

The symmetric spelling needs no Mid: ErrorBy reads the measurement from Y and derives both ends from it, so the value is already named.

func OnMissing

func OnMissing(m Missing) Option

OnMissing sets the NaN/Inf policy.

func OnX2

func OnX2() Option

OnX2 is OnY2 turned a quarter turn: it binds this layer to the chart's secondary *horizontal* axis, the one github.com/timzifer/figure.Plot.X2 sets, drawn along the top of the panel.

It is the option two series measured over different extents of the same thing need — a run indexed by cycle against one indexed by elapsed time, or a spectrum read in wavelength against the same spectrum in wavenumber. The two are independent: a layer may name both, and then it reads the top axis and the right one.

func OnY2

func OnY2() Option

OnY2 binds this layer to the chart's secondary vertical axis, the one github.com/timzifer/figure.Plot.Y2 sets, instead of to its primary one.

It is the option a chart of two quantities in different units needs — revenue as bars against a left axis, margin as a percentage line against a right one — and it is on the *layer* because that is where the binding is: a scale does not know which marks read it, and a chart with two Y axes is one chart with two of them rather than two charts overlaid.

A layer that asks for it in a chart with no secondary axis draws against the primary one, silently, for the reason Explode is silent under a Cartesian coord: an option every mark accepts must not make a chart's validity depend on something set somewhere else.

func Opacity

func Opacity(f float64) Option

Opacity scales the fill alpha, in [0, 1]. The default is 1 for an explicit Fill colour and 0.25 for an area that takes its colour from the palette — a filled band has to sit behind the lines it belongs to.

func Order

func Order(o Ordering) Option

Order sets the order a layer's groups are stacked and listed in.

func Outliers

func Outliers(show bool) Option

Outliers turns the individual points beyond the whiskers on or off. They are on by default: a boxplot that hides them is a boxplot that hides exactly the rows a reader opened the chart to find.

func Overlap

func Overlap(f float64) Option

Overlap sets how far a Ridgeline's tallest ridge rises, in slots of its categorical axis. The default is 1.6.

Overlapping is the point of the chart rather than a defect of it: the ridges are read against each other, and separating them into a grid of little densities is the small multiple this chart exists to compress. Values below 1 keep each ridge inside its own slot.

func Padding

func Padding(f float64) Option

Padding is the gap left between adjacent shapes of a layout that places its own: the space between a treemap's cells, between a sunburst's rings, between a sankey's nodes and between a chord diagram's arcs. It is a fraction of the plot, in [0, 1), and zero — the default — means the shapes touch.

It is a gap rather than a stroke on purpose. A border drawn round a cell is ink a reader has to discount from the area they are being asked to compare, and under hit-testing a stroked outline sits *above* the shape it outlines, so a pointer on the border reports the border — see docs/adr/0015-hit-testing.md.

func Parent

func Parent(col string) Option

Parent is ID's other end: the node this row hangs under.

func Rotate

func Rotate(radians float64) Option

Rotate turns a text annotation about its anchor, in radians clockwise.

func Shape

func Shape(m ir.Marker) Option

Shape sets the marker shape for scatter geoms. Setting it explicitly opts the layer out of a theme's redundant-encoding ladder — see github.com/timzifer/figure/theme.Redundant.

func Size

func Size(s float32) Option

Size sets the marker diameter in device units.

func SizeBy

func SizeBy(col string, s scale.SizeScale) Option

SizeBy maps a column through a size scale, giving every mark its own size. It applies to Scatter; geoms whose mark has a width the axes decide ignore it.

It is the bubble chart, and it is a channel rather than a mark for the same reason a pie is not a geom: what changes is which column decides a mark's size, not what the mark is. The layer contributes a third guide kind beside the legend and the colourbar — a ladder of sample marks with the values they stand for — because a size is a continuum a single swatch cannot represent. See scale.Size for why the mapping is by area.

A sized layer draws circles rather than markers, and that is a consequence of the IR rather than a preference: ir.Backend.Markers carries one style per call, so a layer whose size varied per row would be one drawing call per row. A circle per subpath of one path is one call per colour, and it gives a pointer the mark it is actually inside rather than the nearest centre.

func Smooth

func Smooth(m Smoothing) Option

Smooth sets how a Trend fits.

func Span

func Span(f float64) Option

Span sets the fraction of the rows one local fit of a Trend sees, in (0, 1]. The default is stat.DefaultSpan. It has no effect on a straight fit, which sees all of them.

func Stack

func Stack(s Stacking) Option

Stack sets the position adjustment for a grouped layer. It has no effect on a layer with no GroupBy: one series stacked on itself is the series.

A stacked layer takes its baseline from the adjustment rather than from Baseline, because that is what the adjustment decides: StackZero grows from zero, StackSilhouette centres each slot, and StackWiggle puts the baseline wherever the interior boundaries are flattest.

func Steps

func Steps(where StepPos) Option

Steps sets where a Step geom changes value.

func Tension

func Tension(t float64) Option

Tension smooths a line. 0 (the default) is a plain polyline; values in (0, 1] progressively round the corners using a Catmull-Rom spline.

func TextBy

func TextBy(col string) Option

TextBy names the column a Text layer reads its label from, one label per row. Any column will do: a text column is used as it is, and a numeric or temporal one is formatted the way a category name is, so an axis tick and a label for the same value are the same string.

It is the channel that makes text data rather than annotation. Note places one literal string at one literal position; a layer that labels its rows reads them from the same data.Source every other mark reads, and needs no rebuild when the rows change.

func Thickness

func Thickness(f float64) Option

Thickness is how much of its slot a node fills, in (0, 1]: the width of a sankey's columns and the depth of a chord diagram's ring of arcs. Zero — the default — means the mark's own, which differs between them because a sankey's node is a landmark and a chord's is a rim.

It is not BarWidth, although it means something close, because BarWidth's default is 0.8 rather than zero: a mark could not tell a caller who asked for 0.8 from one who asked for nothing, and a default that cannot be told from a choice is the trap Dash and Stack each carry a companion flag to avoid.

func To

func To(col string) Option

To is From's other end.

func Value

func Value(col string) Option

Value names the column carrying the magnitude of an edge or of a node: the units flowing along a link, the bytes in a directory, the weight of a chord.

For a hierarchy it is usually the leaves that carry a number and the internal nodes that carry none, because an internal node's size is what is under it. A row that carries one anyway keeps it, and its share shows as the part of its box its children do not cover — which is how an unaccounted-for remainder becomes visible rather than invisible.

func Whisker

func Whisker(k float64) Option

Whisker sets how far a boxplot whisker reaches, as a multiple of the interquartile range. The default is 1.5, Tukey's original choice.

func Width

func Width(w float32) Option

Width sets the stroke width in device units.

func WidthBy

func WidthBy(col string) Option

WidthBy reads a bar's width from a column instead of from the spacing of the data, in the axis's own units.

It is what a mosaic reads as: a slot whose width is itself a value. Combine it with Stack and X positions the caller has already accumulated — a marimekko is one layer whose X column holds each column's centre and whose width column holds its share — and the vertical proportions come out of StackFill.

A row with no width, or a negative one, falls back to the slot the spacing of the data implies.

func X

func X(col string) Option

X selects the column mapped to the horizontal axis.

func X2

func X2(col string) Option

X2 selects a second X column, giving a Rect or a Bar its far edge on that axis: a gantt bar runs from a start column to an end column, a candle from open to close.

A mark with no X2 spans its slot instead — a band scale's own bandwidth, or the closest spacing in the data narrowed by BarWidth — which is what a heatmap wants and what makes the cell the size of the category.

Under github.com/timzifer/figure/coord.Donut the X axis is the radius, so the pair is a slice's inner and outer radius and both are dimensions of the data rather than a constant the coord chose:

geom.Bar(src, geom.X("floor"), geom.X2("reach"), geom.Y("share"),
    geom.GroupBy("browser"))

The angular extent is still the stacked Y value, so such a layer reads three columns as three dimensions: how far round the slice goes, where it starts and where it stops.

Dodge still divides the span between the layer's series, whether the span came from a column or from the slot: dodging is about sharing a mark's width, and a row that named its own edges named the width to share.

func Y

func Y(col string) Option

Y selects the column mapped to the vertical axis.

func Y2

func Y2(col string) Option

Y2 selects a second Y column, turning an area into a band between the two series rather than between one series and a baseline. It is how a confidence interval or a min/max envelope is drawn, and how a Rect is given a per-row baseline.

func Z added in v0.9.0

func Z(col string) Option

Z selects the column mapped to the depth axis.

Nothing in this package reads it: a flat chart has no depth, and a geom accepts and ignores an option it has no use for. It lives here rather than in a second option set of its own because the options are one namespace — a parallel set would mean two spellings of X, Color and GroupBy — and because a depth column is a channel of the data rather than a knob of the renderer. The layers that read it are in github.com/timzifer/figure/three. ADR 0056 is the record.

type Ordering

type Ordering uint8

Ordering is the order the groups of a layer are stacked and listed in.

const (
	// OrderAppearance is the order the groups first appear in the table.
	OrderAppearance Ordering = iota

	// OrderValue puts the largest group at the bottom of the stack, measured
	// by its total.
	OrderValue

	// OrderInsideOut puts the groups that peak earliest in the middle of the
	// stack and works outwards, which is the ordering a streamgraph is read
	// with. Byron & Wattenberg pair it with [StackWiggle].
	OrderInsideOut
)

The group orders. OrderAppearance is the default and is the convention the rest of figure already uses: a facet's panels, a categorical axis and a boxplot's groups all come out in order of first appearance in the source table. ADR 0012 is why it is not negotiable — a parallel render must be byte-identical to a serial one, and an order that depends on hashing is an order that depends on scheduling.

type Rows

type Rows interface {
	// Marks attributes device positions to source rows.
	Marks(m MarkRows)
}

Rows is where a layer reports which source row is behind each mark it draws.

It exists for the one question a hit test cannot answer from geometry alone: *which row is this*. A tooltip is happy with the values under the pointer, which come from inverting the scales; highlighting the matching row of a table is not, because a neighbouring row highlighted confidently is a wrong answer rather than a missing one.

Why it is separate from drawing

What a layer *draws* and what a reader can *point at* are not the same points. A smoothed line is a Bézier path whose control points are not measurements; a staircase draws two points per row; a bar is four corners. Attributing rows to the points of a drawing call would therefore mean attributing them to whichever encoding the geom happened to use. So a geom reports its marks — the positions a row actually landed at — separately, and the two are correlated afterwards.

Cost

Nothing, unless someone is listening: Frame.Rows is nil for an ordinary render, and every geom checks it before doing the bookkeeping. See Frame.Marks.

Rows is implemented outside the geom — by whoever wants the answer — so it never gains a method.

type SizeGuide

type SizeGuide struct {
	// Label titles the key. It defaults to the name of the sized column.
	Label string
	// Scale is the trained size scale the key samples.
	Scale scale.SizeScale
	// Color is what the samples are drawn in, so that a key beside a chart of
	// blue bubbles is a key of blue bubbles.
	//
	// It is the transparent zero when the layer takes its colour from the
	// palette, because which palette entry that is depends on where the layer
	// sits in the chart and a guide is collected before there is a frame to ask.
	// The renderer fills it in; see render's guide collection.
	Color ir.Color
}

SizeGuide is the guide a layer needs when its marks take their size from a column: a ladder of sample marks with the values they stand for.

It is the third guide kind, and it is a third kind rather than a variation on a legend because what it shows is neither a swatch nor a ramp — it is the mark itself at several sizes, which is the only way a reader can measure one. A single swatch cannot represent a continuum, and a ramp has no size.

func (SizeGuide) Key

func (g SizeGuide) Key() string

Key identifies a guide by what it looks like, exactly as ColorGuide.Key does and for the same reason: two layers sharing one size scale must produce one key, and comparing the scales themselves is not available through an interface.

The colour is deliberately not part of it. A size key answers "how big is how much", and two layers that read one column through one scale are answering it identically whatever colour they are painted in — so they get one key, drawn in the first of their colours, rather than two keys saying the same thing.

type Sized

type Sized interface {
	// SizeGuide returns the size guide this layer contributes, or ok == false
	// if it has none.
	SizeGuide() (SizeGuide, bool)
}

Sized is implemented by a layer whose marks take their size from a column.

It is an optional interface for the reason Guided is: a layer whose marks are all one size has nothing to say here, and every third-party geom would otherwise have to write a stub returning false.

type Smoothing

type Smoothing uint8

Smoothing is how a Trend fits its line.

const (
	// Loess fits a locally weighted line through the neighbours of each
	// abscissa. See stat.Loess.
	Loess Smoothing = iota

	// LinearFit is ordinary least squares over the whole column: one straight
	// line, which is the right answer when the claim being made is that the
	// relationship *is* linear.
	LinearFit
)

The smoothings. Loess is the default: a trend line's job is to show what the data is doing, and a straight line shows what a straight line would do.

type Sourced

type Sourced interface {
	// Source returns the layer's data.
	Source() data.Source
}

Sourced is implemented by a layer that can hand back the data it holds.

It is the first half of Faceter, spelled on its own because two different callers want it for two different reasons and only one of them can also split a layer. A facet needs both — read the column, then cut the rows. A caller resolving a row's identity needs only the read, and asking it to satisfy an interface with a Subset it will never call would be asking for the wrong thing.

Every Faceter satisfies it, so nothing had to be implemented to make this true and no geom changed.

type Stacking

type Stacking uint8

Stacking is how the segments of one slot are stacked, or NoStack when they are not.

const (
	// NoStack draws every group from the layer's own baseline. Groups then
	// overplot unless [Dodge] separates them, which is right for a line or a
	// scatter and rarely what a bar wants.
	NoStack Stacking = iota

	// StackZero stacks each group on the one before it, from the baseline up.
	StackZero

	// StackFill stacks the groups and scales each slot to fill the axis, so
	// the chart reads as proportions rather than as magnitudes. The axis then
	// runs from 0 to 1.
	StackFill

	// StackSilhouette centres each slot on the baseline: the ThemeRiver.
	StackSilhouette

	// StackWiggle offsets each slot to minimise how much the interior
	// boundaries climb: the streamgraph. See [stat.StackOffsets].
	StackWiggle
)

The position adjustments. NoStack is the default for a layer with no groups; a grouped Bar or Area stacks from zero unless it is told otherwise, which is what a reader expects of a bar chart with a series column and what Vega-Lite does with the same document.

type StepPos

type StepPos uint8

StepPos is where a step geom changes value between two rows.

const (
	StepPost StepPos = iota
	StepPre
	StepMid
)

The step positions. StepPost is the default: the value holds from each row until the next one, which is what a sampled signal or a state over time actually did.

type SwatchKind

type SwatchKind uint8

SwatchKind is how a legend entry draws its sample.

const (
	SwatchLine SwatchKind = iota
	SwatchMarker
	SwatchBox
)

The swatch kinds.

type Training

type Training struct {
	// X and Y are the scales for the horizontal and vertical channels. Both
	// are non-nil.
	X, Y scale.Scale

	// Z is the depth scale, and it is the field this struct was widened for.
	//
	// It is nil for every chart [github.com/timzifer/figure/render] draws,
	// because a panel has two dimensions; it is non-nil for the layers of a
	// [github.com/timzifer/figure/three.Scene]. A geom that never heard of a
	// third dimension reads X and Y and is right to — and a layer that needs
	// one is a three.Layer rather than a Geom, which is what keeps the two
	// from being confusable. ADR 0056 is the record.
	Z scale.Scale
}

Training is what Geom.Train is handed: the scales a layer feeds its data into so they can establish their domains.

It is a struct rather than a parameter list because a chart can gain a dimension — a depth scale, a second radial scale, an axis of time — and a struct with exported fields gains a field where a method signature cannot. Train is implemented outside this module, so widening it again would be a breaking change every time; widening this is additive. ADR 0056 is the record.

Jump to

Keyboard shortcuts

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