unichart
A stateful, multi-dataset plotting and dashboard toolkit for Jupyter notebooks, built on Plotly.
unichart is designed for the common real-world workflow of comparing many
datasets (test runs, simulation cases, measurement series) that share the same
schema. Instead of hand-assembling Plotly traces, you load your data once,
choose what to show, and get publication-ready interactive figures with a short,
memory-ful API. A companion module, unichart.dashboard, wires those same
figures into interactive Dash boards or self-contained HTML files.
unichart— theUnichartNotebookplotting environment (core).unichart.dashboard— Dash dashboards built from notebook panels.unichart.terminal— theexplore()GUI: a sidebar, a chart pane and a Python terminal for plotting on the fly.unichart.cli— theunichartcommand: open that GUI on a data file straight from a terminal.
See every plot type, with the code that made it, in the example gallery.
Installation
pip install unichart
Optional extras:
# Trend lines / regressions (LOWESS smoothing via statsmodels)
pip install "unichart[trend]"
# High-resolution PNG export (kaleido)
pip install "unichart[png]"
# Everything
pip install "unichart[all]"
The unichart command comes with the package:
unichart runs.csv
For the latest development version, install from GitHub:
pip install "unichart[all] @ git+https://github.com/Cunon/unichart.git"
Requirements
- Python >= 3.10
pandas,numpy,plotly,scipy,ipywidgets,ipythonanddash(installed automatically)- Optional:
statsmodelsfor trend lines,kaleidofor static-image / PNG export. Thepngextra installs kaleido 1.x, which renders through Chrome; if none is installed, runplotly_get_chromeonce.
Core concepts
A UnichartNotebook holds any number of datasets (nb.sets, each a
Dataset) backed by a single shared DataFrame. Each dataset carries its own
style state (color, marker, line style, selection flag, query, title). The
typical loop is:
| Step | Methods |
|---|---|
| 1. Load | load_df · load · load_clipboard |
| 2. Select | select · omit · query · restore |
| 3. Plot | plot · plot_ymult · plot_marginal · bar · box · histogram · contour · table |
| 4. Style | color · marker · var_format · set_default_format · toggle_darkmode · set_plot_style |
| 5. Analyse | delta · table · summary · reg_info · combine_sets |
Two conveniences run through the whole API:
- Sticky arguments. Plot calls remember their last
x/y(nb.last_x,nb.last_y, grid, format), so follow-up styling and analysis calls don't need to re-specify them. - Built-in help.
nb.help()prints a categorized method + attribute overview;nb.help('delta')prints one method's full signature and docstring;nb.help('Plotting')lists a single category. Headings, method names and signatures are colored in a terminal, in Jupyter and in the explorer's terminal (plain when piped;NO_COLOR=1disables it).
Quick start
from unichart import UnichartNotebook
nb = UnichartNotebook()
# 1. Load — one DataFrame split into one Dataset per unique set id,
# or a list of DataFrames loaded as separate sets.
nb.load_df(df, set_idx_column='run_id', set_name_column='run_name')
# 2. Select which datasets participate in the next plot
nb.select([0, 1, 2]) # or nb.omit(3), nb.query('all', 'temp > 100')
nb.select('1:10') # range shorthand — sets 1-9 (exclusive stop,
# like a Python slice). Also '0,3,7:', '::2', '-3:'.
# Works anywhere a set selector is accepted:
# nb.omit('5:8'), nb.color('0:3', 'red'), ...
# 3. Plot — one subplot per Y variable by default
nb.plot(x='time', y=['temperature', 'pressure'])
# 4. Style
nb.color(0, 'red') # dataset 0 red
nb.var_format('temperature', linestyle='--') # every temperature line dashed
nb.toggle_darkmode(True)
# 5. Analyse
nb.plot(x='time', y='temperature', by='ymult') # shared X, multiple Y axes
nb.delta(base_idx=0, study_indices='all', delta_parms='temperature')
Loading data
load_df(df, ...)— split one DataFrame into one dataset per uniqueset_idx_columnvalue, or load a list of DataFrames as separate sets (combined=Trueconcatenates them into one). Auto-detectsSETNUMBER/INDEX/TITLEcolumns if you don't name them.load(source, ...)— load from a file path (CSV/Excel/etc.) or other source.load_clipboard()— pull a table straight from the system clipboard.add_column,set_column,combine_sets/combine,clear_data— manage the shared frame after loading.
Selecting & querying
Every plot draws only the selected datasets, so selection is how you slice:
nb.select([0, 2]) # show only sets 0 and 2
nb.omit(1) # hide set 1
nb.restore() # re-select everything
nb.query(1, 'rpm > 5000') # row-level filter on set 1 (pandas query syntax)
nb.selected() # list currently selected sets
Most styling/analysis methods accept the same uset_slice argument: an int, a
list of ints, 'all', or 'selected'.
Plot types
All plotting methods return an interactive Plotly go.Figure (cached in
nb.last_fig) and share layout options (figsize, ncols/nrows,
hspace/vspace, suptitle, footer, legend, by).
| Method | What it draws |
|---|---|
plot(x, y, by=...) |
Line / scatter. by='vars' = one subplot per Y variable (default); by='sets' = one subplot per dataset; by='ymult' = single plot with multiple Y axes. |
plot_ymult(x, y) |
One plot, multiple stacked/overlaid Y axes for differently-scaled variables. |
plot_marginal(x, y, marginal=) |
Scatter with marginal distribution strips: x's distribution above the plot, y's to its right ('histogram' default, or 'box', 'violin', 'rug', 'kde'; marginal_x/marginal_y set one side or drop it with False). Also plot(x, y, by='marginal'). |
bar(x, y, barmode=, agg=) |
Bar charts: one bar per dataset and x category, height = agg of y over that category's rows ('mean' default; any pandas reducer name, a callable, or agg=False for pre-reduced rows). Grouped or stacked; hover shows the reducer and row count. |
box(x, y, points=, notched=) |
Box / distribution plots. |
histogram(x, histfunc=, nbins=) |
Histograms with configurable binning and normalization. |
contour(x, y, z, overlay_sets=) |
Filled/line contour maps from scattered data, with optional scatter overlays. |
table(cols=, x_in=, kind=) |
Rendered data table; optionally interpolate values at given x_in points. |
nb.plot(x='time', y='temp', by='sets', ncols=2, suptitle='Per run')
nb.plot_marginal(x='rpm', y='torque', marginal='kde')
nb.bar(x='config', y='efficiency', barmode='group', agg='mean')
nb.histogram(x='error', nbins=40, histnorm='probability')
nb.contour(x='rpm', y='torque', z='efficiency', overlay_sets=[1, 2])
Styling & formatting
Style resolves in layers — per-variable overrides beat per-dataset style, which beats notebook defaults — each on a per-attribute basis.
Per-dataset (by uset_slice):
nb.color(0, 'red'); nb.marker([1, 2], 's'); nb.linestyle('all', '--')
nb.markersize(0, 12); nb.alpha('selected', 0.5); nb.fill(1, True)
nb.linewidth(0, 3); nb.edgewidth(0, 1); nb.hue(0, 'category')
nb.alpha_marker(0, 0.3); nb.alpha_line(1, 0.2) # opacity of just the markers / just the line
nb.zorder(2, 1) # draw set 2 on top of the rest (higher = later = on top)
nb.legend(3, False) # still plot set 3, but leave it out of the legend
nb.sig_figs(0, 4) # set 0's values display to 4 significant figures
nb.decimals(1, 2) # set 1's to two decimal places instead
sig_figs and decimals are the display-precision knob, in its two
spellings: significant figures, or places after the point (trailing zeros
kept, 0 = whole numbers). They round what is shown — table() cells,
summary() statistics and the plot hover readouts (x/y/z and the
display_parms lines) — and never the stored data, so output='df' and
ds.df keep full precision. A value is rounded one way or the other, so
setting either clears the other. One argument is the notebook-wide form (it
also restyles the sets already loaded), two are selector-then-value, and
'reset' restores the built-in precision:
nb.sig_figs(4) # 4 sig figs everywhere, and for sets loaded later
nb.decimals(2) # ... or two decimal places everywhere
nb.sig_figs(0, 6) # just set 0 (use this form when you mean a set index)
nb.sig_figs('reset') # back to the built-in precision
nb.sig_figs() # report the current setting
Per-variable (applies wherever that column is plotted):
nb.var_format('Temperature', linestyle='--') # all Temp lines dashed
nb.var_format('Pressure', color='blue', marker='s') # Pressure = blue squares
nb.var_format('Pressure', color='reset') # drop just the color override
nb.var_format(['CHT1', 'CHT2'], marker='x') # a list gets the same overrides
nb.var_format(['CHT1', 'CHT2'], reset=True) # drop all their overrides
Notebook-wide defaults & appearance:
set_default_format(...)— persistent defaults (markersize, linestyle, sig_figs/decimals, legend, grid, subplot spacing, barmode, agg, alpha, …) applied to future plots/datasets.set_color_palette/color_map/marker_map— the ordered lists assigned to datasets by index (integer lookups cycle).toggle_darkmode(True/False)— dark theme.set_plot_style('matplotlib')— Matplotlib-look figures (see below).set_font_sizes/get_font_sizes— named sizes ('sm','lg','xl', …) for title, legend, axes, ticks, table cells, hover, etc.set_plot_size— pin the plot area so every panel comes out the same size and aspect ratio regardless of titles, legends or subplot count (see below).grid(...)— gridline formatting: visibility, color, width, dash pattern, per axis (axis='x'/'y'/'both') and major/minor (which=), e.g.nb.grid(color='lightgray', dash=':')ornb.grid(which='minor', visible=True).watermark(...)— stamp a logo or seal onto every plot, with control over opacity, position and size (see below).set_static_images(True)/save_png(...)— render flat PNGs inline (keeps notebook file size down) or export a high-resolution PNG (needskaleido:pip install "unichart[png]"). Every saved PNG also carries the full plotting session (data, queries, formatting and the plot call) in a metadata chunk, soUnichartNotebook.from_session('plot.png')ornb.load_session('plot.png')remakes the plot from the image alone;read_png_session('plot.png')shows what is embedded,save_png(..., embed_session=False)writes a plain image.parms=['x', 'y']whitelists the columns embedded (plotted, query and hue columns are always kept) so a wide table doesn't bloat the file.save_session(path)/load_session(path)— the same session as a standalone.json(file references or embedded rows,embed_data=; the sameparms=whitelist applies to embedded sets, and it likewise keeps the columns the recorded plot needs). A.jsonsession records the last plotting call just as a PNG does, soload_sessionbrings the figure back with the data; passreplay=Falsefor the data and formatting alone. A call that won't replay — afigsizeJSON could only store as text, say — warns and leaves everything else restored.
Both formats are openable from the unichart command and droppable on the
explorer's sidebar; see Command line below.
Marker and line-style strings are Matplotlib-compatible ('o', 's',
'^', '--', '-.', ':') and translated to Plotly automatically.
Matplotlib look
Plots are drawn with Plotly and look like it. If your figures need to sit next
to Matplotlib output — a paper, a report, a deck already full of pyplot —
switch the whole environment over:
nb.set_plot_style('matplotlib') # or 'mpl' / 'plt'
nb.set_plot_style('plotly') # back to the default look
That restyles plots to approximate Matplotlib's defaults: a white (or black, in dark mode) plot area framed by spines on all four sides, outward ticks, no zero lines, a gray grid, DejaVu Sans at Matplotlib's point sizes, the tab10 color cycle, and viridis for contours and hue-colored scatters.
- It is orthogonal to
toggle_darkmode— each style has a light and a dark variant — and to the rest of the formatting API:color,markersize,var_format,set_font_sizesand friends still win wherever you set them. - Existing datasets are restyled too. That clears manual
color()/markersize()/hue_palette()overrides on them; passsets=Falseto keep those and apply the style only to the layout and to future loads. - Two Matplotlib habits stay opt-in: it draws lines without markers
(
nb.set_default_format(marker=None)if you want that), and dashboards keep the board's UI font so charts and chrome read as one surface. reset_format('all')(or'defaults') returns to the'plotly'style along with the other stored defaults.
Decorations
nb.line('rpm', level=5000, color='red', linestyle='--') # reference line
nb.highlight('time', (10, 20), color='yellow', alpha=0.2) # shaded band
nb.scale('pressure', (0, 100)) # fix an axis range
nb.suptitle = 'Overview'; nb.footer = 'source: rig A' # figure text
Reference lines can carry a label, drawn on the line inside the plot area:
nb.line('rpm', 5000, label='redline') # default: far end of the line
nb.line('cht', 400, color='orange', label='limit',
label_size='lg', label_position='left') # named size + position
nb.line('time', 12.5, label='event', label_position=0.25) # 0-1 fraction along the line
nb.line('cht', 350, label='target', label_position='center below', label_color='gray')
label_size— a number or a size name ('small','lg', …, same vocabulary asset_font_sizes). Defaults to theaxes_ticksize when one is set.label_position— a0–1fraction along the line, or position tokens (which may be combined with a fraction, e.g.'0.25 left'). A vertical line slides with'top'/'middle'/'bottom'and picks its side with'left'/'right'; a horizontal line slides with'left'/'center'/'right'and picks its side with'above'/'below'('top'/'bottom').label_color— defaults to the line'scolor.legend— add the line to the legend:Truenames the entry afterlabel(or'<column> = <level>'without one), a string names it explicitly, e.g.nb.line('egt', 900, color='orange', legend='EGT limit'). Line entries sit after the data entries, and clicking one toggles the line.
Fixed plot size / aspect ratio
figsize sets the size of the whole figure, so the actual drawing area moves
around as titles wrap, legends grow or a plot splits into more subplots — and
two plots in the same notebook end up different shapes. set_plot_size pins
the plot area instead:
nb.set_plot_size(4, 3) # every panel exactly 4x3in, in every plot
nb.plot(x='t', y='CHT') # 1 panel, 4x3in
nb.plot(x='t', y=['CHT', 'EGT', 'RPM']) # 3 panels, 4x3in each
nb.set_plot_size(height=3) # pin height only; width follows figsize
nb.set_plot_size(reset=True) # back to figsize-driven sizing
The size applies to one subplot panel by default, and the figure grows to
fit the grid plus its margins — so panels keep the same size and aspect ratio no
matter how many variables you plot or how tall the title and legend get. Pass
per_subplot=False to pin the combined grid area instead (the behaviour this
method had before per-panel sizing), which holds the figure size steady but
shrinks each panel as the grid grows.
Multi-axis plots (plot_ymult, or bar/box with by='dataset_x') are
handled too: each stacked right-hand Y axis keeps a fixed pixel slot for its
tick labels and title, so a narrow or pinned plot pushes the axes further out
instead of crushing them into each other.
Each call replaces the previous setting, per_subplot included — a later
set_plot_size(height=3) returns to per-panel mode unless you pass
per_subplot=False again.
Note that in per-panel mode a wide grid makes a wide figure — five 6in panels
side by side is a ~22in figure. Use a smaller per-panel size or ncols=1 when
that is inconvenient. Dashboard panels are rendered at the size the board gives
them, so the pin governs notebook figures, not board tiles.
Subplot spacing
The gap between subplots is a fixed pixel budget — 80px between columns, 70px
between rows (enough for the neighbouring panel's tick labels, axis title and
subplot title) — whatever the grid size, so a 6-row plot doesn't overlap and a
2-row plot doesn't waste a third of the figure on the gap. Contour plots and
secondary-axis plots reserve more for their colorbars and extra axes. When a
grid is too crowded for its figure the gaps are clamped and a warning says so;
a bigger figsize or set_plot_size is the cure.
To change it, every gridded plot method (plot, bar, box, histogram,
contour) takes hspace (columns) and vspace (rows), and
set_default_format sets the standing default:
nb.plot(x='t', y=['CHT', 'EGT', 'RPM'], ncols=1, vspace=100) # 100px rows
nb.plot(x='t', y=['CHT', 'EGT'], hspace=0.05) # 5% of the plot width
nb.set_default_format(hspace='60px', vspace=40) # for every plot from now on
A value of 1 or more is pixels (60 or '60px'); below 1 it is a fraction of
the plot area, the way Plotly's horizontal_spacing/vertical_spacing take
it. With set_plot_size pinned, the pixel gap is exact and the figure grows to
hold it.
Watermarks
Stamp a logo, seal or "DRAFT" graphic onto every plot:
nb.watermark('logo.png') # faint, centered
nb.watermark('logo.png', opacity=0.4,
position='bottom right', size=0.15) # corner logo
nb.watermark('draft.png', opacity=0.08, layer='above') # tint over the data
nb.watermark(opacity=0.3) # tweak; other settings kept
nb.watermark() # show current settings
nb.watermark(reset=True) # remove it
opacity—0–1(default0.15).position— any combination of'top'/'upper','bottom'/'lower','left','right','center'/'middle'— e.g.'center'(default),'bottom right','top','center left'. Or an explicit(x, y)pair in paper coordinates (0–1across the plot area,(0, 0)= bottom left), which centers the image on that point.size— fraction of the plot area the image is fitted into (default0.3); pass(width, height)to set the two separately.sizing='contain'(default) preserves the image's aspect ratio inside that box;'fill'crops to cover,'stretch'distorts to fit.layer—'below'(default) draws under the data,'above'over it.source— a file path (png, jpg, gif, webp, bmp, svg), anhttp(s)URL, adata:URI, raw bytes, or a PIL image. Local files are read and inlined as base64 at call time, so the watermark also appears insave_png,set_static_imagesmode, the ⧉ copy button and a dashboard's chart panels — and a bad path raises immediately instead of silently drawing nothing. A URL is passed through as-is and only renders where the viewer can reach it. The image rides along in every figure, so keep the file small.
Settings persist across plots and merge across calls, like grid().
Resetting formatting
Two consistent rules cover every reset:
-
Any formatting setter accepts
'reset'as its value to restore the default for whatever it targets — a dataset attribute, a variable override, or a decoration:nb.color(0, 'reset') # dataset 0 back to its color_map color nb.marker('all', 'reset') # every set back to its marker_map marker nb.color('Pressure', 'reset') # drop the Pressure color override nb.var_format('CHT', color='reset') # same, per attribute nb.line('all', 'reset') # remove reference lines ('clear' also works) nb.highlight('rpm', 'reset') # remove highlights on one column nb.scale('all', 'reset') # clear every fixed axis range
-
reset_format()is the single bulk-reset hub, with optional scopes'sets','vars','lines','highlights','scales','fonts','plot_size','grid','watermark','defaults','all':nb.reset_format() # all applied formatting nb.reset_format('lines', 'scales') # just those nb.reset_format([0, 1]) # just datasets 0 and 1 nb.reset_format(vars='CHT') # just one variable's overrides nb.reset_format('all') # everything, incl. set_default_format state
set_font_sizes(reset=True),set_plot_size(reset=True),set_default_format(reset=True), andclear_var_format()still work and are equivalent to the matchingreset_formatscope.
Analysis & stats
delta(base_idx, study_indices, delta_parms=...)— compute absolute (DL_<P>) and percentage (DLPCT_<P>) differences of each study dataset against a baseline, aligned by nearest-match merge on a chosen column (with optional interpolation at specifiedx_ins). The delta set inherits the study set's color/marker so it reads as a continuation of that series.table(...)/table_read(...)— tabulate columns, or interpolate a Y column at arbitrary X inputs (kind='linear', extrapolation controllable).sig_figs=/decimals=round the displayed cells; without them each set's ownsig_figs/decimals(seenb.sig_figs,nb.decimals) applies.reg_info(...)/reg_order— fit and report regressions / trend lines (polynomial or LOWESS; LOWESS needsstatsmodels).summary(cols=...)— per-dataset descriptive statistics (count / min / mean / max / std), displayed as the same sortable, filterable, copyable table astable(...);output='df' | 'md' | 'fig',sig_figs=anddecimals=work the same way too.min/max/mean/median— quick per-column aggregates.
Dashboards (unichart.dashboard)
The same figures compose into an interactive Dash board with one shared data context: a header bar owns the dataset selection and the light/dark theme for every panel, and each panel keeps only the controls that are genuinely its own (plot type, x / y / z variables, title, legend position).
# Inline in a Jupyter notebook:
nb.dashboard(panels=[
{'method': 'plot', 'x': 'time', 'y': 'temp'},
{'method': 'bar', 'x': 'config', 'y': 'efficiency',
'kwargs': {'barmode': 'group', 'agg': 'mean'}},
{'method': 'contour', 'x': 'rpm', 'y': 'torque', 'z': 'eff',
'datasets': [0], 'kwargs': {'overlay_sets': [1, 2]}},
], ncols=2, title='Test-rig overview')
Panel spec keys:
method— one ofplot,plot_ymult,plot_marginal,bar,box,histogram,contour,table(defaultplot).x,y,z— variables (thez/ legend controls appear only for the methods that use them).suptitle— the card title (also names CSV exports).datasets=[...]— pin the panel to specific datasets; it then ignores the header picker and carries a "pinned" badge.kwargs={...}— method-specific passthrough (e.g.{'nbins': 20},{'barmode': 'stack'},{'overlay_sets': [1, 2]}); each is applied only when the active method accepts it, so it survives plot-type switches.
Key options:
controls=False— render a locked presentation board: a clean grid of titled figure cards with all editing chrome hidden.jupyter_mode—'inline'(default), or'external'/'tab'to open a browser. Ports are auto-selected if the preferred one is busy.
Explorer — a plotting terminal
dashboard() renders a board you specified in code. explore() opens a
workspace you drive by typing: a sidebar with a drop zone, the loaded
datasets and a clickable cheat sheet; a chart pane showing the latest figure;
and a Python terminal underneath.
from unichart.dashboard import explore
explore(data='runs.csv') # standalone — serves the board and opens your browser
explore() # empty; drop a file on the sidebar, or hit "Load demo data"
nb.explore() # on a notebook you already have (inline in Jupyter)
nb.explore(app_window=True) # in its own desktop window instead of a browser tab
The notebook's methods are bound as bare names in the terminal, so the cheat
sheet reads the way the library does, and nb covers everything else:
>>> nb.load('runs.csv')
>>> plot(x='time', y=['temperature', 'pressure'])
>>> select([0, 1]); color(0, 'red')
>>> summary()
- Enter runs, Shift+Enter adds a line, ↑ / ↓ walks history.
- ⧉ copy chart in the top bar puts the current figure on the clipboard as a
2× PNG, rasterized from what's on screen (no
kaleidoneeded). The same⧉ copyaffordance unichart shows under plots in a notebook —nb.plot()in Jupyter still has its own; this is the board's. - Syntax highlighting everywhere Python appears: the cheat-sheet snippets, every command in the transcript, and the input line as you type. The transcript is coloured by Python's own tokenizer, so f-strings, comments and nested quotes are handled properly; half-typed input degrades to plain text rather than breaking.
- Drag the pane edges to resize: the sidebar's right edge and the divider between the chart and the terminal. Double-click an edge to reset it. Sizes are remembered per browser, and the chart reflows as you drag.
- A trailing expression echoes its value, like any REPL —
1 + 1prints2. - Plots go to the chart pane;
table()/summary()/list_parms()render as their real sortable, filterable HTML tables inline in the transcript. - Errors show a traceback trimmed to the line you typed.
- Dropping a file loads it through a visible
nb.load(...)command, so the transcript is a real record of the session. Drop a saved session —.jsonor asave_pngimage — and it restores through an equally visiblenb.load_session(...), bringing its datasets, formatting and plot back. The file's contents decide, not its extension, so a data.jsonstill loads as data. - ✕ close in the top bar shuts the board down: it asks first, then stops
the server and ends the process — so
unichart runs.csvin a shell returns to the prompt without aCtrl-C. Typingexit()in the terminal pane does the same. Inside a Jupyter kernel neither is offered, because the process they would end is the kernel. - 💾 save session in the top bar downloads the board as a session file:
everything loaded, however it is styled, and whatever is currently plotted.
Reopen it with
unichart that-file.json, by dropping it back on the sidebar, or withnb.load_session(...)from Python. - The board runs against your notebook: what you load or restyle there is
on
nbafterwards.
The board is dark, and explore() switches the notebook to dark mode to match
unless it already is. Options: data=, panels= (dashboard-style specs,
replayed as startup commands), title=, port=, open_browser=False for
headless hosts, and jupyter_mode=.
The terminal executes real Python in your process. It can do anything you could do at a Python prompt, so the server binds to
127.0.0.1only. It is not a sandbox — don't expose it to a network.
Command line
Installing the package puts a unichart command on your PATH, so a quick look
at a data file never needs a Python session at all:
unichart # empty explorer; load from the data bar
unichart runs.csv # open the explorer on one file
unichart runs.csv --app # ...in its own window, like a desktop app
unichart a.csv b.csv --combine # several files, merged into one dataset
unichart runs.csv --set-col run_id # split into one dataset per run
unichart runs.csv --info # print datasets + columns, then exit
unichart runs.csv --html board.html # write a static board instead of serving
A FILE can also be a saved session — a .json from save_session, or a
PNG from save_png, which carries its session in a metadata chunk. Those are
restored rather than read as data, so the datasets, queries, formatting and the
plot come back:
unichart plot.png # reopen the plot that PNG came from
unichart board.json --info # inspect a session without serving
unichart runs.csv board.json # data first, then the session on top
unichart runs.csv --panel plot:time:temp --save-session board.json
Data files load first (as one batch, so --combine still means what it says),
then sessions in the order given — so a session's own formatting is the one that
sticks, and any --panel draws on top of the restored state. A session records
its own theme, which the explorer honours; --dark overrides it. --combine,
--set-col and --name-col describe how to read a data file, so passing one
with nothing but a session on the line is an error rather than a no-op.
Seed the board with panels (repeat --panel, as method:x:y[,y2][:z]). For
the GUI each one is replayed as a startup command in the transcript; for
--html each becomes a card:
unichart runs.csv --panel plot:time:temp,press --panel histogram:temp
unichart map.csv --panel contour:rpm:torque:eff --html map.html --embed-js inline
unichart --help is colored when it's printing to a terminal — flags in cyan,
metavars and choices in green, section headings in bold — and plain the moment
it's piped or redirected. NO_COLOR=1 turns it off; FORCE_COLOR=1 forces it
back on.
A window of its own (--app)
By default the explorer opens in a browser tab, next to everything else you had
open. --app opens it as a standalone window instead — no tabs, no address
bar, no bookmarks: just the board, titled and iconed as itself, with its own
entry in the taskbar.
unichart runs.csv --app
explore(data='runs.csv', app_window=True) # the same thing from Python
nb.explore(app_window=True)
It works by handing the URL to a Chromium-family browser's --app mode (Chrome,
Chromium, Brave or Edge — whichever is found first; UNICHART_APP_BROWSER=/path/to/browser
names one the search misses). There is no extra dependency and no packaging
step: it's the browser you already have, wearing a different window. On a
machine with none of them — a Firefox-only box — the board opens in an ordinary
tab and says why.
Two things to know. --no-browser still wins, so a headless host is unaffected.
And closing the window does not stop the server — the board is still running
in the terminal you launched it from. To end it, use the top bar's ✕ close
(or type exit() in the terminal pane), which asks first and then stops the
server and the program; Ctrl-C in the launching terminal still works too.
Inside a Jupyter kernel there is no close button, because the process it would
end is the kernel.
A note on the icon. The board serves its own — a line over three bars, in the
board's palette — and the browser paints it in the window's title bar, the tab
and the page. Whether it also reaches the taskbar is the desktop's call, not
the browser's: X11 and Windows take the icon from the window, so it follows;
Wayland matches windows to .desktop files instead, so an --app window there
shows the browser's generic icon. The fix on those desktops is to install the
board, which the served web app manifest exists for: Chrome's Install page as
app (in the ⋮ menu) writes a real launcher entry carrying the unichart icon,
and starting the board from it gets you the icon everywhere.
Other flags: --title, --port, --no-browser (serve without opening a
browser — headless or remote hosts), --save-session, and --version;
--ncols --width --height apply to --html, and --dark to --html and
to a session's stored theme (the terminal board's own chrome is always dark).
unichart --help lists them all.
Panel options beyond method / x / y / z — kwargs like nbins or
barmode, dataset pins — aren't expressible as a flag; use nb.dashboard /
nb.explore from Python, where the spec dict is clearer than any encoding
would be. A missing file or malformed --panel prints one line and exits
non-zero rather than raising.
If unichart isn't found after installing, the module is runnable directly:
python -m unichart runs.csv.
Tab completion
Completion is built in — no extra package to install. Add one line to your shell's rc file:
eval "$(unichart --completion bash)" # ~/.bashrc
eval "$(unichart --completion zsh)" # ~/.zshrc
Then --<TAB> lists the flags, and --panel completes field by field against
the real column names of the files already on the command line — so a board
can be built without opening the data first:
$ unichart runs.csv --panel <TAB>
plot: plot_ymult: plot_marginal: bar: box: histogram: contour: table
$ unichart runs.csv --panel plot:<TAB>
time temp press rpm
$ unichart runs.csv --panel plot:time:te<TAB>
$ unichart runs.csv --panel plot:time:temp,<TAB> # y takes a list
--set-col / --name-col complete column names the same way, --embed-js and
--completion complete their choices, and filenames fall through to the
shell's own completion. Only the column lookups read a file (header row only),
so completing a flag costs nothing.
Export to standalone HTML
nb.dashboard_to_html(panels, path='board.html')
Renders each panel once and writes a self-contained HTML file (the frozen
presentation view). The Plotly charts stay fully interactive (hover, zoom, pan,
modebar), a global dataset filter is recreated as offline chips
(global_select=True), and table panels become real HTML tables with
click-to-sort headers. embed_js controls how plotly.js is included:
'cdn' (small, needs internet), True (embeds the full library for a truly
offline file), or 'directory'.
Dash is imported lazily, so the core toolkit never requires it.
Utility & design notes
- One shared frame, many views. All datasets live in a single backing DataFrame with per-set style/selection state, so cross-dataset operations (deltas, combined sets, consistent color/marker assignment) are cheap and consistent.
- Notebook-friendly memory management. Static-image mode and last-figure
clearing keep notebook file sizes manageable even with many large plots, while
nb.last_figstill caches the real interactive figure for re-styling or PNG export. - Discoverable.
nb.help(),nb.list_sets(),nb.list_parms(), andnb.summary()let you inspect the environment without leaving the notebook.
Learning more
nb.help()— live, categorized API reference inside the notebook.PLOTTING_STYLE_GUIDE.md— conventions for producing clean, consistent figures.demo_notebooks/— runnable examples covering the main features:UnichartNotebook_Tutorial.ipynb,unichart_data_model_tutorial.ipynb— start here.dashboard_demo.ipynb,dashboard_contour_demo.ipynb,dashboard_progression_demo.ipynb— dashboards.delta_demo.ipynb,interpolation_table_tests.ipynb— analysis.variable_color_formatting_demo.ipynb,marker_map_tests.ipynb,color_map_tests.ipynb,environment_presets_demo.ipynb— styling.plot_style_demo.ipynb— the Matplotlib look (set_plot_style).contour_overlay_demo.ipynb,static_images_demo.ipynb,large_data_showcase.ipynb— specialized plotting.
Release files for unichart 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| unichart-0.1.0.tar.gz | 309.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| unichart-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 554.6 kB
Release files / unichart-0.1.0.tar.gz
| Download URL | unichart-0.1.0.tar.gz |
|---|---|
| Size | 309.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4083016706eb2fabd8d3fb6146dc815896cd091555e87849521768bbe7ee6c51
|
|
BLAKE2b-256 checksum How to use checksums |
ad533ec305c3ecef25a738920877bd994c8b3e3852e942252972e8164f932181
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.
Transparency logRelease files / unichart-0.1.0-py3-none-any.whl
| Download URL | unichart-0.1.0-py3-none-any.whl |
|---|---|
| Size | 245.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2e0a6ead504b318abccbeeef4377c97e5288b48811afe4a3c105f078a6cec3fa
|
|
BLAKE2b-256 checksum How to use checksums |
69eedba762be7e9794765ae4abc88f1f9879d965716290b0a200c424552c57d1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.
Transparency log