GlyphX
A next-generation Python visualization library — SVG-first, interactive, and built to replace Matplotlib, Seaborn, and Plotly.
GlyphX renders crisp, interactive SVG charts that work everywhere — Jupyter notebooks, CLI pipelines, FastAPI servers, and static HTML files — with zero configuration and no plt.show() required.
View the documentation page here for more detailed explanations of everything.
Why GlyphX?
| Feature | GlyphX | Matplotlib | Seaborn | Plotly |
|---|---|---|---|---|
Auto-display (no show()) |
✅ | ❌ | ❌ | ❌ |
| Method chaining API | ✅ | ❌ | ❌ | Partial |
DataFrame accessor (df.glyphx.*) |
✅ | ❌ | Partial | ❌ |
| Linked interactive brushing | ✅ | ❌ | ❌ | ✅ (needs server) |
| Self-contained shareable HTML | ✅ | ❌ | ❌ | ❌ |
| Statistical significance brackets | ✅ | ❌ | ❌ | ❌ |
| ECDF plot | ✅ | ❌ | ✅ | ❌ |
| Raincloud plot | ✅ | ❌ | ❌ | ❌ |
| 3-D scatter / surface / line / bar | ✅ (WebGL + SVG) | ❌ | ❌ | ✅ (WebGL) |
| Bubble chart | ✅ | ✅ | ❌ | ✅ |
| Sunburst chart | ✅ | ❌ | ❌ | ✅ |
| Parallel coordinates | ✅ | ❌ | ✅ | ✅ |
| Diverging bar | ✅ | ❌ | ❌ | ✅ |
| Auto large-data downsampling (SVG) | ✅ M4+LTTB+voxel | Rasterises | ❌ | ❌ |
| Perceptually-uniform colormaps | ✅ (9 built-in) | ✅ | ✅ | ✅ |
| Continuous color encoding (scatter) | ✅ | ✅ | ✅ | ✅ |
| Candlestick / OHLC | ✅ | ❌ | ❌ | ✅ |
| Waterfall / bridge chart | ✅ | ❌ | ❌ | ✅ |
| Treemap (squarified) | ✅ | ❌ | ❌ | ✅ |
| Streaming / real-time series | ✅ (no server) | ❌ | ❌ | ✅ (needs server) |
| Synchronized crosshair | ✅ | ❌ | ❌ | ✅ (needs server) |
| PPTX export | ✅ | ❌ | ❌ | ❌ |
CLI tool (glyphx plot data.csv) |
✅ | ❌ | ❌ | ❌ |
| Full ARIA / WCAG 2.1 AA accessibility | ✅ | ❌ | ❌ | Partial |
Full type annotations (py.typed) |
✅ | ❌ | ❌ | Partial |
tight_layout() |
✅ auto | Manual | Auto | Auto |
| Log-scale axes | ✅ | ✅ | ✅ | ✅ |
| Dual Y-axis | ✅ | ✅ | ❌ | ✅ |
| Error bars (X and Y) | ✅ | ✅ | ✅ | ✅ |
Installation
pip install glyphx
# Optional extras
pip install "glyphx[export]" # PNG/JPG raster export (resvg-py + pillow)
pip install "glyphx[cairo]" # PDF export (cairosvg)
pip install "glyphx[pptx]" # PowerPoint export (python-pptx + cairosvg)
pip install "glyphx[all]" # Everything
Requirements: Python 3.10+ · NumPy ≥ 1.26 · pandas ≥ 2.1
Quick Start
from glyphx import plot
# One-liner — auto-displays in Jupyter, opens browser in CLI
plot([1, 2, 3], [4, 5, 6], kind="line", title="My First Chart")
from glyphx import Figure
from glyphx.series import LineSeries, BarSeries
months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
revenue = [120, 145, 132, 178, 159, 203]
costs = [ 80, 90, 85, 105, 98, 115]
fig = (
Figure(width=800, height=500)
.set_title("Revenue vs Costs — H1")
.set_theme("dark")
.set_xlabel("Month")
.set_ylabel("USD (thousands)")
.add(LineSeries(months, revenue, color="#60a5fa", label="Revenue"))
.add(LineSeries(months, costs, color="#f87171", label="Costs", linestyle="dashed"))
.add_stat_annotation("Jan", "Jun", p_value=0.004)
.set_legend("top-left")
.tight_layout()
)
fig.show() # Jupyter inline or browser tab
fig.save("chart.svg") # SVG vector
fig.save("chart.html") # Interactive HTML
fig.save("chart.png") # Raster PNG (requires cairosvg)
fig.save("chart.pptx") # PowerPoint (requires glyphx[pptx])
fig.share("report.html") # Zero-CDN self-contained HTML
Core APIs
plot() — One-liner charts
The fastest path to any chart type. Mirrors pandas' df.plot():
from glyphx import plot
plot([1,2,3], [4,5,6], kind="line", title="Line")
plot(["A","B","C"], [10,20,15], kind="bar", title="Bar")
plot([1,2,3], [4,5,6], kind="scatter", title="Scatter")
plot(data=[30, 40, 30], kind="pie", labels=["A","B","C"])
plot(data=[30, 40, 30], kind="donut", labels=["A","B","C"])
plot(data=raw_values, kind="hist", bins=20)
plot(data=raw_values, kind="box")
plot(data=matrix, kind="heatmap")
Method-Chaining API
Every method returns self. Build the entire chart in one expression:
fig = (
Figure(width=900, height=520, theme="warm")
.set_title("Q3 Performance")
.set_xlabel("Month").set_ylabel("Revenue ($M)")
.set_legend("bottom-right")
.add(LineSeries(x, revenue, label="Revenue"))
.add(BarSeries(x, costs, label="Costs"), use_y2=True)
.annotate("Record High", x=10, y=5.4, arrow=True, color="#dc2626")
.add_stat_annotation("Jan", "Jun", p_value=0.001)
.vline(x=6, color="#888", linestyle="dashed")
.hline(y=5.0, color="#888", linestyle="dotted")
.tight_layout()
.share("dashboard.html")
)
DataFrame Accessor
Import glyphx once — every pd.DataFrame gains .glyphx:
import pandas as pd
import glyphx # registers accessor automatically
df = pd.read_csv("sales.csv")
# One-liner charts from column names
df.glyphx.line(x="date", y="revenue", title="Daily Revenue")
df.glyphx.bar( x="product", y="sales", title="Sales by Product")
df.glyphx.scatter(x="spend", y="revenue")
df.glyphx.hist(col="response_time", bins=20)
df.glyphx.box(col="score", groupby="region")
df.glyphx.pie(labels="category", values="share")
df.glyphx.donut(labels="segment", values="revenue")
df.glyphx.heatmap(title="Correlation Matrix")
# Groupby aggregation
df.glyphx.bar(groupby="region", y="revenue", agg="sum",
title="Revenue by Region")
# Hue splitting — one BarSeries per unique region value, auto-colored
df.glyphx.bar(x="month", y="revenue", hue="region",
title="Revenue by Month and Region")
# Full chain from the accessor
(df.glyphx
.bar(x="month", y="revenue", auto_display=False)
.set_theme("dark")
.add_stat_annotation("Jan", "Jun", p_value=0.002)
.share("report.html"))
Chart Types
Core charts
from glyphx.series import (
LineSeries, BarSeries, ScatterSeries,
PieSeries, DonutSeries, HistogramSeries,
BoxPlotSeries, HeatmapSeries,
)
# Line — multiple linestyles, error bars
LineSeries(x, y,
color="#2563eb", label="Revenue",
linestyle="dashed", # solid | dashed | dotted | longdash | step
width=2,
yerr=error_values, # symmetric Y error bars with caps
xerr=x_error_values, # symmetric X error bars
)
# Bar — error bars, per-bar color list
BarSeries(x, y,
color="#7c3aed", label="Units",
bar_width=0.7,
yerr=std_errors,
)
# Scatter — continuous color encoding
ScatterSeries(x, y,
c=z_values, # per-point values → colormap
cmap="viridis", # any of 9 built-in colormaps
size=6,
marker="circle", # circle | square
)
# Histogram
HistogramSeries(data, bins=20, color="#0891b2")
# Box plot — single or multi-group
BoxPlotSeries([group_a, group_b, group_c],
categories=["Control", "Drug A", "Drug B"],
box_width=24,
)
# Heatmap — colorbar, row/col labels, value overlay
HeatmapSeries(matrix,
row_labels=row_names,
col_labels=col_names,
show_values=True,
cmap=["#1e40af", "#f0f0f0", "#b91c1c"], # custom diverging
)
Statistical
# ECDF — no bin-width choice needed, shows full distribution
from glyphx.ecdf import ECDFSeries
fig.add(ECDFSeries(data, label="Control", complementary=False))
fig.add(ECDFSeries(data2, label="Treatment"))
# KDE — smooth density curve (no scipy required)
from glyphx.kde import KDESeries
fig.add(KDESeries(data, filled=True, alpha=0.20, label="Density"))
# Area / fill-between
from glyphx.fill_between import FillBetweenSeries
fig.add(FillBetweenSeries(x, y_lower, y_upper, color="#2563eb", alpha=0.25,
label="95% CI"))
# Raincloud — jitter + half-violin + box in one plot
from glyphx.raincloud import RaincloudSeries
fig.add(RaincloudSeries(
data=[control, drug_a, drug_b],
categories=["Control", "Drug A", "Drug B"],
violin_width=35,
))
# Violin plot
from glyphx.violin_plot import ViolinPlotSeries
fig.add(ViolinPlotSeries([grp_a, grp_b], show_median=True, show_box=True))
# Statistical significance brackets (built-in, no extra package)
fig.add_stat_annotation("Control", "Drug A", p_value=0.001) # → ***
fig.add_stat_annotation("Control", "Drug B", p_value=0.031, y_offset=30) # stack
fig.add_stat_annotation("Drug A", "Drug B", p_value=0.18, style="numeric")
Financial
# Candlestick / OHLC
from glyphx.candlestick import CandlestickSeries
fig.add(CandlestickSeries(
dates=["Mon", "Tue", "Wed", "Thu", "Fri"],
open= [150, 153, 149, 155, 158],
high= [155, 157, 153, 160, 162],
low= [148, 151, 146, 154, 156],
close=[153, 149, 155, 158, 160],
))
# Waterfall / bridge chart
from glyphx.waterfall import WaterfallSeries
fig.add(WaterfallSeries(
labels=["Q2 Revenue", "New Sales", "Upsell", "Churn", "Q3 Revenue"],
values=[8_200_000, 2_100_000, 650_000, -420_000, None], # None = auto-total
show_values=True,
))
Hierarchical
# Treemap — squarified layout, area-proportional rectangles
from glyphx.treemap import TreemapSeries
fig.add(TreemapSeries(
labels=["Cloud", "AI", "Mobile", "Security", "Data"],
values=[4200, 3100, 2800, 2100, 1900],
cmap="viridis",
show_values=True,
))
3-D Charts
Use Figure3D for interactive Three.js WebGL output with an SVG fallback.
All 3-D series support per-series threshold= and expose
last_downsample_info after rendering.
from glyphx import Figure3D, plot3d
from glyphx.scatter3d import Scatter3DSeries
from glyphx.surface3d import Surface3DSeries
from glyphx.line3d import Line3DSeries
from glyphx.bar3d import Bar3DSeries
from glyphx.contour import ContourSeries
import numpy as np
# 3-D scatter with colormap
fig = Figure3D(title="Clusters", theme="dark", azimuth=45, elevation=30)
fig.add(Scatter3DSeries(xs, ys, zs, c=zs, cmap="plasma", size=4))
fig.show() # WebGL interactive; .save("chart.html") for sharing
# 3-D surface — auto-decimated for large grids
x = np.linspace(-3, 3, 200)
y = np.linspace(-3, 3, 200)
Z = np.sin(np.sqrt(x[None,:]**2 + y[:,None]**2))
Figure3D().add(Surface3DSeries(x, y, Z, cmap="viridis", wireframe=True)).show()
# Helix polyline
t = np.linspace(0, 4*np.pi, 2000)
Figure3D().add(Line3DSeries(np.cos(t), np.sin(t), t/(4*np.pi))).show()
# One-liner
plot3d(xs, ys, zs, kind="scatter", title="Quick 3D")
plot3d(x, y, Z, kind="surface", title="Quick Surface")
New Chart Types (v1.5+)
# Bubble — scatter with size encoding
from glyphx.bubble import BubbleSeries
fig.add(BubbleSeries(x, y, sizes=market_cap, c=growth, cmap="plasma"))
# Sunburst — multi-ring hierarchy
from glyphx.sunburst import SunburstSeries
fig.add(SunburstSeries(labels=[...], parents=[...], values=[...]))
# Parallel coordinates
from glyphx.parallel_coords import ParallelCoordinatesSeries
fig.add(ParallelCoordinatesSeries(data=df[cols], labels=df["species"]))
# Diverging bar
from glyphx.diverging_bar import DivergingBarSeries
fig.add(DivergingBarSeries(categories=[...], values=[12,-8,21,-3]))
Streaming / Real-Time
from glyphx.streaming import StreamingSeries
fig = Figure(title="Live Sensor Feed")
stream = StreamingSeries(max_points=100, color="#7c3aed", label="Sensor")
fig.add(stream)
# Manual push
stream.push(42.0)
stream.push_many([41.5, 42.3, 43.1])
# Jupyter live mode — re-renders at target FPS, no server needed
with stream.live(fig, fps=10) as s:
for reading in sensor_generator():
s.push(reading)
Large-Data Downsampling
GlyphX automatically keeps SVG files fast on large datasets. All algorithms are fully vectorised with NumPy.
| Series type | Algorithm | Threshold |
|---|---|---|
LineSeries |
Two-stage M4 → LTTB | M4 at 50k pts; LTTB at 5k pts |
ScatterSeries |
2-D voxel grid thinning | 5k pts |
Line3DSeries |
LTTB in screen space (camera-aware) | 5k pts |
Scatter3DSeries |
3-D voxel grid thinning | 5k pts |
Surface3DSeries |
Grid decimation + face culling | 5k faces |
# Per-series threshold override
from glyphx.series import LineSeries
ls = LineSeries(x, y, threshold=1_000) # keep at most 1 000 points
# Inspect what happened after render
ls_info = ls.last_downsample_info
# {'algorithm': 'M4+LTTB', 'original_n': 200000, 'thinned_n': 1000}
# Global kill-switch (thread-local — safe for multi-threaded renderers)
import glyphx.downsample as ds
ds.disable() # no downsampling on this thread
fig.render_svg()
ds.enable()
# Manual use of any algorithm
from glyphx.downsample import lttb, m4, voxel_thin_2d, lttb_3d, decimate_grid
x_down, y_down = lttb(x, y, threshold=2_000)
x_m4, y_m4 = m4(x, y, pixel_width=800)
xt, yt, ct = voxel_thin_2d(xs, ys, c=labels, max_points=5_000)
See the Downsampling docs for the full API, benchmark results, and the test suite.
Interactivity
All charts rendered to HTML include:
| Interaction | How |
|---|---|
| Tooltips | Hover any data point |
| Zoom | Mouse wheel |
| Pan | Click and drag |
| Reset zoom | Double-click |
| Linked brushing | Shift + drag — filters all charts on the page |
| Keyboard navigation | Tab / Arrow keys between data points |
| Legend toggle | Click a legend item to show/hide its series |
| Export | SVG / PNG buttons in the toolbar |
| Synchronized crosshair | fig.enable_crosshair() |
Linked Brushing
Hold Shift and drag a selection rectangle on any chart. All charts on the page with matching X values highlight together and dim non-matching points. Press Escape to clear.
from glyphx.layout import grid
f1 = Figure(auto_display=False).add(ScatterSeries(x, y1, label="Sales"))
f2 = Figure(auto_display=False).add(LineSeries(x, y2, label="Revenue"))
html = grid([f1, f2], rows=1, cols=2)
open("dashboard.html", "w").write(html)
Advanced Layout
# Dual Y-axis
fig.add(LineSeries(x, prices, label="Price (left)"))
fig.add(BarSeries(x, volume, label="Volume (right)"), use_y2=True)
fig.set_ylabel("Price ($)").set_y2label("Volume (units)")
# Log-scale axes
fig = Figure(yscale="log")
fig = Figure(xscale="log", yscale="log")
Right-hand tick rows are derived from the left axis, so the two sets of
gridlines always coincide — including when set_yticks() gives the left axis
a non-default tick count. set_tick_format() applies to all three axes.
# Subplot grid
fig = Figure(rows=2, cols=2, width=1000, height=700)
ax0 = fig.add_axes(0, 0); ax0.add_series(LineSeries(x, y))
ax1 = fig.add_axes(0, 1); ax1.add_series(BarSeries(cats, vals))
ax2 = fig.add_axes(1, 0); ax2.add_series(ScatterSeries(x, y2))
ax3 = fig.add_axes(1, 1); ax3.add_series(HistogramSeries(data))
# Reference lines
fig.vline(x=50, color="#e11d48", linestyle="dashed")
fig.hline(y=3.5, color="#0284c7", linestyle="dotted")
# Text annotations with optional arrows
fig.annotate("Peak", x=10, y=5.4, arrow=True, color="#dc2626", font_size=12)
fig.annotate("Baseline", x=0, y=2.0, anchor="start")
# Auto tight layout (adjusts padding, rotates crowded X labels)
fig.tight_layout()
# Wrap long X labels onto a second line instead of rotating them
fig.bar(["Product Engineering", "Sales & Marketing", "R&D"], [10, 20, 15])
fig.tight_layout().set_tick_wrap()
Shared X axis
Stacked panels that all plot against one X range — the standard layout for a price/volume/indicator stack, or any set of series measured over the same period:
fig = Figure(rows=3, cols=1, width=820, height=640, shared_x=True)
fig.add_axes(0, 0).add_series(LineSeries(t, price))
fig.add_axes(1, 0).add_series(LineSeries(t, volume))
fig.add_axes(2, 0).add_series(LineSeries(t, rsi))
fig.show()
Every cell gets the union of all cells' X domains, so panels line up vertically even when their series cover different spans. X tick labels are drawn only on the lowest occupied cell in each column; grid lines and tick marks stay on all of them, so the alignment is still readable. Sparse grids work — if a column's bottom row is empty, the lowest cell that exists keeps its labels.
Zoom and pan are already synchronised: a subplot grid renders as one <svg>
with the cells as translated groups, and zoom.js works on the SVG viewBox,
so dragging moves every panel together.
Inset axes
A small panel drawn on top of the main plot area, with its own independent scales — for a zoomed detail view, or an overview thumbnail beside a zoomed main chart:
from glyphx import Figure, LineSeries
fig = Figure(width=820, height=520).line(x, y, label="full range")
inset = fig.inset_axes(0.55, 0.14, 0.38, 0.34) # x, y, w, h as 0-1 fractions
inset.add_series(LineSeries(x[:45], y[:45]))
fig.show()
Position and size are fractions of the figure canvas, not the plot area, so
an inset stays where you put it regardless of how padding changes. The panel
inherits the parent's theme unless you pass theme=, gets a padding scaled to
its own size, and draws on an opaque background so the parent's grid lines
don't show through — pass background="none" for a transparent panel. Insets
render last and in the order added, so a later one overlaps an earlier one.
Wrapping long tick labels
set_tick_wrap() is an alternative to GlyphX's default auto-rotation. Rotation
is compact but harder to read; wrapping keeps labels horizontal by splitting
them across up to two lines. Labels that already fit are left alone, and the
two are mutually exclusive — enabling wrap suppresses rotation for that axes.
Colormaps
Nine perceptually-uniform colormaps:
| Name | Type | Best for |
|---|---|---|
viridis |
Sequential | Default continuous encoding |
plasma |
Sequential | High-contrast continuous |
inferno |
Sequential | Print-safe dark backgrounds |
magma |
Sequential | Heatmaps and density |
cividis |
Sequential | Deuteranopia-safe |
coolwarm |
Diverging | Correlation matrices |
rdbu |
Diverging | Positive / negative values |
spectral |
Multi-hue | Categorical ranges |
greys |
Sequential | Monochrome / print export |
from glyphx.colormaps import apply_colormap, colormap_colors, list_colormaps
apply_colormap(0.75, "plasma") # → "#eb5f34"
colormap_colors("viridis", 6) # → list of 6 hex colors
list_colormaps() # → ["cividis", "coolwarm", ...]
# Color-encode scatter by a third variable
ScatterSeries(x, y, c=z_values, cmap="inferno")
Themes
Seven built-in themes:
Figure(theme="default") # clean white background
Figure(theme="dark") # charcoal background
Figure(theme="colorblind") # Okabe-Ito palette — safe for all color vision types
Figure(theme="pastel") # soft, presentation-friendly
Figure(theme="warm") # earthy tones, Georgia serif font
Figure(theme="ocean") # blue palette, light blue background
Figure(theme="monochrome") # grayscale, print-safe
# Custom theme dict
Figure(theme={
"colors": ["#ff6b6b", "#4ecdc4", "#45b7d1"],
"background": "#1a1a2e",
"text_color": "#eeeeee",
"axis_color": "#555555",
"grid_color": "#333333",
"font": "Roboto, sans-serif",
})
# Mid-chain theme swap
fig.set_theme("dark")
Registering your own theme
A theme dict works with Figure(theme=...), but everything else — df.glyphx.*,
facet_plot, clustermap, Figure3D, the CLI — takes a theme name. Register
one and it works everywhere:
from glyphx import register_theme, list_themes
register_theme(
"acme",
base="dark", # inherit unspecified keys
colors=["#e6194b", "#3cb44b", "#4363d8"],
font="Inter, sans-serif",
)
Figure(theme="acme").line(x, y).show()
df.glyphx.scatter(x="a", y="b", theme="acme")
list_themes() # -> ['acme', 'colorblind', 'dark', 'default', ...]
register_theme validates as it goes: a misspelled key (colours) or a bad
colors value is rejected with a message naming the problem, and built-in
names are protected from being overwritten. Unknown theme names now raise
rather than silently falling back to default:
Figure(theme="darkk")
# ValueError: Unknown theme 'darkk'. Did you mean 'dark'? Available: ...
Series without an explicit color= cycle through the active theme's palette,
so a three-line chart on colorblind gets three distinguishable Okabe-Ito
colors rather than three identical blues. The same applies to the multi-color
types — pie, donut, grouped bar and stacked bar all take their slice and
segment colors from the active theme. Colormap-driven charts (treemap,
raincloud, bump) keep their cmap.
Accessibility note: The
colorblindtheme uses the Okabe-Ito palette — the scientific standard for color-vision-deficiency-safe visualization. It is safe for deuteranopia, protanopia, and tritanopia.
Export Options
fig.save("chart.svg") # SVG vector — scales to any size
fig.save("chart.html") # interactive HTML with tooltips, zoom, export buttons
fig.save("chart.png") # raster PNG (requires: pip install "glyphx[export]")
fig.save("chart.jpg") # raster JPG (requires: pip install "glyphx[export]")
fig.save("chart.pptx") # PowerPoint slide (requires: pip install "glyphx[pptx]")
# Self-contained HTML — all JS inlined, works fully offline
html_str = fig.share() # returns string
html_str = fig.share("report.html") # also writes to disk
html_str = fig.share(title="Q3 Report") # custom <title> tag
fig.share() inlines all JavaScript so the output works in:
email clients · Confluence · Notion · GitHub Pages · air-gapped environments
Multi-figure export
SubplotGrid lays out several independent figures on one page, and now saves
directly:
from glyphx import Figure
from glyphx.figure import SubplotGrid
sg = SubplotGrid(2, 2)
sg.add(revenue_fig, 0, 0)
sg.add(costs_fig, 0, 1)
sg.save("dashboard.html") # composite page, all figures inline
sg.save("quarterly_review.pptx") # one slide per figure, row-major order
Each PPTX slide is titled from that figure's own title. Empty grid cells are
skipped. Single-image formats (.svg, .png, .pdf) are rejected with a
pointer to saving each Figure individually — the grid's cells are separate
<svg> documents, so there is no one image to rasterise.
CLI Tool
Plot any CSV, JSON, or Excel file from the terminal — no Python script needed:
# Basic
glyphx plot sales.csv --x month --y revenue --kind bar -o chart.html
# Full options
glyphx plot data.csv \
--x date --y revenue \
--kind line \
--groupby region \
--agg sum \
--theme dark \
--title "Monthly Revenue" \
--xlabel "Date" --ylabel "Revenue ($M)" \
--width 900 --height 500 \
--no-legend \
-o report.html \
--open # auto-open in browser after rendering
# Print version
glyphx version
Supported inputs: .csv .tsv .json .jsonl .xlsx .xls
Supported outputs: .svg .html .png .jpg .pptx
Accessibility
Every GlyphX chart meets WCAG 2.1 AA standards automatically:
role="img"andaria-labelledbyon every<svg>root<title>and<desc>landmark elements with auto-generated descriptionstabindex="0"androle="graphics-symbol"on every interactive data pointTab/Arrowkeys navigate between data pointsEnter/Spacetriggers tooltips from keyboardEscapedismisses and blursfocusable="false"prevents focus stealing
# Auto-generated plain-English description for screen readers
print(fig.to_alt_text())
# → 'Line chart titled "Monthly Revenue". X axis: Month. Y axis: USD.
# Series "Revenue": 12 data points. Ranges from 98 (Mar) to 203 (Dec).'
Type Annotations
GlyphX ships with a py.typed marker (PEP 561). All public APIs have complete type hints:
from glyphx import Figure
from glyphx.series import LineSeries
fig: Figure = Figure(width=640, height=480, theme="dark")
s: LineSeries = LineSeries([1, 2, 3], [4, 5, 6], label="Revenue")
fig.add(s).set_title("Typed Chart").tight_layout().show()
Works with mypy, pyright, and all major IDEs out of the box.
Comparison with Matplotlib
# Matplotlib — 12 lines, no interactivity, no shareable output
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
ax.plot(months, revenue, color="blue", label="Revenue")
ax.plot(months, costs, color="red", label="Costs", linestyle="--")
ax.set_title("Revenue vs Costs")
ax.set_xlabel("Month")
ax.set_ylabel("USD")
ax.legend(loc="upper left")
plt.tight_layout()
plt.show()
# GlyphX — 7 lines, interactive, shareable
(Figure()
.set_title("Revenue vs Costs")
.set_xlabel("Month").set_ylabel("USD")
.add(LineSeries(months, revenue, color="#2563eb", label="Revenue"))
.add(LineSeries(months, costs, color="#dc2626", label="Costs", linestyle="dashed"))
.set_legend("top-left").tight_layout()
.share("report.html"))
Comparison with Seaborn
# Seaborn — significance brackets need a separate package
import seaborn as sns
from statannotations.Annotator import Annotator
ax = sns.barplot(data=df, x="group", y="score")
annotator = Annotator(ax, [("Control","Drug A")], data=df, x="group", y="score")
annotator.configure(test="t-test_ind", text_format="star")
annotator.apply_and_annotate()
# GlyphX — built-in, no extra package
(Figure()
.add(BarSeries(["Control","Drug A","Drug B"], means, yerr=errors))
.add_stat_annotation("Control", "Drug A", p_value=0.001)
.add_stat_annotation("Control", "Drug B", p_value=0.031, y_offset=30)
.show())
Comparison with Plotly
# Plotly — HTML has CDN dependency, breaks offline
import plotly.express as px
fig = px.line(df, x="month", y="revenue")
fig.write_html("chart.html") # requires CDN at view time
# GlyphX — truly self-contained, works on a USB stick
fig.share("chart.html") # all JS inlined, zero dependencies
Full API Reference
Figure
| Method | Returns | Description |
|---|---|---|
Figure(width, height, theme, rows, cols, legend, xscale, yscale) |
Figure |
Create figure |
.add(series, use_y2=False) |
Figure |
Add a series |
.line(x, y, ...) |
Figure |
Shorthand LineSeries |
.bar(x, y, ...) |
Figure |
Shorthand BarSeries |
.scatter(x, y, ...) |
Figure |
Shorthand ScatterSeries |
.hist(data, ...) |
Figure |
Shorthand HistogramSeries |
.box(data, ...) |
Figure |
Shorthand BoxPlotSeries |
.heatmap(matrix, ...) |
Figure |
Shorthand HeatmapSeries |
.pie(values, ...) |
Figure |
Shorthand PieSeries |
.donut(values, ...) |
Figure |
Shorthand DonutSeries |
.area(x, y1, y2, ...) |
Figure |
Shorthand FillBetweenSeries |
.kde(data, ...) |
Figure |
Shorthand KDESeries |
.ecdf(data, ...) |
Figure |
Shorthand ECDFSeries |
.raincloud(data, ...) |
Figure |
Shorthand RaincloudSeries |
.candlestick(dates, o, h, l, c) |
Figure |
Shorthand CandlestickSeries |
.waterfall(labels, values, ...) |
Figure |
Shorthand WaterfallSeries |
.treemap(labels, values, ...) |
Figure |
Shorthand TreemapSeries |
.stream(max_points, ...) |
StreamingSeries |
Add streaming series; returns stream |
.vline(x, ...) |
Figure |
Vertical reference line |
.hline(y, ...) |
Figure |
Horizontal reference line |
.set_title(text) |
Figure |
Chart title |
.set_theme(name_or_dict) |
Figure |
Apply theme |
.set_size(width, height) |
Figure |
Resize canvas |
.set_xlabel(text) |
Figure |
X-axis label |
.set_ylabel(text) |
Figure |
Y-axis label |
.set_y2label(text) |
Figure |
Right-hand Y-axis label (no-op without use_y2 series) |
.set_legend(position) |
Figure |
Legend position or False |
.set_tick_format(fn) |
Figure |
Tick label formatter, applied to X, Y1 and Y2 |
.set_minor_ticks(n) |
Figure |
Minor tick subdivisions |
.set_tick_wrap(enabled) |
Figure |
Wrap long X tick labels instead of rotating |
.add_axes(row, col) |
Axes |
Get / create subplot cell |
.inset_axes(x, y, w, h) |
Axes |
Panel drawn over the plot area, own scales |
.annotate(text, x, y, ...) |
Figure |
Text annotation with optional arrow |
.add_stat_annotation(x1, x2, p_value, ...) |
Figure |
Significance bracket |
.tight_layout() |
Figure |
Auto-adjust padding and rotate labels |
.enable_crosshair() |
Figure |
Synchronized crosshair |
.to_alt_text() |
str |
Screen-reader description |
.show() |
Figure |
Display in Jupyter or browser |
.save(filename) |
Figure |
Write SVG / HTML / PNG / JPG / PPTX |
.share(filename, title) |
str |
Generate self-contained HTML |
.render_svg() |
str |
Raw SVG string |
DataFrame Accessor (df.glyphx.*)
| Method | Description |
|---|---|
.line(x, y, yerr, ...) |
Line chart |
.bar(x, y, groupby, agg, yerr, ...) |
Bar chart with optional groupby |
.scatter(x, y, ...) |
Scatter plot |
.hist(col, bins, ...) |
Histogram of a column |
.box(col, groupby, ...) |
Box plot, optional multi-group |
.pie(labels, values, ...) |
Pie chart |
.donut(labels, values, ...) |
Donut chart |
.heatmap(...) |
Heatmap from numeric columns |
.plot(kind, x, y, ...) |
Unified dispatcher |
All accessor methods return Figure for chaining.
An unrecognised column name raises KeyError naming the closest match, rather
than being silently ignored:
df.glyphx.line(x="Month", y="revenue")
# KeyError: "Column 'Month' not found. Did you mean 'month'?
# Available columns: ['month', 'revenue', 'region']"
Applies to x, y, yerr, hue and groupby. Omitting x is still valid
and falls back to the row index — only a name that doesn't exist is an error.
CLI
| Command | Description |
|---|---|
glyphx plot <file> [options] |
Render a chart from a data file |
glyphx version |
Print version and exit |
Running the Examples
git clone https://github.com/kjkoeller/glyphx
cd glyphx
pip install -e ".[all]"
python examples.py # generates HTML files in ./glyphx_output/
OPEN=1 python examples.py # also auto-opens each chart in browser
Contributing
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
# Setup dev environment
git clone https://github.com/kjkoeller/glyphx
cd glyphx
pip install -e ".[all]"
pip install pytest pytest-cov
# Run the test suite
pytest tests/ -v
# Run with coverage
pytest tests/ --cov=glyphx --cov-report=term-missing
Please ensure all new chart types include:
- A series class with a
to_svg(ax)method - Tests in
tests/ - A
to_alt_text()compatible description - An entry in
__init__.pyand__all__
License
MIT License — © 2025 Kyle Koeller and GlyphX contributors.
See LICENSE for the full text.
Release files for glyphx 3.0.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 | |
|---|---|---|---|
| glyphx-3.0.0.tar.gz | 1.3 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| glyphx-3.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.5 MB
Release files / glyphx-3.0.0.tar.gz
| Download URL | glyphx-3.0.0.tar.gz |
|---|---|
| Size | 1.3 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f62c61b6f66fe9c29c0381da29caa0374c43cdf4cacc594c8bd53038171efc90
|
|
BLAKE2b-256 checksum How to use checksums |
21be4fc81db16c7591db49a1bc316d4207646c25584b868ae91a4ac4cfbbafa4
|
| 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 Aug 31, 2026.
Transparency logRelease files / glyphx-3.0.0-py3-none-any.whl
| Download URL | glyphx-3.0.0-py3-none-any.whl |
|---|---|
| Size | 226.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
06e81021c1b623940f310e959999d2b928ee967fe6f56a810b46546dc4de3091
|
|
BLAKE2b-256 checksum How to use checksums |
2080e679396b58c3f7a7b833f3b4f83e6f6ff296b9c139c45c88d4c2d6453120
|
| 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 Aug 31, 2026.
Transparency log