Skip to content

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.0Mark.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.

Structs