Advanced Features

Interactive Brushing

Hold Shift and drag on any chart to draw a selection rectangle. All charts on the page sharing the same X values highlight matching points and dim everything else.

from glyphx.layout import grid
from glyphx.series import ScatterSeries

f1 = Figure(); f1.add(ScatterSeries(x, y1, label="Sales"))
f2 = Figure(); f2.add(ScatterSeries(x, y2, label="Revenue"))

html = grid([f1, f2], rows=1, cols=2)
open("linked_dashboard.html", "w").write(html)
  • Shift+drag — draw the selection

  • Escape — clear the selection

A readout appears with the count, mean, sum and range of what is selected, updating live as the rectangle grows rather than only once you let go. Non-numeric values are skipped, so a categorical axis still reports a count. The readout is an ARIA live region, so the numbers are announced rather than being visual-only.

Selection Events

Clicking a point dispatches a glyphx:select event on document, so anything on the page can react. Clicking the same point again, or pressing Escape, dispatches glyphx:deselect.

fig.add(ScatterSeries(x, y, meta=[
    {"customer": "Acme", "region": "North"},
    {"customer": "Belltown", "region": "South"},
]))
document.addEventListener('glyphx:select', (e) => {
    console.log(e.detail.x, e.detail.y, e.detail.meta.customer);
});

meta is whatever was passed in Python, parsed back from JSON, so the listener receives the structure that was written rather than a flattened string. detail.data carries every data- attribute the element has, so a listener gets whatever that chart type knows about the thing clicked – percent on a pie, q1/q2/q3 on a box plot, open/high/low/close on a candlestick.

Detail Panel

For the common case – click a point, show its record – there is no need to write a listener at all:

fig.add(ScatterSeries(x, y, meta=records))
fig.add_detail_panel(["customer", "region", "tier"], title="Selected")
fig.share("chart.html")

The panel renders beside the chart, fills in on click, and returns to its empty message on Escape or a second click. fields fixes the display order and omits anything else; leave it out to show whatever each point carries.

It is an ordinary listener on the same glyphx:select event, so it composes rather than competes – your own listeners still fire for the same click, and cross-filtering still applies. Values render as text, never markup.

Filter Controls

Checkboxes, radio buttons and a search box that filter the chart in the browser, with no server and no callbacks:

fig.add(ScatterSeries(x, y, meta=records))
fig.add_controls(checkboxes="region", radio="tier", search="customer",
                 title="Filter")

You name a field; GlyphX reads the distinct values out of the data and builds one control per value. A field is found wherever it lives – in a point’s meta, in its own data- attributes, or as the series label via "series".

Filters combine with AND, which is how a stack of controls reads: tick two regions and type a name and you get that name within those regions. A running “Showing 12 of 40” sits underneath and is announced to screen readers.

Note

Checkboxes start ticked and radio groups get an “All” option. A panel that hides the data on load looks broken, and a radio group without “All” is a one-way trip.

Zoom, Pan and Touch

Scroll to zoom, drag to pan, double-click empty space to reset. Axis labels are redrawn for whatever region is visible, so a zoomed chart still shows its scale rather than losing the numbers along with the rest of the drawing. Linear axes only; a log axis keeps its original ticks.

On a touch device, one finger pans and two pinch to zoom, anchored on the midpoint between them.

A Reset view button appears in the toolbar as soon as the view moves and disappears once it is back to the default, so the way out is visible exactly when it is wanted. It restores zoom, position and axis labels together, across every chart on the page.

Synchronized Crosshair

fig.enable_crosshair()
fig.share("report.html")

Self-Contained Shareable HTML

fig.share("report.html")                    # writes to disk
fig.share("report.html", title="Q3 Report") # custom page title
html = fig.share()                           # returns string

3-D Interactive Charts

Figure3D renders via Three.js WebGL with mouse orbit controls, surface value probe, click-to-select scatter, camera presets, auto-rotate, and screenshot-to-PNG. A static SVG is generated as a fallback for environments where JavaScript is not available. See Examples for a full gallery with live interactive HTML files.

Series types

Class

Description

Scatter3DSeries

Scatter points; c= encodes a fourth variable as colour; size= controls marker radius. 3-D voxel thinning applied above AUTO_THRESHOLD (5 000 pts).

Surface3DSeries

Regular z = f(x, y) grid; wireframe=True adds a mesh overlay. Grid decimation + sub-pixel face culling applied on large grids.

Line3DSeries

Connected 3-D polyline; color=, width=. LTTB downsampling runs in camera-projected screen space.

Bar3DSeries

Vertical bars on a 2-D (x, y) base; cmap= colours bars by height.

ContourSeries

Filled contour lines on a 2-D regular grid (marching squares).

Quick-start

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

# Scatter -- fourth variable as colour
rng = np.random.default_rng(42)
xs, ys = rng.normal(0, 1, 400), rng.normal(0, 1, 400)
zs = np.sin(xs) + np.cos(ys)

fig = Figure3D(title="Scatter", theme="dark")
fig.scatter(xs, ys, zs, c=zs, cmap="plasma", size=5, label="Points")
fig.show()                  # interactive WebGL
fig.save("scatter3d.html")  # self-contained HTML

# Surface (method chaining)
x = np.linspace(-3, 3, 60)
y = np.linspace(-3, 3, 60)
X, Y = np.meshgrid(x, y)
Z = np.sin(np.sqrt(X**2 + Y**2))

(Figure3D(title="Sinc Surface", theme="dark")
 .surface(x, y, Z, cmap="viridis", wireframe=True)
 .show())

# Line -- parametric helix
t = np.linspace(0, 6 * np.pi, 2_000)
(Figure3D(title="Helix")
 .line3d(np.cos(t), np.sin(t), t / (2 * np.pi),
         color="#7c3aed", width=2.5)
 .show())

# Bar
heights = np.abs(np.random.randn(5, 5)) * 3 + 1
(Figure3D(title="3D Bars")
 .bar3d(np.arange(1, 6), np.arange(1, 6), heights, cmap="viridis")
 .show())

# Contour (2-D grid, regular Figure)
fig = Figure3D(title="Contour")
fig.add(ContourSeries(x, y, Z, levels=12, filled=True, cmap="coolwarm"))
fig.show()

# One-liner shorthand
plot3d(xs, ys, zs, kind="scatter", title="Quick Scatter")
plot3d(x,  y,  Z,  kind="surface", title="Quick Surface")

Overlay multiple series

Any Figure3D accepts multiple .add() or shorthand calls:

import numpy as np
from glyphx import Figure3D

x = np.linspace(-2, 2, 30)
y = np.linspace(-2, 2, 30)
X, Y = np.meshgrid(x, y)
Z_fit = X**2 + Y**2

obs_x = np.random.uniform(-2, 2, 80)
obs_y = np.random.uniform(-2, 2, 80)
obs_z = obs_x**2 + obs_y**2 + np.random.randn(80) * 0.3

(Figure3D(title="Model vs Observations", theme="dark")
 .surface(x, y, Z_fit, cmap="viridis", alpha=0.55)
 .scatter(obs_x, obs_y, obs_z, color="#f87171", size=5, label="Data")
 .show())

Figure3D constructor parameters

Parameter

Default

Description

width

900

Canvas width in pixels

height

650

Canvas height in pixels

title

""

Chart title displayed top-centre

theme

"default"

Theme name; all seven 2-D themes work in 3-D

azimuth

45.0

Initial camera azimuth in degrees

elevation

30.0

Initial camera elevation in degrees

xlabel

"X"

X-axis label

ylabel

"Y"

Y-axis label

zlabel

"Z"

Z-axis label

Downsampling

All 3-D series auto-downsample before SVG generation. Pass threshold=N to override the default budget of 5 000 points/faces per series. After rendering, series.last_downsample_info reports the algorithm used and the before/after counts. See Large-Data Downsampling for full details.

New 2-D Chart Types (v1.5+)

Bubble Chart

Scatter plot with size encoding for a fourth variable:

from glyphx.bubble import BubbleSeries

fig = Figure()
fig.add(BubbleSeries(x, y, sizes=market_cap, c=growth_rate,
                     cmap="plasma", label="Companies"))
fig.show()

Sunburst Chart

Multi-ring hierarchical chart:

from glyphx.sunburst import SunburstSeries

fig = Figure(width=600, height=600)
fig.add(SunburstSeries(
    labels=["Total", "A", "A1", "A2", "B", "B1"],
    parents=["",     "Total","A","A","Total","B"],
    values= [0,       40,    25,  15,  60,    60],
))
fig.show()

Parallel Coordinates

High-dimensional data visualization:

from glyphx.parallel_coords import ParallelCoordinatesSeries

fig = Figure(width=900, height=500)
fig.add(ParallelCoordinatesSeries(
    data=df[["sepal_length","sepal_width","petal_length","petal_width"]],
    labels=df["species"],
    cmap="viridis",
))
fig.show()

Diverging Bar Chart

Horizontal bars diverging from a center baseline:

from glyphx.diverging_bar import DivergingBarSeries

fig = Figure()
fig.add(DivergingBarSeries(
    categories=["Q1","Q2","Q3","Q4"],
    values=    [  2,  -3,   5,  -1],
    color_pos="#2563eb",
    color_neg="#dc2626",
))
fig.show()

Statistical Charts

ECDF

from glyphx.ecdf import ECDFSeries

fig = (
    Figure()
    .set_title("Response Time Distribution")
    .add(ECDFSeries(control_data,   label="Control",   color="#3b82f6"))
    .add(ECDFSeries(treatment_data, label="Treatment", color="#ef4444"))
)
fig.show()
ECDF comparing control vs treatment groups

Raincloud Plot

Combines jittered strip plot, half-violin, and box in one panel. Use seed= for reproducible jitter:

from glyphx.raincloud import RaincloudSeries

fig = Figure()
fig.add(RaincloudSeries(
    data=[control, low_dose, high_dose],
    categories=["Control", "Low Dose", "High Dose"],
    violin_width=35,
    seed=42,   # reproducible jitter
))
fig.show()
Raincloud plot showing distribution by group

Box Plot

from glyphx.series import BoxPlotSeries

fig = Figure()
fig.add(BoxPlotSeries(
    [control, drug_a, drug_b, drug_c],
    categories=["Control","Drug A","Drug B","Drug C"],
    box_width=30,
))
fig.show()
Multi-group box plot

Heatmap

from glyphx.series import HeatmapSeries

fig = Figure()
fig.add(HeatmapSeries(
    corr_matrix,
    row_labels=labels,
    col_labels=labels,
    show_values=True,
))
fig.show()
KPI correlation heatmap

Financial Charts

Candlestick / OHLC

from glyphx.candlestick import CandlestickSeries

fig = Figure().set_title("AAPL — Daily OHLC")
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],
))
fig.show()
OHLC candlestick chart

Waterfall / Bridge Chart

from glyphx.waterfall import WaterfallSeries

fig = Figure().set_title("Q3 Revenue Bridge ($M)")
fig.add(WaterfallSeries(
    labels=["Q2 Revenue","New Logos","Expansions","Churn","Discounts","Q3 Revenue"],
    values=[8.2, 2.1, 0.9, -0.8, -0.4, None],
    show_values=True,
))
fig.show()
Waterfall bridge chart

Hierarchical Charts

Treemap

from glyphx.treemap import TreemapSeries

fig = Figure(width=700, height=500)
fig.add(TreemapSeries(
    labels=["Cloud","AI/ML","Mobile","Security","Data","Networking","IoT"],
    values=[4200, 3100, 2800, 2100, 1900, 1400, 900],
    cmap="viridis",
    show_values=True,
))
fig.show()
Squarified treemap

Pie and Donut Charts

from glyphx.series import PieSeries, DonutSeries
from glyphx.colormaps import colormap_colors

fig = Figure(width=500, height=440)
fig.add(PieSeries([38,25,18,11,8],
    labels=["GlyphX","Matplotlib","Plotly","Seaborn","Other"],
    colors=colormap_colors("plasma", 5)))
fig.show()
Pie chart with plasma colormap Donut chart with viridis colormap

Streaming / Real-Time

from glyphx.streaming import StreamingSeries

fig = Figure().set_title("Live Sensor Feed")
stream = StreamingSeries(max_points=100, color="#7c3aed", label="Sensor")
fig.add(stream)

# Push values one at a time
stream.push(42.0)

# Jupyter live mode — no server required
with stream.live(fig, fps=10) as s:
    for reading in sensor_generator():
        s.push(reading)
Streaming real-time sensor feed

Accessibility

Every chart meets WCAG 2.1 AA standards:

  • role="img" and aria-labelledby on the root <svg> element

  • <title> and <desc> with auto-generated descriptions

  • tabindex="0" and role="graphics-symbol" on every data point

  • Tab / Arrow key navigation between data points

  • Enter / Space to show tooltips from keyboard

  • Escape to dismiss

alt = 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 300 (Dec).'

PPTX Export

Requires pip install "glyphx[pptx]" and the system libcairo library.

fig.save("chart.pptx")

Note

On macOS: brew install cairo. On Ubuntu/Debian: sudo apt-get install libcairo2.

Violin Plots

Pure-NumPy KDE — no scipy required:

from glyphx.violin_plot import ViolinPlotSeries

fig = Figure()
fig.add(ViolinPlotSeries(
    data=[group_a, group_b, group_c],
    show_median=True,
    show_box=True,
))
fig.show()

Seaborn-Style Composite Plots

from glyphx import pairplot, jointplot, lmplot, facet_plot

pairplot(df)
jointplot(df, x="a", y="b")
lmplot(df, x="x", y="y")
facet_plot(df, x="value", col="group", kind="hist")

Large-Data Performance

See Large-Data Downsampling for a full description of the automatic downsampling pipeline, per-series threshold overrides, the global kill-switch, and the last_downsample_info metadata API.

Quick reference:

# Per-series threshold
from glyphx.series import LineSeries
ls = LineSeries(x, y, threshold=1_000)   # keep at most 1 000 points

# Global kill-switch (thread-local)
import glyphx.downsample as ds
ds.disable()   # no downsampling on this thread
# ... render ...
ds.enable()

# Inspect what happened after render
print(ls.last_downsample_info)
# {'algorithm': 'M4+LTTB', 'original_n': 200000, 'thinned_n': 1000}