Guidance for AI coding agents (and humans) working on compas_plotter.
This is the canonical onboarding doc; CLAUDE.md just imports it.
compas_plotter is the COMPAS 2.x revival of the 2D matplotlib visualisation
package that shipped inside COMPAS up to 1.17. It draws COMPAS geometry and data
structures onto a matplotlib canvas via the modern compas.scene system, by
registering a "Plotter" visualisation context — the same mechanism the Rhino,
Blender and Viewer backends use.
Status: v0.1.0, pre-release. Core is complete and working; not yet published to PyPI and the GitHub repo may not exist yet.
Plotter(src/compas_plotter/plotter.py) — wraps a matplotlib figure/axes. Public API:add,add_from_list,find,zoom_extents,draw,redraw,show,save,pause,register_listener,on(dynamic/animation + GIF).PlotterScene(compas.scene.Scene)(scene/plotterscene.py) — the container. EveryPlotterowns one (plotter.scene), context hardcoded to"Plotter".Plotter.adddelegates toPlotterScene.add, which injects the owning plotter into each scene object and draws it. This gives a real scene tree (parent/child, composedworldtransformation) and serialization. Pattern copied fromcompas_viewer'sViewerScene.PlotterSceneObject(scene/plotterobject.py) — base for all drawable objects. Subclasses implementdraw()(returns the list of matplotlib artists, stored onself._mpl_objects) andviewdata()(the[x, y]points used byzoom_extents).clear()andredraw()are provided. Also exposes helpersto_rgb()andto_color().- Registration (scene/init.py) —
@plugin(category="factories") register_scene_objects()maps each COMPAS type to its object withregister(T, O, context="Plotter"). It is also called eagerly at import so the package works without entry-point discovery (editable installs). Drawing is managed by thePlotter(eager on add +Plotter.redraw); like the viewer, we do not rely onScene.draw()in normal use.
| Category | Objects |
|---|---|
| Geometry | Point, Vector, Line, Polyline, Polygon, Circle, Ellipse, Frame |
| Shapes (XY projection) | Box (dedicated), and Sphere / Cylinder / Cone / Capsule / Torus / Polyhedron via the shared ShapeObject |
| Data structures | Mesh, Graph (with vertex/edge/face and node/edge labels) |
Not yet supported (roadmap): Brep, Surface, NurbsCurve, VolMesh, Plane.
python -m venv .venv
.venv/bin/pip install -e ".[dev]" # compas, matplotlib, pillow + dev toolsCommon tasks (via invoke, configured in tasks.py):
invoke test # pytest
invoke lint # ruff check
invoke format # ruff format + import sort
invoke docs # build mkdocs siteOr directly:
pytest -q
ruff check src tests && ruff format --check src tests
python -m mkdocs serve # live docs previewEnvironment note: the package needs a working numpy/matplotlib. If
import numpy/matplotlibfails with alibgfortran/openblas dlopen error, your active (conda) environment is broken — create a freshvenvand install there. Tests set the matplotlibAggbackend in conftest.py, so they run headless.
- Python: modern 3.9+ —
from __future__ import annotations, native generics (list[float] | None), no Python-2 compat shims. - Docstrings: numpy style, fully type-hinted in signatures, and no redundant types repeated in the docstring Parameters/Returns (mkdocstrings renders types from annotations). Match the existing scene objects.
- Lint/format: ruff (line-length 179,
select = E, F, I, isort force-single-line). Keepruff checkandruff format --checkclean. - Changelog: add an entry under
## Unreleasedin CHANGELOG.md for anything user-facing (CI enforces this on PRs).
- Color descriptors coerce.
SceneObject.color/GeometryObject.linecoloretc. are descriptors whose setter runsColor.coerce, which rejects mixed int/float rgb like(0.5, 0, 0.5). When assigning to one of these, wrap the value into_color(...)(theColorconstructor is lenient). Plain attributes you define yourself (facecolor,edgecolor,size, …) are unaffected — just convert withto_rgb(...)when handing them to matplotlib. - matplotlib API drift. Use
figure.add_subplot(1, 1, 1), not the string"111"(removed in mpl 3.11). Pin behaviour is tested across mpl versions in CI. - Graph node size.
GraphObjectdefaultsizepolicy="absolute"(screen-constant, like mesh vertices);"relative"divides by node count and makes tiny graphs huge. - Sub-object duplication. Objects that spawn children during
draw()(Line/Polyline/Vectorwithdraw_points=True) will duplicate those children ifdraw()is re-run viaScene.draw(). UsePlotter.redraw()(the managed path) instead of callingScene.draw()directly.
There are two patterns already in the tree — copy the closest one.
A. A single COMPAS type → dedicated object. See scene/circleobject.py (geometry) or scene/boxobject.py (shape).
- Create
scene/<thing>object.pywithclass <Thing>Object(PlotterSceneObject, GeometryObject)(orMeshObject/GraphObjectbase for data structures). - Implement
__init__(callsuper().__init__(zorder=..., **kwargs); set your style attrs),viewdata(), anddraw()(build matplotlib artists, append toself._mpl_objects,return self._mpl_objects). - In scene/init.py: import it, add
register(<Thing>, <Thing>Object, context="Plotter")insideregister_scene_objects(), and add it to__all__. - Add a test in tests/test_plotter.py and a CHANGELOG entry.
B. A family of types → one shared object. See
scene/shapeobject.py: ShapeObject
tessellates any Shape with to_vertices_and_faces() and projects to XY; the
registration loop maps several classes to it.
Breps and surfaces are the main remaining gap. Plan: tessellate to a mesh
(Brep.to_tesselation() / surface sampling) and project to XY, mirroring
ShapeObject. Breps require an optional backend (e.g. compas_occ), so register
them inside a try/except ImportError block (see git history of
scene/init.py for the guarded pattern).
Versioning via bump-my-version (pyproject.toml [tool.bumpversion],
which also bumps CITATION.cff and stamps the CHANGELOG). Tagging v* triggers
.github/workflows/release.yml (build across OS/Python, publish to PyPI).
Docs deploy from main via .github/workflows/docs.yml (mkdocs + mike).