Skip to main content

FSB: Figure-Statistics Bundle

Python 3.9+ License: AGPL-3.0

FSB is a specification and Python API for reproducible scientific figures. It defines a structured bundle format that captures everything needed to reproduce a figure: data, encodings, theme, and statistical analyses.

Why FSB?

Scientific figures typically become "black boxes" once exported to PNG/PDF:

  • Data is lost or separated from the figure
  • Statistical test parameters are not recorded
  • Visual encoding decisions (color = condition) are implicit
  • Reproducing or modifying the figure requires the original script

FSB solves this by packaging figures as self-contained bundles with:

  • Canonical data (CSV + metadata)
  • Encoding specification (data → visual channel mappings)
  • Theme (decorative styles, separable from meaning)
  • Statistical results (with full reproducibility info)
  • Derived outputs (PNG, SVG, PDF)

Installation

pip install fsb

Or install with optional dependencies:

pip install fsb[all]  # Include matplotlib, scipy, statsmodels

Quick Start

Create a Bundle

import fsb

# Create a new plot bundle
bundle = fsb.Bundle(
    "my_plot",
    create=True,
    node_type="plot",
    name="My First Plot",
    size_mm={"width": 80, "height": 60},
)

# Configure encoding
bundle.encoding = {
    "traces": [{
        "trace_id": "line1",
        "data_ref": "data/data.csv",
        "x": {"column": "time", "scale": "linear"},
        "y": {"column": "value", "scale": "linear"},
    }]
}

# Save
bundle.save()

Load and Inspect

bundle = fsb.Bundle("my_plot")
print(f"Type: {bundle.bundle_type}")
print(f"Size: {bundle.node.size_mm}")
print(f"Traces: {len(bundle.encoding['traces'])}")

Create Figure with Children

# Create a multi-panel figure
figure = fsb.Bundle(
    "my_figure",
    create=True,
    node_type="figure",
    size_mm={"width": 170, "height": 130},
)

# Add child plots
for panel in ["A", "B", "C", "D"]:
    figure.add_child(f"plot_{panel}", node_type="plot")

figure.save()

Validate Against Schemas

bundle = fsb.Bundle("my_plot")
results = bundle.validate(raise_on_error=False)

for file_name, (is_valid, error) in results.items():
    status = "✓" if is_valid else "✗"
    print(f"{status} {file_name}")

Bundle Structure

my_plot/
├── node.json           # Canonical: structure, bbox, refs
├── encoding.json       # Canonical: data → visual mappings
├── theme.json          # Aesthetics: colors, fonts, styles
├── data/
│   ├── data.csv        # Canonical: raw data
│   └── data_info.json  # Canonical: column metadata
├── stats/
│   ├── stats.json      # Canonical: statistical results
│   └── stats.csv       # Derived: human-readable table
├── exports/            # Derived: rendered outputs
│   ├── plot.png
│   ├── plot.svg
│   └── plot.pdf
├── cache/              # Regenerable: can be deleted
│   ├── geometry_px.json
│   ├── render_manifest.json
│   └── hitmap.svg
└── children/           # For figures: child plot bundles

File Categories

Category Files Description
Canonical node.json, encoding.json, theme.json, data/*, stats/stats.json Source of truth. Don't regenerate.
Derived exports/*, stats/stats.csv Generated from canonical. Can regenerate.
Cache cache/* Temporary/UI data. Can delete.

Schema Reference

node.json

Defines the figure/plot structure:

{
  "id": "my_plot",
  "type": "plot",
  "name": "Firing Rate Over Time",
  "bbox_norm": {"x0": 0, "y0": 0, "x1": 1, "y1": 1},
  "size_mm": {"width": 80, "height": 60},
  "axes": {
    "xlim": [0, 100],
    "ylim": [0, 50],
    "xlabel": "Time (ms)",
    "ylabel": "Rate (Hz)"
  },
  "refs": {
    "encoding": "encoding.json",
    "theme": "theme.json",
    "data": "data/",
    "stats": "stats/stats.json"
  },
  "children": ["subplot_A", "subplot_B"]
}

encoding.json

Maps data to visual channels:

{
  "traces": [
    {
      "trace_id": "control",
      "data_ref": "data/data.csv",
      "x": {"column": "time", "scale": "linear"},
      "y": {"column": "firing_rate", "scale": "linear"},
      "group": {"column": "condition"}
    }
  ],
  "legends": {
    "color": {
      "control": "Control Group",
      "treatment": "Treatment Group"
    }
  }
}

theme.json

Visual aesthetics (decorative, not semantic):

{
  "colors": {
    "palette": ["#1f77b4", "#ff7f0e", "#2ca02c"],
    "background": "#ffffff"
  },
  "typography": {
    "family": "sans-serif",
    "size_pt": 10
  },
  "lines": {"width_pt": 1.5},
  "markers": {"size_pt": 6}
}

stats/stats.json

Statistical analyses with reproducibility info:

{
  "analyses": [
    {
      "result_id": "ttest_main",
      "method": {
        "name": "t-test",
        "variant": "independent",
        "correction": {"method": "bonferroni", "n_comparisons": 3}
      },
      "inputs": {
        "data_refs": [{"path": "data/data.csv", "sha256": "abc123..."}],
        "groups": ["control", "treatment"],
        "n_per_group": [25, 23]
      },
      "results": {
        "statistic": 2.45,
        "p_value": 0.018,
        "p_corrected": 0.054,
        "effect_size": {"name": "cohens_d", "value": 0.72}
      },
      "display": {
        "significance_label": "*",
        "bracket": {"start": "control", "end": "treatment"}
      }
    }
  ],
  "software": {
    "python": "3.11",
    "scipy": "1.11.0",
    "fsb": "0.1.0"
  }
}

data/data_info.json

Column metadata:

{
  "source": {
    "path": "data/data.csv",
    "sha256": "abc123...",
    "created_at": "2025-12-19T12:00:00"
  },
  "shape": {"rows": 100, "columns": 4},
  "columns": [
    {
      "name": "time",
      "dtype": "float64",
      "unit": "ms",
      "role": "x",
      "min": 0,
      "max": 100
    },
    {
      "name": "firing_rate",
      "dtype": "float64",
      "unit": "Hz",
      "role": "y"
    },
    {
      "name": "condition",
      "dtype": "category",
      "role": "group",
      "categories": ["control", "treatment"]
    }
  ]
}

Key Concepts

Canonical vs Derived

  • Canonical: The source of truth. These files define the figure.
  • Derived: Generated from canonical files. Can be regenerated.
  • Cache: Temporary files for UI/rendering. Can be deleted.

Recursive Structure

Figures and plots share the same node structure. A figure is simply a node with children:

figure/
├── node.json (type: "figure", children: ["plot_A", "plot_B"])
└── children/
    ├── plot_A/
    │   └── node.json (type: "plot")
    └── plot_B/
        └── node.json (type: "plot")

Encoding vs Theme

  • Encoding: Semantic mappings (color = condition, linestyle = group)
  • Theme: Decorative properties (font family, line width, color palette)

Changing theme = same meaning, different look. Changing encoding = different meaning.

API Reference

Bundle

class Bundle:
    def __init__(self, path, create=False, node_type="plot", name=None, size_mm=None): ...

    # Properties
    @property def node(self) -> Node: ...
    @property def encoding(self) -> dict: ...
    @property def theme(self) -> dict: ...
    @property def stats(self) -> dict: ...
    @property def bundle_type(self) -> str: ...
    @property def children(self) -> List[str]: ...

    # Methods
    def save(self, path=None) -> Path: ...
    def validate(self, raise_on_error=True) -> dict: ...
    def add_child(self, child_id, **kwargs) -> Bundle: ...
    def get_child(self, child_id) -> Bundle: ...
    def clear_cache(self) -> None: ...
    def to_dict(self) -> dict: ...

Schema Validation

from fsb import validate, load_schema, SCHEMA_NAMES

# Validate data against a schema
is_valid, error = validate(data, "node", raise_on_error=False)

# Load a schema
schema = load_schema("encoding")

# Available schemas
print(SCHEMA_NAMES)  # ['node', 'encoding', 'theme', 'stats', 'data_info', 'render_manifest']

Models

from fsb import Node, Encoding, Theme, Stats, DataInfo
from fsb.models import BBox, SizeMM, Axes

# Create a node programmatically
node = Node(
    id="my_plot",
    type="plot",
    bbox_norm=BBox(0, 0, 1, 1),
    size_mm=SizeMM(80, 60),
)

Examples

See the examples/ directory for complete working examples:

  1. 01_create_bundle.py - Create a basic plot bundle
  2. 02_figure_with_children.py - Create multi-panel figures
  3. 03_validate_bundle.py - Validate bundles against schemas
  4. 04_data_and_stats.py - Work with data files and statistics

Development

# Clone repository
git clone https://github.com/ywatanabe1989/fsb.git
cd fsb

# Install in development mode
pip install -e ".[dev]"

# Run tests
pytest

# Format code
black src tests
ruff check src tests

License

AGPL-3.0 License - see LICENSE for details.

Citation

If you use FSB in your research, please cite:

@software{fsb2025,
  title = {FSB: Figure-Statistics Bundle},
  author = {Watanabe, Yusuke},
  year = {2025},
  url = {https://github.com/ywatanabe1989/fsb}
}

Metadata

Release files for fsb 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for fsb 0.1.1
File Size Uploaded Provenance
fsb-0.1.1.tar.gz 35.4 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for fsb 0.1.1
File Interpreter ABI Platform
fsb-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 66.6 kB

Release files / fsb-0.1.1.tar.gz

Download URL fsb-0.1.1.tar.gz
Size 35.4 kB
Tags Source
SHA-256 checksum
How to use checksums
83c677475c109c3638cbde85701e133b862127e4ea4909e70a70174ba49250fe
BLAKE2b-256 checksum
How to use checksums
699fe89c59ff645d41f040ce06bd23218481dcb0ffddcf3818e98bbc2f1a797b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.10.12

Release files / fsb-0.1.1-py3-none-any.whl

Download URL fsb-0.1.1-py3-none-any.whl
Size 31.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
65fc8442614eb8d094b9daf6f36fbbb8c0fea92dd4399b2bb05ff96b928d00ba
BLAKE2b-256 checksum
How to use checksums
24258dcc5b6459f27bb4836b8fdbf7e55ba38e5808463aedb1f24c2b256ce8c3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.10.12

Security for fsb 0.1.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page