Polar Chart Quantitative Encoding and Gate Testing: A Complete Technical Guide

A polar chart in the diagram-design repository encodes numeric values as radial distances using strict data-polar-* SVG attributes, validated by a Python gate that parses geometry and rejects any deviation from the contract.

The diagram-design project implements a rigorous quantitative visualization system where polar charts represent single numeric series through angular sectors and radial rays. Every generated SVG carries machine-readable metadata that a lightweight gate parses to verify geometric accuracy and semantic consistency. This article explains the encoding mechanics and the validation pipeline that enforces them.

Polar Chart Quantitative Encoding Rules

The polar chart type follows a declarative contract defined in skills/diagram-design/references/type-polar.md. Each SVG element must carry specific data-polar-* attributes that encode both visual properties and semantic meaning.

Root SVG Metadata

The <svg> element establishes the coordinate system with eight required attributes:

  • data-polar-cx and data-polar-cy — chart center coordinates
  • data-polar-radius — maximum radial extent R
  • data-polar-min and data-polar-max — value domain boundaries
  • data-polar-start-angle — angular zero point in degrees
  • data-polar-clockwise — direction of angular progression (true|false)
  • data-polar-inner-radius — must be 0 (no donut support)

Optional: data-polar-radius-encoding permits only linear values. Non-linear encodings trigger validation failures.

Category Structure

Each data point nests inside a <g> element with:

  • data-polar-category — trimmed, unique category name
  • data-polar-index — contiguous integer 0..N-1
  • data-polar-value — finite numeric within [min, max]
  • data-polar-focal — at most one category may mark true

The category contains three child elements:

  1. Ray <line> — stem from center to value-proportional radius, stroke-width 0.8px
  2. Marker <circle> — radius 4px (5px for focal), stroke-width 1.2px
  3. Value label <text> — text content must exactly match data-polar-value

Grid and Decorative Elements

  • Five rings <circle data-polar-ring> — concentric circles at radii R·k/5 for k=1..5
  • Optional background <rect data-polar-background> — full-coverage rectangle
  • Optional rule <line data-polar-rule> — at most one reference line

Any element not explicitly permitted—<path>, <polygon>, <image>, inline style, class attributes, CSS imports, or transforms—causes immediate gate failure.

Gate Testing Architecture

The validation system lives in scripts/verify-polar.py, a dependency-free Python script implementing a two-phase pipeline: parsing then verification.

PolarParser Implementation

Lines 887–894 define the PolarParser class subclassing html.parser.HTMLParser:

class PolarParser(HTMLParser):
    def __init__(self):
        super().__init__(convert_charrefs=True)
        self.chart = None
        self.category_stack = []
        self.findings = []

The parser executes through these stages:

  1. Root detection — locates <svg data-polar-chart> and instantiates a Chart object (1029–1050)
  2. Category collection — pushes each <g data-polar-category> onto a stack (1080–1089)
  3. Element recording — tracks rays, markers, rings, backgrounds, rules, and rejects unsupported elements (1100–1130)

Validation Orchestration

The public entry point check(path: Path) -> list[str] at lines 606–607 coordinates verification:

def check(path: Path) -> list[str]:
    parser = PolarParser()
    parser.feed(path.read_text())
    return parser.findings if parser.findings else ["OK"]

Key validation steps include:

Check Implementation Tolerance
Metadata extraction 514–538 Exact match for enumerated values
Ring geometry 558–575 0.75px or R/20, whichever is smaller
Category indexing 891–901 Contiguity and uniqueness
Spoke geometry 1289–1306 Coordinate tolerance
Ray length 1318–1339 Endpoint position
Marker/label 1345–1385 Radius, stroke, text match

The geometry validation recomputes expected positions using polar-to-cartesian conversion:

expected_angle = radians(start_angle) + (1 if clockwise else -1) * 2 * pi * index / N
expected_x = cx + radius * cos(expected_angle)
expected_y = cy + radius * sin(expected_angle)

Ray endpoints must satisfy r = R * value / max where value is the category's data-polar-value.

Variant Drift Detection

The check_variants(paths) function (637–645) validates multiple SVG variants (light, dark, full themes) and compares their data signatures. Any discrepancy triggers a single "variant data drift" failure, ensuring visual variants share identical underlying data.

Practical Implementation

Minimal Valid Polar Chart

The canonical example resides in skills/diagram-design/assets/example-polar.html:

<svg data-polar-chart
     data-polar-cx="500" data-polar-cy="230"
     data-polar-radius="160" data-polar-min="0"
     data-polar-max="100" data-polar-start-angle="-90"
     data-polar-clockwise="true"
     data-polar-radius-encoding="linear"
     data-polar-inner-radius="0">
  <!-- Grid rings -->
  <circle data-polar-ring r="32" cx="500" cy="230" fill="none" stroke-width="0.8"/>
  <circle data-polar-ring r="64" cx="500" cy="230" fill="none" stroke-width="0.8"/>
  <circle data-polar-ring r="96" cx="500" cy="230" fill="none" stroke-width="0.8"/>
  <circle data-polar-ring r="128" cx="500" cy="230" fill="none" stroke-width="0.8"/>
  <circle data-polar-ring r="160" cx="500" cy="230" fill="none" stroke-width="0.8"/>
  
  <!-- Category: value 32 at 20% radius -->
  <g data-polar-category="00-03" data-polar-index="0" data-polar-value="32">
    <line data-polar-ray x1="500" y1="230" x2="551.2" y2="230" stroke-width="0.8"/>
    <circle data-polar-marker cx="551.2" cy="230" r="4" stroke-width="1.2"/>
    <text data-polar-value-label x="551.2" y="210">32</text>
  </g>
</svg>

Running Gate Validation

Validate a single file:

python3 scripts/verify-polar.py skills/diagram-design/assets/example-polar.html

Validate all default variants (light, dark, full):

python3 scripts/verify-polar.py

The script prints OK <path> on success or FAIL <path>: <message> per violation, returning exit code 0 or 1 respectively (889–898).

Automated Testing

scripts/test-verify-polar.py exercises the gate with fixtures covering:

  • Valid four-category baseline (0, 25, 50, 100)
  • Failure: non-zero inner radius
  • Failure: visible zero-length ray
  • Failure: out-of-range value
  • Failure: duplicate index
  • Failure: illegal wedge element

CI runs these tests automatically via .github/workflows/ci.yml.

Source File Reference

File Purpose
scripts/verify-polar.py Core gate: parser and validation engine
scripts/test-verify-polar.py Automated adversarial test suite
skills/diagram-design/references/type-polar.md Human-readable contract specification
skills/diagram-design/assets/example-polar*.html Canonical valid examples (light/dark/full)
docs/superpowers/specs/2026-08-18-polar-chart-design.md Design rationale and evolution

Summary

  • Polar chart quantitative encoding maps single numeric series to radial distances using strict data-polar-* attributes on SVG elements
  • Seven element types are permitted: root <svg>, category <g>, ray <line>, marker <circle>, ring <circle>, background <rect>, and rule <line>
  • Linear radius encoding is mandatory; values are normalized against data-polar-max to determine ray length
  • scripts/verify-polar.py implements a dependency-free gate that parses SVG, recomputes expected geometry, and reports violations with sub-pixel tolerance
  • Variant drift detection guarantees that light, dark, and full theme variants share identical data signatures
  • Zero-tolerance policy for forbidden elements, transforms, inline styles, and URL-valued attributes ensures predictable rendering

Frequently Asked Questions

What happens if a polar chart uses a non-linear radius encoding?

The gate rejects any data-polar-radius-encoding value other than linear. According to type-polar.md, only linear interpolation from center to maximum radius is permitted. Attempting logarithmic or power scales triggers a FAIL message citing the invalid attribute value.

How does the gate handle floating-point precision in coordinate comparisons?

Validation uses a tolerance of 0.75px or R/20, whichever is smaller, as implemented at lines 991–1004. This accommodates reasonable SVG generation rounding while detecting meaningful geometric errors.

Can the focal marker be larger than standard markers?

Yes. The focal category—at most one per chart—receives a marker radius of 5px versus 4px for standard categories. Both use stroke-width 1.2px. The gate validates these exact measurements in check() at lines 1345–1385.

Why does the contract forbid CSS classes and transforms?

These restrictions guarantee deterministic rendering across environments. According to the design spec, inline styles and transforms introduce variability that breaks the quantitative encoding contract—visual appearance must derive solely from explicit data-polar-* attributes and computed geometry.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →