theme

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

Documentation

Overview

Package theme holds the visual tokens a chart is drawn with: colours, fonts, sizes and spacings.

A theme is plain data. It is resolved once, before lowering, and every decision that is "how should this look" reads from it rather than hardcoding a value — which is what makes a second theme a data change instead of a code change.

Tokens

The full Theme is wide, and most of it follows from a handful of choices. Tokens holds those choices and Build derives the rest, so a new theme is a dozen lines rather than fifty. Theme.With edits a built theme, and Register and ByName make a theme reachable by name — which is what a configuration file or a command-line flag needs.

Index

Constants

This section is empty.

Variables

Dark is the inverted theme: light ink on a near-black surface.

View Source
var DarkTokens = Tokens{
	Name:       "dark",
	Background: ir.RGB(0x14, 0x16, 0x1A),
	Panel:      ir.Transparent,
	Strip:      ir.RGB(0x22, 0x26, 0x2D),

	Ink:       ir.RGB(0xF2, 0xF3, 0xF5),
	InkMuted:  ir.RGB(0xC8, 0xCB, 0xD0),
	InkSubtle: ir.RGB(0xA0, 0xA4, 0xAB),

	Line:       ir.RGB(0x9A, 0x9E, 0xA6),
	LineSubtle: ir.RGB(0x2A, 0x2E, 0x35),

	FontSize: 12,
	Space:    4,

	Palette:    palette.OkabeIto,
	Sequential: palette.Viridis,
	Diverging:  palette.BlueOrange,
}

DarkTokens are the choices the dark theme makes: light ink on near-black.

The axis is deliberately not the same grey as the labels. On a dark surface a line at the label's lightness reads heavier than the same line does on white, because it is the brighter of the two against its background rather than the darker.

View Source
var DefaultSeriesDashes = [][]float32{
	nil,
	{6, 3},
	{1.5, 2.5},
	{7, 3, 1.5, 3},
	{12, 4},
	{4, 2, 1.5, 2, 1.5, 2},
}

DefaultSeriesDashes is the ladder Redundant uses. The first entry is nil — a solid line — so that a single-layer chart looks the way it always did.

The patterns are in device units at the theme's own scale; Scaled scales them with everything else, because a dash that stayed 6pt long on a chart drawn at half size would read as a different pattern.

DefaultSeriesMarkers is the shape ladder Redundant uses, starting with the circle a scatter draws anyway.

Light is the default theme: dark ink on a white surface.

View Source
var LightTokens = Tokens{
	Name:       "light",
	Background: ir.RGB(0xFF, 0xFF, 0xFF),
	Panel:      ir.Transparent,
	Strip:      ir.RGB(0xEE, 0xEF, 0xF1),

	Ink:       ir.RGB(0x1A, 0x1A, 0x1A),
	InkMuted:  ir.RGB(0x33, 0x33, 0x33),
	InkSubtle: ir.RGB(0x55, 0x55, 0x55),

	Line:       ir.RGB(0x33, 0x33, 0x33),
	LineSubtle: ir.RGB(0xDD, 0xDD, 0xDD),

	FontSize: 12,
	Space:    4,

	Palette:    palette.OkabeIto,
	Sequential: palette.Viridis,
	Diverging:  palette.BlueOrange,
}

LightTokens are the choices the light theme makes: dark ink on white.

Functions

func Names

func Names() []string

Names lists the registered theme names in order.

func Register

func Register(t Theme)

Register adds t to the registry under its own name, replacing any theme already registered under it. A theme with an empty name is ignored.

Types

type Option

type Option func(*Theme)

Option edits a theme. Options exist so that "the dark theme, in my typeface, without a grid" is a call rather than a copy of the struct with three fields changed and forty copied.

func AxisLines

func AxisLines(x, y bool) Option

AxisLines turns the axis rules on or off.

func Background

func Background(c ir.Color) Option

Background sets the canvas colour behind everything.

func Density

func Density(f float64) Option

Density scales every spacing by f, tightening or loosening the whole chart at once. Text sizes are left alone: a dense chart with unreadable labels is not denser, it is worse.

func FontFamily

func FontFamily(name string) Option

FontFamily sets the logical font family. It is a name, not a file: the backend resolves it, and the SVG backend passes it through to the viewer.

func FontSize

func FontSize(size float64) Option

FontSize rescales all three text sizes so that the label size becomes size, keeping the ratios between title, label and tick.

func Grid

func Grid(x, y bool) Option

Grid turns the horizontal and vertical grid lines on or off.

func Named

func Named(name string) Option

Named sets the theme's name, which is what Register keys it under.

func Palette

func Palette(p palette.Qualitative) Option

Palette sets the qualitative sequence layers take their colours from.

func PlotFill

func PlotFill(c ir.Color) Option

PlotFill sets the fill of the plot area itself, which is transparent by default so the background shows through.

func Ramps

func Ramps(sequential, diverging palette.Ramp) Option

Ramps sets the sequential and diverging ramps a colour scale falls back to. A nil argument leaves that ramp alone.

func Redundant

func Redundant(on bool) Option

Redundant turns redundant encoding on: every layer gets a dash pattern and a marker shape of its own alongside its colour, so that a chart survives being printed in greyscale, photocopied, or read by the eight percent of men who cannot separate its first two palette entries.

It changes nothing about a layer that named its own [Dash] or [Shape]: an explicit choice is a choice, and this is a default.

The dash ladder runs solid, dashed, dotted, dash-dot, long-dash, and the marker ladder circle, square, triangle, diamond, plus, cross. Both start with what a chart already draws, so the first layer is unchanged and only the second one onwards picks up a difference — which is the right shape for a default, and the reason the ladders are not alphabetical.

Pass false to turn it back off, which is what a theme built from tokens already is.

func Scaled

func Scaled(f float64) Option

Scaled multiplies every length in the theme by f: type sizes, stroke widths, spacings, the marker diameter, the margin. It is what makes a chart responsive — a plot drawn at half the size it was designed for wants half the type, not the same 12pt labels crowding a smaller canvas.

It differs from Density, which moves the spacings and deliberately leaves the text alone: Density is a decision about how tightly a chart is packed, and this one is a decision about how big the whole drawing is. Applying it to the text is the entire point.

Colours are not lengths and are left alone. See github.com/timzifer/figure.Responsive, which applies this to a plot from the size it is actually being drawn at.

func TickCounts

func TickCounts(x, y int) Option

TickCounts sets how many ticks each axis aims for. They are hints: a scale rounds to whatever produces readable labels.

func Ticks

func Ticks(x, y bool) Option

Ticks turns each axis's tick marks and tick labels on or off. It is what a pie or a donut asks for: neither has an axis worth labelling.

type Theme

type Theme struct {
	Name string

	// Surfaces.
	Background ir.Color // outside the plot area
	PlotFill   ir.Color // inside the plot area

	// Text.
	FontFamily string
	TitleSize  float64
	LabelSize  float64
	TickSize   float64
	TitleColor ir.Color
	LabelColor ir.Color
	TickColor  ir.Color

	// Axes and grid.
	AxisColor     ir.Color
	AxisWidth     float32
	GridColor     ir.Color
	GridWidth     float32
	GridDash      []float32
	TickLength    float32
	TickLabelPad  float32
	AxisTitlePad  float32
	ShowGridX     bool
	ShowGridY     bool
	ShowAxisLineX bool
	ShowAxisLineY bool
	// ShowTicksX and ShowTicksY turn each axis's tick marks and tick labels on
	// or off. They are the third of the three furniture switches, and the one
	// a chart drawn in a coordinate system that does not want an axis reaches
	// for: a pie has no radial quantity to label, and the numbers a linear
	// scale puts round the outside of one are noise about a slot rather than
	// information about the data.
	ShowTicksX     bool
	ShowTicksY     bool
	TickCountHintX int
	TickCountHintY int

	// Legend.
	LegendSwatch  float32
	LegendPad     float32
	LegendGap     float32
	LegendColor   ir.Color
	LegendBG      ir.Color
	LegendBorder  ir.Color
	LegendPadding float32

	// Guides. A chart can carry more than one — a legend for the layers that
	// have a colour each, a colourbar for a layer whose colour comes from a
	// continuous scale — and they stack in one column beside the plot.
	GuideGap float32 // vertical gap between stacked guides

	// Colourbar.
	ColorbarThickness float32 // across the bar
	ColorbarFraction  float64 // of the plot height, before clamping
	ColorbarBorder    ir.Color
	ColorbarTickCount int

	// Size key: the ladder of sample marks a size channel is read off.
	// BubbleSize is the diameter of the largest mark a size scale draws, and
	// therefore the widest sample the key shows; SizeKeyCount is how many
	// samples it shows.
	BubbleSize   float32
	SizeKeyCount int

	// Annotations: reference lines, bands and labels that are not data.
	AnnotationColor   ir.Color
	AnnotationWidth   float32
	AnnotationDash    []float32
	AnnotationOpacity float64 // fill alpha for a shaded band

	// Panels: subplots and facets.
	PanelGap    float32 // between adjacent panels
	StripBG     ir.Color
	StripColor  ir.Color
	StripSize   float64
	StripPad    float32
	StripBorder ir.Color

	// Spacing.
	Margin float32 // outer margin on all four sides

	// Series colours, and the ramps a colour scale falls back to.
	Palette    palette.Qualitative
	Sequential palette.Ramp
	Diverging  palette.Ramp

	// SeriesDashes and SeriesMarkers are the redundant encoding: a second and
	// a third channel a layer is told apart by, so that colour is not the only
	// one. A layer that set neither [github.com/timzifer/figure/geom.Dash]
	// nor [github.com/timzifer/figure/geom.Shape] takes the entry at its own
	// index, the same way it takes its colour from Palette.
	//
	// Both are empty by default, which draws exactly what figure has always
	// drawn. [Redundant] fills them in. See docs/adr/0024.
	SeriesDashes  [][]float32
	SeriesMarkers []ir.Marker

	// A projected scene. A flat chart needs none of this; a three-dimensional
	// one needs to know which way the light falls and what a face turned
	// toward it and away from it are mixed toward. There is one directional
	// light and no model beyond it: no specular, no shadows, no ambient
	// occlusion, no textures — see docs/adr/0056-three-dimensional-charts.md.
	//
	// DepthTop and DepthSide are ADR 0055's two shades under ADR 0055's
	// names: a mark extruded by an oblique coord and a face of a surface are
	// both a colour seen in depth, and one ladder is right for both.
	LightDir   [3]float32 // unit direction the light comes from, in scene space
	LightFloor float64    // how lit the least-lit face still is, in [0, 1]
	DepthTop   ir.Color   // a face turned toward the light mixes toward this
	DepthSide  ir.Color   // one turned away mixes toward this
	CubeFill   ir.Color   // the three faces a projected cube shows
	CubeEdge   ir.Color   // its edges
	CubeGrid   ir.Color   // the grid drawn on those faces

	// Default geometry weights.
	LineWidth  float32
	MarkerSize float32
}

Theme is the full set of visual tokens for a chart.

func Build

func Build(t Tokens) Theme

Build derives a full theme from its tokens.

The type scale is a perfect fourth up for the title and one step down for ticks — 12 gives 16 and 11, which is where the hand-written v0.1 themes already were. The ratios are written as exact fractions rather than decimals so that a round base size produces round derived sizes rather than 15.999.

Spacings are multiples of Tokens.Space. With the default unit of 4 that reproduces the v0.1 numbers exactly, which is the point: a derived theme has to be able to express what was there before it, or the derivation is telling you what to want.

func ByName

func ByName(name string) (Theme, bool)

ByName returns the registered theme with the given name.

func (Theme) DivergingRamp

func (t Theme) DivergingRamp() palette.Ramp

DivergingRamp returns the theme's diverging ramp, falling back to the package default.

func (Theme) Font

func (t Theme) Font(size float64) ir.FontRef

Font builds a font reference from the theme's family at the given size.

func (Theme) SequentialRamp

func (t Theme) SequentialRamp() palette.Ramp

SequentialRamp returns the theme's sequential ramp, falling back to the package default so that a zero Theme still produces colours.

func (Theme) SeriesDash

func (t Theme) SeriesDash(i int) []float32

SeriesDash returns the dash pattern for the layer at index i, or nil when the theme carries no redundant encoding.

func (Theme) SeriesMarker

func (t Theme) SeriesMarker(i int) (ir.Marker, bool)

SeriesMarker returns the marker shape for the layer at index i, and ok false when the theme carries no redundant encoding — which leaves the geom's own default in place rather than replacing it with a circle.

func (Theme) Tokens

func (t Theme) Tokens() Tokens

Tokens returns the tokens a theme was built from, as far as they can be read back off it.

It is not an inverse of Build — a theme that has been edited afterwards may no longer be derivable from any tokens at all. It is what makes "the dark theme, but in my brand's typeface" one call rather than a rebuild from scratch.

func (Theme) With

func (t Theme) With(opts ...Option) Theme

With returns a copy of t with the options applied. The receiver is not modified, so package-level themes stay what they are.

type Tokens

type Tokens struct {
	// Name identifies the theme, and is the key it registers under.
	Name string

	// Background is the canvas outside the plot area; Panel is the fill
	// inside it, normally transparent so the background shows through; Strip
	// is the band a facet's label sits in.
	Background ir.Color
	Panel      ir.Color
	Strip      ir.Color

	// Ink runs from the strongest text to the faintest: a title, a label, a
	// tick. Three levels is enough to build a hierarchy and few enough that a
	// second theme has to answer only three questions.
	Ink       ir.Color
	InkMuted  ir.Color
	InkSubtle ir.Color

	// Line is the axis; LineSubtle is the grid behind the data.
	Line       ir.Color
	LineSubtle ir.Color

	// FontFamily is the logical family; "" means the backend's default sans.
	FontFamily string

	// FontSize is the label size in device units. The title and tick sizes
	// derive from it — see [Build].
	FontSize float64

	// Space is the spacing unit every padding and gap is a multiple of.
	Space float64

	// Palette colours the layers of a chart; Sequential and Diverging are the
	// ramps a colour scale falls back to when it is given none.
	Palette    palette.Qualitative
	Sequential palette.Ramp
	Diverging  palette.Ramp
}

Tokens is the small set of decisions a theme actually makes.

A Theme has around fifty fields, and almost all of them are consequences rather than choices: a tick label is the same colour as the other faint ink, the gap under an axis title is the same rhythm as the gap beside a legend. Writing all fifty out by hand is how two themes drift apart in ways nobody intended. Tokens holds the choices — four inks, two line weights, one type size, one spacing unit, the colours — and Build derives the rest.

Anything derived can still be overridden afterwards: Build returns a plain struct, and Theme.With edits it. Tokens is the starting point, not a cage.

Jump to

Keyboard shortcuts

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