hypertools.plot¶
- hypertools.plot(x, fmt='-', marker=None, markers=None, markersize=None, linewidth=None, linestyle=None, linestyles=None, color=None, colors=None, palette='hls', hue=None, color_reduce=None, labels=None, names=None, legend=None, colorbar=None, title=None, size=None, elev=10, azim=-60, ndims=3, reduce='IncrementalPCA', cluster=None, align=None, normalize=None, manip=None, pipeline=None, impute=None, resample=None, n_clusters=None, random_state=None, predict=None, t=10, save_path=None, animate=False, duration=30, tail_duration=2, rotations=1, zoom=1, chemtrails=False, precog=False, bullettime=False, frame_rate=30, focused=None, morph_samples=None, interactive=False, explore=False, backend='auto', mpl_backend='auto', show=True, transform=None, vectorizer='CountVectorizer', semantic='LatentDirichletAllocation', corpus='wiki', ax=None, frame_kwargs=None, stream_init=10000, stream_chunk=100, stream_max=None, stream_window=None, return_model=False, surface=None, density=None, font=None, label_alpha=None, xlabel=None, ylabel=None, zlabel=None, **kwargs)[source]¶
Plots dimensionality reduced data and parses plot arguments
- Parameters:
- xNumpy array, DataFrame, String, or mixed list
Data for the plot. The form should be samples (rows) by features (cols). A plain python list of equal-length numeric lists (e.g.
[[1., 2.], [3., 4.]]) is treated as ONE dataset, exactly like the equivalentnp.array. A bare scalar (e.g.hyp.plot(5)) is likewise accepted and treated as a single one-column observation, drawn as a single point. When a list of several datasets is given, every dataset must have the same number of columns (features); to combine datasets with different feature counts, bring them into a shared space first (e.g.hyp.plot(hyp.align(data, align='hyper'), ...)).Display space: static plots do NOT draw the input values in their original units. The (possibly reduced/aligned) coordinates are mean-centered and rescaled into
[-1, 1](a single shared affine transform across all datasets) to fit hypertools’ unitless square/cube frame – so coordinates read off the returned Figure are an affine image of the analyzed data, not the raw values, and scales are not comparable across separately-created figures. Usereturn_model=Trueto retrieve the analyzed (pre-rescale) data.A DataFrame with a row MultiIndex (
x.index.nlevels >= 2) is handled specially (GH #95): it is expanded, BEFORE the format_data/ analyze/reduce pipeline runs, into one “leaf” dataset per unique full index combination (level order as given), so leaves flow through normalize/reduce/align exactly like any other list of datasets. AFTER that pipeline transforms them, one MEAN trajectory is computed (in the transformed/reduced space) for every unique value-combination of each non-leaf level – from the deepest such level up to the top (outermost) level – and appended as additional traces. For levels numbered 0 (top) through L-1 (leaf), where L =x.index.nlevels, a trace whose deepest represented level islevel_idx(L - 1for a leaf;kfor a mean over the prefixlevels[0:k+1]) gets:linewidth = 1 + (L - 1 - level_idx)– i.e. 1 plus the number of levels averaged over: leaves are always 1, and each level higher up is one point thicker, so the TOP-level means are the thickest (L).alpha = min(1.0, 1 / (level_idx + 1) + 0.2)– leaves are the most transparent, the top-level mean is fully opaque (1.0), with intermediate levels smoothly in between.colorassigned purely by the trace’s TOP-level index value (from palette, in order of that value’s first appearance) – every leaf and every mean sharing the same top-level value shares one color.
Example (2 levels, e.g.
(condition, subject)): leaves get lw=1, alpha=0.7; the condition-means (the only non-leaf level, which is also the top level here) get lw=2, alpha=1.0, and are the only traces with a legend label. Example (3 levels, e.g.(group, condition, subject)): leaves lw=1, alpha=1/3+0.2≈0.533; (group, condition)-means lw=2, alpha=0.7; group-means (top level) lw=3, alpha=1.0.legend is automatically populated with one entry per unique top-level index value: only each top-level mean trace carries that label; every other trace (all leaves, and any intermediate-level means) is drawn with
label='_nolegend_'(excluded from the legend, matching the convention predict=’s forecast overlay already uses). If linestyle/linestyles is given as a list, its length MUST equal the number of unique top-level index values (one style per top-level group, applied to every trace in that group); a mismatched length raisesValueError. Any color/colors/ linewidth kwarg is ignored (with aUserWarning) since MultiIndex grouping owns those. hue= is superseded with aUserWarning(MultiIndex grouping takes precedence); cluster=/ n_clusters= raiseValueError(both would fight the MultiIndex color assignment) – reset the index first (df.reset_index(drop=True)) to cluster instead. predict= also raisesValueErrorwhen combined with MultiIndex expansion: forecasts are computed one-per-leaf BEFORE the per-level mean traces are appended, so the leaf count no longer matches the final trace count – reset the index first to use predict=. Row averaging assumes member leaves align by row POSITION at each timepoint; leaves of unequal length are averaged over their overlapping prefix (the shortest member’s length), with a singleUserWarningper affected group (deduplicated even when a 3+-level tree causes multiple groupings to share members). Works with both static and animated plots and both rendering backends, since the expansion happens upstream of drawing. A MultiIndex on the COLUMNS (as opposed to the row index) is unrelated to this and is unaffected – it is handled by the existing column-formatting pipeline in hypertools.tools.format_data/hypertools.tools.df2mat. A single-level (or default RangeIndex) DataFrame, or a plain array/list input, is completely unaffected by any of the above.Expansion is ONLY applied when a single bare DataFrame is passed as x. If x is a LIST containing one or more MultiIndex DataFrames (whether alone or mixed with arrays/other DataFrames), the MultiIndex is silently treated as a flat index on each such element by the normal list-of-datasets pipeline – a
UserWarningis raised naming each offending element’s position in the list.- fmtstr or list of strings
A list of format strings. All matplotlib format strings are supported, including color letters (e.g.
'ro-'draws red markers joined by a red line, exactly as in matplotlib; an explicit color=/colors= kwarg wins over a fmt color letter).A single fmt string is broadcast to every drawn trace. A fmt LIST is distributed one-entry-per-DRAWN-TRACE, and normally there is one trace per input dataset.
hue=/cluster=/n_clusters=(and a MultiIndex) regroup the data so the drawn-trace count can differ from the input-dataset count; in the ONE reconciled case – a categorical (or cluster) LINE, which splits each dataset into one trace per contiguous same-category run so lines never join separate trajectories (GH #291) – a fmt list given at INPUT-dataset length is automatically propagated to each dataset’s runs, sohyp.plot([A, B], hue=h, fmt=['.', '-'])draws every run of A with markers and every run of B as a line. Otherwise (marker-only grouping, MultiIndex) the fmt list must match the drawn-trace count. A list matching neither the input-dataset count nor the drawn-trace count raises aValueErrornaming fmt and both counts. A fmt tuple is accepted and treated exactly like the equivalent list.Static line rendering is DATA-FAITHFUL: line styles are smoothed by PCHIP interpolation, which only ever ADDS points between samples – every original sample (including the final one) is always among the drawn line vertices, and trajectories with ~900+ samples are drawn as-is (never decimated).
A format string combining a LINE style with a MARKER (e.g. ‘o-‘, ‘s–’) gets the SAME connecting-line smoothing/interpolation a pure line style (e.g. ‘-’) gets (GH #141 follow-up; previously marker+line combos silently skipped interpolation, drawing straight/unsmoothed segments between raw points). The line and markers are drawn as two separate artists on the STATIC (non- animated) matplotlib backend: the smoothed/interpolated line, plus markers at the TRUE (pre-interpolation) sample points – so markers never drift onto the dense interpolated curve. Pure line- only and pure marker-only styles are unaffected (still one artist, as before). For ANIMATED matplotlib plots, and for the plotly backend (static or animated – it always draws a marker+ line combo as a single ‘lines+markers’ trace), a marker+line combo’s line is likewise now smoothed (the interpolation gate fix is backend-agnostic), but its markers currently render at the same (interpolated) points as the line rather than only the original samples – splitting those into separate artists/traces for every animated style and for plotly is a follow-up.
- linestyle(s)str or list of str
A list of line styles
- marker(s)str or list of str
A list of marker types
- markersizeint or float
Size of the markers in points (default: matplotlib’s 6.0). Applies to both backends.
- linewidthint or float
Width of plotted lines in points (default: matplotlib’s 1.5 for static plots, 1 for animations). Applies to both backends.
- color(s)str or list of str
A list of colors
- **kwargsany other matplotlib-style keyword argument
GH #206: any keyword argument that isn’t one of plot()’s own named parameters above is passed straight through to each drawn artist – e.g. zorder=3, alpha=0.5, dashes=(4, 2), markeredgecolor=’k’. Applied VERBATIM, identically, to every drawn dataset – unlike color/marker/linestyle/etc. (see below), an extra kwarg’s value is NEVER interpreted as “one entry per dataset” even if it happens to be a list/tuple (e.g. dashes=(4, 2) is a single dash-pattern VALUE, not per-dataset values 4 and 2) – so there is no per-dataset form for an extra kwarg; use one of the dedicated per-dataset-aware kwargs (color/marker/linestyle/markersize/linewidth) for that. Merged in AFTER the named style kwargs are resolved, so an explicit named kwarg (or internal styling logic, e.g. MultiIndex/ mixture-cluster alpha, legend=’s label, explore=’s picker) always wins on a naming collision. A kwarg that no backend can use (not a matplotlib line-artist property or alias, nor a plotly-mappable name) raises
TypeErrornaming it – with a did-you-mean hint for near-misses of plot’s own parameters (e.g.n_dims->ndims) – BEFORE the pipeline runs, rather than surfacing a cryptic matplotlib internals error after it (release-1.0 audit; the legacy 0.xgroup=kwarg gets a dedicated “renamed to hue=” message). On the plotly backend, only a small subset maps onto an actual trace property (color, alpha, linewidth, markersize, marker, linestyle, label); anything else is ignored with aUserWarningnaming every unmapped kwarg (rather than raising, since plotly’s trace objects were never going to support the same kwarg surface as matplotlib).Every list/tuple-valued NAMED styling kwarg plot() itself broadcasts (color/colors, marker/markers, linestyle/ linestyles, linewidth, markersize – NOT the generic **kwargs passthrough above, which is applied verbatim and never broadcast, so alpha=, zorder=, etc. must be a single value) is distributed one-entry-per-DRAWN-TRACE and its length is validated against the FINAL drawn-trace count (GH #206); a mismatch raises a
ValueErrornaming the kwarg, the length given, and that count (previously it silently degraded to None for every trace).cluster=/hue=/n_clusters=/MultiIndex regroup the data, so the final drawn-trace count can differ from the number of INPUT datasets. In ONE case the two layouts are reconciled for you: a categorical (or cluster) LINE splits each dataset into one trace per contiguous same-category run (GH #291), and a style list given at INPUT-dataset length is automatically propagated to every run that dataset produced – so
hyp.plot([A, B], hue=h, linewidth=[1, 3])draws all of A’s runs at width 1 and all of B’s at width 3 (a list already at run length is used verbatim). For every OTHER regrouping – marker-only hue/cluster grouping (which merges observations across datasets into one per-category trace, so a per-dataset style is not even well defined), and MultiIndex expansion – a style list must match the resulting drawn-trace count, not the input-dataset count.- palettestr, list of colors, or matplotlib.colors.Colormap
A matplotlib or seaborn color palette (name), an explicit list of colors (hex strings like ‘#ff0000’, named colors like ‘red’, or RGB(A) tuples – usable on every path: categorical, continuous, matrix hue, and the colorbar), or a matplotlib Colormap instance (sampled evenly). For a CONTINUOUS hue, a short color list is blended into a smooth gradient using the listed colors as anchors (seaborn
blend_palettesemantics); for categorical/matrix hue the list must supply at least one color per category/component. Note the default ‘hls’ (like ‘husl’) is CYCLIC: for a continuous hue mapping, hypertools samples only ~5/6 of its hue circle so the minimum and maximum hue values stay visually distinguishable; categorical palettes are used as-is.- huelist, numpy array, pandas Series/Index/Categorical, or 2D matrix
Values used to color the plot, one per observation, matched to the observations POSITIONALLY (a pandas Series’ index is ignored). Accepts categorical labels (one per observation; grouped and colored by category), continuous numeric values (mapped through the palette; combined with a line format this produces multicolored lines whose color varies continuously along each trajectory, and a marker+line combo format like
'o-'keeps BOTH components – the multicolored line plus per-point-colored markers at the true sample points), or a 2D matrix with one row per observation (e.g. mixture proportions or model weights; colors are blended per observation). Non-finite (NaN/inf) continuous/matrix hue values are drawn in a neutral light gray (with a warning) and are excluded from the color mapping, so the remaining observations keep their full color range. To label a subset of points categorically, use None entries (i.e. [‘a’, None, ‘b’, ‘a’]): the None-labeled points are drawn in the same de-emphasized neutral gray, get no legend entry, and do not consume a palette slot (the named categories keep the first palette colors, in first-appearance order).The categorical-vs-continuous choice: string labels always take the CATEGORICAL path (one trace per category, legend-able, categories in first-appearance order). A 1-D numeric hue takes the CONTINUOUS path (per-point palette-mapped colors, no legend), EXCEPT that integer (or boolean) values with at most 12 unique values – and fewer unique values than observations – are treated as categorical group ids (e.g. the cluster labels
hyp.clusterreturns): one trace per id, palette-colored and legend-labeled in sorted numeric order. Float-valued or higher-cardinality integer hues are always continuous. To force grouping, pass the ids as strings (hue=[str(g) for g in ids]); to force a continuous mapping, cast to float (hue=np.asarray(ids, dtype=float)).A SCALAR hue (a single string or number, e.g.
hue='red') is broadcast to one group covering every observation – a single color – and emits a UserWarning, since this is usually a mistake (e.g. a DataFrame column NAME passed seaborn-style; pass the column’s values,hue=df['col'], instead).When the data is a list of datasets, hue may mirror that nesting – one hue sub-sequence per dataset, each matching that dataset’s length (e.g.
hyp.plot([d0, d1], hue=[h0, h1])); it is flattened to one value (or matrix row) per observation.A 2D matrix hue with MORE than 3 columns (or any matrix, if color_reduce= is given) is first reduced to 3 columns and mapped directly to (r, g, b) – see color_reduce.
- color_reducestr, dict, class, instance, or None
How to reduce an arbitrary high-dimensional matrix hue to the 3 columns used as (r, g, b). Any hyp.reduce spec (default: None -> ‘IncrementalPCA’). Only applies when hue is a 2D matrix; the three reduced dimensions are min-max scaled to [0, 1] per column and used as the red/green/blue channels, so an arbitrary per-observation feature matrix becomes a continuous RGB coloring. A matrix hue with <=3 columns is left on the palette-blend path unless color_reduce= is given explicitly.
- nameslist or None
Per-DATASET names, one per dataset in a list input (default: None). Distinct from labels (per-POINT text call-outs) and hue (per- observation coloring): each name labels its dataset’s trace and turns the legend on, so hyp.plot([raw, a, b], names=[‘raw’, ‘a’, ‘b’]) shows a legend naming the three datasets. Must have exactly one entry per dataset; mutually exclusive with passing a legend= list (use one or the other). Rendered on both the matplotlib and plotly backends. Incompatible with a CATEGORICAL hue (which regroups the data by category, so the drawn traces are no longer the named datasets); that combination raises
ValueError– label the hue categories withlegend=[...]instead.- labelslist
A list of point labels: exactly one entry per OBSERVATION (row) across all datasets, or a nested list with one sub-list per dataset; a length mismatch raises
ValueErrornaming labels and both counts. If no label is wanted for a particular point, input None for that entry.In an ANIMATION whose frame grid is coarser than the data (fewer than one frame per sample), each label is attached to the nearest drawn frame point, so labels are never silently dropped.
Supported on BOTH backends (GH #205/#F3): matplotlib draws these as ax.annotate call-outs; plotly draws the same points as layout.scene.annotations (3D) or layout.annotations (2D), at the same data coordinates, honoring the resolved font= (see below) the same way the legend/colorbar/title do.
- label_alphafloat or None
Opacity of the translucent background box drawn behind each labels= point annotation (GH #103). None (default) keeps the historical opacity, 0.5, on both backends. Must be a number in
[0, 1]; any other value raises ValueError. On matplotlib this sets the annotation bbox’s alpha; on plotly it sets the alpha channel of the annotation’s bgcolor ('rgba(255,255,255,<label_alpha>)'). Works for both static and animated plots (labels are drawn once, at the original data coordinates, and persist across every frame on both backends).- legendlist, str, or bool
If set to True, legend is implicitly computed from data. Passing a list will add string labels to the legend (one for each list item); the list must have exactly one entry per drawn dataset/ group (
ValueErrornaming legend otherwise). A bare string is treated as a single-entry list (valid only for a single dataset).- colorbarbool or dict
If True, draws a colorbar reflecting the color mapping in use (GH #100). For a continuous 1D hue (or continuous hue combined with a line format, which produces multicolored lines), the colorbar is a continuous ScalarMappable spanning the ACTUAL hue value range, using the SAME palette as the lines/markers. For discrete groups (categorical hue, cluster/n_clusters, or a plain list of datasets with no hue/cluster), the colorbar is segmented (one BoundaryNorm-style block per group), with tick labels taken from an explicit
legend=[...]list if given, else the categorical hue’s own category names (nolegend=Trueneeded), else1..n. Pass a dict for finer control:{'label': str, 'ticks': [...], 'location': 'right'|'left'|'top'| 'bottom'}(all keys optional;locationdefaults to'right', the same side as the legend – when both a legend and a right-side colorbar are shown, the figure is widened so neither is clipped or overlaps the other). RaisesValueErrorif requested with no color mapping available at all (e.g. a single dataset with no hue/cluster). Default None (no colorbar).- titlestr
A title for the plot
- fontNone, str, or matplotlib.font_manager.FontProperties
Controls the font used for every text surface hypertools draws, on BOTH backends (GH #205): point annotations (labels=), the legend, colorbar tick labels/axis label, and the plot title – on matplotlib via ax.annotate/ax.legend/etc.; on plotly via layout.scene.annotations/layout.annotations, the legend, colorbar title/ticks, and the plot title.
None (default): hypertools uses its own sans-serif FALLBACK STACK, led by the Noto Sans face bundled with the package (SIL OFL 1.1, in
hypertools/external/fonts). matplotlib is handed that font FILE, so the MATPLOTLIB backend renders in the bundled Noto Sans identically on every platform. The PLOTLY backend can only pass a family NAME to the rendering browser (it cannot use a font file), so it prefers Noto Sans but falls back to the next installed system face when Noto isn’t present – plotly typography may therefore vary by platform. Both backends resolve their stack PER GLYPH (matplotlib walks afont.familylist; a browser walks a CSS stack), so text mixing scripts renders completely from several faces (Latin from Noto Sans, Japanese from an installed CJK face, math symbols from DejaVu Sans) instead of showing tofu for whatever the primary face lacks – and, crucially, the primary face stays Noto Sans, so a stray accent or Greek letter does NOT swap the whole plot onto some other font. Only when the stack has a genuine COVERAGE GAP (a script no stack family can draw) does hypertools scan for an installed font covering that gap and ADD it as an extra fallback (Noto stays primary) – to matplotlib’sfont.familylist and, appended near the end, to the plotly CSS stack (the latter still needs that family installed in the browser to take effect; see the backend note below). AUserWarningis raised only for characters NOTHING available can draw, naming them. Bundling every script is infeasible (a pan-CJK face alone is ~16 MB), so for full CJK coverage install a pan-Unicode font –apt-get install fonts-noto-cjkon most Linux distros; macOS/Windows usually already ship one (Hiragino Sans/Yu Gothic).str: either the name of an installed font FAMILY (e.g.
'Noto Sans CJK JP'), or a path to a.ttf/.otf/.ttcfont FILE (existing paths are detected automatically, relative or absolute). RaisesValueErrorif the string is neither a resolvable family name nor an existing file.matplotlib.font_manager.FontProperties: used as-is.
Backend semantics differ because matplotlib and plotly resolve fonts differently: matplotlib accepts a font FILE and sets a FontProperties object on each Text artist individually (exact glyph outlines, embedded at save time). plotly (rendered by a browser, or by Chromium via kaleido for static image export) only understands FAMILY NAMES – there is no way to point it at a specific font file – so hypertools takes the resolved font’s family name (FontProperties.get_name()) and puts it at the FRONT of hypertools’ curated sans-serif CSS stack (e.g.
'"<name>", "Noto Sans", "Helvetica Neue", ..., sans-serif'), and sets it as layout.font.family, which every plotly text surface hypertools creates inherits unless it overrides its own font.family (none do, after this change). Static plotly image export (save_path=…png/.jpg etc., via kaleido) still depends on the exporting machine’s OS having a font that actually covers the requested family/characters – unlike matplotlib, hypertools cannot embed a specific font file into a plotly export.- xlabel, ylabel, zlabelstr or None
Axis labels, on BOTH backends, for STATIC and ANIMATED plots, in 2-D and 3-D (round17 #7). None (default): no label, EXCEPT that when a single DataFrame with named (non-default, non-duplicate) columns is plotted and the drawn axes correspond 1:1 to its (df2mat-transformed) columns – a 2- or 3-column DataFrame drawn with no real dimensionality reduction – the column names become the default axis labels (release-1.0 audit, F08-016). Explicitly passed labels always win (pass e.g.
xlabel=''to suppress an inferred label), and nothing is inferred when transform= or pipeline= replace the standard analysis pipeline. matplotlib: ax.set_xlabel/ax.set_ylabel/ax.set_zlabel; hypertools draws its own cube/square frame in place of matplotlib’s default axes box (ticks/spines/panes are hidden), so whenever any of these three is given, only the specific label Text artist(s) are kept visible rather than the whole axis (ticks/spines/gridlines/3-D panes stay hidden either way). plotly: layout.scene.xaxis.title/ .yaxis.title/.zaxis.title for 3-D, layout.xaxis.title/ .yaxis.title for 2-D – again with only that axis’s title shown (ticks/gridlines/zero-line stay hidden). zlabel on a 2-D plot (ndims < 3, or data that is intrinsically lower-dimensional) raises ValueError (no z-axis to label) – pass ndims=3 (the default) to use zlabel=, or use xlabel=/ylabel= for 2-D data.- sizelist
A [width, height] pair of numbers, in inches, to resize the figure (anything else raises
ValueErrornaming size)- elevint or float
The camera elevation angle, in degrees, for 3-D plots: the angle above (positive) or below (negative) the x-y plane (default: 10, matplotlib’s Axes3D.view_init convention). Must be a number; ignored for 2-D/1-D plots.
- azimint or float
The camera azimuth angle, in degrees, for 3-D plots: the rotation of the viewpoint about the z axis (default: -60, matplotlib’s Axes3D.view_init convention). Must be a number; ignored for 2-D/1-D plots. For every rotating 3-D animation style (‘spin’, ‘parallel’/True, ‘window’, ‘serial’, ‘morph’) this is the STARTING azimuth; the camera sweeps rotations full turns from it, so rotations=0 gives a fixed camera at exactly this angle.
- normalizestr, False, or None
If set to ‘across’, the columns of the input data are z-scored across lists. If set to ‘within’, the columns are z-scored within each list that is passed. If set to ‘row’, each row of the input data is z-scored. If set to False or None, no normalization is applied (default: None).
- manipmodel spec or None
A hypertools.manip spec (a registry name, dict spec, class/ instance, or a list chaining several – see hypertools.manip.manip.manip), run at the canonical manip stage position (GH #153): FIRST, before normalize/reduce/align/ cluster – e.g.
hyp.plot(data, manip=[{'model': 'Smooth', 'kwargs': {'kernel_width': 25}}, {'model': 'Resample', 'kwargs': {'n_samples': 1000}}], align={'model': 'HyperAlign'}, reduce='UMAP')runs the whole cross-module pipeline in one call (GH #275). resample= (below) is independent sugar for a single Resample step applied BEFORE this stage’s data reaches it (so resample sugar always runs first when both are given). Mutually exclusive with pipeline= (default: None).- pipelinehypertools.Pipeline or None
A previously-FITTED Pipeline (e.g. from
hyp.analyze(data, ..., return_model=True)or this function’s own return_model=True bundle’s ‘pipeline’ key) to apply to x via .transform instead of fitting new manip/normalize/ reduce/align/cluster models (GH #227) – e.g. fit on dataset A viap = hyp.analyze(A, manip='Smooth', reduce='PCA', align='HyperAlign', return_model=True)[1]and reuse those exact fitted parameters on a structurally-identical dataset B viahyp.plot(B, pipeline=p). Mutually exclusive with manip=/ normalize=/reduce=/ndims=/align=/cluster= (each must be left at its default) – passing both raises ValueError naming the conflicting kwarg(s). resample= is still applied (as sugar, before pipeline.transform runs) since it is not one of the stage kwargs the fitted Pipeline itself covers (default: None).- reducestr, dict, class, instance, or fitted Reducer
Decomposition/manifold learning model to use (default: ‘IncrementalPCA’). Models supported: PCA, IncrementalPCA, SparsePCA, MiniBatchSparsePCA, KernelPCA, FastICA, FactorAnalysis, TruncatedSVD, DictionaryLearning, MiniBatchDictionaryLearning, TSNE, Isomap, SpectralEmbedding, LocallyLinearEmbedding, MDS, and UMAP; the mixture (soft-clustering) models GaussianMixture, BayesianGaussianMixture, LatentDirichletAllocation and NMF (which return per-observation membership proportions, GH #174); and the torch-backed autoencoders Autoencoder, DeepAutoencoder, SparseAutoencoder, ConvolutionalAutoencoder, SequenceAutoencoder and VariationalAutoencoder (GH #162, pip install “hypertools[torch]”). Can be passed as a string, or for finer control of the model parameters as a dictionary, e.g. reduce={‘model’: ‘PCA’, ‘kwargs’: {‘whiten’: True}}. See scikit-learn specific model docs for details on parameters supported for each model. A model INSTANCE (including an already-FITTED reducer, which is applied via .transform without refitting) is also accepted; if its output still has more than 3 dimensions (e.g.
PCA(n_components=5)), a second display-only reduction with the default reducer projects it to 3 dimensions for plotting. If None, no reduction is applied – valid only when the data already has at most 3 (or ndims) dimensions; otherwise aValueErrorexplains that the data cannot be drawn unreduced.- ndimsint
An int representing the number of dims to reduce the data x to. If ndims > 3, the data is analyzed at that dimensionality but plotted in 3 dimensions (a second, display-only reduction with the default reducer); use
return_model=Trueto retrieve the higher-dimensional analyzed data. Default is 3 (plot in 3 dimensions).- alignstr, dict, False, or None
Alignment model to bring a list of datasets into a shared space. If str, ‘hyper’ (hyperalignment) or ‘SRM’ (shared response model). You can also pass a dictionary for finer control, where the ‘model’ key specifies the model and ‘kwargs’ holds its parameters, e.g. align={‘model’: ‘HyperAlign’, ‘kwargs’: {‘n_iter’: 10}}. If False or None, no alignment is applied (default: None).
- clusterstr, dict, class, instance, False, or None
If cluster is passed, HyperTools will perform clustering using the specified clustering model (a registry name, dict spec, model class, or sklearn-API model instance – an instance’s own parameters are used, so n_clusters= is ignored, with a warning, alongside one). Supported algorithms are: KMeans, MiniBatchKMeans, AgglomerativeClustering, Birch, SpectralClustering, MeanShift, DBSCAN, OPTICS, AffinityPropagation and HDBSCAN, plus the mixture (soft-clustering) models GaussianMixture, BayesianGaussianMixture, LatentDirichletAllocation and NMF. Can be passed as a string, or for finer control of the model parameters as a dictionary, e.g. cluster={‘model’: ‘KMeans’, ‘kwargs’: {‘max_iter’: 100}}. See scikit-learn specific model docs for details on parameters supported for each model. If no parameters are specified a default set of parameters will be used: 3 clusters/components for most models (the same default as hyp.cluster), 20 components for LatentDirichletAllocation and NMF (default: None). Clustering runs on the REDUCED (post normalize=/reduce=/align=) scores, not the raw input – so LatentDirichletAllocation and NMF, which require non-negative data, fail here even when the raw data is non-negative (the reduced scores are signed); run hyp.cluster on the raw data instead to use those models. FeatureAgglomeration raises a
ValueError: it clusters features (columns), not observations, so its labels cannot group the plotted rows – use hyp.cluster(data, cluster=’FeatureAgglomeration’) directly. Cluster labels are drawn as one trace per cluster (palette colors, legend entries in sorted label order).- n_clustersint
If n_clusters is passed, HyperTools will perform clustering with the cluster count set to n_clusters, using k-means unless cluster= selects another model. The resulting clusters are plotted in different colors according to the color palette. Default None: each model’s default count is used (3, matching hyp.cluster; 20 components for LatentDirichletAllocation/NMF). Ignored, with a
UserWarning, for models that discover the number of clusters themselves (HDBSCAN, MeanShift, DBSCAN, OPTICS, AffinityPropagation). If the cluster= spec itself carries a cluster count (an instance’s own setting, or n_clusters/n_components in a dict spec’s kwargs), the spec’s value wins and aUserWarningnotes the conflict – the same precedence hyp.cluster applies.- random_stateint, RandomState, or None
Seed for reproducibility, threaded to the reduce/cluster stages: it is injected into any stage model whose constructor accepts a random_state (UMAP, TSNE, KMeans, GaussianMixture, …), so e.g. hyp.plot(x, reduce=’UMAP’, random_state=0) gives a repeatable embedding. Deterministic models and pre-constructed instances are unaffected (default: None).
- imputestr or dict or class or class instance or None
Overrides the default PPCA fill for missing (NaN) values with a different hypertools.impute model, e.g. ‘Kalman’, ‘KNNImputer’ (default: None, i.e. PPCA – observed values are preserved exactly and only the NaN entries are reconstructed; see hypertools.impute.ppca). See hypertools.impute.impute for accepted forms.
- resampleint or False/None
If set to an integer N (GH #94), each input dataset is PCHIP-resampled to exactly N rows via the existing hypertools.manip
Resamplemanipulator, applied right after hypertools.tools.format_data (so it sees whatever x has been normalized into – a plain list of per-dataset numpy arrays) and BEFORE the normalize/reduce/align pipeline. resample=500 on a 100-row dataset produces per-dataset arrays with exactly 500 rows going into normalize/reduce/align/cluster/hue – and the SAME values hyp.manip(data, model=’Resample’, n_samples=500) produces on that same input, since it is the identical manipulator call under the hood. This is independent of, and happens well before, the later line-smoothing interpolation (GH #141) applied for animation/line-drawing purposes. Default None (no resampling, unchanged pre-existing behavior); False is equivalent to None. RaisesValueErrorif resample is anything other than False/None or an integer>= 2.- predictstr or dict or class or class instance or None
If set, forecasts t new rows per input dataset (in the plotted, post normalize/reduce/align space) using the specified hypertools.predict model, e.g. ‘Kalman’, ‘ARIMA’, ‘GaussianProcess’ (see hypertools.predict.predict for accepted forms), and overlays one dashed, low-opacity (alpha 0.6) forecast trace per dataset in the SAME color as its source line (no separate legend entry). The drawn overlay prepends the last observed row so the dashed trace connects to the trajectory (t + 1 drawn vertices); the forecast DATA itself – e.g. in the
return_model=Truebundle – has exactly t rows, matching hyp.predict. Only supported for STATIC plots (default: None; raisesNotImplementedErrorif combined withanimate).- tint or datetime-like
Forecast horizon passed to predict (see hypertools.predict.common.resolve_t); ignored unless predict is set (default: 10).
- save_pathstr or path-like
Path to save the image/movie; the format is chosen by the file extension, which must be included (e.g. save_path=’/path/to/file/image.png’).
pathlib.Pathobjects work everywhere a str does, a leading~is expanded, and the target directory must already exist (a missing directory/empty path/ non-path value fails fast with a clear error before the plot is computed). Supported formats: STATIC matplotlib plots accept any matplotlib.pyplot.savefig format (.png, .pdf, .svg, .eps, .jpg, …); ANIMATED matplotlib plots accept .gif, .png/.apng (animated PNG), and .svg (animated vector graphics) with no extra dependencies, plus the video formats .mp4/.mov/.avi/.m4v/.mkv, which – and ONLY which – require FFmpeg (https://ffmpeg.org; e.g.brew install ffmpegon macOS with Homebrew (https://brew.sh) orapt-get install ffmpegon Debian/Ubuntu). The plotly backend saves .html natively (static or animated); its static image export (.png/.jpg/.svg/.pdf, via kaleido) and animated .gif/.png/.apng/video export render each frame through kaleido. Note on ANIMATION export cost: every frame is rendered and encoded, and the default duration=30 x frame_rate=30 yields 900 frames – roughly a minute of encoding and a multi-MB file even for small datasets; encoding time and file size scale linearly with duration * frame_rate, so pass a shorter duration= (e.g. 2-5 seconds) for quick exports.- animatebool, ‘parallel’, ‘spin’, ‘serial’, ‘window’, ‘morph’, or list
If True or ‘parallel’, plots the data as an animated trajectory, with each dataset plotted simultaneously. If ‘spin’, all the data is plotted at once but the camera spins around the plot. If ‘serial’, datasets appear ONE AT A TIME in list order: each grows point-by-point into place while all previous datasets stay fully drawn, and datasets are never connected to each other – useful for e.g. conversation turns accumulating in a shared embedding space (default: False). This MODE is always GLOBAL – there is exactly one camera and one frame loop driving every dataset in the animation, so it cannot vary per dataset (unlike chemtrails/precog/bullettime below, which CAN).
2-D animations (round17 #9, GH #123): every style EXCEPT ‘spin’ works for ndims=2 as well as ndims=3, in both backends, using a FIXED (non-rotating) viewport – there is simply no camera-angle bookkeeping to do in 2-D. ‘spin’ rotates the camera and nothing else, so it is meaningless for 2-D data and raises ValueError (naming the other styles) instead of silently doing nothing. rotations=/zoom= are 3-D camera controls with no 2-D equivalent; passing either as a non-default value alongside a 2-D animate warns once (UserWarning) that it is ignored, in both backends – including animate=’morph’, whose rotations doubles as a per-segment PACING control in 3-D (not purely a camera control there – see the note under ‘morph’ below), but which is ignored the SAME way for consistency with every other 2-D style: 2-D morphs always use even segment timing.
If ‘window’ (round17 #8, GH #275 – Jeremy’s own definition: “like bullettime, but without the precog and chemtrail parts”), a sliding, FULLY-OPAQUE window of length focused (seconds; see focused below) moves along each trajectory – nothing outside the window is drawn at all, not even a faded trail (unlike bullettime/ chemtrails/precog, which paint a low-opacity backdrop outside their own in-focus window). In 3-D, the camera still rotates at a constant speed per rotations, exactly like True/’parallel’; in 2-D there is no camera, so only the window itself moves. Any of chemtrails/precog/bullettime passed alongside animate=’window’ is ignored (UserWarning, naming the ignored flag(s) and dataset indices – see the note under bullettime below), since ‘window’ has no trail artist/trace to configure. Both 2-D and 3-D, in both backends.
If ‘morph’ (maintainer request, 2026-07-06), every dataset is treated as a POINT CLOUD (not a trajectory, regardless of fmt) and morphed ds_1 -> ds_2 -> … -> ds_N through a hold/morph/hold/… schedule (
2N - 1segments:[hold_1, morph_1->2, hold_2, ..., hold_N]). Every dataset keeps its FULL point count (maintainer request, 2026-07-06 follow-up): the target count n is the LARGEST morphing dataset’s own size (after the optional morph_samples cap below), and any dataset with m < n points is padded up to n by duplicating n - m of its OWN points, chosen at random (seeded) – no real data point is ever dropped. The duplicated (padding) points are hidden during that dataset’s own HOLD segments (so semi-transparent markers alpha-composite exactly like a plain plot of that dataset’s true points) and shown, like every other point, during MORPH segments. Consecutive (now equal- sized, n-point) clouds are chain-matched point-for-point with the Hungarian algorithm (scipy.optimize.linear_sum_assignment on pairwise distances, so each point travels the shortest total distance to its partner in the next cloud – exactly examples/plot_shape_morph.py’s original hand-rolled algorithm, now built into the library), and eased between clouds with smoothstep interpolation. A SINGLE point artist/trace is drawn (one per plot, not one per dataset): its color linearly (RGB) interpolates between the two datasets’ own colors during a morph segment and is solid during a hold. Requires at least 2 datasets; raises ValueError otherwise. Both 2-D and 3-D data are supported (round17 #9, GH #123 – previously 2-D raised NotImplementedError); surface=True recomputes that one artist’s hull every frame from its current interpolated positions (unaffected by which points are duplicates – a duplicate is an exact copy of an existing point, so it never changes a convex hull’s shape), but this hull-tracking is still 3-D only – surface= is silently a no-op for an animated 2-D ‘morph’ (or any other 2-D animate style; see surface’s own docstring).animate may ALSO be a per-dataset LIST (length = the number of FINAL, post cluster/hue-reshape datasets), with each entry ‘morph’, None, or False: ‘morph’-tagged datasets join the morph sequence IN LIST ORDER; untagged datasets are drawn as STATIC (unanimated) backdrops, present in every frame. At least 2 entries must be ‘morph’ (ValueError otherwise); any other mode string inside a list raises ValueError (list form only supports tagging datasets for animate=’morph’ – ‘spin’/’serial’/etc. cannot vary per dataset, see above). A scalar animate=’morph’ is equivalent to tagging every dataset ‘morph’.
animate may ALSO be a dict (GH #154 resolution): a mega-dict SPEC for the animation, mirroring the model-spec grammar used elsewhere in hypertools – ‘style’ plays the role of model (REQUIRED; the value is any of the scalar animate forms above, e.g. ‘spin’) and every OTHER key maps onto one of the flat animation kwargs below (duration, tail_duration, rotations, zoom, chemtrails, precog, bullettime, frame_rate, focused, morph_samples) – e.g.
animate={'style': 'spin', 'rotations': 2, 'duration': 15}is exactly equivalent toanimate='spin', rotations=2, duration=15. The dict is unpacked into the flat kwargs at the very top of plot(), before anything else runs, so every downstream code path only ever sees the flat form. Raises ValueError if ‘style’ is missing (message shows an example dict), if the dict has any key that isn’t ‘style’ or one of the flat animation kwargs above (message lists the valid keys), or if a dict key’s value CONFLICTS with that same flat kwarg passed explicitly (a different value) – naming the conflicting key and both values. This mega-dict form is additive sugar, not a new pipeline concept – flat kwargs remain the primary/documented direction (GH #154); note that a style=/ labels= mega-dict covering EVERY plot() kwarg (not just animation) was considered and explicitly rejected as unnecessary churn.- backendstr
Rendering backend: ‘matplotlib’ (the classic renderer), ‘plotly’ (interactive; requires plotly – install with pip install hypertools[interactive]), or ‘auto’ (default), which uses plotly on Google Colab / Kaggle notebooks where interactivity matters most and matplotlib everywhere else. With the plotly backend, the return value is a plotly Figure (any animation frames are embedded directly in it, so no separate animation object is returned).
- duration (animation only)float
Length of the animation in seconds (default: 30 seconds). Has no effect on static plots (static line smoothing uses a fixed density, independent of the animation kwargs). Note: when saving with save_path=, every frame is rendered and encoded (duration * frame_rate frames – 900 at the defaults), so export time and file size scale linearly with duration; use a short duration (e.g. 2-5 seconds) for quick exports.
- tail_duration (animation only)float
Sets the length of the tail of the data (default: 2 seconds)
- rotations (animation only)float or list
Number of rotations around the box over the course of the animation (default: 1 – with the default 30-second duration, one revolution every 30 seconds). Identical pacing on both backends. A list is ONLY valid with animate=’morph’: it must have exactly
2N - 1entries (N = the number of morphing datasets), one per hold/morph segment ([hold_1, morph_1->2, hold_2, ..., hold_N]) – e.g. rotations=[1, 0.25, 2, 0.25, 1] for 3 morphing datasets spins 1 full rotation during the first hold, a quarter rotation during the first morph, 2 rotations during the second hold, etc. Camera rotation speed (degrees/frame) is CONSTANT across the whole animation: each segment’s SCREEN TIME (frame count) is proportional to its own rotation count – not split evenly across segments – so a segment with more rotations gets more time, never faster spinning (seehypertools.plot.morph.segment_frame_counts()and its ZERO_ROTATION_FLOOR: a segment with 0 rotations still gets a small amount of screen time so it stays visible). Within a segment, that segment’s own rotation count is spread uniformly over its own frames, and the camera azimuth accumulates CONTINUOUSLY across segment boundaries (no jump). N is the number of morphing datasets AFTER the reduce/align/cluster/hue pipeline (the FINAL, drawn dataset count), which can differ from the number of datasets originally passed in. ValueError if a list is given with any animate mode other than ‘morph’ (checked immediately, before the pipeline runs – this only depends on animate itself), or if the list length doesn’t match2N - 1(names the expected length; only knowable once N is, so checked after the pipeline runs).- zoom (animation only)float
How far to zoom into the plot, positive numbers will zoom in (default: 1)
- chemtrails (animation only)bool or list of bool
A low-opacity trail is left behind the trajectory (default: False). Pass a list of bool (one entry per drawn dataset – i.e. the FINAL count after any cluster/hue/n_clusters regrouping) for per-dataset control (GH #127): e.g. chemtrails=[True, False] turns chemtrails on for dataset 0 only. A bare bool is broadcast to every dataset. Raises ValueError if a list’s length does not match the number of drawn datasets (naming both counts). Trail styles (chemtrails/precog/bullettime) only apply when animate=True/’parallel’ – see the note under bullettime below.
- precog (animation only)bool or list of bool
A low-opacity trail is plotted ahead of the trajectory (default: False). Accepts a per-dataset list exactly like chemtrails above, and the two may be mixed per dataset (e.g. dataset 0 chemtrails, dataset 1 precog, dataset 2 bullettime).
- bullettime (animation only)bool or list of bool
A low-opacity trail is plotted ahead and behind the trajectory (default: False). Accepts a per-dataset list exactly like chemtrails above. For any single dataset, bullettime=True (or chemtrails=True AND precog=True together) shows the FULL trail; chemtrails alone shows only the past window; precog alone shows only the future window; none of the three shows just the moving window (no separate trail artist/trace at all for that dataset). GH #127: trail styles apply ONLY to animate=True/’parallel’. ‘spin’ has no “current position” for a trail to lead/follow (only the camera moves), and ‘serial’’s point-by-point reveal already communicates elapsed time, so animate=’spin’/’serial’ ignore chemtrails/precog/bullettime entirely (no trail artist/trace is created) and emit a UserWarning naming the mode, the ignored flag(s), and which dataset indices had them set.
- frame_rate (animation only)int or float
Frame rate for animation in frames per second (default: 30). Both backends generate exactly frame_rate * duration frames, so matplotlib and plotly animations play at identical speed, duration, and framerate. Has no effect on static plots (static line smoothing uses a fixed density, independent of the animation kwargs).
- focused (animation only)float or None
Round17 #8 (GH #275): the length, in SECONDS – the SAME unit as tail_duration – of the “in-focus” (fully-opaque) window: the portion of a trajectory drawn opaque by default under chemtrails/ precog/bullettime, or the sliding window size for the new animate=’window’ (see animate above). Default None: resolves to tail_duration’s own value – today’s hardcoded/tail_duration- derived focus length – so omitting focused= never changes existing behavior; pass an explicit focused= to decouple the in-focus window’s length from tail_duration (e.g. a wide chemtrails fade with a narrow opaque head, or vice versa). Silently ignored (no error, no warning – this is the documented, expected no-op case) for animate=’spin’/’parallel’ (or True) with NO chemtrails/precog/bullettime flag set on any dataset, and for animate=’morph’ – none of these has a separate “in-focus window distinct from the whole trajectory” concept for focused to control. Must be a non-negative number if given; raises ValueError otherwise.
- morph_samples (``animate=’morph’`` only)int or None
An OPTIONAL cap on morphing-dataset size, applied BEFORE the duplicate-padding described under animate above: any morphing dataset larger than morph_samples is first downsampled (without replacement, seeded) to exactly morph_samples points. Default None: no cap – every dataset keeps its full point count, and the target count is simply the largest dataset’s own size. Since the Hungarian assignment’s cost is roughly
O(n^3)in the (post-cap) target point count, morph_samples is RECOMMENDED for clouds larger than ~2000 points (e.g. morph_samples=1000) – the uncapped default can be slow, or memory-heavy, for very large datasets. Must be a positive integer (or None); anything else raisesValueError. Ignored for every other animate mode.- interactivebool
If True, display the plot using an interactive matplotlib backend. Useful for inspecting and manipulating static plots. If animate=True, an interactive backend is required and this argument has no effect (default: False).
- explorebool
If True, hovering over a data point displays that point’s user-defined label (from labels=); if no labels were passed, the point’s index and coordinates are shown instead. Explore mode is currently only supported for 3D static plots (
ValueErrorotherwise), and is an experimental feature (i.e. it may not yet work properly). Hover labels require an interactive matplotlib backend: under a non-interactive backend (e.g. Agg in scripts, CI, or the docs build) the figure is drawn as a static plot and aUserWarningexplains that hover labels are unavailable.- mpl_backendstr
The matplotlib backend used to create interactive and animated plots. May be ‘auto’ (default), ‘disable’, or a backend key accepted by matplotlib. If ‘auto’, hypertools will use a backend determined automatically based on your environment (from hypertools.plot.backend import HYPERTOOLS_BACKEND). If ‘disable’, experimental backend-switching is disabled and the current global matplotlib backend (matplotlib.get_backend()) is used. Otherwise, try to use the backend specified. NOTES: This feature is experimental. For a list of interactive matplotlib backends, see matplotlib.rcsetup.interactive_bk. For a list of backends available in IPython, run %matplotlib –list. Set the $HYPERTOOLS_BACKEND environment variable or use hypertools.set_interactive_backend() to override the backend used by ‘auto’ in non-IPython environments. If animate=False and interactive=False, this argument has no effect. Within the hypertools.set_interactive_backend(backend) context manager, the value of backend is prioritized over this argument.
- showbool
If set to False, the figure will not be displayed, but it is still returned (and remains valid/savable; see Returns). With show=False, hypertools also closes/deregisters its pyplot figure once drawing (and any save_path= export) is done – including animated figures on non-GUI backends – so batch-export loops never accumulate open figures. Note that show=True displays the figure in notebook/ IPython contexts (and the plotly backend calls its own renderer in scripts); in a plain non-interactive Python script the matplotlib backend registers the figure with pyplot but does not itself call
plt.show()– callplt.show()yourself to open a window. Default: True.- transformlist of numpy arrays or None
The transformed data, bypasses transformations if this is set (default : None).
- vectorizerstr, dict, class or class instance
The vectorizer to use. Built-in options are ‘CountVectorizer’ or ‘TfidfVectorizer’. To change default parameters, set to a dictionary e.g. {‘model’ : ‘CountVectorizer’, ‘kwargs’ : {‘max_features’ : 10}} (the legacy {‘model’, ‘params’} form is also still accepted). See https://scikit-learn.org/stable/api/sklearn.feature_extraction.html for details. You can also specify your own vectorizer model as a class, or class instance. With either option, the class must have a fit_transform method (see https://scikit-learn.org/stable/data_transforms.html). To set parameters, use the dict form (or a configured class instance); a bare class is instantiated with its defaults.
- semanticstr, dict, class or class instance
Text model to use to transform text data. Built-in options are ‘LatentDirichletAllocation’ or ‘NMF’ (default: LDA). To change default parameters, set to a dictionary e.g. {‘model’ : ‘NMF’, ‘kwargs’ : {‘n_components’ : 10}} (the legacy {‘model’, ‘params’} form is also still accepted). See https://scikit-learn.org/stable/api/sklearn.decomposition.html for details on the two model options. You can also specify your own text model as a class, or class instance. With either option, the class must have a fit_transform method (see https://scikit-learn.org/stable/data_transforms.html). To set parameters, use the dict form (or a configured class instance); a bare class is instantiated with its defaults.
- corpuslist (or list of lists) of text samples or ‘wiki’, ‘nips’, ‘sotus’.
Text to use to fit the semantic model (optional). If set to ‘wiki’, ‘nips’ or ‘sotus’ and the default semantic and vectorizer models are used, a pretrained model will be loaded which can save a lot of time.
- axmatplotlib.Axes
Axis handle to plot the figure
- frame_kwargsdict
Keyword arguments for styling the frame drawn around the plot. For 3D plots, the frame is a cube and frame_kwargs are forwarded to mpl_toolkits.mplot3d.axes3d.Axes3D.plot_wireframe. For 2D plots, the frame is a square and frame_kwargs are forwarded to matplotlib.patches.Rectangle.
- stream_initint
Streaming data only (iterators/generators and Hugging Face
datasets.IterableDatasetare detected automatically): number of initial samples used to estimate the normalization and reduction parameters (default: 10000). Those fitted models are then applied to all future samples, which are added to the plot dynamically. Only a subset of plot’s parameters applies to streaming inputs: fmt, the four stream_* parameters, ndims, reduce, normalize, align/cluster/n_clusters (rejected with aValueError– not yet supported for streams – but accepted at their defaults), save_path, show, frame_rate, markersize, linewidth, color, palette, title, size, elev, azim, and ax. Any other parameter explicitly set alongside a streaming input is ignored, with aUserWarningnaming it. In particular, streaming plots are always drawn with the matplotlib backend: a backend= request (e.g.backend='plotly') is ignored with that warning, and the return value is a matplotlibFigureeven when the plotly backend was requested.- stream_chunkint
Streaming data only: number of new samples fetched from the stream per update (default: 100). Each fetched chunk is projected through the fitted models and rendered as one animation frame / live redraw, so this sets both the download batch size and the temporal resolution of the resulting animation.
- stream_maxint or None
Streaming data only: stop streaming after this many samples. Exactly stream_max samples are consumed from the stream (never more), and the returned figure’s
stream_info['truncated']is then True – it means streaming was stopped (by stream_max, an interrupt, or an error) before the stream was observed to end. Default None streams continually until the stream is exhausted or the user interrupts (Ctrl-C); infinite streams render incoming data indefinitely, and any animation being saved via save_path is finalized whenever streaming stops (including on interrupt). For streams, save_path supports .gif/.png/.apng (Pillow) and, with FFmpeg installed, .mp4/.mov/.avi/.m4v/.mkv; other extensions raiseValueErrorbefore any samples are consumed.- stream_windowint or None
Streaming data only: if set, only the most recent stream_window samples are displayed (comet style) while older samples scroll off; all consumed samples are still retained on the returned figure’s
stream_infodict (its'data'/'xform_data'entries). Default None displays the full accumulated trajectory.- surfacebool, dict, or list of bool/dict, or None
If set, overlays a smooth, lit surface over each dataset’s convex hull (GH #109): a filled smooth outline for 2D data, or a shaded 3D “blob” (inflated, subdivided, and Taubin-smoothed hull – see hypertools.plot.meshutil.smooth_hull_3d) for 3D data. Pass
Truefor the defaults below, a dict to override specific keys (unset keys use their default), or a list of bool/dict (one per drawn dataset, matching the final – post cluster/hue regrouping – dataset count) for per-dataset control; a bareFalse/Noneentry in the list disables that dataset’s surface. RaisesValueErrorfor 1D data (no hull concept), for an unrecognized dict key or an out-of-range dict value (see the per-key constraints below), or if a list’s length does not match the number of drawn datasets. A dataset with too few points to form a hull (< 3 for 2D, < 4 for 3D) or whose points are exactly collinear/coplanar has its surface silently skipped with aUserWarning(never a crash). Default None (no surfaces).Accepted dict keys, with defaults:
alpha(float, default 0.6): surface opacity; must be in (0, 1]. A translucent (< 1.0) surface shows the enclosed data points through the hull on BOTH backends. Note that a translucent 3D matplotlib surface REQUIRES the built-in backface culling (always applied) to avoid interior-face “cracks” showing through; plotly renders a translucent surface as a genuinely translucentMesh3d(its doubled-winding mesh gets per-layer opacity1 - sqrt(1 - alpha), compositing to exactlyalphatotal), which keeps the full mesh but may show per-triangle depth-sorting noise (a known WebGL/plotly limitation – plotly.py issue #3554 – not a hypertools bug) – preferalpha=1.0if this is objectionable: atalpha >= 0.999the plotly mesh instead renders through an artifact-free fully-opaque path (the alpha is baked into the surface color), and data points enclosed by their own opaque surface are hidden from that dataset’s trace (they would be invisible behind it anyway, and hiding them avoids a WebGL “punch-through” defect).color(color spec or None, default None): surface base color.Noneinherits the dataset’s own drawn line/marker color (resolved from color/colors if given, else the palette color cycle).lighting(dict, default{}): overrides the two-light Blinn-Phong lighting model BOTH backends use identically (see hypertools.plot.meshutil.blinn_phong_colors/ blinn_phong_vertex_colors) – matplotlib shades per-FACE; plotly shades per-VERTEX (precomputed and handed togo.Mesh3dasvertexcolor, with plotly’s own lighting engine forced to the identity so it reproduces those colors verbatim – needed so the double-sided winding workaround below doesn’t render dark self-shaded patches) – so every key below visibly affects both backends the same way. Accepted keys:ambient(float, default 0.45): flat, direction-independent base brightness; higher values flatten/wash out shading (matte look), 0 makes unlit faces fully black.diffuse(float, default 0.55): key-light (Lambertian) contribution; scales how strongly faces facing the key light brighten relative to those facing away.fill(float, default 0.25): weaker opposite-side fill-light contribution, so faces angled away from the key light are not rendered fully flat/black.specular(float, default 0.30): strength of the glossy highlight; 0 gives a fully matte surface, higher values (e.g. 0.9) give a glossy/wet look.shininess(float, default 48): specular exponent – higher values (e.g. 128) tighten the highlight into a small glossy spot; lower values spread it into a broad sheen.lightdir(3-vector(x, y, z)or None, default None): explicit key-light direction in scene/data coordinates (need not be normalized; must not be the zero vector).None(default) derives the key light automatically from the current camera view (offset above and to the side), matching each backend’s own default camera-relative lighting.
plotly’s light position (for its own, identity-forced lighting engine, unrelated to the vertex-color computation above) is fixed at
(2.5, -1.5, 3.0)in scene coordinates. Ignored for 2D surfaces (flat fills have no lighting). Unrecognized keys (e.g. the pre-GH-109-round-3 plotly-onlyroughness/fresnel, which no longer affect either backend’s rendering) raiseValueErrorrather than being silently accepted.smoothing(int, default 3): number of interleaved [subdivide, Taubin-smooth] rounds for a 3D hull (face count scales as4 ** smoothing); must be in [0, 6] (beyond 6 the face count – 4096x the raw hull’s at 6 – is a memory/time footgun with no visible smoothness gain); ignored for 2D.pre_inflate(float, default 1.0): scale factor applied to the 3D hull about its centroid before smoothing (default: no blanket inflation); must be a positive, finite number. Any shrinkage smoothing introduces is instead recovered by a minimal, grow-only post-hoc rescale targeting ~99% containment of the actual input points, so the surface hugs the data rather than ballooning past it. The rescale is mathematically bounded (hard-capped at 3.0x growth): well-sampled clouds typically need at most ~1.25x, and only tiny (4-5 point) hulls – whose coarse meshes lose proportionally far more of their bulge to smoothing – approach the cap (see hypertools.plot.meshutil.smooth_hull_3d). Ignored for 2D.keep_points(bool, default True): if False, hides that dataset’s own line/marker (only the surface is shown). Note that on plotly, points enclosed by their own FULLY-OPAQUE (alpha >= 0.999) surface are hidden even whenkeep_points=True– see thealphaentry above; translucent surfaces always show their points.
Out-of-range values for any key above raise an eager
ValueError(naming the key, the constraint, and the received value) BEFORE the analyze/reduce pipeline runs, exactly like density’s validation.Animated plots (matplotlib and plotly, 3D only – round17 #9, GH #123: 2-D animate is now supported, but per-frame hull tracking is not, so surface= is silently a no-op on an animated 2-D plot, in both backends) recompute each dataset’s hull every frame from its CURRENTLY VISIBLE window: the revealed portion for
animate='serial', the sliding head/tail window foranimate=True/'parallel'(matching the window drawn by chemtrails/tail_duration), or the full, precomputed-once dataset foranimate='spin'(only the camera orbits, so only per-frame shading/backface-culling – not the mesh itself – needs recomputing). Animated surfaces keep the same per-vertex hue coloring static surfaces use (each frame’s hull is colored from its currently-visible points’ own hue colors) on both backends. Surfaces never gain a legend entry (label='_nolegend_'/showlegend=False) in either backend.- densitybool, dict, or None
If set, overlays a subtle KDE (kernel density estimate) “glow” behind the data (GH #108, #191): a 2-D alpha-ramped heatmap, or a 3-D volumetric cloud, showing where each dataset’s points are concentrated. Pass
Truefor the defaults below, or a dict to override specific keys (unset keys use their default). Unlike surface, density has no per-dataset list form and no color override – every density layer always inherits its dataset’s own drawn color (or, withper_group=False, a single neutral-gray layer is drawn for the pooled data). RaisesValueErrorfor 1D data (no 2-D/3-D density concept) or an unrecognized dict key. A dataset with too few points (< 3) or degenerate (singular covariance – e.g. exactly duplicated/collinear/coplanar points) has its density silently skipped with aUserWarning(never a crash). Default None (no density shading).Accepted dict keys, with defaults:
alpha(float, default 0.2): base opacity, kept subtle by design so the density layer never dominates the actual data. matplotlib’s 2-D layer ramps linearly from fully transparent up to exactly this alpha at the KDE’s peak; matplotlib’s 3-D iso-surface/fog alphas and both plotly layers’ opacities scale proportionally with it (see the backend-specific notes below).levels(int, default 3): number of nested 3-D iso-surface shells. Wired into BOTH 3-D backends: matplotlib draws one Poly3DCollection per level, at density-fraction thresholds spaced evenly across[0.10, 0.65]via numpy.linspace (levels=3, the default, reproduces the original hand-tuned thresholds – 10%/35%/65% of peak density, alphas 0.03/0.05/0.07 – EXACTLY, since evenly-spacedlinspace(0.10, 0.65, 3)would instead give a 37.5%-not-35% middle shell); plotly’sgo.Volumelayer usessurface_count=5*levels(15 at the default). 2-D density has no ``levels`` concept at all – the 2-D layer is a single continuous alpha/heatmap ramp with no discrete shells, solevelsis silently ignored for 2-D data (no error; the key is still valid, it’s just a no-op there).grid(int, default None): KDE evaluation grid resolution per axis.Noneauto-resolves to 200 for 2-D data or 50 for 3-D data (a 3-D grid is grid**3 KDE evaluations, so much coarser by default).per_group(bool, default True): fit and draw one density layer per drawn dataset.Falsepools every dataset’s points into a single combined KDE, drawn as one neutral-gray layer instead.
Backend rendering: matplotlib’s 2-D layer is an alpha-ramped
imshow(a LinearSegmentedColormap from transparent to the dataset’s color at alpha, bilinear-interpolated, drawn below the data) – not contourf, whose hard per-level boundaries read as banding rather than a smooth glow. matplotlib’s 3-D layer is nested translucent iso-surfaces via skimage.measure.marching_cubes (levels shells spanning 10%-65% of peak density, alphas ramping 0.03-0.07, both scaled by alpha / 0.2; see the levels entry above for the exact spacing) when scikit-image is installed (pip install hypertools[density3d]); otherwise it falls back to a translucent scatter “fog” (4000 points resampled from the KDE, alpha 0.03 scaled the same way) and emits a UserWarning suggesting the extra orbackend='plotly'(which always renders a full volumetric go.Volume, no extra required). plotly’s 2-D layer is a go.Contour heatmap (coloring=’heatmap’, no contour lines, an alpha-ramped colorscale to 1.5 * alpha – note this peak alpha is deliberately 1.5x the mpl 2-D layer’s alpha, a documented cross-backend visibility difference, not a bug: plotly’s heatmap reads fainter than mpl’s imshow at the same alpha value, so the ramp is boosted to compensate); its 3-D layer is a go.Volume with, for a scene-filling dataset (boost=1), isomin=0.05, isomax=1.0, surface_count=5*levels, opacity=min(2 * alpha, 0.4), and an opacityscale ramp tuned so the volume stays visible at plotly’s 3-D scene scale, over a solid per-dataset colorscale. For a dataset SMALL relative to the scene (e.g. widely-separated clusters), the auto-boost shifts all of these together – opacity and surface_count scale up (opacity capped at 0.75), isomin drops (down to 0.01), and the opacityscale breakpoints and the KDE grid’s padding widen to expose more of the KDE’s outer tail – see hypertools.plot.density.resolve_plotly_volume_params for the exact formulas. Density layers never gain a legend entry in either backend.3-D static-export caveat (both backends): when per_group=True (the default) draws more than one dataset’s translucent 3-D density layer, the overlapping surfaces/volumes can composite unevenly in STATIC exports (PNG/SVG via matplotlib’s Agg renderer or plotly’s kaleido-based
write_image/to_image) – a WebGL/rasterizer alpha-blending-order limitation, not a data or fitting bug. The interactive view (a live matplotlib window or plotly’s browser/ notebook widget) renders correctly; only static snapshots of multi-dataset 3-D density can look off.Animated plots (both backends, any animate style): the density is computed ONCE from the FULL dataset and drawn as a static background – a single KDE evaluation is far too slow (~500ms at a 50^3 grid) to redo every animation frame, so, unlike surface, the density layer does not track the currently-visible window and is never touched by per-frame updates.
- return_modelbool
If True, return a dict bundle
{'fig': ..., 'xform_data': ..., 'animation': ..., 'pipeline': ..., 'models': ..., 'predict': ...}instead of the bare figure, wherexform_datais the normalized/reduced/aligned data,animationis thematplotlib.animation.Animationhandle (Noneunlessanimate=Truewith the matplotlib backend),pipelineis a fitted hypertools.Pipeline covering whichever of manip=/ normalize=/reduce=/align=/cluster= ran (the SAME pipeline= object passed in, if any; None when transform= was used, since then there is no raw data to have fit one on) – pass it back in as hyp.plot(new_data, pipeline=bundle[‘pipeline’]) to reuse these exact fitted parameters (GH #227),modelsholds the reduce/align/cluster/impute specs, andpredictisNoneunless predict was set, in which case it is{'model': ..., 'params': {'t': t}, 'forecasts': [...]}(one forecast array per input dataset, in the analyzed/plotted – pre-center/scale – space). Each bundled forecast has exactly t rows, matching whathyp.predict(xform_data, model=..., t=t)returns; the DRAWN dashed overlay additionally prepends the last observed row as a connector, so the drawn trace has t + 1 vertices. Default False.
- Returns:
- figmatplotlib.figure.Figure or plotly Figure
The rendered figure. Static plot coordinates are drawn in the centered/rescaled
[-1, 1]display space described under x above. For animated matplotlib plots aHyperAnimationis returned instead: a(fig, animation)tuple subclass (sofig, anim = hyp.plot(...)unpacking works) that also exposes.figure/.to_html5_video()/.to_jshtml()/.save()and auto-plays inline in notebooks – keep a reference to it so the underlyingmatplotlib.animation.FuncAnimationstays alive. Whenreturn_model=True, a dict{'fig': ..., 'xform_data': ..., 'animation': ..., 'pipeline': ..., 'models': ..., 'predict': ...}is returned (animationincluded so the handle isn’t dropped for animated plots;pipelineis the fitted hypertools.Pipeline covering the stages that ran, reusable viahyp.plot(new_data, pipeline=...)).
Examples
Plot a single high-dimensional dataset as a static 3-D trajectory (the data is reduced to 3 dimensions with the default reducer):
>>> import numpy as np >>> import hypertools as hyp >>> x = np.cumsum(np.random.default_rng(0).standard_normal((50, 8)), ... axis=0) >>> fig = hyp.plot(x, show=False) >>> fig.axes[0].name '3d'
Plot two datasets as labeled point clouds (one legend entry each):
>>> fig = hyp.plot([x, x + 10], '.', names=['a', 'b'], show=False) >>> [t.get_text() for t in fig.axes[0].get_legend().get_texts()] ['a', 'b']
Color a trajectory continuously by time, in a 2-D projection:
>>> fig = hyp.plot(x, ndims=2, hue=np.arange(50), show=False) >>> fig.axes[0].name 'rectilinear'