theme
Mojo module đźˇ
theme
Visual defaults for a Plot – colors, sizes, margins – kept as one small struct with sensible defaults rather than a dozen optional parameters scattered across Plot’s builder methods. This is deliberately the one place in this early vertical slice that borrows ECharts’ “config object” ergonomics (a bundle of display knobs) rather than the grammar-of-graphics vocabulary the rest of dataviz follows – appropriate here specifically because a theme isn’t part of the data grammar (it doesn’t change what a mark or scale means), just how it looks.
Every field below typed Color takes a dataviz_mojo.colors named
constant exactly as it does a hand-built Color(r, g, b) – Theme( mark_color=CORNFLOWERBLUE) instead of Theme(mark_color=Color(100, 149, 237)) – see that module’s docstring for the full list.
scale (default 1.0, purely multiplicative, so every existing Theme
keeps rendering exactly as it always has) uniformly multiplies every
other pixel-sized quantity render() computes – font size, margins,
point radius, line width, tick length, legend layout, all of it –
without changing what any individual field means at scale=1.0. Meant
for rendering the exact same chart at a higher pixel density (pair
Theme(scale=2.0) with a Canvas twice the width/height) so text and
strokes stay crisp when a viewer displays the output larger than its
native pixel size – see the wiki for the concrete case
that motivated this (a small raster canvas, viewed upscaled in an
Electron/webview-based image preview, loses sharpness to the
viewer’s interpolation; more native pixels per glyph is the fix
that holds regardless of which viewer is doing the upscaling). Not a
crop/zoom – the plot’s logical layout (data domain, tick
positions, legend contents) is identical at every scale, only the
pixel measurements of everything drawn change.
Every one-call convenience function (bar(), scatter(), …)
reads scale exactly the same way a hand-built Plot does – it
returns a plain Plot (dataviz_mojo.plot._finished’s docstring),
not a rendered Canvas, so there’s no separate quickplot-only
scaling behavior to distinguish scale from here at all.
Distinct from render()’s own internal supersampling
(_RASTER_SUPERSAMPLE, plot.mojo): that’s a fixed, unconditional
multiplier render() applies and un-applies around one raster render
pass so PNG/BMP output just looks good, not a Theme field or
something this value composes with explicitly – scale is the one
knob a caller actually sets to ask for a bigger/sharper export;
render()’s own supersampling is invisible plumbing underneath that
choice, not a second version of it.
donut_inner_radius_fraction (default 0.0 – an ordinary pie, Mark. ARC unchanged) is a fraction of the outer radius _render_arc
already computes from the plot area, not a pixel value – the outer
radius itself already scales with canvas size, so a fraction keeps
the donut hole proportionally correct at any size the same way a
percentage would, rather than a fixed pixel value that would look
right at one canvas size and wrong at every other. Must be in
[0.0, 1.0) – _render_arc raises otherwise (an inner radius that
reaches or exceeds the outer one has no ring left to draw).
color_by_sign (default False – every Mark.BAR bar stays
mark_color, unchanged) switches _render_bar to color each bar by
whether its value is negative (mark_color_negative) or not
(mark_color) – what a diverging bar chart’s coloring
conventionally means (Mark.BAR already draws bars extending below a
zero baseline for negative values with no changes needed – see
_zero_baseline_y_extent’s docstring; this is the one further
thing a genuinely diverging bar chart adds on top of that: making
the sign visually obvious by color too, not just by direction). An
explicit opt-in flag, not inferred from whether mark_color_negative
differs from mark_color, so there’s no ambiguous default to guess
at from color equality.
Mark.WATERFALL and Mark.CANDLESTICK also use mark_color/
mark_color_negative, but unconditionally, with no color_by_sign
equivalent of their own – sign coloring isn’t an optional extra for
either (a waterfall’s rising/falling color and a candlestick’s up/down
color both are what the chart conventionally shows), the way it is
for an otherwise-complete plain bar chart. See either mark’s _render_* docstring in plot.mojo.
bullet_range_color_light/bullet_range_color_dark are Mark.BULLET’s pair, unrelated to the sign-coloring fields above: the two ends of
a small monochrome gradient (via dataviz_mojo.color_scale.ColorScale, the
same stop-interpolation machinery Plot.encode(color=...)’s continuous channel uses) _render_bullet shades each category’s
qualitative range bands with, lightest-to-darkest by range index –
deliberately grayscale by default (Stephen Few’s original bullet-
chart convention), not mark_color-derived, so the shaded background
bands read as neutral context behind the one thing that is colored
with mark_color: the measure bar itself. Unlike mark_color_negative,
Mark.BULLET’s measure bar is never colored by sign – see
_render_bullet’s docstring for why.
color_scale_low/color_scale_mid/color_scale_high are Plot.encode( color=...)’s continuous channel (and every mark built directly on
dataviz_mojo.color_scale.ColorScale over its data domain –
Mark.HEATMAP/CORRPLOT/CALENDAR_HEATMAP, see each one’s
_render_* docstring) – three stops, not two: a real,
rendering-caught readability bug. Two stops alone (the
low/high colors directly, no color_scale_mid) linearly interpolate
in plain RGB space, and the midpoint of two saturated, hue-opposite
colors (the default low/high pair is blue/orange, chosen for
contrast) in RGB space is a desaturated, muddy brownish-grey – not a
blend a viewer reads as “partway between blue and orange” at all. A
mark whose data happens to sit near the domain’s extremes never shows
this, but the legend always spans the full domain end to end, so
that muddy
middle dominated most of its length – reading as “one flat color”
even though the underlying gradient math was working correctly the
whole time. color_scale_mid (default a light neutral grey, Color( 235, 235, 235)) is the fix every real diverging colormap (matplotlib’s
coolwarm, ColorBrewer’s RdBu, …) already uses: route the
transition through a genuine third, deliberately desaturated color
instead of letting linear RGB interpolation pick an accidental one.
Added at gradient offset 0.5 alongside the existing 0.0/1.0
stops everywhere a ColorScale gets built from Theme (see dataviz_ mojo.color_scale.ColorScale.from_theme’s docstring) – a caller
who genuinely wants a plain two-hue transition (a sequential, not
diverging, scale) can still set color_scale_mid to whatever reads
right for that specific pair, the same way every other color field
here is a real, overridable default, not a hardcoded internal.
line_smoothing (default 0.0 – Mark.LINE/Mark.AREA draw exactly
the straight point-to-point segments they always have) controls how
much _build_line_path curves a line (or an area’s top edge)
through its data points, via a Catmull-Rom-derived cubic Bezier
spline – 0.0 builds a plain straight-segment Path (no curve math
touched at all, not merely a degenerate curve that happens to look
straight, so the default is byte-for-byte identical to every pre-
existing Mark.LINE/AREA render, not just visually close), 1.0 the
full, standard Catmull-Rom curve through every point, and anything in
between scales each segment’s tangent vector by that fraction –
so 0.5 bows exactly half as far from the straight path as 1.0 does
at the same point. Must be in [0.0, 1.0] – _render_generic raises
otherwise, an overshoot tension this package assigns no meaning to
rather than silently rendering an unbounded, likely-self-intersecting
curve. Mark.AREA only smooths its top edge (through the data
points) – the bottom edge down to/along the zero baseline stays
straight, since baseline is a fixed reference line, not data, with
nothing to curve through. See _build_line_path’s docstring
(plot.mojo) for the control-point formula, shared unchanged by both
marks.
title_font_size/subtitle_font_size/axis_title_font_size are the
three sizes Plot.labels() needs (see that method’s docstring for
the four strings themselves – title/subtitle/x_title/y_title
– and the layout math that uses these): a chart title reads as a
heading, so it defaults larger than everything else on the plot (18.0,
vs. font_size’s 12.0 for tick/legend labels); an axis title (a
caption under the x-axis or rotated alongside the y-axis, e.g.
“Revenue ($)”) reads as a subordinate label, not body text or a
heading, so it defaults between the two (14.0); subtitle shares that
same 14.0 default – the classic editorial two-tier headline reads
title-then-subtitle as “heading, then a smaller supporting line,” the
identical size relationship an axis title already has to the title,
not a fourth distinct size this package would need to separately
justify. All three are plain Float64 points, scaled by Theme.scale
the same as font_size itself – see _Scaled’s docstring
(plot.mojo) for why every pixel-sized quantity goes through that one
struct rather than each render path applying * scale itself. No
titles are drawn by default (Plot._labels’s title/subtitle/
x_title/y_title all default to ""), so these three sizes only matter
once a caller actually calls .labels(...) – an empty string never
reserves layout space or emits a _TextRequest, the same “absent
means absent, not a zero-size version of present” rule Plot.encode_ gantt’s start/end
pair and every other optional feature in this file follow.
subtitle_color (default a muted gray, Color(110, 110, 110),
distinct from text_color’s default Color(40, 40, 40)) is
subtitle’s dedicated color, not text_color reused – the
second half of the two-tier-headline reading title_bold’s docstring gives for the title itself: a subtitle is
supporting context, not body text or a heading, so it recedes rather
than competing with either – the same “a genuinely distinct visual
role gets its color, not a borrowed one” reasoning waterfall_ total_color below gives.
waterfall_total_color is Mark.WATERFALL’s third color, for a
row encode_waterfall()’s is_total marks as a running-total
checkpoint rather than a rising/falling delta – deliberately a third,
neutral color (default a plain gray, Color(100, 100, 100)), not
mark_color/mark_color_negative reused: a total bar isn’t “a big
increase” or “a big decrease,” it’s a different kind of thing on the
same chart (where things stand, not what just changed), so it reads
clearest with its color rather than borrowing meaning from the
rising/falling pair. See _render_waterfall’s docstring for the
full total-bar drawing story (also wider than a delta bar – full band
width vs. waterfall_delta_width_fraction below).
waterfall_delta_width_fraction (default 0.6) is how much of its band a rising/falling delta bar occupies; a total bar always spans the
full band, so this is what visually separates the two.
bullet_measure_width_fraction (default 0.35) is how thick Mark. BULLET’s measure bar is relative to its band, and
chord_ring_fraction (default 0.08) how thick Mark.CHORD’s node ring
is relative to the outer radius – the same fraction of something the
layout already computed shape donut_inner_radius_fraction has, and
for the same reason: a fixed pixel value would look right at one canvas
size and wrong at every other.
radialbar_track_color (default a light grey) is the unfilled track
Mark.RADIALBAR sweeps its rings over, and treemap_label_color
(default white) the label drawn on a Mark.TREEMAP leaf rect – both
sit on top of palette-colored shapes, so both are real contrast
choices a caller may need to change rather than internal details.
halo_alpha (default 90) is the opacity Mark.EFFECT_SCATTER blends
each point’s halo at, before flattening it against white – see
_lighten’s docstring for why it is flattened rather than drawn
translucent. radar_fill_alpha (also 90) is the same treatment for
Mark.RADAR’s filled series polygons.
Two fields rather than one shared “tint alpha”, even though both default to 90: a single shared constant would silently couple a scatter halo’s tint to a radar fill’s. Nothing connects those two beyond the number having happened to suit both, so retheming one should not move the other.
annotation_color (default a plain medium gray, Color(150, 150, 150)) is Plot.annotate_line()’s color – both the reference
line itself and its optional label share this one color, reading as
one cohesive annotation rather than two independently colored pieces.
Distinct from mark_color (a reference line is explicitly not data,
so borrowing the data’s color would blur that distinction) and
from axis_color/gridline_color (this needs to read as more present
than either – a reference line is meant to be noticed, not recede
into the chrome). Not subtitle_color reused either, even though both
default to a similarly muted gray – two different roles that happen
to share a similar visual weight today, not a promise they’ll always
share a value; each gets its field so retheming one doesn’t
silently retheme the other.
annotation_area_color (default a pale blue-gray at partial opacity,
Color(224, 236, 246, 200)) is Plot.annotate_area()’s fill – a
genuinely separate field from annotation_color, not that same gray
reused, because a filled rectangle needs to read very differently
from a line: solid medium gray as a fill would read as an opaque,
obtrusive block, while a 1px stroke in the same gray reads as a thin,
unobtrusive mark. Plot.annotate_area()’s label text still uses
annotation_color, not this field – ink and fill are two different
jobs even on the same annotation, the same split mark_color/
text_color already have everywhere else. Real alpha (a=200, not
the _lighten()-style pre-blend-against-white halo_alpha/
radar_fill_alpha use), unlike those two: a halo/radar fill wants a
consistent tint regardless of what happens to be behind it, but a
reference band’s whole point is marking a region on the existing
chart, so it should let whatever the mark drew there keep showing
through instead of painting fully over it. Both canvas_mojo backends
already composite Color.a correctly (Canvas.write_pixel’s
blend_over, SvgCanvas.fill_rect’s fill-opacity), so this is a
plain color value, not special-cased draw logic. Tuned so the over-white look
stays close to the old fully-opaque pale fill (a=200 over white
lands within a few units of (224, 236, 246)), while a mark’s own
color still visibly shows through wherever a band overlaps it.
font_family (default "sans-serif") is every _TextRequest’s typeface – tick/legend labels, axis titles, the chart title, all of
it, baked into each _TextRequest at the point it’s built (the same
“read straight off theme, per construction site” convention every
other _TextRequest field – color, size – follows,
not a single value read once by render()/render_svg()’s final draw loop: render_facets()/render_layers() combine several
independently themed Plots into one shared draw pass, so a family
read once, globally, would silently apply the wrong plot’s choice
to every other plot sharing that canvas). "sans-serif" is
deliberately a value valid in both worlds a caller’s chosen family
ends up in – canvas_mojo.text.draw_text’s raster path resolves
it as a fontconfig family/alias (fontconfig ships sans-serif as a
recognized generic alias, the same generic-substitution concept CSS’s sans-serif keyword is, alongside its older capitalized Sans
form), while SvgCanvas.draw_text’s family parameter is a
literal CSS font-family value the SVG viewer interprets directly –
two genuinely different value spaces (a fontconfig alias resolves to
one concrete font file; a CSS value is interpreted by whatever’s
rendering the SVG, with no file resolution on this package’s side
at all), which happen to agree on this one generic keyword. A caller
naming a specific installed font instead ("Georgia", "Helvetica Neue") gets that request honored identically by both backends too,
as long as it’s a real, installed font name – but a real CSS fallback
stack ("Helvetica Neue, Arial, sans-serif") only means anything to
the SVG side; fontconfig has no comma-separated-list syntax of its
own, so a raster render would treat the whole string as one (almost
certainly unmatched) family name. Threading a stack through safely for
both backends is a real, separate feature, not something this single
string field takes on implicitly.
title_bold (default True) bolds Plot.labels()’s chart
title – and only the title: x_title/y_title and every other
_TextRequest (tick/legend labels) stay normal weight always, not
configurable here, the same “one deliberate exception, not a general
knob” scope this field itself is. The one default in this whole
struct that isn’t backward-compatible: every other field’s default
reproduces exactly what render() already produces without it (see
font_family’s docstring for why that one does); this one exists
because a plain-weight title doesn’t read as polished enough on its
own – the one place a caller-visible aesthetic default, not just a
new capability, changed. Still overridable
(Theme(title_bold=False) reproduces the old look exactly) for a
caller who wants it. Threaded the identical way font_family is –
_TextRequest’s bold: Bool = False field, left untouched at
every construction site except the title’s in _label_text_ requests (bold=theme.title_bold) – rather than baked in
everywhere font_family needed to be, since nothing else ever wants
True.