Guide

Chart Builder Architecture & State Management

Last Updated: August 2026 · Version 3.14.0


System Overview

PRC Chart Builder is composed of three tiers that work together:

┌─────────────────────────────────────────────┐
│           Admin (Chart Library)             │  wp-admin/edit.php?post_type=chart
│  DataViews gallery · Create modal · AI gen  │
└─────────────────────┬───────────────────────┘
                      │ creates / edits
┌─────────────────────▼───────────────────────┐
│         Block Editor (Chart CPT post)       │  Gutenberg
│  Controller block · Chart block · Popover   │
└─────────────────────┬───────────────────────┘
                      │ renders via
┌─────────────────────▼───────────────────────┐
│        Charting Library + Utilities         │  prc-charting-library / @prc/charting-utilities
│  @visx components · D3 helpers              │
└─────────────────────────────────────────────┘

Tier 1: Admin (Chart Library)

Introduced in 3.5.0 (issue #1400). Lives in includes/admin/.

Responsibilities

  • Lists and manages all chart CPT posts via @wordpress/dataviews
  • Provides the "Create New Chart" modal with three entry paths: blank template, pattern picker, and AI generation
  • Handles CSV drag-and-drop to bootstrap new chart posts from data

Key components

ComponentFileRole
ChartLibrarysrc/chart-library.jsxPage root; composes DataViews + modal + dropzone
DataViewssrc/components/dataviews.jsxFilterable/searchable chart grid with live block previews
CreateNewChartModalsrc/components/create-new-chart-modal.jsxMulti-step creation flow; chart type picker → template/pattern/AI
AICreateStepsrc/components/ai-create-step.jsxAI generation panel (text + image + CSV → block markup)
DropZonesrc/components/dropzone.jsxPage-level CSV drop target

AI generation flow

User input (text description + optional PNG + optional CSV)
  ↓
REST request to AI experiment endpoint (Claude Sonnet by default)
  ↓
Block markup string returned
  ↓
parse_blocks() → BlockPreview rendered live
  ↓
Accept → post created with that markup | Regenerate → retry

The AI feature is gated by window.prcChartBuilderLibrary.aiEnabled, set server-side by Chart_AI_Experiment. It is off by default.


Tier 2: Block Editor

Block structure

prc-chart-builder/controller   (outer, provides context + data table)
  └── prc-chart-builder/chart  (inner, holds all chart config in attributes)
  └── core/table               (optional, canonical data source)

prc-chart-builder/synced-chart is a reference block used when embedding a chart CPT post into an article. It holds a ref (post ID) and delegates rendering to the chart CPT's controller block.

Visibility of the referenced chart: Anonymous visitors only see charts whose CPT post is publish. Logged-in users and preview requests may also see draft, future (scheduled), and private charts, so embedded synced charts can resolve before a chart’s publish date. Password-protected chart posts never render for anonymous users.

Data flow

core/table (source of truth for data)
  ↓ formatCellContent()
io.chartData (derived, stored in chart block attributes)
  ↓
prc-charting-library components (render)
  ↓
SVG output

Chart configuration (axes, colors, labels, layout, etc.) is stored entirely in the chart block attribute object on prc-chart-builder/chart. See README.md at the plugin root for the full attribute reference.

Element-level customizations

As of 3.5.0, per-element style overrides (label colors, shape fills, line styles, etc.) are stored in separate top-level block attributes using a {x}::{category} key format:

{
	"labels": {
		"customLabels": { "2020::Democrats": "52%" },
		"customVisibility": { "2020::Democrats": false },
		"customStyles": {
			"2020::Democrats": { "color": "#ff0000", "fontWeight": "bold" }
		},
		"customPositions": { "2020::Democrats": { "dx": 10, "dy": -5 } }
	},
	"shapes": {
		"customStyles": {
			"2020::Democrats": { "fill": "#ff0000", "opacity": 0.8 }
		},
		"segmentStyles": {
			"2020::2024::Democrats": {
				"stroke": "#ff0000",
				"strokeDasharray": "5,5"
			}
		}
	}
}

Key normalization: Date values are always converted to ISO strings before key generation to ensure consistency between the editor (where dates are JS Date objects) and the frontend (where they are ISO strings from JSON serialization).

This approach was explicitly considered and deferred in the v1.3.12 architecture document as "Option 2: Separate Attribute with Key-Based Lookup." It was ultimately adopted over the __labelPositions-in-chartData approach because:

  • Customizations survive all data changes (not just x-value-matching ones)
  • Clean separation of concerns — data and presentation are distinct
  • The orphaned-key concern is manageable at the scale of typical chart data

Element popover system

The popover system (src/chart/edit/popover/) is the primary editing paradigm for per-element customization.

User clicks element in chart canvas
  ↓
wpEditorFunctions.{type}.onClick() fires
  ↓
handleElementClick() in edit/index.jsx
  ↓
setSelectedElement({ elementType, dataPoint, ... })
  ↓
ChartElementPopover renders appropriate panel (Label / Shape / LineSegment / TickLabel / ...)
  ↓
User makes changes → hook.handleStyleChange() updates local state
  ↓
onUpdate() → setAttributes() → chart re-renders

Viewport awareness: All customizations respect the current device preview context (Desktop / Tablet / Mobile) via useViewportAttributes. The same element can carry different overrides per viewport.

State management

The block editor tier uses only WordPress-native patterns:

NeedSolution
Persisting chart configBlock attributes on prc-chart-builder/chart
Persisting per-element overridesSeparate top-level block attributes (key-based)
Parent → child communicationBlock Context via ChartContext.Provider in the controller
Editor-only UI state (popover open/closed, selected element)src/chart/edit/store.js — a local @wordpress/data store
Frontend runtime stateWordPress Interactivity API (for freeform/interactive charts)
Real-time collaborationAutomatic — Gutenberg syncs block attributes natively

No external Redux or React state management libraries are used.

PNG generation pipeline

As of 3.6.0, featured-image PNGs are generated server-side. When a chart post is saved (or a WP-CLI backfill is run), an Action Scheduler job calls PNG_Export::generate_png(). That job still uses Screenshot_Service::take() with the chart’s saved layout.width / layout.height and the site screenshot defaults (Charts → Settings → Screenshot Settings). It does not loop extra sizes.

Screenshot_Service is a facade over a provider registry (includes/screenshot-providers/). ScreenshotOne is the default auto-detect path. Cloudflare Browser Rendering, Firebase screenshotElement, and a signed HTTP endpoint are available when configured. The registry honors PRC_PLATFORM_CHART_SCREENSHOT_PROVIDER first, then the site screenshot setting, then auto-detect. Every provider receives the same Screenshot_Capture_Spec (CSS viewport, device scale, selector, delay, padding).

The capture URL is {permalink}/export/. That page is a chrome-free chart document. It always uses desktop chart attributes so a 640px chart is not treated as tablet. Explicit take_spec() calls append screenshot_width and screenshot_height so the export page reflows before capture. Named size aliases can be added later through prc_chart_builder_screenshot_variants. The plugin ships only default.

Setup, constants, and take_spec() usage live in screenshot providers.

When capture succeeds, PNG_Export sideloads the PNG as the chart post’s featured image, writes _chart_attributes_hash, and stores _prc_chart_screenshot_provider on the attachment.

Export filenames and downloads

User-facing downloads use a shared sanitization rule: chart metadata title (from metadata.title on the chart block) is normalized to a filename stem—lowercase, spaces → underscores, punctuation stripped, Unicode letters and digits kept—via sanitizeChartExportFilename() in src/chart/utils/sanitize-chart-export-filename.js. Empty or invalid input falls back to chart.

SurfaceFilename patternNotes
Server PNG sideload (PNG_Export in includes/class-png-export.php){stem}-{post_id}-{time}.pngActive screenshot provider → media library on post save / WP-CLI backfill. stem from metadata.title or post_title as fallback.
Context-menu PNG download (view.js){stem}.pngUses pre-generated pngUrl (featured image); name is cosmetic
CSV download (downloadData in view.js){stem}_data_{postPubDate}.csvpostPubDate from controller context
Featured-image download{stem}.png
SVG downloadchart-{id}.svg or chart-{clientId}.svgNot title-based

Social share URLs: The Interactivity API context exposes both postUrl (the page where the chart is embedded) and chartPostUrl (the chart CPT permalink when different). Share actions (shareTwitter, shareBluesky, shareFacebook) use chartPostUrl || postUrl so links point at the chart post when the controller is synced into an article.


Tier 3: Charting Library & Utilities

prc-charting-library

Visx/D3 SVG chart library with a dual build (PRC-17):

SurfaceBundleRuntime
Editor (prc-chart-builder/chart edit component)build/editor.js (classic script)React
Frontend (prc-chart-builder/chart view.js)build/view.js (Script Module @prc/charting-library)Preact via preact/compat

On the frontend, chart components subscribe to the prc-chart-builder/chart interactivity store via useChartStore so any block can call setChart / setData / setConfig on a chart by chartId. The editor build keeps passing inline props — useChartStore is a no-op there.

See reactive-store.md for the store contract and consumer-block recipes.

Chart types, as routed by layout.type in prc-charting-library/src/lib/controller/ChartBuilder.tsx (the authoritative list — update this table when you add a case there):

Typelayout.typeNotes
Bar (vertical, horizontal)barRenderer chosen by layout.orientation
Stacked Barstacked-barRenderer chosen by layout.orientation
Diverging Bardiverging-barRenderer chosen by layout.orientation
Exploded Barexploded-barCategorical breakdown with value comparison
Lineline
Stacked Areastacked-area
ScatterscatterGrouping and regression lines (3.5.0); variable dot sizing (3.14.0)
Dot Plotdot-plot
Bee Swarmbee-swarmNew in 3.14.0. Dodge and force layouts; variable node sizing
Piepie
TreemaptreemapNew in 3.5.0
WafflewaffleNew in 3.14.0. Pie-like encoding on a fixed cell grid
SankeysankeyNew in 3.5.0
Radarradar
Small Multiplessmall-multiplesNew in 3.14.0 (BETA). Grid of repeated charts faceted by series or group
Heat Map Tableheat-map-tableNew in 3.14.0. Value-scaled cell colors (linear / threshold / ordinal)
US States Mapmap-usa
US Block Mapmap-usa-blockResponsive scaling fixed in 3.5.0
US Hex Mapmap-usa-hex
US County Mapmap-usa-counties
US CBSA Mapmap-usa-cbsaNew in 3.9.0. See CBSA matching contract below.
World Mapmap-worldMissing territories (Somaliland, Western Sahara etc.) added in 3.5.0
World Map (Orthographic)map-world-orthographicGlobe projection

DiffColumn is rendered as part of a horizontal bar chart rather than being its own layout.type.

CBSA map matching contract

The US CBSA Map (map-usa-cbsa) matches data rows to geographic features by comparing each row's CBSA identifier against the 5-digit Census GEOID on each topology feature. The component accepts the identifier in any of these column names (checked in order):

Column nameNotes
CBSAPreferred — uppercase, matches Census field naming
cbsaLowercase alias
GEOIDDirect GEOID reference
geoidLowercase alias
xFallback — the Power Table's independent-variable column

Column setup in the Power Table: right-click the CBSA code column → Set data typeCBSA code. This enables cell-level validation against the full 935-code allowlist (Census 2023 TIGER cartographic boundary).

Topology source: plugins/prc-charting-library/src/lib/data/maps/usa-cbsa/topology.json — generated from cb_2023_us_cbsa_500k (Census cartographic boundary, 1:500k), further simplified 50% with mapshaper keep-shapes. Feature IDs are the 5-digit CBSA GEOID; properties.name holds the full CBSA title (e.g. "New York-Newark-Jersey City, NY-NJ").

State outline: The CBSA map always renders US state boundaries as a base layer so rural areas (which have no CBSA) still show the full country context. This distinguishes it from the county map, where county shapes tile the entire US.

@prc/charting-utilities

Shared utilities consumed by both prc-charting-library and prc-chart-builder, published from prc-scripts/includes/scripts/src/@prc/charting-utilities. Layout:

  • compute/ — framework-agnostic compute (axes, legend, labels, tooltips, voronoi, beeswarmForce, node shape colors). Formerly named hooks/.
  • hooks/ — React hooks, including useSize
  • labelLayout/ — framework-agnostic label placement
  • utilities/baseConfig, color palettes, resolveCategoryColor, getPointRadiusScale (variable point sizing), createTopologyLoader, regression fits, and generateElementKey in helpers.ts
  • types/configTypes.ts holds the authoritative BaseConfig

Chart type constants live in the chart builder at prc-chart-builder/src/chart/utils/chart-types.js, not in charting-utilities.

Every @prc/* import is externalized to a window.* global at build time — see the externalization note in the repo AGENTS.md.


Dark Mode

As of 3.5.0, charts respond automatically to the OS/browser dark mode preference via CSS light-dark().

How it works: getColor() in charting-utilities checks if a resolved color is a plain hex. If so, it looks up that hex in theme.json's color palette and returns the full light-dark(light-value, dark-value) string. No editor action is required.

Exception: Linear (continuous gradient) map color scales do not participate in dark mode. Gradient interpolation doesn't translate cleanly to the light-dark swap approach.


PHP Rendering (Server Side)

  • class-chart.php — registers the prc-chart-builder/chart block, handles render callback for freeform/static chart variants
  • class-controller.php — registers prc-chart-builder/controller, manages the synced chart relationship and data resolution
  • class-markdown-for-agents-integration.php — registers markdown callbacks with prc-markdown-for-agents so charts render as structured Markdown tables (title + data table + metadata) rather than SVG/HTML when processed by AI agent workflows

Decision Matrix

Use block attributes when:

  • Data needs to persist with the post
  • Standard chart config (axes, colors, layout, metadata)
  • Per-element style overrides (key-based, separate from data)

Use Block Context when:

  • Parent → child communication within the controller/chart relationship
  • Passing callbacks or functions to inner blocks

Use the local @wordpress/data store (edit/store.js) when:

  • Editor-only ephemeral UI state (which element is selected, popover position)
  • State that should not persist across saves

Use the Interactivity API when:

  • Frontend-only runtime interactions (hover, scroll, click on published charts)
  • Lightweight state that doesn't need to persist

Do not use:

  • External Redux or global React state — WordPress-native patterns cover all current needs
  • __labelPositions inside chartData — superseded by the key-based attribute approach in 3.5.0

Was this helpful?