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
chartCPT 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
| Component | File | Role |
|---|---|---|
ChartLibrary | src/chart-library.jsx | Page root; composes DataViews + modal + dropzone |
DataViews | src/components/dataviews.jsx | Filterable/searchable chart grid with live block previews |
CreateNewChartModal | src/components/create-new-chart-modal.jsx | Multi-step creation flow; chart type picker → template/pattern/AI |
AICreateStep | src/components/ai-create-step.jsx | AI generation panel (text + image + CSV → block markup) |
DropZone | src/components/dropzone.jsx | Page-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:
| Need | Solution |
|---|---|
| Persisting chart config | Block attributes on prc-chart-builder/chart |
| Persisting per-element overrides | Separate top-level block attributes (key-based) |
| Parent → child communication | Block 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 state | WordPress Interactivity API (for freeform/interactive charts) |
| Real-time collaboration | Automatic — 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.
| Surface | Filename pattern | Notes |
|---|---|---|
Server PNG sideload (PNG_Export in includes/class-png-export.php) | {stem}-{post_id}-{time}.png | Active 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}.png | Uses pre-generated pngUrl (featured image); name is cosmetic |
CSV download (downloadData in view.js) | {stem}_data_{postPubDate}.csv | postPubDate from controller context |
| Featured-image download | {stem}.png | |
| SVG download | chart-{id}.svg or chart-{clientId}.svg | Not 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):
| Surface | Bundle | Runtime |
|---|---|---|
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):
| Type | layout.type | Notes |
|---|---|---|
| Bar (vertical, horizontal) | bar | Renderer chosen by layout.orientation |
| Stacked Bar | stacked-bar | Renderer chosen by layout.orientation |
| Diverging Bar | diverging-bar | Renderer chosen by layout.orientation |
| Exploded Bar | exploded-bar | Categorical breakdown with value comparison |
| Line | line | |
| Stacked Area | stacked-area | |
| Scatter | scatter | Grouping and regression lines (3.5.0); variable dot sizing (3.14.0) |
| Dot Plot | dot-plot | |
| Bee Swarm | bee-swarm | New in 3.14.0. Dodge and force layouts; variable node sizing |
| Pie | pie | |
| Treemap | treemap | New in 3.5.0 |
| Waffle | waffle | New in 3.14.0. Pie-like encoding on a fixed cell grid |
| Sankey | sankey | New in 3.5.0 |
| Radar | radar | |
| Small Multiples | small-multiples | New in 3.14.0 (BETA). Grid of repeated charts faceted by series or group |
| Heat Map Table | heat-map-table | New in 3.14.0. Value-scaled cell colors (linear / threshold / ordinal) |
| US States Map | map-usa | |
| US Block Map | map-usa-block | Responsive scaling fixed in 3.5.0 |
| US Hex Map | map-usa-hex | |
| US County Map | map-usa-counties | |
| US CBSA Map | map-usa-cbsa | New in 3.9.0. See CBSA matching contract below. |
| World Map | map-world | Missing territories (Somaliland, Western Sahara etc.) added in 3.5.0 |
| World Map (Orthographic) | map-world-orthographic | Globe 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 name | Notes |
|---|---|
CBSA | Preferred — uppercase, matches Census field naming |
cbsa | Lowercase alias |
GEOID | Direct GEOID reference |
geoid | Lowercase alias |
x | Fallback — the Power Table's independent-variable column |
Column setup in the Power Table: right-click the CBSA code column → Set data type → CBSA 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 namedhooks/.hooks/— React hooks, includinguseSizelabelLayout/— framework-agnostic label placementutilities/—baseConfig, color palettes,resolveCategoryColor,getPointRadiusScale(variable point sizing),createTopologyLoader, regression fits, andgenerateElementKeyinhelpers.tstypes/—configTypes.tsholds the authoritativeBaseConfig
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 theprc-chart-builder/chartblock, handles render callback for freeform/static chart variantsclass-controller.php— registersprc-chart-builder/controller, manages the synced chart relationship and data resolutionclass-markdown-for-agents-integration.php— registers markdown callbacks withprc-markdown-for-agentsso 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
__labelPositionsinsidechartData— superseded by the key-based attribute approach in 3.5.0
Related Documentation
- Plugin docs landing page
- REST API —
prc-chart-builder/v1custom routes - Abilities —
prc-chart-builder/generate - Plugin README — full block attribute reference
- Viewport attributes — viewport-aware attribute system
- Viewport usage guide — practical guide to responsive customizations
- Viewport breakpoints — breakpoint values, detection, and fallback behavior
- Reactive store — the
prc-chart-builder/chartinteractivity store - Console helpers —
window.prcChartBuilder.*devtools handles - Release notes 3.9.0 — CBSA map, bubble map
- Release notes 3.5.0
- Element popover README
- WordPress Data Package
- WordPress Interactivity API