Skip to main content

GlyphX

A next-generation Python visualization library — SVG-first, interactive, and built to replace Matplotlib, Seaborn, and Plotly.

CI Documentation PyPI Release License: MIT Python 3.12+

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 colorblind theme 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" and aria-labelledby on every <svg> root
  • <title> and <desc> landmark elements with auto-generated descriptions
  • tabindex="0" and role="graphics-symbol" on every interactive data point
  • Tab / Arrow keys navigate between data points
  • Enter / Space triggers tooltips from keyboard
  • Escape dismisses and blurs
  • focusable="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__.py and __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)

Source distribution for glyphx 3.0.0
File Size Uploaded
glyphx-3.0.0.tar.gz 1.3 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for glyphx 3.0.0
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

4.0.0

2 release files

This release

3.0.0 This release

2 release files

2.1.0

2 release files

2.0.2

2 release files

2.0.1

2 release files

2.0.0

2 release files

1.2.10

2 release files

1.2.9

2 release files

1.2.8

2 release files

1.2.7

2 release files

1.2.6

2 release files

1.2.5

2 release files

1.2.4

2 release files

1.2.3

2 release files

1.2.2

2 release files

1.2.1

2 release files

1.2.0

2 release files

1.1.2

2 release files

1.1.1

2 release files

1.1.0

2 release files

1.0.0

2 release files

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