Documentation
¶
Overview ¶
Package scale maps data values onto visual positions and generates the ticks that label them.
A Scale owns two things: the domain→range mapping, and the choice of tick positions and labels for that domain. Keeping both in one place is what lets a time axis label itself in calendar units while a linear axis labels itself with round numbers, without either the geom or the layout knowing which is which.
How the options are named ¶
Each constructor has its own option type — LinearOption, LogOption, SymLogOption, TimeOption, OrdinalOption, SizeOption, ColorOption — because the choices differ: a log axis has a base and a linear one does not. The bare names belong to the default scale: Domain, Nice, Zero and Format configure Linear, which is what a plot has when nobody chose, and every other family carries its constructor's name as a prefix — LogDomain, [SymLogNice], SizeRange, ColorReverse — with Time's In and Origin the exceptions that read as English. A name that does not compile against the scale it was meant for is the intended failure: the option types are what keep Nice from being silently accepted and ignored by a log axis.
Index ¶
- Constants
- Variables
- func ColorPositionOf(cs ColorScale, v float64) float64
- func ColorValueOf(cs ColorScale, t float64) float64
- func FromNanos(v float64) time.Time
- func InstantOf(s Scale, v float64) time.Time
- func LabelOf(s Scale, v float64) string
- func Localize(s Scale, l *Locale) bool
- func Nanos(t time.Time) float64
- func Register(k Kind, build func(Desc) (Scale, error))
- func RegisterColor(k ColorKind, build func(ColorDesc) (ColorScale, error))
- func RegisterLocale(l *Locale)
- func ValueOf(s Scale, t time.Time) float64
- type Band
- type Categorical
- type ClassedColorScale
- func Classed(s ColorScale) (ClassedColorScale, bool)
- func Quantile(ramp palette.Ramp, classes int, opts ...ColorOption) ClassedColorScale
- func Quantize(ramp palette.Ramp, classes int, opts ...ColorOption) ClassedColorScale
- func Threshold(ramp palette.Ramp, breaks []float64, opts ...ColorOption) ClassedColorScale
- type Cloner
- type ColorDesc
- type ColorDescriber
- type ColorKind
- type ColorOption
- func ColorCenter(v float64) ColorOption
- func ColorDomain(min, max float64) ColorOption
- func ColorFallback(p palette.Qualitative) ColorOption
- func ColorLog(base float64) ColorOption
- func ColorReverse() ColorOption
- func ColorSymLog(base, threshold float64) ColorOption
- func ColorUndefined(col ir.Color) ColorOption
- type ColorScale
- type ColorTransform
- type ColorTransformer
- type Definite
- type Desc
- type Describer
- type DiscreteColorScale
- type Kind
- type Labeller
- type LinearOption
- type Locale
- type Localizer
- type LogOption
- type OrdinalOption
- type Scale
- type SizeDesc
- type SizeDescriber
- type SizeOption
- type SizeScale
- type Snapshotter
- type SymLogOption
- type Temporal
- type Tick
- type TickRequest
- type TimeOption
- type Zoomer
Constants ¶
const DefaultMaxSize = 32
DefaultMaxSize is the largest bubble a size scale draws when nothing has set its range. A geom overrides it from the theme before it draws; this is what a scale used on its own, in a test or by a caller driving one directly, answers with.
Variables ¶
var ( // LocaleDE is German. The group separator is a full stop and the decimal // separator a comma, which is the pair that makes an unlocalised German // chart not merely foreign but wrong: "1.234" reads as one and a bit. LocaleDE = &Locale{ Name: "de", Decimal: ",", Group: ".", Minus: "-", Percent: " %", Months: [12]string{"Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"}, ShortMonths: [12]string{"Jan", "Feb", "Mär", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"}, Days: [7]string{"Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"}, ShortDays: [7]string{"So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"}, } // LocaleFR is French. Its group separator is the narrow no-break space // U+202F, which is what typesetting practice and CLDR both call for; a // plain space would let a label break across two lines. LocaleFR = &Locale{ Name: "fr", Decimal: ",", Group: " ", Minus: "-", Percent: " %", Months: [12]string{"janvier", "février", "mars", "avril", "mai", "juin", "juillet", "août", "septembre", "octobre", "novembre", "décembre"}, ShortMonths: [12]string{"janv.", "févr.", "mars", "avr.", "mai", "juin", "juil.", "août", "sept.", "oct.", "nov.", "déc."}, Days: [7]string{"dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi"}, ShortDays: [7]string{"dim.", "lun.", "mar.", "mer.", "jeu.", "ven.", "sam."}, } // LocaleES is Spanish. LocaleES = &Locale{ Name: "es", Decimal: ",", Group: ".", Minus: "-", Percent: " %", Months: [12]string{"enero", "febrero", "marzo", "abril", "mayo", "junio", "julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre"}, ShortMonths: [12]string{"ene", "feb", "mar", "abr", "may", "jun", "jul", "ago", "sep", "oct", "nov", "dic"}, Days: [7]string{"domingo", "lunes", "martes", "miércoles", "jueves", "viernes", "sábado"}, ShortDays: [7]string{"dom", "lun", "mar", "mié", "jue", "vie", "sáb"}, } // LocaleIT is Italian. LocaleIT = &Locale{ Name: "it", Decimal: ",", Group: ".", Minus: "-", Percent: "%", Months: [12]string{"gennaio", "febbraio", "marzo", "aprile", "maggio", "giugno", "luglio", "agosto", "settembre", "ottobre", "novembre", "dicembre"}, ShortMonths: [12]string{"gen", "feb", "mar", "apr", "mag", "giu", "lug", "ago", "set", "ott", "nov", "dic"}, Days: [7]string{"domenica", "lunedì", "martedì", "mercoledì", "giovedì", "venerdì", "sabato"}, ShortDays: [7]string{"dom", "lun", "mar", "mer", "gio", "ven", "sab"}, } // LocaleNL is Dutch. LocaleNL = &Locale{ Name: "nl", Decimal: ",", Group: ".", Minus: "-", Percent: "%", Months: [12]string{"januari", "februari", "maart", "april", "mei", "juni", "juli", "augustus", "september", "oktober", "november", "december"}, ShortMonths: [12]string{"jan", "feb", "mrt", "apr", "mei", "jun", "jul", "aug", "sep", "okt", "nov", "dec"}, Days: [7]string{"zondag", "maandag", "dinsdag", "woensdag", "donderdag", "vrijdag", "zaterdag"}, ShortDays: [7]string{"zo", "ma", "di", "wo", "do", "vr", "za"}, } // LocalePT is Portuguese. LocalePT = &Locale{ Name: "pt", Decimal: ",", Group: ".", Minus: "-", Percent: "%", Months: [12]string{"janeiro", "fevereiro", "março", "abril", "maio", "junho", "julho", "agosto", "setembro", "outubro", "novembro", "dezembro"}, ShortMonths: [12]string{"jan", "fev", "mar", "abr", "mai", "jun", "jul", "ago", "set", "out", "nov", "dez"}, Days: [7]string{"domingo", "segunda-feira", "terça-feira", "quarta-feira", "quinta-feira", "sexta-feira", "sábado"}, ShortDays: [7]string{"dom", "seg", "ter", "qua", "qui", "sex", "sáb"}, } // LocaleJA is Japanese. Its months are numbered rather than named, which // is why the tables are strings rather than an index into anything. LocaleJA = &Locale{ Name: "ja", Decimal: ".", Group: ",", Minus: "-", Percent: "%", Months: [12]string{"1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"}, ShortMonths: [12]string{"1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"}, Days: [7]string{"日曜日", "月曜日", "火曜日", "水曜日", "木曜日", "金曜日", "土曜日"}, ShortDays: [7]string{"日", "月", "火", "水", "木", "金", "土"}, } )
German, French, Spanish, Italian, Dutch, Portuguese and Japanese ship because they are the ones figure could get right, not because they are the ones the world needs. A locale is eight strings and four tables; anything else is three lines and a RegisterLocale call, and that is the supported way to add one rather than a workaround. Shipping a table figure cannot check is worse than shipping none: a chart with a misspelled month is a chart nobody can trust the rest of.
var English = &Locale{ Name: "en", Decimal: ".", Group: ",", Minus: "-", Percent: "%", Months: [12]string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}, ShortMonths: [12]string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}, Days: [7]string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}, ShortDays: [7]string{"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}, }
English is the locale every scale uses until it is given another. Its tables are Go's own, so a chart that never mentions a locale draws exactly what it drew before locales existed — which is what makes this addition invisible to every existing golden file.
var ErrFormat = fmt.Errorf("figure/scale: invalid number format")
ErrFormat reports a number format spec this package cannot read. It is returned by FromDesc, which reads a spec that came from a document.
var ErrUnknownKind = fmt.Errorf("figure/scale: unknown scale kind")
ErrUnknownKind reports a Desc naming a scale this package does not have.
Functions ¶
func ColorPositionOf ¶
func ColorPositionOf(cs ColorScale, v float64) float64
ColorPositionOf returns where v sits along cs's ramp, in [0, 1], falling back to a linear reading of the domain for a scale that is not a ColorTransformer.
func ColorValueOf ¶
func ColorValueOf(cs ColorScale, t float64) float64
ColorValueOf returns the value cs's ramp reaches at t, the inverse of ColorPositionOf.
func InstantOf ¶
InstantOf converts a domain value of s back into an instant, falling back to FromNanos for a scale that is not Temporal.
func LabelOf ¶
LabelOf writes v the way s labels its ticks, falling back to the label of a tick at exactly v for a scale that is not a Labeller, and to the empty string when there is none.
func Localize ¶
Localize sets a scale's locale if it has one, reporting whether it did.
It is how a chart gives every one of its axes the same language in one place: github.com/timzifer/figure.Locale walks the scales a plot holds and calls this. Doing it per scale at construction works too and is what a scale used on its own does.
func Register ¶
Register makes a scale kind this package does not define buildable by FromDesc, and therefore readable from a JSON spec.
The kind is the one the scale's Describer reports, and the one a document carries as the scale's type. A kind this package owns — every Kind constant — is refused with a panic, because shadowing a built-in would change what every existing document means. Registering a kind twice replaces the earlier builder.
A registered scale is rebuilt from the fields of Desc alone: a fixed domain, the framing flags, a category list. A scale whose configuration does not fit in a Desc round-trips what does and no more, the way a scale with a Go formatter does — see Desc.Formatted.
Nothing iterates the registry and lookups are guarded by a lock, so registering from an init function is safe and registration order never changes what a chart draws.
func RegisterColor ¶
func RegisterColor(k ColorKind, build func(ColorDesc) (ColorScale, error))
RegisterColor is Register for colour scales: it makes a ColorKind this package does not define buildable by ColorFromDesc.
func RegisterLocale ¶
func RegisterLocale(l *Locale)
RegisterLocale makes a locale findable by name, so that a chart written down as JSON reads back with the labels it was written with.
It is the same bargain Register makes for a third party's scale kind: the document carries a name, the process carries the tables, and a name nothing registered reads back as the default rather than as an error — a chart in the wrong language is a chart, and refusing to draw it would be worse. Registering a name twice replaces the earlier locale.
Types ¶
type Band ¶
type Band interface {
// Bandwidth returns the width of one slot in device units, after padding.
Bandwidth() float32
}
Band is implemented by scales that give each category a slot of finite width, which is what a bar or a boxplot needs in order to size itself.
A geom that finds a Band on its axis takes the width from the scale instead of guessing one from the spacing of the data.
type Categorical ¶
type Categorical interface {
// Encode returns the domain value for a category label, registering the
// label if the scale has not seen it before.
Encode(label string) float64
// Labels returns the categories in axis order.
Labels() []string
}
Categorical is implemented by scales that position named categories rather than numbers, so that a geom can map a string column onto an axis.
The numeric domain of such a scale is the category index: Encode turns a label into the index that Scale.Map positions, which is what lets one Scale interface serve both continuous and categorical axes.
type ClassedColorScale ¶
type ClassedColorScale interface {
ColorScale
// Breaks returns the interior class boundaries, ascending. A scale with n
// classes has n-1 of them, and the outer edges of the outermost classes
// are the ends of [ColorScale.Domain].
//
// The slice is freshly allocated, so a caller may keep it.
Breaks() []float64
// Classes reports how many classes the scale cuts its domain into.
Classes() int
}
ClassedColorScale paints a finite set of classes rather than a continuum: the domain is cut at a handful of boundaries and everything inside one gets a single colour.
It rides the ColorScale interface the way DiscreteColorScale does, and it is a third thing rather than a variation on either. Its input is a number, so a layer binds it through the same option and reads it through ColorScale.Color — but its output is a short list of colours, so a reader can name the class a mark is in instead of estimating a value from a ramp. That is what a choropleth is for, and what a heatmap over counts is often better read as: "between a hundred and a thousand" is a fact a reader can carry, and "about a fifth of the way up a green" is not.
A classed scale still contributes a colourbar rather than legend entries, because its classes are intervals of a quantity rather than names: the bar is drawn in steps and labelled at the boundaries.
func Classed ¶
func Classed(s ColorScale) (ClassedColorScale, bool)
Classed reports whether a colour scale paints a finite set of classes, which is what decides whether its colourbar is a gradient or a ladder of steps.
func Quantile ¶
func Quantile(ramp palette.Ramp, classes int, opts ...ColorOption) ClassedColorScale
Quantile returns a colour scale that cuts the domain so that each class holds as near as possible the same number of observations.
It is the scale for a quantity whose distribution is the thing worth seeing: a count, an income, a latency. Equal-width classes over such data put nearly every observation in one class and leave the rest to name a handful of outliers, which is Quantize's honest failure and this scale's whole purpose. What it gives up in exchange is that a class no longer stands for a fixed span — two charts drawn from different rows cut at different values, so a quantile ramp is read against its own bar and never against another's.
Cost ¶
Quantiles cannot be computed from a running minimum and maximum, so this is the one colour scale that keeps what it is trained on: every finite value, held until the scale is discarded. That is a fixed cost per row rather than per scale, and it is the reason the boundaries are recomputed at the end of each ColorScale.Train rather than lazily — training is serial and drawing is not, so a scale that sorted itself the first time a mark asked for a colour would be sorting itself from several panels at once.
A chart redrawn from a github.com/timzifer/figure/data.Stream should therefore hand each frame a scale of its own, or compute the boundaries once and pin them with Threshold. A scale trained on every frame of a live chart has kept every frame of it.
A nil ramp uses palette.DefaultRamp.
func Quantize ¶
func Quantize(ramp palette.Ramp, classes int, opts ...ColorOption) ClassedColorScale
Quantize returns a colour scale that cuts the domain into equal classes.
Equal in the space the ramp runs in: under ColorLog the classes are decades rather than equal spans, which is the point of asking for both. A count of zero or less means one class.
It is the scale for a quantity a reader should read in bands without the bands claiming to say anything about the distribution. When the bands should say something about it — equally many observations in each — the scale is Quantile.
A nil ramp uses palette.DefaultRamp.
func Threshold ¶
func Threshold(ramp palette.Ramp, breaks []float64, opts ...ColorOption) ClassedColorScale
Threshold returns a colour scale that cuts the domain at boundaries given explicitly. A value lands in the class above a boundary it is equal to, so breaks of 10 and 100 make three classes: below 10, 10 up to 100, and 100 and over.
It is the scale for boundaries that come from outside the data — a legal limit, a service level, a clinical range. Boundaries derived *from* the data are Quantize's and Quantile's business.
The breaks are sorted and de-duplicated, so the caller need not. Passing none makes a scale of one class, which is a solid colour: that is what "cut this nowhere" means, and it is more useful than a panic in a chart whose breaks came from a configuration file.
A nil ramp uses palette.DefaultRamp.
type Cloner ¶
type Cloner interface {
// Clone returns an untrained copy with the same configuration. A fixed
// domain counts as configuration and is kept.
Clone() Scale
}
Cloner is implemented by a scale that can hand back a fresh copy of itself: the same configuration, with nothing trained into it yet.
Faceting needs it for free scales. "Each panel gets its own Y axis" means each panel gets its own scale object, configured the way the plot's scale was but trained only on that panel's rows — and the only thing that knows how a scale was configured is the scale.
It is an optional interface. A scale that does not implement it can still be shared across panels, which is the default and by far the common case; only a free axis needs a copy, and a facet says so rather than guessing.
type ColorDesc ¶
type ColorDesc struct {
Kind ColorKind
Ramp string
Colors palette.Ramp
Min, Max float64
Fixed bool
Center float64
Reverse bool
Undefined ir.Color
// Transform is the shape of the ramp's traversal of the domain, empty for
// the linear default. Base and Constant configure it: the logarithm's
// base, and a symmetric logarithm's linear threshold. Zero means the
// default for both, which is what an omitted field in a document reads as.
//
// They are separate from Kind because they are a separate choice: a
// diverging ramp over a log-fold change is diverging *and* logarithmic,
// and folding the two into one word would make one of them unsayable.
Transform ColorTransform
Base float64
Constant float64
// Labels are a [KindNamed] scale's enumerated categories, sorted, with
// Colors holding the colour of each index for index. Only the categories
// the caller named are written: one the data happened to contain is data
// rather than configuration, the same line this struct draws for a
// classed scale's derived breaks. Fallback is the palette that scale
// colours an unnamed category from, spelled out where it has no
// registered name — Ramp carries the name where it has one.
Labels []string
Fallback palette.Ramp
// Breaks are a [KindThreshold] scale's class boundaries, and Classes the
// class count of a [KindQuantize] or [KindQuantile] one. Each kind carries
// only its own: boundaries a scale derives from the data are not
// configuration, and a document that pinned them would stop them being
// derived the next time it is drawn over different rows.
Breaks []float64
Classes int
}
ColorDesc is a colour scale reduced to what configures it.
Ramp is a name from palette.RampByName rather than a list of colours: a registered ramp is a word, and a chart that named one should read back as having named it. A ramp nobody registered has no name, so Colors carries it literally instead — an unregistered ramp is still a ramp, and losing it would be worse than spelling it out.
A KindQualitative scale uses the same two fields for its palette, named through palette.QualitativeByName. Min, Max, Fixed and Center have no meaning for one: its domain is the labels it has been shown, and those are the data rather than the scale — the same line Desc draws for a discovered ordinal domain.
func DescribeColor ¶
func DescribeColor(s ColorScale) (ColorDesc, bool)
DescribeColor reports s's configuration, or ok == false if s cannot describe itself.
type ColorDescriber ¶
type ColorDescriber interface {
// DescribeColor returns the scale's configuration.
DescribeColor() ColorDesc
}
ColorDescriber is implemented by a colour scale that can say what it is.
type ColorKind ¶
type ColorKind string
ColorKind names a colour scale's type.
const ( KindSequential ColorKind = "sequential" KindDiverging ColorKind = "diverging" // KindQualitative is a discrete scale: one colour per category, from a // qualitative palette rather than from a ramp. See [Qualitative]. KindQualitative ColorKind = "qualitative" // KindNamed is a discrete scale whose categories are coloured by name // rather than by the order they appear in. See [Named]. KindNamed ColorKind = "named" // KindThreshold cuts the domain at boundaries given explicitly. See // [Threshold]. KindThreshold ColorKind = "threshold" // KindQuantize cuts it into equal classes. See [Quantize]. KindQuantize ColorKind = "quantize" // KindQuantile cuts it so that each class holds equally many // observations. See [Quantile]. KindQuantile ColorKind = "quantile" )
The colour scale kinds.
type ColorOption ¶
type ColorOption func(*colorScale)
ColorOption configures a colour scale.
func ColorCenter ¶
func ColorCenter(v float64) ColorOption
ColorCenter pins the value that lands on the middle of a diverging ramp. The default is 0.
It has no effect on a sequential scale, where there is no middle to pin.
func ColorDomain ¶
func ColorDomain(min, max float64) ColorOption
ColorDomain pins the domain explicitly, disabling training.
func ColorFallback ¶
func ColorFallback(p palette.Qualitative) ColorOption
ColorFallback sets the palette a Named scale colours a label it was not given a colour for from. The default is palette.Default.
It is a separate option from ColorUndefined because the two answer different questions. Undefined is for a value that is not a category at all — a null, a NaN — and drawing nothing is the honest answer to it. A label the caller did not enumerate *is* a category, and it is one the chart should show: it is how a reader learns that the machine reported a state nobody wrote down. It has no effect on any other kind of scale.
func ColorLog ¶
func ColorLog(base float64) ColorOption
ColorLog runs the ramp logarithmically across the domain, so that each decade of the data gets an equal share of the ramp. A base at or below 1 means the default, 10.
The domain is strictly positive, exactly as Log's is. Training ignores zero and negative values, and ColorScale.Color returns the undefined colour for one rather than clamping it to the low end of the ramp — a heatmap that painted an empty cell the colour of its rarest observation would be inventing a count. With the default transparent undefined colour that is what a reader expects anyway: the empty cells of a binned heatmap are the background.
On a Diverging scale the transform runs on the signed deviation from the centre rather than on the value, so a logarithm there is a symmetric one — see ColorSymLog. A plain logarithm has nothing to say about a negative deviation, and nothing to say about zero, which is the centre itself.
func ColorSymLog ¶
func ColorSymLog(base, threshold float64) ColorOption
ColorSymLog runs the ramp across a symmetric logarithm of the domain: linear within threshold of zero, logarithmic outside it, and defined for every finite value including negative ones. A base at or below 1 means the default, 10; a threshold at or below 0 means the default, 1.
This is the transform for a quantity that spans orders of magnitude *and* crosses zero — a signed residual, a change, a log-fold ratio — which is also the quantity a Diverging ramp is for. Choose the threshold to match the smallest magnitude that carries meaning.
func ColorUndefined ¶
func ColorUndefined(col ir.Color) ColorOption
ColorUndefined sets the colour for a value the scale cannot place — NaN, an infinity, or a category outside a fixed set. The default is fully transparent, which draws nothing.
type ColorScale ¶
type ColorScale interface {
// Train extends the scale's domain to include vs, ignoring NaN and
// infinities. Calling Train repeatedly accumulates.
Train(vs ...float64)
// Domain reports the current data domain.
Domain() (min, max float64)
// Color returns the colour for a value. Values outside the domain clamp to
// its ends; NaN and infinities return the undefined colour.
Color(v float64) ir.Color
}
ColorScale maps data values onto colours, the way a Scale maps them onto positions.
It is a separate interface rather than a Scale because the two answer different questions and are trained from different columns: a chart commonly has two positional scales and one colour scale over a third column.
Like Scale, it is implemented outside this module and never gains a method; DiscreteColorScale is the optional interface beside it, and RegisterColor is how a kind this package does not define is read back.
func ColorFromDesc ¶
func ColorFromDesc(d ColorDesc) (ColorScale, error)
ColorFromDesc builds the colour scale d describes. A kind this package does not define is built by whoever registered it — see RegisterColor.
func Diverging ¶
func Diverging(ramp palette.Ramp, opts ...ColorOption) ColorScale
Diverging returns a colour scale that puts the middle of a ramp on a centre value and stretches both halves to the further end of the domain, so that equal deviations in either direction get equally strong colours.
It is the scale for a quantity read against a reference: a residual, a change, an anomaly. Use ColorCenter to move the centre off zero.
A nil ramp uses palette.BlueOrange.
func Sequential ¶
func Sequential(ramp palette.Ramp, opts ...ColorOption) ColorScale
Sequential returns a colour scale that runs a ramp across the domain from end to end. It is the scale for a quantity with a natural low and high — a count, a duration, a temperature.
A nil ramp uses palette.DefaultRamp.
type ColorTransform ¶
type ColorTransform string
ColorTransform names the shape of a ramp's traversal of the domain.
It is the colour channel's answer to the choice between Linear, Log and SymLog on an axis, and it exists for the same reason: a quantity that spans orders of magnitude — a bin count, a hexbin density, a duration — has every value but the largest few rounded to one end of a ramp that runs linearly.
const ( // TransformLinear runs the ramp evenly across the domain. It is the // default and the zero value. TransformLinear ColorTransform = "" // TransformLog runs it evenly across the logarithm of the domain. See // [ColorLog]. TransformLog ColorTransform = "log" // TransformSymLog runs it evenly across a symmetric logarithm: linear // within a threshold of zero and logarithmic outside it. See // [ColorSymLog]. TransformSymLog ColorTransform = "symlog" )
The colour transforms.
func ColorTransformOf ¶
func ColorTransformOf(cs ColorScale) ColorTransform
ColorTransformOf names cs's transform, reporting TransformLinear for a scale that is not a ColorTransformer.
type ColorTransformer ¶
type ColorTransformer interface {
// ColorPosition returns where v sits along the ramp, in [0, 1]. It is the
// same number [ColorScale.Color] paints from.
ColorPosition(v float64) float64
// ColorValueAt inverts ColorPosition: the value the ramp reaches at t.
// It is how a gradient is sampled evenly along the bar rather than evenly
// across the domain, which for a compressed ramp is not the same thing.
ColorValueAt(t float64) float64
// ColorAxis returns a positional scale over the domain whose ticks are
// the values worth labelling. It is a fresh scale, and the caller owns it.
ColorAxis() Scale
// ColorTransform names the transform, so that a caller which supplies its
// own compression — a density raster, a hexbin — can tell that the scale
// is already doing the job.
ColorTransform() ColorTransform
}
ColorTransformer is the optional interface a colour scale implements when the ramp does not run linearly across its domain.
It is what a colourbar reads. Painting one means answering three questions — which colour is at this point of the bar, where on the bar does this value sit, and which values are worth labelling — and a scale that compresses its domain answers all three differently from one that does not. A scale that does not implement it is read as linear over ColorScale.Domain, which is what every colour scale did before the interface existed.
type Definite ¶
type Definite interface {
// Defined reports whether v has a position on this scale.
Defined(v float64) bool
}
Definite is implemented by scales whose domain excludes some finite values. A log scale cannot place zero or a negative number anywhere on an axis, and Scale.Map returns NaN for one.
Geoms consult it so that such a value is treated as missing — subject to the layer's own missing-data policy — rather than being handed to a backend as a NaN coordinate. A scale that does not implement Definite accepts every finite value.
type Desc ¶
type Desc struct {
// Kind is which scale this is.
Kind Kind
// Min and Max are the domain. They are meaningful only when Fixed is set;
// a trained domain belongs to the data, not to the scale.
Min, Max float64
// Fixed reports a domain pinned at construction by [Domain], [LogDomain]
// or [SymLogDomain], or afterwards by [Zoomer.SetDomain].
Fixed bool
// Nice and Zero are the linear and log framing options.
Nice, Zero bool
// TickValues is a linear scale's pinned tick sequence, ascending, and is
// empty for an axis that chooses its own. See [TickValues].
TickValues []float64
// Base is the log or symlog base, and Threshold the symlog linear region.
Base, Threshold float64
// MinorTicks reports the unlabelled subdivisions of a log or symlog axis.
MinorTicks bool
// Origin is a time scale's epoch, in Unix nanoseconds: the instant its
// domain values are measured from. It is zero for every other kind, and
// for a time scale left on the Unix epoch. It is not a formatting choice
// like Location is — it decides what the numbers in Min and Max *mean* —
// so a document that dropped it would read back a different axis. See
// [Origin].
Origin int64
// Categories is an ordinal scale's fixed category set, empty when it
// discovers its categories from the data. Padding is the fraction of each
// slot left blank.
Categories []string
Padding float64
// Location is a time scale's zone, by IANA name.
Location string
// Formatted reports a scale carrying a formatter that a Desc cannot hold.
Formatted bool
// Format is the declarative tick format of a numeric scale, as
// [NumberFormat] spells it, and empty for a scale that labels its ticks
// the standard way.
Format string
// Layout is a time scale's fixed tick layout, as [TimeLayout] spells it —
// a Go reference layout — and empty for a scale choosing one per tick
// spacing.
Layout string
// Locale is the name of the language the labels are punctuated and named
// in, and empty for [English]. It is a name rather than the tables
// themselves for the reason a registered scale kind is a name: a document
// carries what it is, and the process carries what it means. See
// [RegisterLocale].
Locale string
}
Desc is a scale reduced to what configures it.
It is the same bargain github.com/timzifer/figure/geom.Desc makes: a Scale is an interface over an unexported type, which is right for mapping values and useless for writing one down, so every scale here answers Describer and FromDesc builds one back.
What does not survive ¶
A tick formatter is a Go function. Format, LogFormat, SymLogFormat and TimeFormat therefore have no place in a Desc, and a scale carrying one says so through Formatted — a chart that is written down and read back labels its ticks the standard way. Nothing else about the scale is lost.
The *declarative* spelling of the same choice does survive, which is what NumberFormat and TimeLayout are for: Format, Layout and Locale below hold it, and a chart configured in a document can say how its ticks read. A scale carrying both writes both, and the function is what it uses — a document that dropped the spec would silently change what the chart says the first time somebody deleted the Go code.
type Describer ¶
type Describer interface {
// Describe returns the scale's configuration.
Describe() Desc
}
Describer is implemented by a scale that can say what it is. It is optional: a third-party scale that does not implement it still draws, and is simply not serializable.
type DiscreteColorScale ¶
type DiscreteColorScale interface {
ColorScale
// Encode returns the index of a label, registering it if the scale has not
// seen it before. Registration is in order of first sight.
Encode(label string) float64
// ColorOf returns the colour of a label, registering it the same way.
ColorOf(label string) ir.Color
// Labels returns the categories in the order they were registered, which
// is the order a legend lists them in.
Labels() []string
}
DiscreteColorScale paints named categories rather than numbers: one colour per distinct label, taken from a qualitative palette.
It rides the ColorScale interface the way Categorical rides Scale, and for the same reason. A layer binds a column to a colour scale through one option — github.com/timzifer/figure/geom.ColorBy — and a geom that paints per mark already reads its colours through ColorScale.Color; a category is simply a value that had to be encoded before it was a number. Which *guide* the layer contributes then follows from which kind of scale it was handed: a ramp gets a colourbar, a qualitative scale gets one legend entry per label.
The numeric domain is the category index, so `Color(2)` is the third label's colour and a scale trained on the encoded column has the domain [0, n-1].
func Discrete ¶
func Discrete(s ColorScale) (DiscreteColorScale, bool)
Discrete reports whether a colour scale paints categories, which is what decides whether a layer using it contributes legend entries or a colourbar.
func Named ¶
func Named(colors map[string]ir.Color, opts ...ColorOption) DiscreteColorScale
Named returns a discrete colour scale whose categories are coloured by name rather than by the order they turn up in.
It is Qualitative's sibling and it exists because first appearance is the wrong rule for some categories. A machine state, a severity, a pass/fail — these carry a colour before any data does: a reader knows that a fault is red without consulting the legend, and a chart that painted it green on a window where no fault came first would be worse than one with no colour at all. Qualitative hands out palette entries in the order the rows arrive, so the colour of a state there depends on which slice of the stream is on screen. That is right for series nobody has an opinion about and wrong for these.
The named categories are listed first and in sorted order, which is also the order the legend lists them in. Sorted rather than as-written because a Go map has no order to preserve, and ADR 0012 does not allow one that depends on map iteration: a parallel render must be byte-identical to a serial one.
scale.Named(map[string]ir.Color{
"RUN": palette.Green,
"FAULT": palette.Red,
})
A label the map does not mention is still drawn. It takes the next colour from the fallback palette, in order of first appearance, and is appended to DiscreteColorScale.Labels after the named ones — see ColorFallback. It is not left undefined because a status code nobody enumerated would then be a hole in the mark, and a hole in a line reads as a gap in the measurements rather than as a category the chart was not told about.
A nil or empty map is legal and makes this a Qualitative scale with extra steps, which is what a configuration file that named no colours should draw. Of the colour options ColorUndefined, ColorReverse and ColorFallback mean something here; the rest are accepted and ignored, exactly as they are on a qualitative scale.
func Qualitative ¶
func Qualitative(p palette.Qualitative, opts ...ColorOption) DiscreteColorScale
Qualitative returns a colour scale that gives each distinct label a colour from p, in order of first appearance in the data.
First appearance rather than sorted order is the same convention faceting and the ordinal axis already use, and 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 map iteration is an order that depends on scheduling.
A domain larger than the palette wraps rather than fails — palette.Qualitative.At does — which is the behaviour a layer index already gets. A chart with more series than the palette has colours needs a second encoding channel, not a longer palette; see github.com/timzifer/figure/theme.Redundant.
A nil palette uses palette.Default. Of the colour options only ColorUndefined and ColorReverse mean anything here: there is no domain to pin and no middle to centre, so ColorDomain and ColorCenter are accepted and ignored, exactly as an option a geom has no use for is.
type Kind ¶
type Kind string
Kind names a scale's type. It is the word a written-down chart carries in place of the constructor that built the scale.
type Labeller ¶
type Labeller interface {
// LabelOf writes v the way this scale would label a tick at v: its
// format, its locale, and the precision its current tick sequence uses.
LabelOf(v float64) string
}
Labeller is implemented by a scale that can write any value the way it writes its tick labels. It is an optional interface, for the reason Zoomer is: Scale is implemented outside this package and never gains a method.
It exists because a tick label is not the only place a chart writes a number. A tooltip, a data table beside the plot and an accessible description all write the same values, and a chart whose axis says "1.234,5 €" while its tooltip says "1234.5" is a chart that has been localised in one place.
type LinearOption ¶
type LinearOption func(*linear)
LinearOption configures a linear scale.
func Domain ¶
func Domain(min, max float64) LinearOption
Domain pins the data domain explicitly, disabling training.
func Format ¶
func Format(fn func(v float64) string) LinearOption
Format overrides tick label formatting.
func Nice ¶
func Nice() LinearOption
Nice expands the domain outwards to the tick sequence's own bounds, so the axis starts and ends on a labelled tick rather than on the extreme data value. This is what most charts want and what CONCEPT.md §13 shows.
func NumberFormat ¶
func NumberFormat(spec string) LinearOption
A tick label is described rather than computed.
Format, LogFormat and SymLogFormat take a Go function, which is the most direct thing an axis can be given and the one thing a document cannot hold: Desc says so through Formatted, and a chart written down as JSON and read back labelled its ticks the standard way. That made the one field a dashboard most wants to set — thousands separated, two decimals, a currency, a percentage — reachable only from Go, which is to say not reachable at all from the configuration file the chart actually lives in.
NumberFormat is the same choice written down. It is a string because a string is what survives the round trip and what a person editing a configuration can type; it is a *small* string because an axis asks few questions.
The grammar ¶
spec := prefix "#" [","] ["." digits] [style] suffix style := "%" | "k" | "e"
The "#" is the number, and it is what tells the prefix from the suffix: everything before it is written before the number and everything left after the body is written after it.
- "," groups the whole part with the locale's group separator.
- "." followed by digits fixes the number of decimal places. Without it the axis chooses, as it always has — enough decimals to tell adjacent ticks apart, which is a property of the tick step rather than of any one value and is what keeps a column of labels aligned.
- "%" multiplies by 100 and appends the locale's percent sign, spacing and all: "12.5%" in English and "12,5 %" in German, from one spec.
- "k" is SI: 12500 becomes "12.5k", 0.0012 becomes "1.2m". Three significant digits unless "." says otherwise.
- "e" is scientific, two decimals unless "." says otherwise.
So:
"#," 1,234,567 "#,.2" 1,234,567.00 "€ #,.2" € 1,234,567.00 "#.1%" 12.5% "#k" 1.23M "# kg" 1234567 kg
The last two are worth reading together. A style letter counts as one only where the body ends — directly after the "#", the grouping comma and the decimals — so "#k" is SI and "# kg" is a number with a unit after it. A suffix that begins with one of the three letters wants a space, or a spec that does not need one.
What it does not do ¶
It has no currency table, no accounting negatives, no significant-digit mode and no per-value conditionals. Those are a formatting library, and the seam for one is still Format: a Go function outranks a spec, so a caller who has a formatter keeps it. What the spec is for is the chart that is configured rather than compiled.
An invalid spec ¶
A spec written in Go source is a literal, so a malformed one panics — the same line github.com/timzifer/figure/data.Table draws about a ragged table. A spec read out of a document is input, so FromDesc returns an error for it rather than panicking. The two paths are the same parser.
func TickValues ¶
func TickValues(vs ...float64) LinearOption
TickValues pins the tick positions, replacing the chosen sequence with the one given. It is for an axis whose readable positions are a convention rather than a search result: the 0.2 / 0.5 / 1 / 2 / 5 grid of a Smith chart, the five points of a Likert item, the octaves of a frequency axis.
It says nothing about the domain. A tick outside the domain is dropped rather than stretching the axis to reach it — Domain is how an axis is widened — and an empty or all-infinite list leaves the automatic sequence in place, so TickValues() is not a way to ask for an axis with no ticks at all.
Labels are formatted as they always are: by Format when one was given, and otherwise from the closest spacing in the list, so that a sequence which is not evenly spaced still labels every tick to the same precision.
func Zero ¶
func Zero() LinearOption
Zero forces the domain to include zero. Bar charts need this: a bar chart whose baseline is off-screen misleads.
type Locale ¶
type Locale struct {
// Name is the tag this locale is registered and written down under. It is
// what travels in a JSON document, so a chart written down in one process
// and read in another reaches the same locale by looking it up rather than
// by carrying its tables.
Name string
// Decimal separates the whole part from the fraction: "." in English, ","
// in most of Europe.
Decimal string
// Group separates thousands, and is used only by a format that asks for
// grouping — see [NumberFormat]. It is "," in English, "." in German and a
// narrow no-break space in French.
Group string
// Minus is written before a negative number. It is the ASCII hyphen here
// and in every locale that ships with figure; a caller who wants the
// typographic minus (U+2212) sets it, and gets it on every axis at once.
Minus string
// Percent is appended by the "%" style, and carries its own spacing: "%"
// in English, " %" — a no-break space and a sign — in French and
// German, where the sign is a word of its own.
Percent string
// Months and ShortMonths are January first. A layout naming "January" or
// "Jan" is filled from these rather than from Go's own table.
Months [12]string
ShortMonths [12]string
// Days and ShortDays are Sunday first, which is [time.Weekday]'s order —
// the order the code indexes them by, not a claim about which day a week
// starts on.
Days [7]string
ShortDays [7]string
}
Locale is what a tick label needs of a language: how a number is punctuated and what the months and days are called.
It is deliberately small. A chart axis writes numbers, month names and weekday names, and nothing else — there is no plural rule, no collation and no currency table here, because an axis label never asks any of those questions. What it does ask, figure answered in English until now: `strconv` writes a decimal point, and the tick layouts in Time spell the months "Jan". Both are a choice, and neither was one a caller could make.
A Locale is read, never written, once it is registered — a chart may be rendered from several goroutines — so build one fully and hand it over.
func LookupLocale ¶
LookupLocale returns the locale registered under a name.
A name with a region — "de-AT", "pt-BR" — falls back to its language when the region itself is not registered, because a chart asking for Austrian German is better served by German than by English. Matching is on the tag as written, and the tags figure ships are lower-case language codes.
type Localizer ¶
type Localizer interface {
// SetLocale sets the language this scale's tick labels are written in. A
// nil locale means [English].
SetLocale(l *Locale)
}
Localizer is implemented by a scale whose labels can be written in a language. It is an optional interface, for the reason Zoomer is: Scale is implemented outside this package and never gains a method.
A scale that does not implement it writes English, which is what every scale did before this existed. An ordinal scale deliberately does not: its labels are the caller's own categories, and translating those would be inventing data.
type LogOption ¶
type LogOption func(*logScale)
LogOption configures a log scale.
func LogBase ¶
LogBase sets the base. The default is 10. Bases at or below 1 are ignored, because a logarithm to such a base is not a scale.
func LogDomain ¶
LogDomain pins the data domain explicitly, disabling training. Both bounds must be positive; a non-positive bound is ignored, since a log scale has no position for it.
func LogMinorTicks ¶
LogMinorTicks turns the unlabelled subdivisions inside each decade on or off. They are on by default: without them a reader has no way to judge where 3 sits between 1 and 10, which is the one thing a log axis is bad at.
func LogNice ¶
func LogNice() LogOption
LogNice expands the domain outwards to whole powers of the base, so the axis starts and ends on a labelled decade.
func LogNumberFormat ¶
LogNumberFormat is NumberFormat for a log scale.
type OrdinalOption ¶
type OrdinalOption func(*ordinal)
OrdinalOption configures an ordinal scale.
func Categories ¶
func Categories(labels ...string) OrdinalOption
Categories fixes the category set and its axis order.
Without it the scale discovers categories as it encodes them, in the order the data presents them — which is right for data that is already in the order you want to read it, and wrong for anything else. A chart whose bars reorder themselves because yesterday's export sorted differently is a chart nobody trusts.
A label outside a fixed set has no position: it encodes to NaN and the geom treats the row as missing under its own policy.
func OrdinalPadding ¶
func OrdinalPadding(f float64) OrdinalOption
OrdinalPadding sets the fraction of each slot left blank, in [0, 1). The default is 0.2, which separates adjacent bars without making them look unrelated.
type Scale ¶
type Scale interface {
// Train extends the scale's data domain to include vs. Values that are
// NaN or infinite are ignored. Calling Train repeatedly accumulates.
Train(vs ...float64)
// SetRange sets the device-space output interval. lo may be greater than
// hi, which is how a Y axis is flipped so that larger values are higher on
// screen.
SetRange(lo, hi float32)
// Domain reports the current data domain, after any nicing.
Domain() (min, max float64)
// Map converts a data value to a device position. Values outside the
// domain map outside the range; clipping is the caller's business.
Map(v float64) float32
// Invert converts a device position back to a data value. It is the
// inverse of Map over the whole real line, not just the range.
Invert(pos float32) float64
// Ticks returns tick positions and labels for a request. The result is
// ordered ascending by Value.
Ticks(req TickRequest) []Tick
}
Scale maps data values onto a device-space range.
A scale is trained on data, given a device range, and then queried. The order matters: Map and Ticks are only meaningful once both the domain and the range are set.
Stability ¶
Scale is implemented outside this module, so it never gains a method. What a scale can additionally do is an optional interface beside it — Definite, Categorical, Band, Temporal, Zoomer, Snapshotter, Cloner are the seven that exist — and a caller asks with a type assertion and falls back when the answer is no. A scale this package does not define is written down and read back through Describer and Register.
func ColorAxisOf ¶
func ColorAxisOf(cs ColorScale) Scale
ColorAxisOf returns the axis a colourbar over cs is labelled by. It is never nil: a scale that is not a ColorTransformer gets a linear axis over its domain, which is what a colourbar has always drawn.
func FromDesc ¶
FromDesc builds the scale d describes. A kind this package does not define is built by whoever registered it — see Register — and one nobody did is ErrUnknownKind.
func Log ¶
Log returns a logarithmic scale.
The domain is strictly positive. Training ignores zero and negative values the same way it ignores NaN, and Scale.Map returns NaN for them, which geoms treat as missing data under the layer's own policy — a log chart that silently clamped a negative reading to the axis minimum would be inventing a measurement. Use SymLog for data that genuinely crosses zero.
func Ordinal ¶
func Ordinal(opts ...OrdinalOption) Scale
Ordinal returns a categorical band scale: each category gets an equal slot, and a value sits at the centre of its slot.
It satisfies the same Scale interface as every continuous scale by carrying the category *index* as its numeric domain — see Categorical. A geom mapping a string column onto an ordinal axis encodes the labels through the scale; a geom mapping a numeric or time column onto one treats each distinct formatted value as a category, so a bar chart over the values 10, 20 and 30 gets three equally spaced bars rather than a numeric axis with gaps.
It also satisfies Band, so bars and boxplots take their width from the scale rather than inferring it from the spacing of the data.
func SymLog ¶
func SymLog(opts ...SymLogOption) Scale
SymLog returns a symmetric log scale: linear within a threshold of zero, logarithmic outside it, and defined for every finite value including negative ones.
This is the scale for data that spans orders of magnitude *and* crosses zero — a signed residual, a profit and loss, a delta that is sometimes tiny. A plain Log cannot show such data at all and a Linear one buries everything small.
The transform is sign(v)·log_base(1 + |v|/threshold), which is smooth at the origin rather than merely continuous: there is no visible kink where the linear region hands over to the logarithmic one.
type SizeDesc ¶
type SizeDesc struct {
// Min and Max are the domain, meaningful only when Fixed is set.
Min, Max float64
Fixed bool
// Zero is the value that maps to the minimum size, and ZeroSet reports
// whether the scale chose it. The pair is [Desc]'s Fixed again and for the
// same reason: zero is both the default anchor and an anchor somebody may
// have asked for.
Zero float64
ZeroSet bool
// MinSize and MaxSize are the diameters the domain's ends map to, and
// RangeSet reports whether the caller pinned them rather than leaving the
// theme to say. A range the theme set is not written down: it is a property
// of how the chart was drawn rather than of the chart.
MinSize, MaxSize float32
RangeSet bool
}
SizeDesc is a size scale reduced to what configures it, the way ColorDesc is for a colour scale.
func DescribeSize ¶
DescribeSize reports s's configuration, or ok == false if s cannot describe itself.
type SizeDescriber ¶
type SizeDescriber interface {
// DescribeSize returns the scale's configuration.
DescribeSize() SizeDesc
}
SizeDescriber is implemented by a size scale that can say what it is.
type SizeOption ¶
type SizeOption func(*sizeScale)
SizeOption configures a size scale.
func SizeDomain ¶
func SizeDomain(min, max float64) SizeOption
SizeDomain pins the domain explicitly, disabling training.
func SizeRange ¶
func SizeRange(min, max float32) SizeOption
SizeRange sets the diameters, in device units, that the ends of the domain map to. It is what SizeScale.SetRange sets, given at construction so that a caller can override what the theme would have chosen.
The minimum is 0 by default, and that is the choice that makes the channel honest: a floor is a constant added to every area, so a value of zero draws a mark of some size and the ratio between two marks stops being the ratio between their values. Raise it when the smallest bubbles would otherwise vanish, knowing that is the trade being made.
A range given here is *pinned*: SizeScale.SetRange no longer moves it. That is the same line Zoomer.SetDomain draws between a domain the data decides and one the caller did — a geom sets the range from its theme on every frame, and a caller who named the sizes has not asked for the theme's.
func SizeZero ¶
func SizeZero(v float64) SizeOption
SizeZero anchors the area scale at a value other than zero: the value whose mark has the minimum size.
Zero is the default and is what makes a bubble's ink proportional to its value. Anchoring at the smallest observation instead spreads the sizes over the data's own range, which shows small differences between large values — and stops the drawing being a proportion at all, so it is asked for rather than assumed.
type SizeScale ¶
type SizeScale interface {
// Train extends the scale's domain to include vs, ignoring NaN and
// infinities. Calling Train repeatedly accumulates.
Train(vs ...float64)
// Domain reports the current data domain.
Domain() (min, max float64)
// SetRange sets the diameters the domain's ends map to, in device units.
// It is the size channel's answer to [Scale.SetRange], and a geom calls it
// with the sizes its theme asks for before it draws.
SetRange(min, max float32)
// Size returns the mark diameter for a value, in device units. A value the
// scale cannot place — NaN, an infinity — has no size and reports 0, which
// draws nothing.
Size(v float64) float32
}
SizeScale maps data values onto mark sizes, the way a ColorScale maps them onto colours.
It is a third interface rather than a Scale for the reason ColorScale is: it answers a different question, from a different column, and it does not place anything on an axis. A chart commonly has two positional scales, one colour scale and one size scale over four columns.
Area, not radius ¶
A reader compares two circles by how much ink is in them, not by how far across they are. Mapping a value to a radius therefore exaggerates it by the square: a bubble for twice the value drawn at twice the radius has four times the ink and reads as four times the quantity. So this maps a value to an *area* and reports the diameter that has it, which is what makes doubling a value multiply the diameter by √2.
Like Scale, it is implemented outside this module and never gains a method.
func Size ¶
func Size(opts ...SizeOption) SizeScale
Size returns a scale that maps values onto mark diameters by area.
The domain runs from zero to the largest value trained into it, and the range from a diameter of zero to whatever SizeRange or SizeScale.SetRange names. With those defaults the mapping is exactly d(v) = D·√(v/vmax): the area is proportional to the value, so twice the value is twice the ink and √2 times the diameter.
Negative values have no area and no mark. A quantity that runs both ways is not a size channel — it is a colour channel over a diverging ramp, where a deviation of -3 and one of +3 are equally strong and visibly opposite.
func SizeFromDesc ¶
SizeFromDesc builds the size scale d describes.
type Snapshotter ¶
type Snapshotter interface {
// Snapshot returns an independent copy that maps, inverts and ticks
// identically to the receiver.
Snapshot() Scale
}
Snapshotter is implemented by a scale that can hand back an independent copy of itself exactly as it stands — configuration, trained domain, device range and all.
It is what makes parallel panels possible. Panels that share an axis share one scale object, and drawing sets that object's device range; two panels drawing at once would be writing the same field. A snapshot per goroutine removes the sharing without changing what any panel draws, because a snapshot maps every value exactly as its original does.
It differs from Cloner, which returns an *untrained* copy for a free facet axis. The two exist for opposite reasons: Clone is for a panel that must not inherit the plot's domain, Snapshot for one that must inherit it exactly.
It is an optional interface. A scale that does not implement it is drawn one panel at a time, which is always correct and is what a single-panel chart does anyway.
type SymLogOption ¶
type SymLogOption func(*symlogScale)
SymLogOption configures a symmetric log scale.
func SymLogBase ¶
func SymLogBase(b float64) SymLogOption
SymLogBase sets the base. The default is 10.
func SymLogDomain ¶
func SymLogDomain(min, max float64) SymLogOption
SymLogDomain pins the data domain explicitly, disabling training.
func SymLogFormat ¶
func SymLogFormat(fn func(v float64) string) SymLogOption
SymLogFormat overrides tick label formatting.
func SymLogMinorTicks ¶
func SymLogMinorTicks(show bool) SymLogOption
SymLogMinorTicks turns the unlabelled subdivisions inside each decade on or off. They are on by default.
func SymLogNumberFormat ¶
func SymLogNumberFormat(spec string) SymLogOption
SymLogNumberFormat is NumberFormat for a symlog scale.
func SymLogThreshold ¶
func SymLogThreshold(t float64) SymLogOption
SymLogThreshold sets the half-width of the linear region around zero. The default is 1.
Choose it to match the smallest magnitude that carries meaning: below the threshold the scale is linear, above it logarithmic, and the join is smooth.
type Temporal ¶
type Temporal interface {
// Value converts an instant into the scale's domain space.
Value(t time.Time) float64
// Instant converts a domain value back into an instant. It is the inverse
// of Value.
Instant(v float64) time.Time
}
Temporal is implemented by scales whose domain is time.
It is the seam between an exact timestamp and the float64 a Scale maps: a scale that measures from an Origin converts across it in int64 and loses nothing, and one that does not is exactly Nanos and FromNanos. Anything turning a timestamp into a domain value — a geom reading a time column, a document writing an axis down — goes through it rather than assuming the Unix epoch, which is what ValueOf and InstantOf are for.
It is an optional interface, like every other in this package: a scale that does not implement it is not a time axis.
type Tick ¶
type Tick struct {
// Value is the tick's position in data space.
Value float64
// Pos is the tick's position in device space, already mapped.
Pos float32
// Label is the formatted text for the tick. An empty label means a tick
// mark and grid line are drawn but nothing is written.
Label string
// Minor marks a tick that subdivides the axis without a label.
Minor bool
}
Tick is one labelled position on an axis.
type TickRequest ¶
type TickRequest struct {
// Want is roughly how many ticks the caller would like. A scale treats it
// as a target rather than a count: a search that lands on readable values
// returns the nearest readable sequence, which may be one or two ticks
// either side of it. Zero or negative asks the scale for its own default.
Want int
}
TickRequest is what Scale.Ticks is asked for.
It is a struct rather than a plain count because Scale is implemented outside this module and so never gains a parameter, and because a tick search has more than one input the caller could reasonably supply: the space the axis has, the rotation its labels are allowed, the locale their format follows. Today it carries one. ADR 0060 is the record.
type TimeOption ¶
type TimeOption func(*timeScale)
TimeOption configures a time scale.
func In ¶
func In(loc *time.Location) TimeOption
In sets the location used to compute calendar ticks and format labels. The default is time.UTC, so that a chart renders identically wherever it is built — a server rendering in the client's local zone is a bug, not a feature.
func Origin ¶
func Origin(t time.Time) TimeOption
Origin sets the instant the scale measures its domain from, so that a domain value is nanoseconds since t rather than nanoseconds since 1970.
It is what makes a deep zoom exact. A float64 holds 53 bits of mantissa, and a Unix nanosecond count in this century needs 61 — so two instants a nanosecond apart become the *same* float64, and an axis zoomed to a microsecond window has nothing left to separate them with. Measured from an origin near the data, the same two instants are 1.0 apart, and stay whole numbers of nanoseconds for the hundred days either side of it that a float64 counts exactly.
The origin is part of the scale's domain space: every value handed to Scale.Map or Scale.Train on this scale, and every value it returns from Scale.Invert or Scale.Domain, is measured from it. Use Temporal — or the package helpers ValueOf and InstantOf — to convert, rather than Nanos and FromNanos, which are the origin-free pair and stay so. A geom reading a time column does this for you.
The default origin is the Unix epoch, which is exactly Nanos.
func TimeFormat ¶
TimeFormat overrides tick label formatting. The unit the tick sequence settled on is passed so a caller can vary detail with zoom level.
func TimeLayout ¶
func TimeLayout(layout string) TimeOption
TimeLayout fixes the layout every tick label on this axis is written in, instead of letting the scale choose one per tick spacing.
It is TimeFormat written down: a Go reference layout is a string, so it survives a round trip through the JSON spec, and a chart configured rather than compiled can say that its axis reads "2006-01-02" — which TimeFormat's function could say only from Go. A Go function still outranks it, for the reason it outranks NumberFormat: a caller who wrote one has answered the question.
The scale's own choice is what it always was — a ladder of layouts, one per tick spacing, so that an axis over three hours says "Jan 2 15:04" and one over three years says "Jan 2006". Naming a layout gives that up in exchange for knowing exactly what every label says, which is what a caller who names one is asking for.
The month and weekday names come from the axis's locale rather than from Go's own tables, so "January 2006" is "Januar 2006" on a German axis. See Locale.
type Zoomer ¶
type Zoomer interface {
// SetDomain pins the data domain to [min, max]. A scale that cannot place
// part of that interval — a log scale given a negative bound — clamps to
// what it can place rather than refusing, because the caller is a pointer
// drag rather than a programmer.
SetDomain(min, max float64)
// Autoscale releases a pinned domain and forgets what was trained into it,
// so the next render establishes the domain from the data again.
//
// It releases a domain fixed at construction too. A scale built with a
// fixed domain and then autoscaled is a scale that was asked, twice, to do
// two different things; the later call wins.
Autoscale()
}
Zoomer is implemented by a scale whose domain can be set outright, which is what pan and zoom do.
It is an optional interface, and the third of its family: Cloner hands back an untrained copy for a free facet axis, Snapshotter an exact copy for another goroutine, and Zoomer changes the domain of the scale in place. A scale that does not implement it is simply not pannable — an ordinal axis is the honest example, because half a category is not a view of anything.
Pinning ¶
SetDomain pins the domain the way Domain does at construction: training stops moving it, and a linear scale stops nicing it. Both are what an interactive view needs — a chart whose axis snapped to round numbers after every wheel notch would not follow the pointer, and one that retrained on the next frame would undo the zoom the reader just asked for. [Autoscale] releases the pin.