Themes¶
A theme is the complete set of style attributes the charts read when they are built: the palettes, the fonts, the axes furniture, and the per-chart defaults, one value per key of StyleAttrs. The package ships eight predefined themes, each named for its visual trait; the Theme Gallery shows their color swatches and signature charts, grouped by use. Themes are applied and built through the global config instance:
| Task | Method | Section |
|---|---|---|
| Switch the look of every chart | set_theme, list_themes |
Applying a Theme |
| Switch it for one block only | using_theme |
Applying a Theme |
| Read and change single attributes | config[...], update_config, override |
What a Theme Controls |
| Make your own theme switchable by name | register_theme |
Building Your Own Theme |
| Share a theme as a file and load it back | save_theme, load_theme |
Sharing a Theme |
| Return to the default theme | reset_config |
Applying a Theme |
from datachart.config import config
from datachart.constants import FONT_WEIGHT, THEME
The examples render one figure throughout, a grouped bar chart beside a line chart, so the theme is the only thing that changes between renders. The chart code is left out of this page; see the chart guides for it.
config.list_themes()
['default', 'greyscale', 'ink', 'hatch', 'minimal', 'material', 'sketch', 'quill', 'harbor', 'muted', 'contrast']
Applying a theme replaces the whole configuration. Charts read the configuration when they are built, so set the theme before building the charts it should style; the active theme's name is in config.theme.
config.set_theme(THEME.MINIMAL)
demo().show()
using_theme applies a theme for one with block and restores the configuration that entered the block when it ends, also when the block raises. The scopes are plain save-and-restore on the global configuration, so they are neither thread-safe nor async-safe.
with config.using_theme(THEME.INK):
demo().show()
config.theme
'minimal'
reset_config returns to the default theme, discarding every change made since:
config.reset_config()
config.theme
'default'
Beyond style, a theme carries defaults for chart settings (ThemeDefaultAttrs): chart_default_show_grid supplies the grid when a chart call leaves show_grid unset (every predefined theme but SKETCH and QUILL ships a muted "y" grid), chart_default_show_values does the same for value labels (no predefined theme turns them on), and the plot_hatch_cycle, plot_linestyle_cycle, and plot_marker_cycle attributes tell series apart by pattern where a theme ships them (HATCH and QUILL). A setting given in the chart call always wins over the theme default.
What a Theme Controls¶
The attribute names are the keys of the live configuration, grouped by prefix; the typings reference documents each one.
| Prefix | Controls | Reference |
|---|---|---|
color_*, muted_* |
The palettes: multiple for series sharing one axes and for subplots, singular where one color is needed; the muted color of de-emphasized series |
ColorStyleAttrs |
font_* |
The font family and its stacks, and the size, color, style, and weight of each text role: general, title, subtitle, axis labels | FontStyleAttrs |
axes_*, figure_* |
The spines, the ticks, and the face colors of the figure and the axes | AxesStyleAttrs |
plot_grid_*, plot_legend_*, plot_text_*, plot_value_* |
The furniture every chart shares: grid lines, legend, annotations, value labels | GridStyleAttrs, LegendStyleAttrs, TextStyleAttrs, ValueLabelStyleAttrs |
plot_<chart>_* |
One group per chart type: plot_line_*, plot_bar_*, plot_heatmap_*, and so on |
The chart's guide, under "Customize" |
overlay_* |
How a Panel combines charts: the twin-axis threshold, drawing order, bar mode | Panel |
chart_default_*, plot_*_cycle |
The chart-setting defaults above | ThemeDefaultAttrs |
Two themes add groups of their own: SKETCH the path wobble and halo of its hand-drawn look (SketchStyleAttrs), QUILL the pen strokes and etched fills of its ink look (InkStyleAttrs).
Read an attribute by indexing config or with config.get; the live dictionary is config.config, so a prefix lists a whole group:
config["color_general_multiple"], config.get("font_general_family")
(['#3B76B0', '#E8A63E', '#2F9E82', '#C24E2A', '#7CBCE4', '#444444'], 'sans-serif')
{key: value for key, value in config.config.items() if key.startswith("axes_")}
{'axes_spines_top_visible': False,
'axes_spines_right_visible': False,
'axes_spines_bottom_visible': True,
'axes_spines_left_visible': True,
'axes_spines_width': 0.8,
'axes_spines_zorder': 100,
'axes_ticks_length': 3,
'axes_ticks_label_size': 8,
'axes_facecolor': None,
'axes_spines_color': None,
'axes_ticks_color': None}
update_config changes attributes on top of the active theme; the change persists until the next set_theme or reset_config, and unknown attribute names are skipped with a warning. override does the same for one with block, taking a dictionary or keyword arguments. Palette attributes also accept a single color, used for every series that asks for one.
config.update_config({"font_general_family": "serif", "plot_line_width": 3})
demo().show()
with config.override(color_general_multiple=["#0B3954", "#FF6663", "#E0FF4F"]):
demo().show()
config.reset_config()
Building Your Own Theme¶
A custom theme is a dictionary of the attributes that differ from the default theme. register_theme fills the rest from the default theme and rejects unknown names, so a theme can be as short as one palette. To build on another predefined theme instead, spread it first: {**MINIMAL_THEME, ...} with the dictionaries of the themes module.
The theme built here is a neon noir look: a near-black ground, a cyan, magenta, and amber palette, monospaced type, and the furniture dimmed so the series carry the light. It starts with the palette:
NEON_COLORS = ["#00E5FF", "#FF2D95", "#FFB000", "#7DFF5A", "#B26BFF"]
swatches(NEON_COLORS)
#00E5FF#FF2D95#FFB000#7DFF5A#B26BFFThe rest of the dictionary sets the ground, the type, and the furniture. font_general_family takes serif or sans-serif to use the theme's font stacks, or any family matplotlib resolves, here the generic monospace; a None in a color attribute keeps matplotlib's own color, so every color a dark ground needs is set explicitly:
NEON = {
# ground
"figure_facecolor": "#0B0F19",
"axes_facecolor": "#0B0F19",
# palettes: the series colors, and a two-stop ramp for value scales
"color_general_multiple": NEON_COLORS,
"color_general_singular": ["#1B2A4A", "#00E5FF"],
"muted_color": "#3A4656",
# type
"font_general_family": "monospace",
"font_general_color": "#E6EDF3",
"font_title_color": "#00E5FF",
"font_title_weight": FONT_WEIGHT.BOLD,
"font_subtitle_color": "#9AA5B1",
"font_xlabel_color": "#9AA5B1",
"font_ylabel_color": "#9AA5B1",
# furniture: open top and right, dim spines, dotted grid, dark legend
"axes_spines_top_visible": False,
"axes_spines_right_visible": False,
"axes_spines_color": "#2A3548",
"axes_ticks_color": "#9AA5B1",
"plot_grid_color": "#222D40",
"plot_grid_alpha": 1.0,
"plot_grid_linestyle": ":",
"plot_legend_face_color": "#111827",
"plot_legend_edge_color": "#2A3548",
"plot_legend_label_color": "#E6EDF3",
"plot_value_color": "#E6EDF3",
# marks: heavier strokes, no bar outlines
"plot_line_width": 2.2,
"plot_bar_edge_width": 0,
}
Try it before registering: override renders the figure under the dictionary and leaves the configuration untouched.
with config.override(NEON):
demo().show()
config.theme
'default'
Register the dictionary under a name and it behaves like a predefined theme: it appears in list_themes, set_theme and using_theme apply it, and update_config tweaks on top of it, here lifting the axes off the ground with a lighter face:
config.register_theme("neon", NEON)
config.set_theme("neon")
config.list_themes()
['default', 'greyscale', 'ink', 'hatch', 'minimal', 'material', 'sketch', 'quill', 'harbor', 'muted', 'contrast', 'neon']
config.update_config({"axes_facecolor": "#131A2A"})
demo().show()
Adding the theme to the datachart package
If you think the theme would be useful to others, open a pull request that adds it to the datachart.themes module.
Sharing a Theme¶
save_theme writes a theme file: a JSON document carrying a name, a format version, and only the attributes that differ from the default theme, so the file stays short and reviewable. A registered theme is saved by name; with no name the live configuration is saved, so a look assembled with update_config leaves the process too.
import tempfile
from pathlib import Path
folder = Path(tempfile.mkdtemp())
config.save_theme(folder / "neon.json", name="neon")
print((folder / "neon.json").read_text())
{
"name": "neon",
"format_version": 1,
"attributes": {
"color_general_singular": [
"#1B2A4A",
"#00E5FF"
],
"color_general_multiple": [
"#00E5FF",
"#FF2D95",
"#FFB000",
"#7DFF5A",
"#B26BFF"
],
"muted_color": "#3A4656",
"font_general_family": "monospace",
"font_general_color": "#E6EDF3",
"font_title_color": "#00E5FF",
"font_title_weight": "bold",
"font_subtitle_color": "#9AA5B1",
"font_xlabel_color": "#9AA5B1",
"font_ylabel_color": "#9AA5B1",
"figure_facecolor": "#0B0F19",
"axes_facecolor": "#0B0F19",
"axes_spines_color": "#2A3548",
"axes_ticks_color": "#9AA5B1",
"plot_legend_label_color": "#E6EDF3",
"plot_legend_edge_color": "#2A3548",
"plot_legend_face_color": "#111827",
"plot_grid_alpha": 1.0,
"plot_grid_color": "#222D40",
"plot_grid_linestyle": ":",
"plot_line_width": 2.2,
"plot_bar_edge_width": 0,
"plot_value_color": "#E6EDF3"
}
}
load_theme registers the theme in a file and returns the name it registered under: the name argument, else the name in the file, else the file's stem. Loading only registers; apply the theme with set_theme or using_theme. The file is validated the way register_theme validates a dictionary, so a hand-edited file cannot register a broken theme.
name = config.load_theme(folder / "neon.json", name="neon-shared")
config.set_theme(name)
config.theme
'neon-shared'
A companion package ships its themes the same way: it registers or loads them on import, and its users apply them with config.set_theme("<name>").
Finally, reset the configuration back to the default theme:
config.reset_config()