A modular and flexible graphing library
Project description
behaviz
A modular, multi-backend plotting library that gets you from raw data to a clean, clear and reproducible figures - fast.
Why behaviz?
Scientific plotting libraries are powerful but can be verbose: you spend more time wrangling keyword arguments, call signatures, and styling than looking at your data. behaviz is built for researchers who want publication-quality plots without becoming matplotlib experts.
It aims to solve two problems:
- Consistent, reproducible plots for similar data: describe a plot once with a
spec, reuse it everywhere. - High-level calls with low-level control: simple functions like
plot_lineandplot_scatterthat still let you reach any underlying plot property through keyword overrides.
The same code can render through matplotlib, seaborn, or bokeh, and you can switch backends with a single line.
Highlights
- One simple call per plot:
plot_line,plot_scatter,plot_bar,plot_step,plot_errorbar,plot_violin,plot_image,plot_fill_between,plot_pie,plot_hexbin - Three interchangeable backends:
set_renderer("matplotlib" | "seaborn" | "bokeh") - Painless colorbars:
plot_image(data, colorbar="label")— auto-sized, no mappable juggling - Plot from anything: NumPy arrays, pandas / polars DataFrames, or plain dicts
- Opt-in hover-tooltips: (
hover_annotate=True) - Cross-backend styling: canonical keywords (
color,linewidth,alpha, …) that work on every backend - Reusable specs & presets: chainable
.with_*()helpers, plussave_preset/load_presetto a personal~/.behavizlibrary - Visual data manipulators: jitter, smoothing, normalising, binning that add visual manipulations without changing the original data
Installation
behaviz uses uv for dependency management.
Requirements: Python ≥ 3.10. Core dependencies (numpy, scipy, matplotlib,
seaborn, bokeh) are installed automatically. pandas / polars are optional —
behaviz never imports them unless you pass one in.
# clone and install
git clone https://github.com/kaancet/behaviz.git
cd behaviz
# It's recommended to create a virtual environment
uv venv --python 3.10
source .venv/bin/activate # unix (for windows: .venv/bin/activate )
uv sync
Or add it to an existing project:
uv add git+https://github.com/kaancet/behaviz.git
# or with pip
pip install git+https://github.com/kaancet/behaviz.git
Once installed, initialize the ~/.behaviz preset directory (not necessary but it's convenient for discoverability and manually dropping/editting preset files)
behaviz init
Quickstart
import numpy as np
import behaviz as bv
x = np.linspace(0, 2 * np.pi, 100)
y = np.sin(x)
# matplotlib is the default backend, nothing else to set up
fig, ax = bv.plot_line(x, y, color="#349888", linewidth=2, label="sin(x)")
Every plot function returns a (fig, ax) tuple, so you can keep customizing with the
native backend objects if you ever need to.
Core concepts
The return contract
| Function | Returns |
|---|---|
plot_line, plot_scatter, plot_bar, plot_step, plot_errorbar, plot_image,plot_fill_between, plot_pie, plot_hexbin |
(fig, ax) |
plot_violin |
(fig, ax, vp)-vp["bodies"] holds the violin artists |
When you pass an existing ax=, the plot is drawn onto it and the same axes is
returned, so you can layer plots:
fig, ax = bv.plot_line(x, np.sin(x), label="sin")
bv.plot_line(x, np.cos(x), ax=ax, label="cos", color="orange") # same axes
Switching backends
bv.set_renderer("matplotlib") # default
bv.set_renderer("seaborn") # matplotlib + seaborn themes
bv.set_renderer("bokeh") # interactive HTML (great for dashboards)
The same plotting code works on all three. Only the display step differs for bokeh,
which renders to HTML and needs an explicit show():
import behaviz as bv
from bokeh.plotting import show
from bokeh.io import output_notebook
bv.set_renderer("bokeh")
fig, ax = bv.plot_line(x, y)
output_notebook() # in a Jupyter notebook
show(ax) # for bokeh, `ax` *is* the figure
The plot functions
import numpy as np
import behaviz as bv
x = np.linspace(0, 10, 60)
y = np.sin(x)
Line
fig, ax = bv.plot_line(x, y, color="steelblue", linewidth=2, label="signal")
Scatter
fig, ax = bv.plot_scatter(x, y, color="crimson", markersize=40, alpha=0.7)
Bar
heights = np.abs(np.sin(x)) + 0.1
fig, ax = bv.plot_bar(x, heights, width=0.25, color="#349888", edgecolor="#000000")
Step
fig, ax = bv.plot_step(x, y, where="post", color="black")
Error bars
err = np.full_like(y, 0.15) # symmetric ±err
fig, ax = bv.plot_errorbar(x, y, err, color="navy", capsize=3)
# asymmetric: shape (2, N) → [lower, upper]
err_asym = np.vstack([np.full_like(y, 0.05), np.full_like(y, 0.20)])
fig, ax = bv.plot_errorbar(x, y, err_asym)
Violin
rng = np.random.default_rng(0)
positions = np.array([1.0, 2.0, 3.0])
distributions = [rng.normal(loc=p, scale=0.5, size=200) for p in positions]
fig, ax, vp = bv.plot_violin(positions, distributions)
ys may be a list of arrays or a 2-D array of shape (n_positions, n_samples);
both produce one violin per position.
Image
Display a 2-D array as a colour-mapped image (heatmap):
data = np.random.default_rng(0).normal(size=(40, 60))
fig, ax = bv.plot_image(data, cmap="magma")
Place it in data coordinates with extent, and flip the vertical origin like matplotlib:
fig, ax = bv.plot_image(data, extent=(0, 6, 0, 4), origin="lower", vmin=-2, vmax=2)
cmap means the same thing on every backend — the matplotlib colormap is converted to a
Bokeh palette under the hood — so the image looks identical when you set_renderer("bokeh").
Colorbar — no plumbing required
A matplotlib colorbar normally means capturing the mappable and wrestling with sizing. Here it's one opt-in keyword, and the bar is auto-sized to match the image height:
bv.plot_image(data, colorbar=True) # default bar
bv.plot_image(data, colorbar="Firing rate (Hz)") # a string is the label
For full control, pass a ColorbarSpec — the same call works on every backend:
from behaviz import ColorbarSpec
bv.plot_image(
data, cmap="viridis",
colorbar=ColorbarSpec(label="Hz", location="bottom", ticks=[-2, 0, 2], tick_fmt="%.0f"),
)
plot_imagecurrently handles 2-D scalar arrays; RGB(A) images are on the roadmap.
Fill between
Shade the band between two curves
x = np.linspace(0, 10, 100)
y = np.sin(x)
sem = 0.2 * np.ones_like(x)
fig, ax = bv.plot_fill_between(x, y - sem, y + sem, color="steelblue", alpha=0.3)
bv.plot_line(x, y, ax=ax, color="navy") # overlay the mean line
y2 defaults to 0 (fill down to the axis). Pass two curves for a ribbon.
Pie
fig, ax = bv.plot_pie([30, 25, 15, 30], labels=["A", "B", "C", "D"], autopct="%.0f%%")
(autopct is matplotlib/seaborn only; on bokeh the slice labels are drawn inside the wedges.)
Hexbin
A 2-D histogram of raw point data, binned into hexagons and coloured by count — with the same
opt-in colorbar:
rng = np.random.default_rng(0)
px, py = rng.normal(size=4000), rng.normal(size=4000)
fig, ax = bv.plot_hexbin(px, py, gridsize=25, cmap="viridis", colorbar="count")
Plotting from DataFrames and dicts
You don't have to unpack your data into arrays. Pass a pandas or polars
DataFrame (or a plain dict) as data= and reference columns by name. behaviz stays
dependency-free (it never imports pandas or polars, it just reads the columns you ask
for)
import numpy as np
import polars as pl # or pandas, both work identically
import behaviz as bv
df = pl.DataFrame({"time": np.linspace(0, 1, 50),
"voltage": np.random.rand(50)})
# keyword column names
fig, ax = bv.plot_line(x="time", y="voltage", data=df)
# positional column names work too
fig, ax = bv.plot_scatter("time", "voltage", data=df)
# mix and match: a column name for x, a raw array for y
fig, ax = bv.plot_line(x="time", y=np.random.rand(50), data=df)
The rule is the same one seaborn uses: when data is given, a string means "column
name"; otherwise everything is treated as raw data. Arrays without data= behave
exactly as before.
When a channel comes from a named column and you haven't set a label, behaviz uses the column name automatically
Supported data sources: anything that responds to
data["column"]and yields an array: pandas DataFrame, polars DataFrame, ordict[str, array]. (Pass an eager polars frame; call.collect()on aLazyFramefirst.)
Input handling & errors
Every plot function declares a contract for its data arguments, and behaviz normalises your input to it before plotting — so you can pass whatever you have:
- lists, tuples, NumPy arrays, pandas/polars Series, ranges, generators — all become arrays
- scalars are promoted where they make sense (
plot_vertical(1.5),plot_bar(..., width=0.2)) - trivial 2-D shapes
(N, 1)/(1, N)are squeezed to 1-D - grouped inputs (
plot_violin'sys) accept a list of arrays — ragged lengths welcome — or a 2-D array read as one group per row
When the input genuinely doesn't fit, behaviz raises a BehavizDataError
(a ValueError subclass) that names the offending argument, shows what it got, and
suggests a fix:
plot_violin: `ys` must have the same length as `x`.
x : ndarray shape (3,)
ys: list of 5 arrays (lengths 30, 30, 30, 30, 30)
Hint: got 3 vs 5 — pass one `ys` entry per `x` entry.
A true 2-D array passed where a single series belongs is an error too (it will not be silently flattened):
plot_scatter: `x` must be 1-D.
x: ndarray shape (2, 100)
Hint: for multiple series, plot them one call at a time (or use a function that
takes a list of series, e.g. plot_violin's ys).
from behaviz import BehavizDataError
try:
bv.plot_line(x, y)
except BehavizDataError as e:
print(e) # tells you which argument to fix, and how
# prints
"""
plot_line: `y` must have the same length as `x`.
x: ndarray shape (100,)
y: ndarray shape (1,)
Hint: got 100 vs 1 — pass one `y` entry per `x` entry.
"""
Hover Tooltips
Turn on interactive value tooltips with a single opt-in keyword. It's off by default and works on every backend.
# custom tooltip labels
fig, ax = bv.plot_line(x, y, hover_annotate=True, hover_labels=("Time (s)", "Voltage"))
- bokeh: adds a native hover tool that snaps to data points (works out of the box).
- matplotlib / seaborn: adds a nearest-point annotation. Hover events only fire on
an interactive matplotlib backend, e.g.
%matplotlib widgetin Jupyter or a Qt/Tk window. (Under the static Agg backend it's a harmless no-op.)
Hover is available for plot_line, plot_scatter, plot_bar, plot_step, and
plot_errorbar.
Styling: one vocabulary, every backend
Any extra keyword you pass is forwarded to the active backend. Crucially, behaviz understands a set of canonical names so the same code styles a plot identically whether you're on matplotlib or bokeh:
# identical call, three backends, same visual result
for backend in ("matplotlib", "seaborn", "bokeh"):
bv.set_renderer(backend)
bv.plot_line(x, y, color="purple", linewidth=3, alpha=0.6, label="trace")
Common canonical keywords: color, alpha, linewidth, linestyle, marker,
markersize / size, label. Backend-native names still pass straight through, so
power users lose nothing:
bv.set_renderer("bokeh")
bv.plot_line(x, y, line_color="teal", line_width=4) # native bokeh names also fine
On bokeh, a single color even fans out to both line_color and fill_color for you.
The spec system
A PlotSpec captures everything about how a plot looks: axes, scales, ticks, legend,
figure size, annotations, so you can define it once and reuse it across many plots for
a consistent look.
from behaviz import PlotSpec, AxisSpec, FigureSpec, ScaleType, LegendPosition
x = np.array([1,2,3,4,5,6])
y = np.array([10,15,35,60,88,100])/100
err = np.array([5,5,5,10,20,5])/100
spec = PlotSpec(
title="Response curve",
x=AxisSpec(label="Contrast", unit="%", scale=ScaleType.LINEAR),
y=AxisSpec(label="Hit rate", unit="%", grid=True),
figure=FigureSpec(figsize=(6, 6), dpi=300),
show_legend=True,
legend_pos=LegendPosition.UPPER_LEFT,
)
fig, ax = bv.plot_errorbar(x, y, err,spec=spec)
Presets
Skip the boilerplate with target-tuned presets:
paper = PlotSpec.preset("paper") # small, thin lines, no grid
poster = PlotSpec.preset("poster") # large figure, big fonts
notebook = PlotSpec.preset("notebook") # medium, grid on
dark = PlotSpec.preset("dark") # dark background
Shortcuts and chaining
Only care about labels? Use from_labels:
spec = PlotSpec.from_labels("Time", "Voltage", xunit="s", yunit="mV")
Every spec is immutable, the .with_*() helpers return a new spec, so they chain
cleanly and never mutate shared defaults:
spec = PlotSpec.from_labels("Contrast", "Hit rate", xunit="%", yunit="%")
spec = (
spec
.with_title("Trial 1")
.with_xlim(0, 10)
.with_scale("y", "log")
.with_fontsize(14)
.with_annotation(6, 1.1, "peak", color="red")
)
fig, ax = bv.plot_errorbar(x, y, err,spec=spec)
Available helpers: with_title, with_xlim, with_ylim, with_xticks, with_yticks,
with_fontsize, with_scale, with_size, with_annotation, with_hook.
AxisSpec options
AxisSpec controls a single axis: label, unit, fontsize, scale
(linear/log/symlog/logit), lim, ticks, tick_fmt, invert, spines,
grid, grid_minor. The displayed label is "Label (unit)" when a unit is set.
Saving and loading presets
Build a spec once, save it, and reuse it across every project and session. Presets are
stored as JSON under ~/.behaviz/presets/, so they travel with your machine, eliminating the need to
copy spec code between notebooks.
import behaviz as bv
# craft a spec you like
my_style = (
bv.PlotSpec.from_labels("Contrast", "Hit rate", xunit="%", yunit="%")
.with_title("Lab figure")
.with_size((10, 4))
.with_fontsize(13)
)
# save it to ~/.behaviz/presets/lab.json
bv.save_preset("lab", my_style)
# ...later, anywhere...
spec = bv.load_preset("lab") # returns a full PlotSpec
fig, ax = bv.plot_line(x, y, spec=spec)
behaviz ships with built-in presets that are always available: default, paper,
poster, notebook, dark:
fig, ax = bv.plot_scatter(x, y, spec=bv.load_preset("paper"))
# start from a built-in, tweak it, save as your own
custom = bv.load_preset("paper").with_title("My paper figure")
bv.save_preset("my_paper", custom)
Manage your library:
bv.list_presets() # {'default': 'builtin', ..., 'lab': 'user'}
bv.delete_preset("lab") # removes a user preset (built-ins can't be deleted)
bv.presets_dir() # the storage directory (Path)
Sharing presets between machines
Presets live in your home directory, but you can export one to a standalone file to email, commit to a repo, or copy to another machine and import it on the other side:
# on machine A: write the preset out to any path
bv.export_preset("lab", "shared/lab.json")
# on machine B: install it into the local ~/.behaviz library
bv.import_preset("shared/lab.json") # now loadable as "lab"
bv.import_preset("shared/lab.json", name="lab_from_alice") # or under a new name
spec = bv.load_preset("lab")
export_preset works for built-ins too (handy for starting points), and import_preset
validates the file is a real behaviz preset before installing it.
Command-line setup
behaviz installs a small behaviz CLI for managing the preset library from the shell:
behaviz init # scaffold ~/.behaviz: presets/, a README, and example presets
behaviz list # list available presets (builtin / user)
behaviz where # print the presets directory path
behaviz init is optional! Built-in presets load and shared presets import without
any setup. What init adds is discoverability: a presets/ folder to drop JSON into, and
an examples/ folder containing the built-ins as editable JSON starting points. Those
examples are reference copies only (not on the load path), so copying one into presets/
and editing it never shadows or freezes the real built-in. Use behaviz init --no-examples
to skip them.
Storage honors
BEHAVIZ_HOME, soBEHAVIZ_HOME=/path/to/shared behaviz initsets up a shared or version-controlled preset library.
Good to know
- A user preset with the same name as a built-in shadows it so you can customize
paperwithout losing the original. - The storage location is
~/.behavizby default, or whatever you point theBEHAVIZ_HOMEenvironment variable at (handy for shared or version-controlled configs).
Visual data manipulators
Sometimes you need to visually tweak data like jittering overlapping points, smooth a noisy
trace, normalise to a baseline, etc., without altering the underlying values. The
VisualManipulator does exactly that and guarantees your originals are never
mutated (inputs are copied and results are returned read-only).
import numpy as np
import behaviz as bv
from behaviz.manipulations import VisualManipulator
x = np.arange(0, 5 * np.pi, 0.1)
y = np.sin(x)
vm = VisualManipulator(seed=42) # seed → reproducible jitter
# jitter
jittered = vm.jitter(x, y, kind="uniform", axis="y", strength=0.2)
bv.plot_scatter(jittered.x, jittered.y, label="jittered")
# smoothing, normalising, binning all share the same shape
smoothed = vm.smooth(x, y, kind="gaussian", sigma=2.0)
normalised = vm.normalise(x, y, kind="zscore", axis="y")
binned = vm.binning(x, y, bins=20, kind="mean", axis="x")
Each call returns a ManipulationResult exposing .x, .y, the untouched .x_original
/ .y_original, and a .metadata dict:
print(jittered.metadata) # {'kind': 'uniform', 'axis': 'y', 'strength': 0.2, ...}
Strategies available out of the box:
| Manipulation | kind= options |
|---|---|
jitter |
uniform, normal, beeswarm |
smooth |
boxcar, gaussian |
normalise |
minmax, zscore, baseline |
binning |
mean, median, sum, count |
Add your own with VisualManipulator.register_strategy(...).
composite plots
Higher-level, composed figures live in behaviz.composite_plots and are built from the
same primitives:
from behaviz.composite_plots.rainplot import plot_rain
from behaviz.composite_plots.psychometric import plot_psychometric
from behaviz.composite_plots.distribution import plot_distribution
from behaviz.composite_plots.impact import plot_impact
fig, ax = plot_rain(positions, distributions, with_cloud=True)
These are evolving, so expect their API to firm up (or disappear) over time.
How it works (architecture)
behaviz is intentionally layered so each piece stays small and testable:
spec/: plain dataclasses (PlotSpec,AxisSpec,FigureSpec) describing what a plot should look like, independent of any backend.core/: the public plot functions. The simple(x, y)ones (plot_line,plot_scatter,plot_step) are generated from a single template incore_factory.py; richer ones are hand-written. A decorator (plot_function) handles figure creation,data=resolution, and spec application uniformly.backends/: oneRendererper backend translating canonical calls into native matplotlib / seaborn / bokeh, plus anOverriderthat routes keyword arguments and an opt-inHoverEngine.- A registry: validates at import that every plot type is fully implemented across all backends so that the gaps fail loudly during development, not at call time.
This is what lets the same call render on three backends and lets you reach any low-level property through a single high-level function.
Roadmap
- Unified
bv.save()/bv.show()across backends group=/hue=for automatic per-category series, colors, and legends- Bokeh-based dashboard layouts
- More composite plots and a documented gallery
- RGB(A) images (
plot_imagecurrently handles 2-D scalar arrays)
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file behaviz-0.4.0.tar.gz.
File metadata
- Download URL: behaviz-0.4.0.tar.gz
- Upload date:
- Size: 142.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.5.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fd6717730245f509d5a95127c02e97ef51cddf634d33263f50332b2e533c1e07
|
|
| MD5 |
0dd9d8f0b8999448642e3cd2d06ec5db
|
|
| BLAKE2b-256 |
49809f9acdbcf28c34c4fb1e0bf001716086bbf82e98e07f9603153ff8e29806
|
File details
Details for the file behaviz-0.4.0-py3-none-any.whl.
File metadata
- Download URL: behaviz-0.4.0-py3-none-any.whl
- Upload date:
- Size: 123.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.5.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3686e1738898f7207ddc422e4f86f8edc888f52b2b5684ae2d5e17c5434dad99
|
|
| MD5 |
1ef63154fdcc7cab7731168d8aa27ac8
|
|
| BLAKE2b-256 |
40f322f919dae9077320a83d5846ffedec4f1fde08bf4203dcc73b0d51839e7c
|