Python reference¶
The functions an analysis script or figure spec calls, by task. Everything else -- building, checking, promoting -- is a command (Command reference); renderer parameters are in the Config reference and a picture of every plot in the gallery.
A new project's script (fa new project) already uses the common ones:
"""my-project: analysis and figure 1.
Created by `fa new project` on <date>. Load the input bundles (read only),
compute what the panels show, and draw them with fig_atlas renderers; every
visual setting comes from the project config (configs/config_my-project.yaml). Record any number a
legend quotes with claims.record() and register the plotted tables with
source_data.add() (AGENTS.md 4.5).
"""
from __future__ import annotations
import matplotlib # noqa: E402
matplotlib.use("Agg")
import matplotlib.pyplot as plt # noqa: E402
from fig_atlas import claims, get_config, save_page_figure, source_data # noqa: E402
from fig_atlas.bundle_figure import draw_plots # noqa: E402
from fig_atlas.renderers import place_panel_tags # noqa: E402
CONFIG = get_config()
CFG = CONFIG.settings
RAW = CONFIG.raw_root
OUT = CONFIG.figures_root
def load(name: str, block: dict):
"""Data for a plots.<name> block that has no `bundle` (read RAW, never modify it).
Blocks with a bundle are loaded from it automatically."""
raise NotImplementedError(f"load the data for plots.{name}")
def main() -> None:
fcfg = CFG.get("figures", {})
plt.rcParams.update({"font.family": fcfg.get("font_family", "DejaVu Sans")}) # AGENTS.md 1.4
width_mm = fcfg.get("width_mm") # None: the page content width
fig_w, fig_h = CONFIG.page.figure_size_in(
height_mm=float(fcfg.get("height_mm", 80)),
width_frac=float(width_mm) / CONFIG.page.content_width_mm if width_mm else 1.0)
fig = plt.figure(figsize=(fig_w, fig_h), dpi=CONFIG.dpi)
# Every plots.<name> block in the config is one panel (fig_atlas.bundle_figure).
columns = draw_plots(fig, CFG.get("plots", {}), CONFIG.project_root, fcfg, load=load)
# claims.record("n_samples", n, source="...") # numbers quoted in the legend
# source_data.add(table, panel="a", title="...") # values a panel plots
tcfg = fcfg.get("tags", {})
place_panel_tags(fig, columns, float(tcfg.get("size", 9.0)), pad_pt=float(tcfg.get("pad_pt", 2.0)))
OUT.mkdir(parents=True, exist_ok=True)
for fmt in CONFIG.formats:
save_page_figure(fig, OUT / f"figure1_main.{fmt}")
plt.close(fig)
if __name__ == "__main__":
main()
Contents¶
- Config and paths
- Final size
- Drawing panels
- Numbers in legends
- Statistics
- Source data
- Publishing
- Parameters of your own code
- Reading provenance
Config and paths¶
Every script starts here: get_config() finds the project's config (the build passes it as CONFIG_PATH) and merges its style presets.
get_config¶
from fig_atlas import get_config
get_config(config_path=None)
Find and load config.yaml (or CONFIG_PATH), walking up from CWD if not given.
ProjectConfig¶
from fig_atlas import ProjectConfig
ProjectConfig(config_path)
Loads config.yaml and exposes typed accessors.
Environment variables override path values: set RAW_ROOT or OUTPUT_ROOT to redirect data without editing config.yaml.
A config may reference reusable named style presets via project.style
(a string or a list). Each preset is a styles/<name>.yaml file whose
figures/plots blocks are deep-merged underneath this config, so
the config always wins and shared styling accumulates as a named asset.
ProjectConfig.settings-- The whole config as a dict, merged with its style presets: thefigures,plots,pathsand any other blocks a script reads (CONFIG.settings["plots"]["volcano"]).ProjectConfig.page-- Final-print page geometry (fig_atlas.page.PageSpec) from apage:block (usually supplied by astyles/page_*.yamlpreset), or None for configs not yet migrated to final-size rendering. Withfigures.page_columns(1, 1.5, 2, ...) the page is narrowed to that many of its named columns (page.columns_mm), so the figure is drawn at the journal's column width.ProjectConfig.raw_root-- Absolute path to the raw-data root. Order: RAW_ROOT env var > paths.raw_root in config. Relative values are anchored at the project root, so callers can use this directly without a furtherproject_root /join.ProjectConfig.figures_root-- Where the script saves its figures:paths.figures_root, elseoutputs/<domain>/<category>/<project>/figures.ProjectConfig.formats--figures.formats(default png, pdf, svg): the files to save.ProjectConfig.dpi--figures.dpi(default 300): resolution of the PNG files.ProjectConfig.project_root-- Repo root: the parent of whichever ancestor directory is namedconfigs. Config files may live directly underconfigs/(flat layout) or nested arbitrarily deep (e.g.configs/lab/paper/) under a namespace subfolder; either way, the ancestor literally namedconfigsmarks the boundary.ProjectConfig.output_root-- Root for all generated, rebuildable outputs (git-ignored). Order: OUTPUT_ROOT env var > paths.output_root in config > "outputs". An unset${OUTPUT_ROOT}placeholder in config falls back to "outputs".ProjectConfig.project_name--project.name: the project identifier.ProjectConfig.require_path(path, label)-- Raise FileNotFoundError naminglabelifpathis missing.
repo_root¶
from fig_atlas import repo_root
repo_root(start=None)
Locate the project repository by walking up from start (default: this
file) to the first ancestor with a .fig-atlas marker (fa init) -- or,
in the fig-atlas source tree and older repositories, a fig_atlas/ directory.
Depth-independent alternative to hardcoding Path(__file__).resolve().parents[N],
which silently breaks if a script's own directory nesting ever changes (e.g. a
project moving into a domain/category namespace subfolder under analysis/).
load_bundle_params¶
from fig_atlas import load_bundle_params
load_bundle_params(bundle_path)
Read plot_params from a bundle's metadata/metadata.yaml.
Returns an empty dict if the metadata file does not exist so callers can merge it unconditionally into their parameter chain.
text_param¶
from fig_atlas import text_param
text_param(params, key, default=None)
Look up a text/label param, distinguishing "not set" from "set empty".
params.get(key) or default is the tempting one-liner but gets this
wrong: "" is falsy in Python, so an explicit empty-string override in
config (meant to hide a label/title) silently falls back to default
instead of being respected. Only an absent key or an explicit YAML
null should fall back; "" should render as nothing.
Final size¶
Figures are drawn at the size they print, so point sizes in the config are the printed sizes (design decision 0001).
PageSpec¶
from fig_atlas import PageSpec
PageSpec(name, content_width_mm, content_height_mm, columns_mm=())
The printed area a figure is drawn at 1:1 (mm), from the config's
page: block (ProjectConfig.page). figure_size_in gives the
figure size in inches for plt.figure(figsize=...).
PageSpec.figure_size_in(height_mm=None, height_frac=None, width_frac=1.0)-- (width, height) in inches for a figure drawn on this page. Width defaults to the full content width. Height comes from an absoluteheight_mmor aheight_fracof the content height (default: the full content height). A height taller than the page is rejected rather than silently allowed: such a figure can only be printed by shrinking it (breaking the 1:1 point sizes) or by splitting it across pages, and that choice belongs to the figure, not here.PageSpec.content_width_in-- Width of the printed area, inches.PageSpec.content_height_in-- Height of the printed area, inches.
save_page_figure¶
from fig_atlas import save_page_figure
save_page_figure(fig, path, pad_mm=0.5, **kwargs)
Save fig at exactly its canvas width, trimmed only vertically.
bbox_inches="tight" crops each figure to its own drawn extent, so
figures drawn on the same page preset come out a few millimetres wider or
narrower than one another (and than the page's content width). This keeps
the full canvas width -- every figure on a page preset saves at the same
width, ready to place 1:1 in the page's content box -- and trims only the
empty space above and below the content (plus pad_mm).
Anything drawn outside the canvas horizontally would be cut off, so that is reported as a warning (the print audit flags it too).
audit_figure¶
from fig_atlas import audit_figure
audit_figure(fig, page, min_font_pt=5.0, min_line_pt=0.25, max_offenders=8)
How fig prints on page: drawn, shrunk to the page width if wider,
and measured. Returns a dict with scale_to_page_width,
pages_needed, the smallest printed text and line (min_font_printed_pt,
min_line_printed_pt), the texts below min_font_pt
(smallest_texts) and the verdicts font_ok, line_ok and
fits_one_page. fa audit-print and preflight run it on every figure.
Drawing panels¶
Draw by plot name (fa renderers lists them; the gallery shows each), or let draw_plots draw every plots: block of the config.
render¶
from fig_atlas import render
render(name, *args, **kwargs)
Draw a plot by name -- fig_atlas.render("volcano", ax, df, params) --
wherever its renderer lives: the core, an installed domain pack, or a
recipe (fig_atlas.renderer_registry.find). Scripts need not know which;
fa renderers lists the names.
draw_plots¶
from fig_atlas.bundle_figure import draw_plots
draw_plots(fig, plots, root, layout=None, load=None)
Draw every plots: block that names a renderer; returns the
place_panel_tags columns ([(letter, axes), ...] per grid column).
plots is the config's plots: mapping, root the repository
(CONFIG.project_root) and layout the figures: block (grid
columns, gaps). A block with a bundle is drawn from that bundle's
table; for one without, load(name, block) returns the data.
draw_bundle¶
from fig_atlas.bundle_figure import draw_bundle
draw_bundle(bundle, width_mm=85.0, height_mm=62.0, root=None)
One bundle drawn on its own, as fa ingest bundle would draw it: its
plot_type's renderer with its plot_params, on a figure of the given
printed size (single column by default). Returns the figure.
Used by the example gallery (docs/reference/gallery.md) and its test.
render_image_panel¶
from fig_atlas.renderers import render_image_panel
render_image_panel(ax, source, params)
Draw a microscope image, blot or gel onto ax from its original file.
source is one image file, or a list of greyscale files (one per
channel) that are tinted and combined. params (the panel's
plots: block; keys in the config reference under image_panel) sets
the crop, the linear display range per channel, gamma and the scale bar
(from pixel_size). Never pass a brightened or cropped copy: the
adjustments are made here and recorded with the source file hashes, so
the figure's README lists them and verify fails if a source image
changes (AGENTS.md 4.7). Returns the recorded entry.
place_panel_tags¶
from fig_atlas.renderers import place_panel_tags
place_panel_tags(fig, columns, label_size, pad_pt=2.0, fontweight='bold', align_rows=None)
Place panel letters on each panel's measured outer bounding box.
columns is a list of layout columns; each column is a list of
(letter, [axes, ...]) for the panels stacked in it. Each tag's left
edge sits on the column's shared plumb line -- the leftmost outer edge
(tick labels, axis titles, legends included) of any panel in that
column -- and its baseline sits pad_pt above its own panel's outer
top edge (AGENTS.md rule 1.5: tags follow the outer bounding box, not the
plot spine, and stacked panels share one vertical line).
align_rows (optional) lists groups of letters that sit in one
layout row, e.g. [["a", "b"], ["c", "d", "e"]]; each group's tags
share the highest of their baselines so a row's tags line up
horizontally even when the panels' titles differ in height.
Call after everything in the panels has been drawn; the layout is measured with a draw pass.
panel_label¶
from fig_atlas.renderers import panel_label
panel_label(subfig, letter, label_size)
Stamp a bold panel letter (a, b, c, …) in the top-left of a SubFigure.
align_title_to_labels¶
from fig_atlas.renderers import align_title_to_labels
align_title_to_labels(ax, title=None)
Start a left-aligned axes title at the axes' outer left edge.
A horizontal bar chart with long category names has its tick labels far
left of the spine; a title at x=0 then starts over the bars, and one
placed by a typed axes fraction (set_x(-1.25)) drifts whenever the
labels or the axes width change. This measures the left edge of the tick
labels and axis title (the title itself left out) and moves the title
there, so the title -- and a panel tag placed on the outer box -- line up
with the labels. title defaults to the axes' left title (what
set_title(..., loc="left") sets), else its centre title. Call after the
axes are fully drawn; returns the title.
readable_text_color¶
from fig_atlas.renderers.text_color import readable_text_color
readable_text_color(background, light='white', dark='#111827')
light or dark, whichever contrasts more with background.
Numbers in legends¶
A number a legend quotes is recorded by the script and written as {name} in the figure spec (AGENTS.md 4.5).
claims.record¶
from fig_atlas import claims
claims.record(name, value, source='')
Register a number this run computed; returns value unchanged.
claims.record("n_anchors", int(n), source="promoter DA peaks, padj < 0.05")
lets the figure spec write {n_anchors:,} (Python format: {share:.0%},
{p:.2g}); publish fills it in and the value is kept in the figure's
provenance record. name must be a Python identifier; source says
what was counted.
claims.use_from¶
from fig_atlas import claims
claims.use_from(project, figure=1, names=None, root=None)
Take numbers another figure recorded (from its published provenance record), so a figure quoting them -- a summary schematic, a legend that refers back -- shows the same values. Reading the record makes it an input of this build, so this figure goes stale when those numbers change. Raises if that figure has not been built with a record.
claims.scan¶
from fig_atlas import claims
claims.scan(text)
Numbers typed into text (placeholders excluded), each classified.
Statistics¶
Stars and brackets come from these tests; the table is published with the figure (AGENTS.md 4.6).
stats.compare_to_control¶
from fig_atlas import stats
stats.compare_to_control(groups, control, test='welch', min_control_n=2)
Each group against control, in the order of groups.
Columns: group, control, test, n, n_control, mean, mean_control,
statistic, p, note. A group with no values gets a row with p NaN; with
fewer than min_control_n control values every p is NaN.
stats.two_group¶
from fig_atlas import stats
stats.two_group(a, b, test='welch')
Test b against a; returns (statistic, p, note). The statistic
is b relative to a: positive when b is higher (for Mann-Whitney, U of b).
When both groups are constant there is nothing to test: p = 1 with note 'both groups constant'. A test that raises gives p = 1 with note 'test failed'. A t-test with a single value in a group gives NaN.
stats.anova¶
from fig_atlas import stats
stats.anova(groups)
One-way ANOVA over the non-empty groups; (F, p), NaN when it cannot be computed (fewer than two groups, or every group constant).
stats.adjust¶
from fig_atlas import stats
stats.adjust(p, method='none')
Multiple-testing correction over one family of P values. NaNs are left out of the family and stay NaN. Methods: none, bonferroni, holm, bh (Benjamini-Hochberg false discovery rate).
stats.stars¶
from fig_atlas import stats
stats.stars(p, thresholds=None, ns='')
Symbol for P value p: the first [threshold, symbol] pair (ascending
thresholds) with p < threshold; ns otherwise or when p is NaN.
stats.describe¶
from fig_atlas import stats
stats.describe(test, correction='none', thresholds=None, against='control', family='')
Legend phrase for the stars, from the same settings that drew them:
"Welch's t-test versus control (*P < 0.05, **P < 0.01, ***P < 0.001)";
with a correction, "..., Benjamini-Hochberg-adjusted
stats.record¶
from fig_atlas import stats
stats.record(df, name, title='', note='')
Register a test table for the figure saved next; returns df.
A name used twice before one save gets a numbered suffix (_2, ...).
Source data¶
The values each panel plots, published as CSV next to the figure.
source_data.add¶
from fig_atlas import source_data
source_data.add(df, panel, title='', name='', note='')
Register the table panel panel is drawn from; returns df.
source_data.add(table, panel="a", title="log2FC per gene") before the
figure is saved: publish writes it as CSV into figure_NN_source_data/
with an index, the per-panel data many journals ask for. Pass the values
as plotted (after filtering and renaming), not the raw input.
Publishing¶
A figure spec describes one figure; fa build runs the script, then publish and verify_atlas.
FigureSpec¶
from fig_atlas import FigureSpec
FigureSpec(number, slug, title, source_stem, primary_code, panel_notes=<factory>, statistics_tables=<factory>, claim_boundary='')
Declarative description of one manuscript figure.
Separates what a figure is (metadata, relationships, claim boundary)
from how it is generated (analysis scripts) and where it lives (publish).
One spec file per figure (figure_specs/.../figure_NN_<project>.py)
holds one SPEC = FigureSpec(...); publish copies the files and
writes the figure's README from it.
number,slug: the published folder isfigure_NN_<slug>.title: the figure's title in its README.source_stem: the figure the script saved, without extension, relative to the output root (<domain>/<category>/<project>/figures/figure1_x).primary_code: the analysis script, repository-relative.panel_notes: one legend text per panel.statistics_tables: test tables to publish with the figure (tables registered withstats.recordare added on their own).claim_boundary: what the figure does and does not show.
title, panel_notes and claim_boundary may hold live numbers,
{name} or {name:,}, filled from claims.record at publish.
publish¶
from fig_atlas import publish
publish(spec, config)
Copy source figure files to a clean output folder and write a panel README.
Source files are resolved as {output_root}/{spec.source_stem}.{ext}, i.e. source_stem is relative to the configured output_root (OUTPUT_ROOT). Raises FileNotFoundError if any expected format file is missing.
verify_atlas¶
from fig_atlas import verify_atlas
verify_atlas(config)
Check that all figure folders, format files, and README files exist and are non-empty.
write_report¶
from fig_atlas import write_report
write_report(rows, out_path)
Write CSV report and return the number of failures (WARN rows don't count).
Parameters of your own code¶
A renderer declares its parameters in a SCHEMA, a script the keys it reads itself in a LAYOUT_SCHEMA; fa param and fa lint read them.
Param¶
from fig_atlas.schema import Param
Param(name, type, default=None, unit='', role='', tune='', range=None, aliases=(), open_keys=False)
One parameter a renderer or script reads from the config.
type is float, int, bool, str, list, dict, "color", "cmap" or
a tuple of allowed values. default is the value used when the config
has none (a number, a StyleRef to a style preset key, or
CODE_DEFAULT). unit, role and tune are the three parts of
the config comment (AGENTS.md 1.6): fa param scaffold writes them next
to the key. range is (min, max), aliases old names still
accepted, and open_keys marks a dict whose keys are free (rename
maps, per-group colours).
Schema¶
from fig_atlas.schema import Schema
Schema(name, params=<factory>, extends=<factory>)
The parameters of one renderer (or, as LAYOUT_SCHEMA, the keys an
analysis script reads itself): a list of Param plus shared groups in
extends (e.g. typography()). fa param and fa lint read it, and
uses_schema checks the values a renderer receives against it.
uses_schema¶
from fig_atlas.schema import uses_schema
uses_schema(schema)
Decorate a renderer whose signature has a params argument: it
receives a BoundParams. A _style entry in params (merged
figures/plots/typography config) feeds StyleRef defaults.
typography¶
from fig_atlas.schema import typography
typography(**fallbacks)
The shared text sizes a renderer reads, e.g. typography(title_size=13,
tick_label_size=8): each defaults to the style preset's typography:
block (final-size presets such as styles/print.yaml define it) and,
where a preset has none, to the renderer's own historical default given
here (CODE_DEFAULT: computed in the code) -- so projects without the
block draw exactly as before.
StyleRef¶
from fig_atlas.schema import StyleRef
StyleRef(path, fallback=None)
A default taken from the merged style/config (figures.title_size),
with fallback when no preset sets it.
CODE_DEFAULT¶
from fig_atlas.schema import CODE_DEFAULT
CODE_DEFAULT
Schema default meaning "whatever the renderer's own call passes":
for defaults derived from other values (a_sz * 0.8) or that differ
between code branches. A missing key then behaves exactly as before.
Reading provenance¶
What a published figure was built from; fa status, fa impact and fa lineage print the same.
published_records¶
from fig_atlas.provenance import published_records
published_records(root, figures_root=None)
(record path, record) for every published figure record under
figures_root (default: all of outputs/).
check_freshness¶
from fig_atlas.provenance import check_freshness
check_freshness(root, record, cache=None)
Why a figure is stale: each recorded input, code file or config whose content no longer matches (empty list = fresh). Content hashes, not timestamps: touching a file without changing it keeps the figure fresh.
impacted_by¶
from fig_atlas.provenance import impacted_by
impacted_by(root, target)
Published figures whose build read target (a file, or every file
under a folder) as an input, config or code.
lineage¶
from fig_atlas.provenance import lineage
lineage(record)
A figure's upstream tree, as text lines: outputs <- build run <- config and styles <- code <- inputs grouped by data bundle (with its exporter).