Source code for glyphx.scatter3d

"""
GlyphX Scatter3DSeries — 3D scatter plot.

Renders interactively via Three.js (HTML output) and as a static
orthographic SVG.  Supports a fourth variable encoded as color.
"""
from __future__ import annotations

import json
import math

import numpy as np

from .projection3d import Camera3D, normalize, _format_3d_tick
from .colormaps     import apply_colormap
from .utils         import svg_escape


[docs] class Scatter3DSeries: """ 3D scatter plot. Args: x, y, z: Data coordinates (same length). color: Flat hex fill color when ``c`` is not used. c: Per-point numeric values for colormap encoding. cmap: Colormap name (default ``"viridis"``). size: Marker size in pixels / Three.js units. label: Legend / tooltip label. alpha: Point opacity 0–1. """ def __init__( self, x, y, z, color: str = "#2563eb", c: list | None = None, cmap: str = "viridis", size: float = 5.0, label: str | None = None, alpha: float = 0.85, threshold: int | None = None, ) -> None: self.x = list(x) self.y = list(y) self.z = list(z) self.color = color self.c = c self.cmap = cmap self.size = float(size) self.label = label self.alpha = float(alpha) self.threshold = threshold self.last_downsample_info = None self.css_class = f"series3d-{id(self) % 100000}" # Pre-compute per-point colors if c is not None: c_arr = np.asarray(c, dtype=float) lo, hi = c_arr.min(), c_arr.max() span = hi - lo or 1.0 self._point_colors = [ apply_colormap(float((v - lo) / span), cmap) for v in c_arr ] else: self._point_colors = [color] * len(self.x)
[docs] def to_svg(self, cam: Camera3D, x_range: tuple, y_range: tuple, z_range: tuple) -> str: """Render as SVG circles using the given camera projection.""" from .projection3d import normalize as _norm from .downsample import voxel_thin_3d, AUTO_THRESHOLD, _ds_comment x_plot, y_plot, z_plot = self.x, self.y, self.z point_colors = self._point_colors _ds_svg = "" _thresh = self.threshold if self.threshold is not None else AUTO_THRESHOLD if len(x_plot) > _thresh: _orig_n = len(x_plot) x_plot, y_plot, z_plot, point_colors = voxel_thin_3d( x_plot, y_plot, z_plot, colors=point_colors, max_points=_thresh ) _ds_svg = _ds_comment(_orig_n, len(x_plot), "voxel-3D") self.last_downsample_info = { "algorithm": "voxel-3D", "original_n": _orig_n, "thinned_n": len(x_plot) } else: self.last_downsample_info = None xn, xlo, xhi = _norm(x_plot) yn, ylo, yhi = _norm(y_plot) zn, zlo, zhi = _norm(z_plot) pts = [cam.project(x, y, z) for x, y, z in zip(xn, yn, zn)] # Sort back-to-front (painter's algorithm) order = sorted(range(len(pts)), key=lambda i: pts[i].depth) elements: list[str] = [_ds_svg] if _ds_svg else [] x_plot_list = list(x_plot) y_plot_list = list(y_plot) z_plot_list = list(z_plot) for i in order: p = pts[i] col = point_colors[i] x_raw = x_plot_list[i] y_raw = y_plot_list[i] z_raw = z_plot_list[i] tip = f"({_format_3d_tick(x_raw)}, {_format_3d_tick(y_raw)}, {_format_3d_tick(z_raw)})" if self.label: tip = f"{self.label}: {tip}" elements.append( f'<circle cx="{p.px:.1f}" cy="{p.py:.1f}" r="{self.size}" ' f'fill="{col}" fill-opacity="{self.alpha}" ' f'stroke="#fff" stroke-width="0.4" ' f'data-label="{svg_escape(tip)}"/>' ) return "\n".join(elements)
[docs] def to_threejs_data(self) -> dict: """Serialise series data for the Three.js HTML renderer.""" return { "type": "scatter", "x": self.x, "y": self.y, "z": self.z, "colors": self._point_colors, "size": self.size, "alpha": self.alpha, "label": self.label or "", }