CoordinateSystem¶
The CoordinateSystem helper builds a complete plotting coordinate system —
grid, axes (with value labels), an optional background plane, and plotted
point paths — inside a single VizGroup. It is not a scene object itself;
it owns the group and the VizObjectRefs of the objects it creates, and
updates them in place when the axis ranges change.
It supports logarithmic scales for any base while keeping the underlying world coordinates linear: tick positions and labels are computed in Python and the data → world conversion is applied before plotting.
For runnable examples showing the effect of several parameter combinations, see coordinate-system.ipynb.
Quick Start¶
from pytanga.viz import Visualizer, CoordinateSystem, PointPathStyle
viz = Visualizer(add_default_axes=False, add_default_grid=False)
cs = CoordinateSystem(viz, xlim=(0.1, 1000), ylim=(1, 1e6),
xscale="log", yscale="log")
xs = [0.1 * (10 ** (0.1 * i)) for i in range(40)]
cs.plot(xs, [x * x for x in xs], color="#ffcc00",
style=PointPathStyle(line_thickness=3))
viz.show()
viz.wait()
CoordinateSystem accepts a Visualizer (main scene) or a VizSceneHandle
(named scene), e.g. CoordinateSystem(viz.scene("plots"), ...).
Data coordinate system¶
| Parameter | Type | Description |
|---|---|---|
xlim / ylim |
(lo, hi) |
Data range per axis. None auto-derives from a configured 2D camera rect, or defaults to (-5, 5) ((0.1, 100) for log). |
xscale / yscale |
"linear" | "log" | Scale |
Axis scale. |
size |
(size_x, size_y) |
External world extent of the plot (plane width/height). None (or a None element) derives that axis from the data range. |
align |
(ax, ay) |
Fractional point of the plot plane that coincides with position: (0, 0) = bottom-left corner, (1, 1) = top-right. Default (0.5, 0.5) (centre). |
axis_origin |
(x, y) |
Data point where the two axes cross. None (or a None element) uses that axis' min edge (the spine layout). |
min_x_span |
float |
Minimum x-range span used when auto-fitting the x axis from registered plots (default 5.0). |
x_intervals / y_intervals |
Sequence[float] |
Allowed tick step values (absolute data units, ascending). None auto-generates 1/2/5 × 10^k steps over the data range. Linear scales only. |
min_tick_spacing_px |
float |
Minimum pixel spacing between adjacent ticks (overlay mode only); the tick count is derived from the live viewport. Default 60.0. |
pan_xlim / pan_ylim |
(lo, hi) |
Pan bounds in data units; the view centre stays inside this rectangle. Defaults to xlim / ylim. |
min_zoom / max_zoom |
float |
Interactive zoom range. min_zoom is the max zoom-out; None (default) derives it so the full data rectangle stays contained (for fill_x/fill_y this lets you zoom out until the overflowing axis is visible). max_zoom defaults to the zoom where the finest allowed interval fills the view. |
base |
float |
Log base when a scale is given as "log" (default 10). |
value_format |
str |
Python format specifier for tick labels (default ".4g"). |
labels |
(str, str) |
Axis name labels (default ("x", "y")). |
grid |
bool |
Whether to draw the grid (default True). |
axes |
bool |
Whether to draw the axes with tick/value labels (default True). |
display_mode |
"world" | "overlay" |
"world" (default) draws axes/grid as world-space scene objects that pan/zoom with the data; "overlay" (2D only, no size) draws the axes as a fixed screen-space frame and the grid as a screen-space underlay behind the data, with tick ranges tracked from the live camera. |
plane |
bool \| None |
Whether to draw the background plane. None auto-enables in 3D and disables in 2D (see 3D: background plane placement). |
camera |
"auto" \| bool |
2D framing-camera control, only when size is not given (see 2D: auto span & camera). |
stretch |
"fit" | "fill" | "fill_x" | "fill_y" |
How the 2D camera frames the plot plane: "fit" (letterbox, default), "fill" (stretch both axes), "fill_x" (x fills, y keeps aspect), or "fill_y". Only affects 2D when this coordinate system owns the camera; forced to "fill" in display_mode="overlay". |
border_px |
float |
2D camera pixel margin so axis labels stay visible (default 60). |
border_world |
float |
Additional world-unit margin for the 2D camera (default 0). |
group_name |
str |
Group name used when creating the coordinate-system group (default "coordsys"). |
Logarithmic scales¶
cs = CoordinateSystem(viz, xlim=(0.1, 100), ylim=(1, 10000),
xscale="log", yscale="log") # base 10
cs = CoordinateSystem(viz, xlim=(1, 64), ylim=(1, 64),
xscale="log", yscale="log", base=2) # base 2
Log axes must have a strictly positive range. Tick labels are integer powers of
the base (e.g. 0.1, 1, 10, 100). The world coordinate of a value v is
log(v, base); everything is converted automatically by plot() /
transform().
The underlying scale classes are available for direct use:
External vs internal dimensions¶
size sets the physical extent of the plot in world/embedding units,
independently of the data range. The data range (xlim/ylim) is stretched
onto that size, so a plane can be e.g. 2×1 world units while the data ranges
from 0 to 4π:
import math
cs = CoordinateSystem(viz, xlim=(0, 4 * math.pi), ylim=(-4, 3),
size=(2.0, 1.0),
position=(1, 2, 3), normal=(0, 1, 0), up=(0, 0, 1))
With size=None (the default) each axis keeps its data-derived extent (the
current behaviour). size affects only the plot geometry — never the camera.
align controls where the plane sits relative to position: with
align=(0, 0) the bottom-left corner of the plane is at position, with
align=(1, 1) the top-right corner is there, and with the default
align=(0.5, 0.5) the plane is centred on position.
2D: auto span & camera¶
In 2D (Visualizer(space_dim=2) or a 2D camera), CoordinateSystem spans the
camera view by default and, when no camera is configured, computes and sets a
default View2DConfig with a pixel border so the axis labels stay visible:
viz = Visualizer(space_dim=2, add_default_axes=False, add_default_grid=False)
cs = CoordinateSystem(viz, xlim=(0.1, 100), ylim=(0.1, 100),
xscale="log", yscale="log")
# → sets a centered View2DConfig with border_px=60
Control this with:
| Parameter | Default | Effect |
|---|---|---|
camera |
"auto" |
"auto" sets a framing camera only if none is configured; True always sets/updates it; False never touches it. |
border_px |
60.0 |
Pixel margin on all sides (applied by the frontend). |
border_world |
0.0 |
Additional world-unit margin. |
Overlay mode¶
display_mode="overlay" (2D only, no explicit size) renders the coordinate
axes as a fixed screen-space frame at the image borders and the grid as a
screen-space underlay behind the data. The frame stays put while the data
pans/zooms underneath; only its tick values and grid lines update to the
current visible range. This works in the live viewer and in standalone HTML
export — see the axes overlay example.
Pan, zoom & tick subdivision¶
The interactive 2D view (overlay and world modes) is bounded by the data range unless you override it:
- Pan is limited so the view centre stays within
pan_xlim/pan_ylim(defaulting toxlim/ylim) — the data edge can reach the centre of the view, never cross it. - Zoom is limited to
[min_zoom, max_zoom].min_zoomdefaults to the zoom where the full data rectangle just fits the view (never below1.0, sofill_x/fill_ycan zoom out to reveal an overflowing axis);max_zoomdefaults to the zoom where the finest allowed tick interval fills the view. - Ticks use the allowed
x_intervals/y_intervalsstep values (absolute data units), chosen so adjacent ticks never fall closer thanmin_tick_spacing_pxpixels (overlay mode) — so resizing or zooming re-densifies the grid. World mode keeps the sameintervalsbut a fixed tick count, since its labels are generated on the backend without a viewport.
cs = CoordinateSystem(
viz, display_mode="overlay",
xlim=(0, 1000), ylim=(0, 0.1), # independent data ranges
x_intervals=[50, 100, 200, 500], # allowed x steps
y_intervals=[0.01, 0.02, 0.05], # allowed y steps
min_tick_spacing_px=60.0, # overlay tick density
pan_xlim=(-200, 1200), pan_ylim=(-0.05, 0.15), # pan bounds
min_zoom=0.5, max_zoom=40.0, # zoom range
)
The default AxisStyles place the x value labels below the axis (with the
name label further down) and the y value labels to the left (right-aligned,
with a 90°-rotated name label). Override via x_style/y_style.
If you already set a View2DConfig on the visualizer, xlim=None/ylim=None
reuse its visible rectangle.
Embedding the fitted camera (fit_view2d)¶
The camera CoordinateSystem would auto-fit can also be computed standalone via
fit_view2d(xlim, ylim, ...). This is useful in a split-view app where you want
the exact per-pane camera at layout-construction time (before
Visualizer.show), and the matching CoordinateSystem is created later with
camera=False:
from pytanga.viz import CoordinateSystem, SceneView, SplitView, fit_view2d
layout = SplitView(
"horizontal",
[
SceneView("sin", camera=fit_view2d((0, 6.28), (-1.2, 1.2))),
# ... more panes ...
],
)
# later (e.g. in a VisualizerApp.init()):
cs = CoordinateSystem(
viz.scene("sin"), xlim=(0, 6.28), ylim=(-1.2, 1.2), camera=False
)
fit_view2d returns a centred View2DConfig whose world rectangle is the
scale-mapped span of xlim/ylim, mirroring the auto-fit camera exactly (so
the embedded pane camera and the CoordinateSystem never disagree).
Manual 2D placement¶
If you pass size, the 2D plot stops auto-configuring the camera and instead
places the plot in the 2D world using position, align, and up (the
in-plane vertical direction, default (0, 1, 0)). This lets you draw several
plots side by side, or next to a geometric animation:
cs = CoordinateSystem(viz, xlim=(0, 10), ylim=(-1, 1), size=(2.0, 1.0),
position=(1, 0, 0), up=(0, 1, 0))
3D: background plane placement¶
In 3D the whole system (background plane + grid + axes + plotted paths) lives
in one group, placed/oriented with position, normal, and up:
import math
cs = CoordinateSystem(viz,
xlim=(0, 4 * math.pi), ylim=(-1.5, 1.5),
position=(0, 1, 0), normal=(0, 1, 0.4), up=(0, 0, 1))
cs.plot(xs, [math.sin(x) for x in xs], color="#44ff44")
| Parameter | Default | Description |
|---|---|---|
position |
(0, 0, 0) |
World point the plot plane sits at (combined with align). |
normal |
(0, 0, 1) |
Plot-plane normal (the group's local +z). |
up |
(0, 1, 0) |
In-plane vertical direction. |
plane |
None |
Whether to draw the background plane. None auto-enables in 3D and disables in 2D. |
The plane is sized to the coordinate system's world span (derived from
xlim/ylim + scales, or the explicit size). A 3D coordinate system never
sets the camera — place and aim it yourself; the plot is meant to sit inside a
larger 3D scene. axis_origin=(x, y) moves where the axes cross (in data
coordinates); the default keeps the spine layout (x-axis along the bottom,
y-axis along the left).
Plotting & transformation¶
# Map a data point to its centred in-plane coordinate.
lx, ly = cs.to_local(10.0, 100.0)
# Map a data point to its embedded 3D world position (applies the group transform).
x, y, z = cs.to_world(10.0, 100.0)
# Map data series to group-local 3D points (inherits the group transform).
pts = cs.transform(xs, ys)
# Plot a series as a PointPath child of the data group (data coordinates).
ref = cs.plot(xs, ys, color="#ffcc00", style=PointPathStyle(line_thickness=3))
Annotations & the data group¶
Every data-space object (plot, add_plot, and the annotation helpers below)
is a child of an inner data group whose transform maps data coordinates onto
the plot plane. For linear axes the group is a pure translate+scale, so you can
draw directly in data coordinates; for log axes the group handles only the
affine part, so the log is still applied in Python (to_data()).
The inner group is exposed as cs.data_group (a VizObjectRef) for custom
drawings:
# Map a data point to data-group coordinates (log-mapped for log axes).
wx, wy = cs.to_data(10.0, 100.0)
# Draw a custom annotation in data coordinates.
path = PointPath()
path.add((1.0, 0.0))
path.add((3.0, 2.0))
cs.data_group.new(path, color="#ffffff")
vline / hline¶
Draw (and animate) vertical/horizontal marker lines at fixed data values:
# Create (or update, by name) a vertical line at x=3 spanning the current ylim,
# with a label anchored at the line midpoint.
v = cs.vline(x=3.0, name="cursor", color="#ff5555", label="x = 3")
# Create (or update) a horizontal line at y=0 spanning the current xlim.
h = cs.hline(y=0.0, name="zero", color="#8888ff", label="zero")
# Move the vertical line each frame (animation):
cs.vline(x=t, name="cursor")
# Remove a named line:
cs.remove_vline("cursor")
cs.remove_hline("zero")
vline(x, *, name=None, y0=None, y1=None, color=None, style=None, label=None, label_style=None)andhline(y, *, name=None, x0=None, x1=None, color=None, style=None, label=None, label_style=None)create a line (or update it in place whennameis given) and return itsVizObjectRef.y0/y1(resp.x0/x1) default to the currentylim(resp.xlim).- These draw a
Lineentity, so style them withLineStyle(screen-spacethicknessin px).label/label_styleattach a label anchored at the line midpoint; useLabelStyle(along=…)to move it along the segment. - Without
name, each call creates a new line. remove_vline(name)/remove_hline(name)remove a named line.
line¶
Draw a line between two arbitrary data points, each given as an (x, y)
2-tuple or a Point() instance:
from pytanga.geometry.entities import Point
cs.line((1.0, 0.0), (3.0, 2.0), color="#ffffff")
cs.line(Point(1.0, 0.0), Point(3.0, 2.0), name="seg", color="#ff88ff")
cs.line(Point(4.0, -1.0), Point(6.0, 1.0), name="seg") # update in place
cs.remove_line("seg")
line(start, end, *, name=None, color=None, style=None, label=None, label_style=None)creates aLinesegment (or updates it in place whennameis given) betweenstartandend, and returns itsVizObjectRef. Style it withLineStyle;label/label_styleattach a label anchored at the midpoint.remove_line(name)removes a named line.
point¶
Draw a point marker at a data location, given as an (x, y) 2-tuple or a
Point() instance:
from pytanga.geometry.entities import Point
from pytanga.viz import PointStyle
cs.point((2.0, 0.5), color="#ffffff")
cs.point(Point(3.0, 1.0), name="marker", color="#ff8888", style=PointStyle(size=0.1))
cs.point(Point(4.0, -0.5), name="marker") # update in place
cs.remove_point("marker")
point(p, *, name=None, color=None, style=None, label=None, label_style=None)creates a point marker (or updates it in place whennameis given) and returns itsVizObjectRef.label/label_styleattach a label anchored at the point.remove_point(name)removes a named point marker.- The marker is added to the outer group at its local position (not the data
group), so its
sizeis not stretched by the data group's non-uniform scale.
Note:
data_groupapplies a non-uniform scale (it stretches data onto the plot'ssize), so it is ideal for paths/lines. Thepoint()helper places shaded markers in the outer group (undistorted); for other shaded entities place them withto_world()instead.
See py/examples/viz/plotting/cs_annotations.py for a full example.
Live plots (trails)¶
For live data (e.g. a trail that grows every frame), register a PointPath
(in data coordinates) with add_plot and re-sync it each frame with
update_plots:
trail = PointPath(max_points=600)
cs.add_plot(trail, color="#ffcc00", style=PointPathStyle(line_thickness=2),
auto_x=True)
# each frame:
trail.add((t, value))
cs.update_plots()
viz.flush()
add_plot(path, *, color=None, style=None, auto_x=False)registers the path and adds it to the group; the path's points are mapped onto the plot plane automatically.update_plots()re-syncs every registered path and, forauto_xpaths, fits the x axis to their current x range with a minimum span ofmin_x_span(default5.0) — useful for a live time axis.position,normal, andupaccept tuples orPoint()/Direction()objects.
See py/examples/viz/plotting/pendulum_plot.py for a full pendulum example.
Updating in place¶
Changing a range rebuilds the children in place (same object IDs), so the scene updates without re-adding objects:
cs.xlim = (1, 1000) # rescale the x axis (grid + axes + labels update)
cs.yscale = "log" # switch the y axis to log
cs.base = 2 # change the log base of both log axes
cs.size = (4, 2) # change the external world/plane extent
cs.align = (0, 0) # move the plane so its bottom-left corner is at `position`
cs.axis_origin = (0, 0) # make the axes cross at the data origin
cs.position = (1, 2, 3) # move the 3D plane
cs.normal = (0, 0, 1) # re-orient the 3D plane
Styles are set at construction via x_style/y_style (AxisStyle),
grid_style (GridStyle), and plane_style (PlaneStyle). The group itself
is exposed as cs.group for further manipulation.