briefing
Build beautiful, self-contained HTML reports from Python analysis.
Why briefing exists
Two libraries shaped how I thought about sharing data science work:
- datapane — the cleanest Python-native report builder I had ever used. Block-based, self-contained HTML output, dead-simple API. It was decommissioned in 2023 and the SaaS shut down shortly after.
- Facets (PAIR / Google) — specifically Facets Dive, a brilliant interactive dot explorer that let you slice any dataset visually with zero configuration. The project went largely unmaintained and quietly disappeared from most data science workflows.
I never found a substitute that matched either of them, let alone both at once. briefing is my attempt to fill that gap: a datapane-style block and layout system with a Facets Dive-class explorer built in, fully offline, no cloud account required.
Building briefing also serves a second purpose: it is a real-world, non-trivial Python project used to benchmark coding agents such as Claude Code. Designing a library from original source code — with concept ofblock hierarchy, rendering pipeline, theming, interactive components — gives a coding agent enough surface area to show where it genuinely helps and where it still struggles.
import briefing as bf
report = bf.Briefing(
bf.Text("# Sales Analysis — Q1 2024"),
bf.Group(
bf.BigNumber("Revenue", "$4.2M", change="-12%", is_upward_change=False),
bf.BigNumber("Active Users", "142K", change="+3%", is_upward_change=True),
columns=2,
),
bf.Select(
bf.Plot(fig, label="Trend"),
bf.DataTable(df, label="Raw Data"),
bf.lab.DataProfile(df, label="Profile"),
),
bf.lab.DataDive(df),
)
bf.save(report, "q1_analysis.html")
Features
- Self-contained HTML — zero CDN, works offline forever
- Zero visualisation dependencies in the core — the report grammar renders with only Jinja2 + markdown-it-py. Charts are opt-in (see below).
- Bring-your-own-figure
Plot— hand it a Plotly, Altair, Matplotlib or Bokeh figure; briefing imports that library only when you pass its figure. - Interactive tables — sortable, searchable DataTable with client-side pagination
briefing.lab— richer custom visualisations (DataProfilecolumn stats,DataDiveFacets-style dot explorer), all hand-rolled SVG + vanilla JS, no CDN- Themes — five built-in presets plus full CSS token control
- Pandas 2.x first-class support; PySpark via
.toPandas()
Installation
pip install briefing # core grammar + HTML renderer, no viz deps
| Extra | Adds |
|---|---|
briefing[pandas] |
pandas engine for Table / DataTable |
briefing[lab] |
briefing.lab custom-visualisation blocks (DataProfile, DataDive) |
briefing[plotly] [altair] [bokeh] [matplotlib] |
the matching backend for Plot |
briefing[charts] |
all four Plot backends at once |
briefing[email] |
email-safe HTML + SMTP sending |
Plot never imports a plotting library unless you actually pass it a figure from
that library, so the extras only need to be installed for the backends you use.
Quick start
import pandas as pd
import briefing as bf
df = pd.read_csv("sales.csv")
bf.save(
bf.Briefing(
bf.Text("# My Report"),
bf.DataTable(df),
),
path="report.html",
open=True, # opens in browser immediately
)
To get an HTML string instead of writing a file (useful in Jupyter):
from IPython.display import HTML, display
display(HTML(bf.stringify(bf.Briefing(bf.Text("# Hello")))))
API
bf.save
bf.save(
blocks, # Briefing, list, or a single block
path, # destination file — e.g. "report.html"
*,
open=False, # open in default browser after saving
name="Report", # browser tab title and report header
formatting=None, # Formatting instance — controls theme
)
bf.stringify
html: str = bf.stringify(
blocks,
*,
name="Report",
formatting=None,
)
Block reference
Text blocks
bf.Text — Markdown
bf.Text("# Heading\n\nSome **bold** and *italic* text.")
# From a .md file
bf.Text(file="notes.md")
| Parameter | Type | Description |
|---|---|---|
text |
str |
Markdown string (dedented automatically) |
file |
str | Path |
Path to a .md file (alternative to text) |
label |
str |
Tab/selector label when used inside Select |
Supports headings, bold, italics, inline code, blockquotes, tables, and lists.
bf.Code — Syntax-highlighted code
bf.Code("SELECT * FROM orders LIMIT 10", language="sql")
bf.Code(
"import briefing as bf\nfl.save(bf.Briefing(bf.Text('# Hi')), 'out.html')",
language="python",
caption="Minimal report",
)
| Parameter | Type | Default | Description |
|---|---|---|---|
code |
str |
— | Source code string |
language |
str |
"python" |
Syntax highlighting language |
caption |
str |
None |
Optional caption shown below the block |
bf.Formula — LaTeX equation
bf.Formula(r"\bar{x} = \frac{1}{n}\sum_{i=1}^{n} x_i", caption="Sample mean")
Rendered via MathJax (inlined in the report — no CDN needed).
| Parameter | Type | Default | Description |
|---|---|---|---|
formula |
str |
— | LaTeX expression (without $$ delimiters) |
caption |
str |
None |
Optional caption |
bf.HTML — Raw HTML fragment
bf.HTML("<p style='color:#4F46E5'>Custom <strong>HTML</strong>.</p>")
Rendered inside a sandboxed container — inline styles work, scripts are stripped.
bf.BigNumber — KPI metric
bf.BigNumber("Revenue", "$4.2M", change="-12%", is_upward_change=False)
bf.BigNumber("Accuracy", 0.924) # no change indicator
| Parameter | Type | Default | Description |
|---|---|---|---|
heading |
str |
— | Metric label |
value |
str | int | float |
— | Headline value |
change |
str |
None |
Delta string, e.g. "+3.1%" |
is_upward_change |
bool |
None |
True = green arrow, False = red arrow. Required if change is set. |
If
changeis provided withoutis_upward_changea warning is issued and no arrow is shown.
bf.Alert — Callout box
bf.Alert("Pipeline completed successfully.", level=bf.AlertLevel.SUCCESS)
bf.Alert("Margin erosion in South region.", level="warning", title="Watch")
bf.Alert("Legacy source decommissioned.", level=bf.AlertLevel.ERROR, title="Breaking change")
| Parameter | Type | Default | Description |
|---|---|---|---|
message |
str |
— | Alert body text |
level |
AlertLevel | str |
"info" |
One of info, success, warning, error |
title |
str |
None |
Optional bold title above the message |
bf.AlertLevel values: INFO, SUCCESS, WARNING, ERROR.
Layout blocks
bf.Group — Grid layout
Arranges child blocks in a responsive column grid.
bf.Group(plot_a, plot_b, columns=2)
bf.Group(a, b, c, columns=3, widths=[2, 1, 1]) # relative column widths
| Parameter | Type | Default | Description |
|---|---|---|---|
*blocks |
Block |
— | Child blocks (positional) |
columns |
int |
1 |
Number of columns |
widths |
list[int | float] |
None |
Relative column widths — must match columns |
valign |
VAlign | str |
"top" |
Vertical alignment: top, center, bottom |
label |
str |
None |
Tab label when used inside Select |
bf.Select — Tabbed or dropdown panel switcher
Shows one child block at a time. Each child's label becomes the tab title.
bf.Select(
bf.Plot(fig, label="Chart"),
bf.DataTable(df, label="Data"),
bf.lab.DataProfile(df, label="Profile"),
type=bf.SelectType.TABS, # or bf.SelectType.DROPDOWN
)
| Parameter | Type | Default | Description |
|---|---|---|---|
*blocks |
Block |
— | Child blocks — each should have a label |
type |
SelectType | str |
"tabs" |
"tabs" or "dropdown" |
Warns if fewer than 2 children are provided.
bf.Toggle — Collapsible section
Collapsed by default; click the label to expand.
bf.Toggle(
bf.Text("Methodology notes — hidden by default."),
bf.Code("SELECT * FROM sales\n", language="sql"),
label="Query details",
)
| Parameter | Type | Default | Description |
|---|---|---|---|
*blocks |
Block |
— | Content blocks (multiple are auto-wrapped in a Group) |
label |
str |
None |
Clickable toggle label |
bf.Page — Top-level page tab
Use at the root of Briefing to create multi-page reports. Pages are converted to a top-level tab bar during rendering.
bf.Briefing(
bf.Page(summary_group, title="Summary"),
bf.Page(detail_group, title="Detail"),
)
| Parameter | Type | Default | Description |
|---|---|---|---|
*blocks |
Block |
— | Page content |
title |
str |
None |
Tab title |
Nested
Pageblocks are not supported — useSelectandGroupinstead.
bf.Briefing — Root document container
Top-level wrapper passed to save. Accepts any combination of blocks.
report = bf.Briefing(
bf.Text("# My Report"),
bf.Plot(fig),
bf.DataTable(df),
)
bf.save(report, "report.html")
Asset blocks
bf.Plot — Chart / figure
Library-agnostic chart block. Auto-detects the figure type at render time:
| Library | Output |
|---|---|
| Plotly | Interactive HTML (inline JS) |
| Altair / Vega-Lite | Embedded Vega spec |
| Matplotlib / Seaborn / Plotnine | Inline SVG |
| Bokeh | Inline resources |
bf.Plot(plotly_fig, caption="Revenue over time")
bf.Plot(altair_chart, label="Chart", responsive=True)
bf.Plot(mpl_fig, scale=1.5)
| Parameter | Type | Default | Description |
|---|---|---|---|
figure |
any | — | Figure object from Plotly, Altair, Matplotlib, or Bokeh |
caption |
str |
None |
Caption shown below the chart |
responsive |
bool |
True |
Scale chart to fill container width |
scale |
float |
1.0 |
Scale multiplier (Matplotlib/static figures) |
bf.Table — Static table (pandas Styler)
Best for formatted DataFrames where you want to preserve Styler rules.
bf.Table(df)
bf.Table(
df.style
.format({"revenue": "€ {:,.0f}"})
.bar(subset=["revenue"], color="#c7d2fe")
.set_caption("Top 20 by revenue"),
)
| Parameter | Type | Default | Description |
|---|---|---|---|
data |
DataFrame | Styler |
— | DataFrame or styled DataFrame |
caption |
str |
None |
Optional caption |
bf.DataTable — Interactive table
Sortable, searchable, paginated table. Handles large datasets gracefully.
bf.DataTable(df, caption="Full sales dataset")
bf.DataTable(df, max_rows=500) # cap at 500 rows
| Parameter | Type | Default | Description |
|---|---|---|---|
df |
DataFrame |
— | Source data |
caption |
str |
None |
Optional caption |
max_rows |
int |
10 000 |
Rows beyond this are truncated with a warning |
Lab blocks — briefing.lab
bf.lab.DataProfile — Column statistics
Renders one card per column with dtype, missing %, and a mini-chart.
- Numeric — mean, std, min/quartiles/max + inline histogram
- Categorical — n_unique, top values + inline bar chart
- Datetime — date range + gap detection
No extra dependencies — mini-charts are pure SVG.
bf.lab.DataProfile(df)
bf.lab.DataProfile(df, missing_threshold=0.05) # red highlight at >5% missing
bf.lab.DataProfile(df, max_categories=10) # cap top-N bars for categoricals
| Parameter | Type | Default | Description |
|---|---|---|---|
df |
DataFrame |
— | Source data |
missing_threshold |
float |
0.20 |
Missing % above which the cell is highlighted red |
max_categories |
int |
20 |
Max top-value bars for categorical columns |
bf.lab.DataDive — Interactive dot explorer
Each DataFrame row becomes a dot. Dropdowns let the viewer dynamically change which columns drive X, Y, colour, and facets — similar to Google Facets Dive.
bf.lab.DataDive(df) # auto-selects axes
bf.lab.DataDive(df, x="revenue", y="margin_pct", color="region")
bf.lab.DataDive(df, x="region", y="channel", color="product", layout="tile")
| Parameter | Type | Default | Description |
|---|---|---|---|
df |
DataFrame |
— | Source data |
x |
str |
None |
Initial X-axis column |
y |
str |
None |
Initial Y-axis column |
color |
str |
None |
Initial colour column |
facet_row |
str |
None |
Column to facet into rows |
facet_col |
str |
None |
Column to facet into columns |
layout |
str |
"scatter" |
"scatter" (2-D axes) or "tile" (packed dot grid) |
max_rows |
int |
10 000 |
Rows beyond this are sampled with a warning |
Theming
Pass a Formatting instance to save to control the visual style.
Built-in presets
bf.save(blocks, "out.html", formatting=bf.Formatting.dark())
bf.save(blocks, "out.html", formatting=bf.Formatting.corporate())
bf.save(blocks, "out.html", formatting=bf.Formatting.minimal())
bf.save(blocks, "out.html", formatting=bf.Formatting.ocean())
bf.save(blocks, "out.html", formatting=bf.Formatting.warm())
| Preset | Description |
|---|---|
dark() |
Slate dark — easy on the eyes, great for dashboards |
corporate() |
Clean blue corporate — neutral tones, sharp corners |
minimal() |
Ultra-clean — white space, black text, hairline borders |
ocean() |
Deep teal — rich mid-dark, emerald accent |
warm() |
Cream backgrounds — amber accent, serif font |
Fine-tuning a preset
Every preset accepts keyword overrides:
formatting=bf.Formatting.dark(accent_color="#f43f5e") # dark theme, rose accent
formatting=bf.Formatting.corporate(width=bf.Width.FULL) # full-width corporate
Building from scratch
formatting=bf.Formatting(
accent_color="#0369a1",
bg_color="#f8fafc",
surface_color="#e2e8f0",
border_color="#cbd5e1",
text_color="#0f172a",
muted_color="#64748b",
radius="0.25rem",
width=bf.Width.NARROW,
)
Formatting token reference
| Token | Default | Controls |
|---|---|---|
accent_color |
#4F46E5 |
Tab underlines, active borders, links, focus rings |
bg_color |
#ffffff |
Page background |
text_color |
#111827 |
Body and heading text |
muted_color |
#6b7280 |
Labels, captions, axis text, icons |
border_color |
#e5e7eb |
Table borders, card borders, dividers |
surface_color |
#f9fafb |
Card / table-header / code-block backgrounds |
font |
Inter, ui-sans-serif, … |
CSS font-family stack |
radius |
0.5rem |
Border-radius on cards, badges, buttons |
width |
Width.MEDIUM |
Container max-width (NARROW / MEDIUM / FULL or raw CSS) |
text_alignment |
left |
Paragraph text alignment |
bf.Width values: NARROW (768 px), MEDIUM (1200 px), FULL (100%).
Recipes
KPI dashboard with tabbed detail
import briefing as bf
bf.save(
bf.Briefing(
bf.Text("# Sales Analysis — 2023"),
bf.Group(
bf.BigNumber("Total Revenue", "€ 1 260 000", change="+12.4%", is_upward_change=True),
bf.BigNumber("Units Sold", "12 640", change="+3.1%", is_upward_change=True),
bf.BigNumber("Avg Margin", "31.2 %", change="-0.8%", is_upward_change=False),
bf.BigNumber("Return Rate", "8.0 %", change="+0.2%", is_upward_change=False),
columns=4,
),
bf.Alert("South region margin dropped below 25% in December.",
level=bf.AlertLevel.WARNING, title="Action needed"),
bf.Select(
bf.Group(bf.DataTable(by_region, caption="Region summary"), columns=1, label="By Region"),
bf.Group(bf.DataTable(monthly, caption="Monthly aggregates"), columns=1, label="Monthly"),
bf.Group(bf.DataTable(df, caption="All transactions"), columns=1, label="Raw Data"),
),
bf.Text("## Column Profile"),
bf.lab.DataProfile(df),
bf.Text("## Interactive Explorer"),
bf.lab.DataDive(df, x="revenue", y="margin_pct", color="region"),
bf.Toggle(
bf.Text("**Refresh cadence**: nightly at 02:00 UTC."),
bf.Code("SELECT * FROM sales WHERE date >= '2023-01-01'\n", language="sql"),
label="Methodology & sources",
),
),
path="sales_2023.html",
name="Sales Analysis — 2023",
formatting=bf.Formatting(accent_color="#0f766e"),
)
Multi-library charts in one report
import matplotlib; matplotlib.use("Agg")
import matplotlib.pyplot as plt
import plotly.express as px
import briefing as bf
fig_plotly = px.scatter(df, x="revenue", y="margin_pct", color="region")
fig_mpl, ax = plt.subplots()
ax.hist(df["revenue"].dropna(), bins=30)
bf.save(
bf.Briefing(
bf.Text("# Chart comparison"),
bf.Group(
bf.Plot(fig_plotly, caption="Interactive (Plotly)"),
bf.Plot(fig_mpl, caption="Static SVG (Matplotlib)"),
columns=2,
),
),
path="charts.html",
)
Hiding methodology behind a toggle
bf.Toggle(
bf.Text("""
**Data source**: internal data warehouse.
**Contact**: analytics@example.com
"""),
bf.Code("SELECT date, region, revenue FROM dw.sales\n", language="sql"),
label="Methodology & sources",
)
Tile layout (Facets Dive style)
# Each (region × channel) cell is a group of packed dots coloured by product
bf.lab.DataDive(df, x="region", y="channel", color="product", layout="tile")
Jupyter inline display
from IPython.display import HTML, display
display(HTML(bf.stringify(
bf.Briefing(bf.Text("# Quick look"), bf.lab.DataProfile(df)),
name="Quick look",
)))
Running the demo
cd /path/to/html_reporting_python
python demo/generate.py
This writes nine self-contained HTML files to demo/:
| File | Contents |
|---|---|
01_blocks.html |
All text and layout blocks |
02_tables.html |
Static Table and interactive DataTable |
03_profile.html |
DataProfile on a mixed dataset |
04_datadive.html |
DataDive scatter explorer |
05_full_report.html |
Full combined KPI report |
06_facets_dive.html |
Tile layout comparison |
07_plotly.html |
Interactive Plotly charts |
08_matplotlib.html |
Static Matplotlib charts (inline SVG) |
09_theme_*.html |
CSS theme showcase (dark, corporate, minimal, ocean, warm) |
Release files for briefing 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| briefing-0.1.0.tar.gz | 64.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| briefing-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 121.3 kB
Release files / briefing-0.1.0.tar.gz
| Download URL | briefing-0.1.0.tar.gz |
|---|---|
| Size | 64.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4983f84980ea721a113e725323a0f9c66bfe0ea5bf036c6cd1a8433df3ae2934
|
|
BLAKE2b-256 checksum How to use checksums |
548ec398c048d32ed8a79fed2368208dae31e5225ad937db16ff80528af83b44
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 10, 2026.
Transparency logRelease files / briefing-0.1.0-py3-none-any.whl
| Download URL | briefing-0.1.0-py3-none-any.whl |
|---|---|
| Size | 56.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
96ca74e88473079d184466962c987b324de3d5642aa4c7ecb53ca0c77279de8f
|
|
BLAKE2b-256 checksum How to use checksums |
3e2b34ea82a87ff02fd7cf99793596b7448cbc282e63ee95f5e3f8acb085f9f8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 10, 2026.
Transparency log