Skip to content

Architecture: RenderParams base class -> formal IR (resolve -> IR -> backend) #700

Description

@timtreis

Summary

This is the architectural capstone of the refactor audit: introduce a RenderParams base class and, in phases, evolve the *RenderParams dataclasses into a real intermediate representation (IR) that fully resolves what to draw before any matplotlib object exists — then have render.py consume that IR as a pure "how to draw it" backend.

Filed as a discussion: the phased plan and the IR boundary deserve maintainer input before execution.

The pain today

render_params.py's 5 dataclasses duplicate 15+ fields (element/color/cmap_params/colorbar/zorder in all 5; transfunc/table_name/palette/groups in 4). This propagates into 5 constructor call sites in basic.py, 5 _validate_*_render_params in utils.py, and 21 hardcoded element-type-string dispatch sites. Adding one cross-cutting field today means editing ~4 dataclasses + 4 constructors + 4 validators.

Deeper: the params are half-baked specs. They carry user input, not resolved semantics. The actual decisions about what to draw (categorical-vs-continuous color resolution via _set_color_source_vec; the >10000 elements → datashader backend choice; extents) happen inside the render functions, fused with matplotlib calls. There is no testable boundary between "scene" and "pixels".

Step 1 (low-risk, do now): RenderParams base class

A @dataclass(kw_only=True) class RenderParams holding the shared fields; the 5 become thin subclasses with only their unique fields. Purely internal (these dataclasses are not public). Removes the plumbing duplication and creates the attachment point for the IR.

Steps 2+ (phased): resolve → IR → backend

  1. resolve_color(params, sdata) -> ColorSpec — pure function extracting the color-resolution logic now inlined in the renderers; add a colortype field describing each layer.
  2. Move backend selection + extent computation into the resolve pass, populating the IR.
  3. Introduce SceneSpec/LayerSpec as the formal boundary; show() builds the Scene, a MatplotlibBackend consumes it. Because layout (extent, colorbar slots) is declared in the IR, axes can be sized once up front — dissolving the two-pass colorbar hack and the imshow-resize intensity problem.
  4. (Optional, separate) Scene.to_dict() + a vega-like exporter.

Why this direction, done this way

The maintainers already attempted this on the stalled origin/viewconfig branch (PR #267), but built the spec post-hoc by reading back matplotlib state (fig.subplotpars, Text, transAxes) at the end of show() — which ballooned to 1500+ lines, required a colortype field that never reached main, and never landed. Building the IR as the source of truth that drives rendering (resolve → IR → backend), rather than an artifact reverse-engineered from pixels, is the structural inversion that makes it tractable. It unlocks matplotlib-free testing (the biggest win given the image-baseline-heavy suite), correct serialization, and a clean backend seam — all behind a frozen public API.

Risk / effort

Impact: very high · Effort: high (multi-PR) · Risk: medium, controllable — public API untouched, each phase behavior-preserving and guarded by existing baselines. Do Step 1 now; sequence the IR after the utils split, show() decomposition, render-quartet, and color-pipeline issues create the seams.


Part of a maintainability/refactor audit of main.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions