Skip to content

Galaxy-Dawn/pubfig

Repository files navigation

pubfig

pubfig logo

Python 3.10+ PyPI version pip install pubfig Matplotlib 3.8+ License GitHub Stars

Language: English | 中文

Publication-style figures for papers, from single plots to panel-first Figma assembly.

pubfig is a Matplotlib-native plotting library for researchers who want figures that already look close to the final paper figure. It combines common scientific plot families, journal-aware export, and a panel-first Figma workflow for assembling large multi-panel figures without rebuilding artwork by hand.

Project links: PyPI · GitHub · Examples

Highlights

  • Paper-Ready Defaults, Not a Blank Canvas — Titles, legends, fonts, line widths, and spacing start from a more publication-like baseline.
  • Common Scientific Plot Families in One API — Statistical plots, distributions, trends, dimensionality reduction, evaluation curves, heatmaps, and flow plots live in one consistent surface.
  • Journal-Aware Export Without Boilerplatesave_figure(...) and batch_export(...) handle explicit output suffixes, column widths, DPI, trimming, and export-time relayout directly.
  • Panel-First Workflow for Composite Figures — Export clean subplot assets, then assemble and refresh full figures in Figma instead of re-drawing panels manually.
  • Matplotlib-Native and Script-Friendly — Plot functions return standard Matplotlib Figure objects, so they drop into existing analysis code easily.

Recent News

  • 2026-04-10: pubfig 0.2.3 makes batch_export(...) follow the same publication-size resize / relayout path as save_figure(...), so multi-format export now keeps export-time layout consistent.
  • 2026-03-31: pubfig 0.2.2 adds ECDF, QQ, Bland–Altman, Calibration, and UpSet, and brings them into the homepage showcase and full gallery.
  • 2026-03-30: pubfig 0.2.1 refreshed the PyPI release with stronger polar/composition coverage and fuller gallery examples.
  • 2026-03-29: pubfig 0.2.0 landed on PyPI with pip install pubfig, suffix-based export, and the documented panel-first Figma workflow.
View older changelog
  • 2026-03-25: Panel-first Figma loop polish — panel export now defaults to title-free assets for cleaner Figma assembly, pubfig-sync now keeps shared title / legend placeholders off by default, and bridge/watch flows now surface bundle provenance plus the exact manual-fallback bundle path.
  • 2026-03-20: Local bridge automation for Figma sync — added a bridge-backed pubfig figma bridge|sync|watch workflow, upgraded pubfig-sync with bridge connection mode, and enabled CLI-triggered vector import/refresh after one-time plugin connection.
  • 2026-03-20: Figma plugin v2 workflow polish — added auto / hero_top relayout presets, upgraded shared title / legend placeholders, and improved refresh behavior so manual Figma positioning is preserved more reliably unless relayout is requested.
  • 2026-03-20: CLI + Figma plugin workflow — added pubfig figma package|validate|inspect, introduced a single-file Figma bundle JSON format for exported panels, and scaffolded the figma-plugin/pubfig-sync plugin for node-level import and refresh.
  • 2026-03-20: Figma-first panel export workflow — added export_panel(...) and export_panels(...) for stable subplot asset export, introduced a minimal panel-index.json sync index, and documented the Codex + Figma MCP refinement path for multi-panel figures.
  • 2026-03-20: README alignment with pubtab style and homepage refresh — reorganized the README into a pubtab-style homepage with centered badges, language switch, highlights, dated recent news, showcase examples, and an embedded gallery hero.
  • 2026-03-20: Default full install and metadata simplification — changed pip install pubfig to install the full plotting stack by default, removed user-facing extras from the main install path, and aligned package metadata, GitHub About, and README wording.
  • 2026-03-19: Raincloud plot support and gallery refresh — added raincloud(...), tuned its default styling, integrated it into the gallery, and regenerated the exported figure set.
  • 2026-03-19: PCA biplot and radar default updates — expanded pca_biplot(...) with loading panel modes and group ellipses, refreshed radar defaults, unified font handling, and re-exported the gallery.

Examples

Showcase

Single-plot examples

Bar scatter example Raincloud example Line example

Radar example Scatter example Heatmap example

New plot families

ECDF example QQ plot example Bland-Altman example

Calibration example UpSet example

Dumbbell example Forest plot example

Hexbin example Volcano example

Grouped scatter example Radial hierarchy example

Circular stacked bar example Circular grouped bar example

Composite figure examples assembled in Figma

Benchmark composite figure assembled in Figma

Intervention composite figure assembled in Figma

Stratification composite figure assembled in Figma

Full Gallery

The full gallery below also includes the newest diagnostics, evaluation, and set/composition families such as ECDF, QQ, Bland–Altman, Calibration, and UpSet.

Full gallery contact sheet

Quick Start

pip install pubfig

Python Quick Start

Start with the fewest possible parameters first:

import numpy as np
import pubfig as pf

rng = np.random.default_rng(0)
means = np.array([
    [0.78, 0.96],
    [0.88, 1.08],
    [0.84, 1.00],
], dtype=float)

data = rng.normal(loc=means[..., None], scale=0.08, size=(3, 2, 18))
data = np.clip(data, 0.0, None)

fig = pf.bar_scatter(data)
pf.save_figure(fig, "figure1.pdf")

This is enough to get your first figure out. You do not need to understand layout, export, or publication-specific parameters before the first run.

Most common next parameters

Once the minimal example works, these are usually the first parameters worth adding:

fig = pf.bar_scatter(
    data,
    category_names=["Condition A", "Condition B", "Condition C"],
    series_names=["Ctrl", "Treatment"],
    title="Bar + Scatter",
)

pf.save_figure(fig, "figure1.pdf", spec="nature", width="single")
  • category_names: names on the x-axis
  • series_names: names in the legend
  • title: figure title
  • spec / width: journal-style export presets

Only add parameters like aspect_ratio or trim when you already know why you need them.

Where to look up detailed parameters

If you want to understand a specific plot in more detail, start here:

help(pf.bar_scatter)
help(pf.line)
help(pf.heatmap)

You can also inspect runnable examples under examples/.

Saving PNG / SVG / PDF

save_figure(...) now expects an explicit filename suffix:

  • pf.save_figure(fig, "figure1.pdf") → write PDF
  • pf.save_figure(fig, "figure1.svg") → write SVG
  • pf.save_figure(fig, "figure1.png") → write PNG
  • pf.save_figure(fig, "figure1.jpg") → write JPG

If you want multiple outputs, use batch_export(...) instead:

pf.batch_export(fig, "figure1", formats=("pdf", "svg", "png", "jpg"))

batch_export(...) now follows the same publication-size export path as save_figure(...): it resizes to the requested output size first, reruns layout/post-layout hooks, and then writes each format.

Plot recipes by family

These rows are the shortest useful plotting calls. When you want to export one, reuse pf.save_figure(fig, "name.pdf") from Quick Start.

Each row assumes:

import numpy as np
import pubfig as pf
Categorical and statistical
Plot Minimal call Common next parameters
bar pf.bar(np.array([3, 5, 4]), category_names=["A", "B", "C"]) category_names, title, color_palette
bar_scatter pf.bar_scatter(np.clip(np.random.default_rng(0).normal(loc=np.array([[0.78, 0.96], [0.88, 1.08], [0.84, 1.00]])[..., None], scale=0.08, size=(3, 2, 18)), 0.0, None)) category_names, series_names, show_statistics
stacked_bar pf.stacked_bar(np.array([[[3, 2], [4, 1]], [[2, 3], [3, 2]]], dtype=float), group_names=["Batch 1", "Batch 2"]) group_names, normalize, title
paired pf.paired(np.array([1.0, 2.0, 2.5, 3.0]), np.array([1.3, 2.1, 2.9, 3.2])) x_labels, y_label, title
dumbbell pf.dumbbell(np.array([0.72, 0.81, 0.77]), np.array([0.79, 0.86, 0.83]), category_names=["Metric A", "Metric B", "Metric C"]) left_label, right_label, sort_by
forest_plot pf.forest_plot(np.array([1.12, 0.84, 1.36]), np.array([0.98, 0.71, 1.10]), np.array([1.29, 0.99, 1.68]), labels=["Age", "BMI", "Smoking"], reference=1.0) reference, group_labels, right_labels
Composition and polar
Plot Minimal call Common next parameters
grouped_scatter pf.grouped_scatter(np.random.default_rng(0).normal(loc=np.array([[0.72, 0.81, 0.86], [0.68, 0.76, 0.82]])[..., None], scale=0.04, size=(2, 3, 14)), category_names=["Overall", "External"], group_names=["R50", "PLIP", "CONCH"]) category_names, group_names, point_size
donut pf.donut(np.array([48, 27, 14, 8]), labels=["Held-out", "External", "Independent", "Zero-shot"], center_text="97\ntasks") labels, center_text, colors
stacked_ratio_barh pf.stacked_ratio_barh(np.array([30, 33, 40, 65]), labels=["EGFR", "KRAS", "PTEN", "BRAF"], group_labels=["LUAD", "LUAD", "UCEC", "SKCM"]) group_labels, negative_values, title
radial_hierarchy pf.radial_hierarchy(np.array([24, 18, 15, 11, 28, 10]), subgroup_labels=["LIHC", "CRC", "GAST", "PAAD", "LUAD", "ESCA"], subgroup_groups=["Digestive", "Digestive", "Digestive", "Digestive", "Thoracic", "Thoracic"], group_labels=["Digestive", "Thoracic"], center_text="2 systems\n6 classes") group_labels, center_text, show_outer_values
circular_stacked_bar pf.circular_stacked_bar(np.array([[9, 11, 7, 4], [8, 10, 8, 4], [7, 9, 7, 3], [10, 12, 8, 4]], dtype=float), item_labels=["LUAD", "LUSC", "SCLC", "COAD"], item_groups=["Thor", "Thor", "Thor", "GI"]) item_groups, stack_labels, group_legend_show
circular_grouped_bar pf.circular_grouped_bar(np.array([[11, 14, 13], [10, 13, 12], [9, 11, 10], [12, 15, 14]], dtype=float), item_labels=["LUAD", "LUSC", "SCLC", "COAD"], item_groups=["Thor", "Thor", "Thor", "GI"], series_labels=["A", "B", "C"]) series_labels, series_colors, legend_show

The polar defaults for circular_stacked_bar(...) and circular_grouped_bar(...) now follow the denser publication-style settings used in the gallery: tighter group/item spacing, shorter single-line inner labels, and built-in warm/cool palettes. circular_grouped_bar(...) now renders true grouped polar bars with multiple side-by-side series inside each item slot.

Distribution
Plot Minimal call Common next parameters
box pf.box(np.random.default_rng(0).normal(size=(80, 3)), category_names=["A", "B", "C"]) category_names, show_means, title
violin pf.violin(np.random.default_rng(0).normal(size=(80, 3)), category_names=["A", "B", "C"]) category_names, show_box, show_points
strip pf.strip(np.random.default_rng(0).normal(size=(80, 3)), category_names=["A", "B", "C"]) category_names, jitter, title
raincloud pf.raincloud(np.random.default_rng(0).normal(size=(80, 3)), category_names=["A", "B", "C"]) category_names, orientation, title
density pf.density(np.random.default_rng(0).normal(size=400)) title, color_palette, bins
histogram pf.histogram(np.random.default_rng(0).normal(size=400), show_kde=True) bins, show_kde, title
ridgeline pf.ridgeline([np.random.default_rng(0).normal(loc=i, size=200) for i in range(4)], category_names=["S1", "S2", "S3", "S4"]) category_names, offset_step, title
Trend and relationship
Plot Minimal call Common next parameters
area pf.area(np.random.default_rng(0).random((20, 3)), series_names=["A", "B", "C"]) series_names, x, title
line pf.line(np.column_stack([0.72 + 0.05 * np.linspace(0, 10, 16) + 0.10 * np.sin(np.linspace(0, 10, 16) / 1.7), 0.88 + 0.035 * np.linspace(0, 10, 16) + 0.08 * np.cos(np.linspace(0, 10, 16) / 2.0 + 0.4)]), x=np.linspace(0, 10, 16), series_names=["Series 1", "Series 2"]) x_label, y_label, series_names, title
scatter pf.scatter(np.random.default_rng(0).normal(size=60), np.random.default_rng(1).normal(size=60)) labels, x_label, y_label
bubble pf.bubble(np.random.default_rng(0).normal(size=30), np.random.default_rng(1).normal(size=30), np.random.default_rng(2).uniform(1, 10, size=30)) labels, size_label, title
contour2d pf.contour2d(np.random.default_rng(0).normal(size=500), np.random.default_rng(1).normal(size=500)) bins, colorscale, title
hexbin pf.hexbin(np.random.default_rng(0).normal(size=4000), 0.7 * np.random.default_rng(0).normal(size=4000) + 0.5 * np.random.default_rng(1).normal(size=4000), gridsize=28) gridsize, log_color_scale, reduce
radar pf.radar([[0.8, 0.7, 0.9, 0.75], [0.65, 0.85, 0.7, 0.8]], categories=["Speed", "Accuracy", "Recall", "Stability"], series_names=["Model A", "Model B"]) categories, series_names, title
Matrix and multivariate
Plot Minimal call Common next parameters
heatmap pf.heatmap(np.random.default_rng(0).uniform(size=(4, 4))) category_names, title, color scale related options
corr_matrix pf.corr_matrix(np.random.default_rng(0).normal(size=(60, 4)), variable_names=["A", "B", "C", "D"]) variable_names, method, title
clustermap pf.clustermap(np.random.default_rng(0).uniform(size=(8, 6))) row_category_names, column_category_names, title
dimreduce fig, _ = pf.dimreduce(np.random.default_rng(0).normal(size=(40, 8)), cluster_id=np.repeat([0, 1], 20), perplexity=10) cluster_id, labels, n_components
pca_biplot pf.pca_biplot(np.random.default_rng(0).normal(size=(40, 5)), labels=np.repeat(["A", "B"], 20), variable_names=["V1", "V2", "V3", "V4", "V5"]) labels, variable_names, loading_panel
parallel_coordinates pf.parallel_coordinates(np.random.default_rng(0).uniform(size=(20, 4)), variable_names=["W", "X", "Y", "Z"]) variable_names, color_col, title
Evaluation and flow
Plot Minimal call Common next parameters
roc pf.roc([np.array([0.0, 0.1, 0.3, 1.0])], [np.array([0.0, 0.7, 0.9, 1.0])], series_names=["Model A"]) series_names, baseline, title
pr_curve pf.pr_curve([np.array([1.0, 0.9, 0.8, 0.6])], [np.array([0.1, 0.4, 0.7, 1.0])], series_names=["Model A"]) series_names, title, xlim / ylim
volcano pf.volcano(np.random.default_rng(0).normal(size=300), np.random.default_rng(1).uniform(1e-4, 1.0, size=300), fc_threshold=1.0, p_threshold=0.05) fc_threshold, p_threshold, labels
sankey pf.sankey([0, 0, 1], [2, 3, 3], [10, 5, 8], node_names=["Input A", "Input B", "Path 1", "Outcome"]) node_names, title, color_palette

For bar_scatter(...), significance spacing parameters now follow explicit orientation-based names:

fig = pf.bar_scatter(
    data,
    show_statistics=True,
    significance_ns_label_offset_ratio_vertical=0.08,
    significance_stars_label_offset_ratio_vertical=-0.12,
    significance_label_offset_ratio_vertical=0.07,
)

pubfig → Figma

What this gives you

pubfig exports clean panel artwork, and Figma stays the place where you assemble and finish the whole publication figure.

For day-to-day use, the main command is pubfig figma push.

Quick Start

  1. Install pubfig-sync in Figma Desktop the first time: go to Plugins → Development → Import plugin from manifest..., then select figma-plugin/pubfig-sync/manifest.json from this repo. After that, reopen it from Plugins → Development → pubfig-sync.
  2. Click Connect Bridge once in the plugin.
  3. Export your panels from Python.
  4. Run pubfig figma push <panel_dir> --figure-id <id> from the terminal.
  5. If the bridge path fails, load the written bundle in the plugin and use the manual buttons.
pubfig figma push panels --figure-id figure-01

Minimal example

Panel export now defaults to clean, title-free art so subplot titles can be handled at the Figma assembly layer. If you explicitly want embedded panel headers, pass include_title=True.

import numpy as np
import pubfig as pf

rng = np.random.default_rng(0)

panels = {
    "a": pf.bar(rng.uniform(0.4, 0.9, size=3), category_names=["A", "B", "C"]),
    "b": pf.scatter(rng.normal(size=40), rng.normal(size=40)),
}

pf.export_panels(panels, "panels", overwrite=True)  # title-free art by default
pubfig figma push panels --figure-id figure-01

This writes panel assets such as a.svg, b.svg, and panel-index.json, then uses push as the primary panel-first handoff into Figma.

How refresh works

  • Keep the same figure_id to refresh the existing figure in place.
  • Use a new figure_id to import a separate figure.

FAQ / Troubleshooting

What does Connect Bridge do?
It links the open Figma plugin to your local terminal workflow so later push commands know which live session to refresh.

What does pubfig figma push do automatically?
It is the primary agent-first command. It ensures the local bridge is available, selects the latest connected session, writes the bundle, and then syncs or refreshes the figure.

What is the .pubfig-figma.json file?
It is the exact Figma handoff bundle for one figure. Keep it around for manual import, refresh, debugging, or recovery.

How do I do manual fallback?
If bridge refresh stalls, load the latest written .pubfig-figma.json bundle in pubfig-sync, then use Import as New, Manual Refresh, or Refresh + Relayout.

When should I use pubfig figma package?
Use it as the secondary path when you only want to write a standalone bundle without pushing immediately.

pubfig figma package panels --figure-id figure-01

Where are the advanced commands?
Use these only for finer control or debugging after the normal push path:

pubfig figma sync figure-01.pubfig-figma.json --session latest
pubfig figma watch figure-01.pubfig-figma.json --session latest
pubfig figma bridge status

If you use Codex locally, the companion skill pubfig-figma-workflow can still orchestrate the panel export → Figma import → MCP review loop.

Plot Families

Categorical and Statistical Plots

Function Description Recipe
bar Simple bar chart and grouped bar chart recipe
bar_scatter Grouped bar chart with raw points and significance annotations recipe
stacked_bar Horizontal stacked bar chart recipe
paired Paired dot plot recipe
dumbbell Connected paired comparison plot recipe
forest_plot Effect-size plot with confidence intervals recipe

Composition and Polar Plots

Function Description Recipe
grouped_scatter Dense grouped scatter / strip benchmark panel recipe
donut Publication-style donut chart recipe
stacked_ratio_barh 100 percent horizontal ratio bar chart recipe
radial_hierarchy Two-level radial hierarchy / sunburst-style chart recipe
circular_stacked_bar Dense circular stacked bar chart with inner group ring recipe
circular_grouped_bar Dense circular grouped bar chart with inner group ring recipe

Distribution Plots

Function Description Recipe
box Box plot recipe
violin Violin plot recipe
strip Strip plot recipe
raincloud Half-violin + box + raw-point raincloud plot recipe
density Density plot with KDE recipe
histogram Histogram with optional KDE recipe
ridgeline Ridgeline plot recipe

Trend and Relationship Plots

Function Description Recipe
line Line chart with optional CI recipe
area Stacked area chart recipe
scatter Scatter plot with optional grouped workflow recipe
bubble Bubble chart recipe
contour2d 2D contour plot with marginals recipe
hexbin Dense-scatter hexbin plot recipe
radar Radar chart recipe

Matrix, Embedding, and Multivariate Plots

Function Description Recipe
heatmap Heatmap recipe
corr_matrix Correlation heatmap recipe
clustermap Clustered heatmap recipe
dimreduce Dimensionality-reduction scatter plot recipe
pca_biplot PCA biplot with optional loadings and group ellipses recipe
parallel_coordinates Parallel coordinates plot recipe

Evaluation and Flow Plots

Function Description Recipe
roc ROC curve with AUC recipe
pr_curve Precision-Recall curve with AP recipe
volcano Volcano plot for effect size vs significance recipe
sankey Sankey diagram recipe

Themes, Specs, and Palettes

Built-in Themes

pubfig currently ships with these themes:

  • default
  • nature
  • science
  • cell
pf.set_default_theme("science")

Figure Specs

For export, save_figure(...) uses named figure specs:

  • nature
  • science
  • cell

Width can be specified as:

  • "single"
  • "double"
  • numeric millimeters such as 120
  • string millimeters such as "120mm"

Built-in Palettes

Built-in palettes include:

  • DEFAULT
  • NATURE
  • SCIENCE
  • LANCET
  • JAMA
from pubfig import NATURE, show_palette

show_palette(NATURE).show()

You can also fetch palettes by name:

palette = pf.get_palette("science")
palette = pf.get_palette("carto_blugrn")

These journal-style palettes are inspired palettes, not official journal standards. In pubfig, the NATURE, SCIENCE, LANCET, and JAMA cards are derived from widely used ggsci-derived community palettes rather than publisher-mandated color specifications.

Source note: ggsci documents these palettes as inspired by NPG / Nature Publishing Group, AAAS / Science, Lancet journals, and JAMA figures. See pal_npg, pal_aaas, pal_lancet, and pal_jama.

For a visual preview of all currently available palettes, see docs/palette-gallery.md.

Featured palettes

Gallery and Examples

Most files under examples/ are either:

  • runnable example scripts, or
  • rendered assets used by this README and the palette docs.

If you only want the main entry points, start here:

  • examples/gallery.py — quick visual walkthrough of supported plots
  • examples/export_gallery.py — exports the gallery to output_figures/
  • examples/export_composite_showcases_panels.py — exports the panel-first composite showcases that are assembled in Figma
  • examples/figma_workflow_demo.md — panel-first pubfig → Figma workflow guide
  • examples/generate_palette_gallery.py — regenerates the palette preview sheets and gallery docs
  • examples/README.md — keep/remove inventory for this folder
  • help(pubfig.circular_stacked_bar) / help(pubfig.circular_grouped_bar) — inspect the fixed dense polar defaults directly from Python

Advanced / secondary:

  • examples/export_gallery_mpl.py — focused Matplotlib export examples
  • figma-plugin/pubfig-sync/ — Figma plugin scaffold for panel import and refresh
  • docs/palette-gallery.md — visual palette gallery for built-in and Plotly-derived palettes

Development

Editable Install

pip install -e .[dev]

Run Tests

pytest

Lint

ruff check src tests examples

Regenerate Gallery

python examples/export_gallery.py

License

MIT

About

Publication-style scientific plotting for papers: common plot families, clean exports, and panel-first Figma assembly.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors