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 ¶
- Variables
- func Names() []string
- func Register(t Theme)
- type Option
- func AxisLines(x, y bool) Option
- func Background(c ir.Color) Option
- func Density(f float64) Option
- func FontFamily(name string) Option
- func FontSize(size float64) Option
- func Grid(x, y bool) Option
- func Named(name string) Option
- func Palette(p palette.Qualitative) Option
- func PlotFill(c ir.Color) Option
- func Ramps(sequential, diverging palette.Ramp) Option
- func Redundant(on bool) Option
- func Scaled(f float64) Option
- func TickCounts(x, y int) Option
- func Ticks(x, y bool) Option
- type Theme
- func (t Theme) DivergingRamp() palette.Ramp
- func (t Theme) Font(size float64) ir.FontRef
- func (t Theme) SequentialRamp() palette.Ramp
- func (t Theme) SeriesDash(i int) []float32
- func (t Theme) SeriesMarker(i int) (ir.Marker, bool)
- func (t Theme) Tokens() Tokens
- func (t Theme) With(opts ...Option) Theme
- type Tokens
Constants ¶
This section is empty.
Variables ¶
var Dark = Build(DarkTokens)
Dark is the inverted theme: light ink on a near-black surface.
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.
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.
var DefaultSeriesMarkers = []ir.Marker{ ir.MarkerCircle, ir.MarkerSquare, ir.MarkerTriangle, ir.MarkerDiamond, ir.MarkerPlus, ir.MarkerCross, }
DefaultSeriesMarkers is the shape ladder Redundant uses, starting with the circle a scatter draws anyway.
var Light = Build(LightTokens)
Light is the default theme: dark ink on a white surface.
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 ¶
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 Background ¶
Background sets the canvas colour behind everything.
func Density ¶
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 ¶
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 ¶
FontSize rescales all three text sizes so that the label size becomes size, keeping the ratios between title, label and tick.
func Palette ¶
func Palette(p palette.Qualitative) Option
Palette sets the qualitative sequence layers take their colours from.
func PlotFill ¶
PlotFill sets the fill of the plot area itself, which is transparent by default so the background shows through.
func Ramps ¶
Ramps sets the sequential and diverging ramps a colour scale falls back to. A nil argument leaves that ramp alone.
func Redundant ¶
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 ¶
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 ¶
TickCounts sets how many ticks each axis aims for. They are hints: a scale rounds to whatever produces readable labels.
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 ¶
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 (Theme) DivergingRamp ¶
DivergingRamp returns the theme's diverging ramp, falling back to the package default.
func (Theme) SequentialRamp ¶
SequentialRamp returns the theme's sequential ramp, falling back to the package default so that a zero Theme still produces colours.
func (Theme) SeriesDash ¶
SeriesDash returns the dash pattern for the layer at index i, or nil when the theme carries no redundant encoding.
func (Theme) SeriesMarker ¶
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 ¶
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.
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.