Release: Chart Builder
3.9.0· PRC Platform1.8Work period: May 2026 Scope:prc-chart-builder,prc-charting-library,prc-charting-utilities,prc-block-library(table column type)
Overview
Chart Builder 3.9.0 is a feature-heavy release focused on map expressiveness, category emphasis, and editor click-to-edit ergonomics. Seven major changes ship together:
- USA CBSA map + bubble-style maps — a brand-new USA Core-Based Statistical Area choropleth, plus a
mapStyle: 'choropleth' | 'bubble'toggle that converts every geographic map (USA, USA counties, USA CBSA, World) into proportional square-root-scaled circles with a dedicated nested-circle radius legend. - Highlighted category styling — first-class highlight/deselected color and opacity controls applied across the bar family, dot plot, line/area, scatter, and sankey, with synchronized swatches in legends and tooltips.
- Diff column custom styling & click-to-edit — header and per-cell color, typography, text outline, and custom text via the chart element popover, with grouped-chart key disambiguation.
- Detached legend mode — a new
legend.variation: 'grouped' | 'detached'config that turns legend items into independently draggable annotations driven by underlying data. - Diverging bar secondary ghost overlay — render a comparison series as a low-opacity ghost layer behind the primary bars, with full color, stroke, and opacity controls.
- CSV export hardening — UTF-8 BOM for Excel compatibility, hidden columns now filtered out of every export path, and a reusable
Table_ExportPHP utility shared acrossJSON_LD, the JSON-LD export controller, andview.js. - Legend order persistence fixes — custom legend ordering survives neutral-column and secondary-overlay toggles, and treemap/sankey charts again derive available categories from data while preserving saved order.
No database migrations and no breaking changes to published content. All new attributes are additive with safe defaults; existing charts render identically until producers opt in.
What's New
1. USA CBSA map + bubble-style geographic maps
Background
PRC researchers regularly need to visualize metropolitan-level data — labor markets, housing markets, demographic trends — that doesn't map cleanly onto either states or counties. The U.S. Census Bureau's Core-Based Statistical Areas (CBSAs) are the right unit for this work, but Chart Builder had no native CBSA variation, so editors were forced to fall back to county maps or hand-rolled visuals.
Separately, choropleth maps shade entire polygons by value — which is the right encoding for rate-style data, but the wrong encoding for count-style data (population, total spend, headcount), where a few large geographies visually dominate small-but-dense ones. Bubble maps solve this by encoding value as area at the feature's centroid, leaving geography as decorative context.
This release ships both, as one issue (#3196) covering one project arc.
CBSA map variation
A new "USA CBSA Map" variation appears in the chart inserter and chart picker.
- Topology —
plugins/prc-charting-library/src/lib/maps/usa-cbsa/topology.json, generated from the USDOT/Census CBSA boundaries and simplified to ~10% viamapshaper. Mirrors the existingusa-counties/topology.jsonlayout (objects.cbsaskeyed by 5-digit CBSA code). - Renderer — new
AlbersUSACBSA.tsxcomponent inprc-charting-library, structurally identical toAlbersUSACounties.tsx, registered viaChartBuilder.tsx's lazy-loaded map switch. - Variation template —
plugins/prc-chart-builder/.shared/variation-templates/map-usa-cbsa.jsships ~20 representative seed rows (Los Angeles–Long Beach–Anaheim 31080, Chicago–Naperville–Elgin 16980, etc.) so editors can preview the chart immediately on insert. - Validation — the Power Table block (
prc-block-library) gains a new "CBSA code" column type. Cells are validated against the Census CBSA code set (~900 valid 5-digit codes) and surface the same inline error styling and inspector panel introduced in 3.8.0. The chart-sync gate refuses to push table data to the chart until all CBSA cells are valid. - Layout —
'map-usa-cbsa'added toALL_CHART_TYPES,MAP_CHART_TYPES, andBUBBLE_MAP_CHART_TYPESinchart-types.js, and to theLayout.typeunion in@prc/charting-utilities.
Bubble map style
Every choropleth-capable geographic map can now switch to bubble rendering via a new "Map Style" select control in the chart's Data inspector panel.
- Gated — bubble mode is available for
map-usa,map-usa-counties,map-usa-cbsa, andmap-worldonly.map-usa-blockandmap-usa-hexremain choropleth-only (their abstracted geometries don't have meaningful centroids). - Encoding — bubble radius is computed with
d3.scaleSqrtso the circle's area — what the eye actually reads — is proportional to value. Linear radius scaling would visually over-weight large values. - Geography becomes context — when
mapStyle === 'bubble', polygon paths render withmap.pathBackgroundFill,pointer-events: 'none', and no mouse/focus handlers. The circle owns the hit target. Hovering an unfilled polygon to learn about a circle drawn on top of it would be a semantic mismatch. - Stacking order — features render in descending order of value, so the smallest circles paint last and stay hoverable inside dense overlap zones (NY metro, Bay Area, the Northeast corridor). Combined with
opacity: 0.7and a thin stroke, the stack is also visually legible. - No invisible hit halos — instead of expanding hit targets behind the scenes (which would cause overlap conflicts in dense regions),
minRadiusdefaults to4so even the smallest bubble is a viable pointer target. Editors can lower it for tighter ranges or raise it for easier clickability.
Three-layer SVG render in all bubble-capable maps
AlbersUSA, AlbersUSACounties, AlbersUSACBSA, and World were refactored to split per-feature work into a shared featureMeta pass, then render in three discrete layers:
polygonLayer → MapBubbleLayer → labelLayer
This guarantees labels always paint on top of both polygons and bubbles, in both modes, on all four maps.
New BubbleRadiusLegend component
Bubble mode swaps the default LegendThreshold / LegendOrdinal / LegendLinear switch for a dedicated nested-circle radius legend (BubbleRadiusLegend.tsx):
- All reference circles share a bottom edge, with leader lines to value labels on the right (Observable Plot–style).
- Sorted descending so the largest circle paints first.
- Driven by
legend.bubbleLegend.refValues— an editable list of d3-formatted values in the inspector. When empty, auto-computes[min, median, max]from the data. legend.bubbleLegend.fill('category' | 'none') toggles filled vs. outline-only circles. Stroke renders in both modes.legend.bubbleLegend.formatis a standard d3-format string (default',.0f').- Inherits dragability — the new
variant: 'radius'prop onStyledLegendskips the flex container but keeps the existingDraggableCoreplumbing. Editors drag the radius legend like any other legend; offsets persist tolegend.offsetX/legend.offsetY. - Default position is bottom-left of the chart area (
offsetX: padding.left + 8,offsetY: height - padding.bottom - 80) on first toggle to bubble mode. - Deliberately ignores
markerStyle,orientation,labelDelimiter, and grouped/detached variations — none of those semantically apply to a continuous radius scale.
Inspector controls
When chartFamily === 'map' and the chart type is bubble-capable, the Data panel exposes:
- Map Style (Heat / Bubble)
SelectControl. - Min radius / Max radius
NumberControls. - Radius legend fill
RadioControl(None / Category color). - Reference values editable repeating-row list, bound to
legend.bubbleLegend.refValues. - Value format text input, bound to
legend.bubbleLegend.format.
New type fields (@prc/charting-utilities)
// dataRender.ts
mapStyle?: 'choropleth' | 'bubble'; // default 'choropleth'
// map.ts
map: {
...,
bubble: {
minRadius: number; // default 4
maxRadius: number; // default 24
opacity: number; // default 0.7
stroke: string; // default '#fff'
strokeWidth: number; // default 1
};
};
// legend.ts
legend: {
...,
bubbleLegend?: {
fill: 'none' | 'category'; // default 'category'
refValues: number[]; // empty = auto [min, median, max]
format: string; // d3-format; default ',.0f'
};
};
All bubble-related fields are optional and gated on dataRender.mapStyle === 'bubble'. Charts that don't opt in render identically to 3.8.0.
2. Highlighted category styling
Background
PRC's editorial story often pivots on one or two highlighted categories within a larger comparison set — e.g., "Democrats and Republicans" highlighted against the full set of partisan leaners. Until now, this required either custom per-element color overrides on every shape, or a separate redacted version of the chart. Both approaches scaled poorly across responsive breakpoints and were brittle under edits.
3.9.0 ships a first-class highlight/deselect model that any multi-series chart can opt into in one click, with synchronized swatches in legends and tooltips.
What shipped
A new Color → Highlighted Categories inspector section in color-controls.jsx exposes:
- Highlighted categories — a category multi-picker. Each selected category renders at full saturation; everything else is "deselected."
- Highlight color — optional override for highlighted series (falls back to the category's normal color).
- Deselected color — flat color applied to non-highlighted series (defaults to a neutral grey).
- Deselected opacity — per-chart alpha multiplier for deselected series.
- Dot-plot node stroke — companion control (
nodes.pointStroke) so dot plots can keep a visible outline on deselected points without inheriting the deselected fill.
Gated to multi-series chart types
The controls are visible only on chart types where category-level emphasis is meaningful. Per-element emphasis charts (pie, treemap, maps) are excluded.
// chart-types.js
export const HIGHLIGHTABLE_CHART_TYPES = [
...BAR_CHART_TYPES,
'dot-plot',
...LINE_CHART_TYPES,
'scatter',
'sankey',
];
Renderer changes
@prc/charting-library adopts the new fields via four shared helpers exported from @prc/charting-utilities:
| Helper | Role |
|---|---|
resolveCategoryColor(category, ctx) | Returns the correct fill for a category given the current highlightedCategories, highlightColor, and deselectedColor. |
resolveCategoryOpacity(category, ctx) | Returns 1.0 for highlighted categories, deselectedOpacity otherwise. |
withCategoryOpacity(color, opacity) | Applies a multiplicative alpha to any color (hex, rgb, css var). |
legendCategoryShapeStyle(category, ctx) | Returns the marker style applied to legend swatches so they match bar fills. |
Every bar variant (BarHorizontal, BarVertical, StackedBarHorizontal, StackedBarVertical, DivergingBarHorizontal, DivergingBarVertical, ExplodedBar), DotPlot, Line, Scatter, Sankey, and ClickableLegend were refactored to call these helpers instead of computing color inline. Tooltip swatches in customTooltips also resolve through the same helpers, so the legend / mark / tooltip color story stays in sync when categories are toggled.
New type fields (@prc/charting-utilities)
// added to BaseConfig (or equivalent root)
highlightedCategories?: string[];
highlightColor?: string;
deselectedColor?: string;
deselectedOpacity?: number; // default 0.35
All four fields are optional. Charts that don't set highlightedCategories behave identically to 3.8.0.
Tests
Two new unit suites cover the resolution logic:
plugins/prc-chart-builder/tests/unit/resolve-category-color.test.js— verifies fill resolution across highlighted, non-highlighted, override, and fallback cases.
3. Diff column custom styling & click-to-edit
Background
The Diff column (introduced in earlier releases on bar-family and dot-plot charts) showed a single fixed-format value per row and could not be styled. As soon as producers started layering Diff columns into visually rich charts, they needed the same per-cell typographic and color control they already had on labels — without round-tripping through Illustrator.
What shipped
Two new popover panels and two new hooks land in src/chart/edit/popover/:
| Panel | Hook | Storage |
|---|---|---|
DiffColumnHeaderPanel | useDiffColumnHeaderCustomizations | diffColumn.columnHeader, diffColumn.style.headerFill, diffColumn.style.headerFontWeight, diffColumn.style.headerFontStyle, diffColumn.style.headerFontFamily, diffColumn.style.headerTextOutline |
DiffColumnLabelPanel | useDiffColumnLabelCustomizations | diffColumn.customLabels[rowKey] (text, fill, fontWeight, fontStyle, fontSize, fontFamily, textOutline) |
Both panels reuse the shared TextStyleControls component (color, weight, style, family, size, outline) so they look and feel identical to label/legend customization elsewhere.
Click-to-edit
The Diff column header and individual cells are now clickable in the editor. Clicks open the popover with the panel routed by ELEMENT_TYPES.DIFF_COLUMN_HEADER / ELEMENT_TYPES.DIFF_COLUMN_LABEL. Per-cell keys use generateElementKey(x, category, groupValue) — the same scheme as labels — so cells in grouped charts disambiguate correctly across group-break categories.
Header / cell style separation
A subtle but important fix: header typography is now stored under headerFontWeight / headerFontStyle / headerFontFamily, separate from the cell-default fontWeight / fontStyle / fontFamily fields. Previously, customizing the header overwrote the default for every cell. Existing diff columns are unaffected — the new header keys are read-through-default until set.
Dark-mode color resolution
Diff column text colors flow through the same getLabelFill helper introduced in 3.8.0, so cells contrast correctly in both light and dark contexts.
Inspector control parity
diff-column-controls.jsx was expanded to surface the new style fields at the chart level (defaults for all cells), and gated to render only when the chart type is in DIFF_COLUMN_POPOVER_CHART_TYPES (bar family + dot plot).
4. Detached legend mode
Background
Chart Builder's legend has historically been a single grouped flex container — items wrap together, drag together, and live as one unit. That's the right default for most charts. But for annotation-style legends — small charts where each category label is positioned next to the line or bar it labels — editors needed per-item positioning, not whole-legend drag.
What shipped
A new legend.variation: 'grouped' | 'detached' field on the shared Legend config:
| Variation | Behavior |
|---|---|
'grouped' (default) | Existing flex container, whole-legend drag, marker-style swatches. Identical to 3.8.0. |
'detached' | Each item is a free-floating position: absolute element with its own customLabels[k].offsetX / offsetY. Drag-positionable per item in the editor. |
Renderer changes (@prc/charting-library)
StyledLegend— whenlegend.variation === 'detached', renders as a transparent full-chart overlay (no chrome, no whole-legend drag) so detached items have a predictable positioning context.ClickableLegend— branches onlegend.variation. Grouped mode is unchanged. Detached mode applies per-item style overrides (markerStyle,fontSize,color,fontWeight) and wraps each item inDraggableCorein the editor.LegendSwatch— new custom SVG component (replacing the visxLegendShape) that renders true SVGrect/circle/lineshapes with customizable solid/outline fills. Solves a class of cross-browser layout bugs in detached mode.
Editor wiring (@prc/chart-builder)
- Legend layout toggle — new control in
legend-controls.jsxto switch a chart's legend between grouped and detached. - Drag handlers —
wpEditorFunctions.legendItems.onItemDragStart / Move / Endpersist each item's offsets back tolegend.customLabels[k]. - LegendItemPanel — when
legend.variation === 'detached', the popover exposes offset X/Y inputs; in grouped mode they're hidden. - Font weight added to legend item customization for richer detached-mode typography.
Type changes (@prc/charting-utilities)
- Adds
Legend.variation?: 'grouped' | 'detached'. - Removes an experimental per-item
LegendItemCustomization.detachedfield that was introduced earlier in this milestone and never used in production, in favor of the legend-wide variation mode. - Keeps the per-item
offsetX,offsetY,fontSize,color,markerStylefields — they are now only meaningful when the legend is detached.
block.json defaults
"legend": { "default": { "variation": "grouped", ... } }
Fully backwards compatible — every existing chart keeps its current rendering. Producers opt in per chart.
5. Diverging bar secondary ghost overlay
Background
Diverging bar charts (Favorable vs. Unfavorable, Approve vs. Disapprove, etc.) are often paired with a comparison series — last year's numbers, a different subgroup, an opinion poll baseline. Editors had no native way to render the comparison; they either built two charts side-by-side or laid one chart over a screenshot in Illustrator.
What shipped
A new optional secondary block on the DivergingBar config in @prc/charting-utilities:
secondary?: {
active: boolean;
positiveCategories: string[];
negativeCategories: string[];
fill: string; // default '#D9D9D9'
stroke: string; // default '#000'
strokeWidth: number; // default 0.5
opacity: number; // default 0.4
};
When active: true, both DivergingBarHorizontal and DivergingBarVertical render a low-opacity ghost layer underneath the primary bars, fanning out from the same central axis but driven by secondary.positiveCategories / secondary.negativeCategories. The primary bars retain full saturation and pointer events.
Editor wiring
diverging-bar-control.jsx exposes:
- Enable secondary overlay toggle.
- Positive categories and Negative categories category-pickers (independent of the primary diverging configuration, so the comparison series can reference different table columns).
- Fill, stroke, stroke width, and opacity controls.
- Optional Add to legend behavior — when active, the legend automatically appends the secondary categories so readers can tell the layers apart.
block.json defaults
"divergingBar": {
"default": {
"secondary": {
"active": false,
"positiveCategories": [],
"negativeCategories": [],
"fill": "#D9D9D9",
"stroke": "#000",
"strokeWidth": 0.5,
"opacity": 0.4
}
}
}
Inactive by default. Existing diverging-bar charts are unaffected.
6. CSV export hardening
Background
The chart frontend has long offered a "Download as CSV" button via view.js. As the platform's editorial output expanded to more international audiences and more columns-with-helper-data tables, two long-standing footguns kept biting:
- Excel was double-decoding UTF-8 because the CSV had no BOM, so non-ASCII characters in titles, sources, or cell text arrived garbled.
- Hidden columns leaked into exports. Editors used hidden columns for IDs, sort keys, and intermediate calculations. Those columns were correctly stripped from the rendered table but still appeared in the CSV download — and in the JSON-LD
csvUrlpayload search engines crawl.
What shipped
A new PRC\Platform\Chart_Builder\Table_Export PHP utility class (includes/utils/class-table-export.php) and a matching JS module (src/controller/utils/csv-export.js).
PHP — Table_Export
UTF8_BOMconstant —"\xEF\xBB\xBF". Prepended to every CSV response, including the JSON-LD export endpoint.get_hidden_column_indices( $table_attrs, $column_count )— resolves hidden virtual column indices using the same precedence rule as the editor: prefercolumnMeta[].hidden, fall back to legacyhiddenColumns.filter_hidden_columns( $table_data, $table_attrs )— strips hidden columns from a parsed{ header, rows }payload before encoding.
Loaded once via the plugin bootstrap and used by:
class-json-ld.php— UTF-8 BOM on the CSV streaming response; hidden columns filtered beforeDatasetJSON-LD is emitted (hasPartrows match what users see).view.js— frontend CSV download uses the same BOM + filter path via the new JS module.
JS — csv-export.js
UTF8_BOMconstant ('\uFEFF'— the JS representation).arrayToCSV(rows, metadata)— RFC 4180 encoder, decodes HTML entities, strips RichText tags, strips invisible characters (zero-width / stray BOMs already embedded in cell text), supports optional metadata header (title/subtitle) and footer (note/source/tag).sanitizeCsvField()/prepareCsvText()/stripHtmlTags()/decodeHtmlEntities()/stripInvisibleCharacters()— exposed as private helpers for unit testing.
Test coverage
tests/unit/csv-export.test.js— unit suite for the JS module (entity decoding, tag stripping, RFC 4180 encoding, metadata serialization).tests/fixtures/csv-export-torture-test.csv— torture test fixture covering emoji, RTL text, commas, embedded quotes, stray newlines, and zero-width characters.tests/test-table-export.php— PHPUnit coverage forTable_Export::get_hidden_column_indicesandTable_Export::filter_hidden_columns.
Behavior change summary
- CSV downloads now open cleanly in Excel (UTF-8 BOM auto-detected; no more "ABCé" garbage).
- Hidden columns never appear in CSV downloads or JSON-LD
hasPart— closes a long-running content-leak risk. - JSON-LD
Datasetrows match what readers see on the rendered table — search engines and downstream consumers index the public view, not the editor's working data.
7. Legend order persistence fixes
Background
Several toggles in the Chart Builder editor — neutral column on diverging bars, the new secondary ghost overlay, the available-categories list on treemap / sankey — could trigger a recomputation of "available legend categories." When that recomputation happened, any custom legend order the producer had saved was overwritten by a fresh default order. Producers learned to avoid these toggles, or to set the legend order after every other edit, which was brittle.
What shipped
A new merge-legend-category-order.js utility (src/chart/utils/) and accompanying unit suite (tests/unit/merge-legend-category-order.test.js).
- Custom order is merged with newly available categories instead of being replaced by the default order. Categories the producer ordered explicitly stay where they were; categories that became newly available are appended at the end.
- Treemap and sankey charts again derive available legend categories from data (
get-available-legend-categories.jswas rewritten with dedicated derivation helpers per chart type) and preserve saved custom order through edits. - Grouped treemaps omit rows with blank group values entirely instead of bucketing them as
"Other"— which previously created a spurious legend entry that ordering would constantly re-introduce. - Secondary overlay toggle for diverging bars now follows the same merge path; toggling on the secondary layer no longer resets the legend.
No attribute migration is required; existing charts pick up the corrected behavior on first edit.
Behind the scenes
- Inspector reorganization —
chart-controls.jsx,color-controls.jsx,data-controls.jsx, andlegend-controls.jsxwere reorganized so the new highlight, diff-column, detached-legend, bubble-map, and secondary-overlay sections sit in the correct semantic panels. get-copyable-style-attributes.jsupdated to include the new top-level styling fields (highlightColor,deselectedColor,deselectedOpacity,diffColumn.style.*header fields) so the "Copy styles → Paste styles" flow carries them between charts.get-config.jsruntime-merges the newbubble/bubbleLegend/secondary/legend.variationdefaults so charts authored before 3.9.0 pick up the new fields without an edit.- Charting library exports —
LegendSwatch,BubbleRadiusLegend, and the fourresolveCategoryColor/resolveCategoryOpacity/withCategoryOpacity/legendCategoryShapeStylehelpers are added to@prc/charting-library/@prc/charting-utilitiespublic exports. get-available-legend-categories.jsrefactored from one monolithic function into a per-chart-type derivation map. Easier to reason about and test, and unblocks future per-chart legend-order rules.- Plugin bootstrap —
utils/class-table-export.phpis now loaded alongside the other includes inPlugin_Bootstrap::load_dependencies(). - Version constants aligned —
PRC_CHART_BUILDER_VERSIONbumped to3.9.0.@prc/chart-builder,@prc/charting-library,@prc/charting-utilities, and the WordPress plugin headers for Chart Builder and Charting Library are all synced.
Files Changed (high level)
| Area | Change |
|---|---|
prc-charting-library/src/lib/maps/usa-cbsa/topology.json | New — simplified USDOT/Census CBSA boundaries |
prc-charting-library/src/lib/Components/maps/AlbersUSACBSA.tsx | New — CBSA map renderer |
prc-charting-library/src/lib/Components/BubbleRadiusLegend.tsx | New — nested-circle radius legend for bubble maps |
prc-charting-library/src/lib/Components/maps/AlbersUSA.tsx, AlbersUSACounties.tsx, World.tsx | Three-layer SVG refactor (polygons → bubbles → labels); bubble mode rendering |
prc-charting-library/src/lib/Components/Legend.tsx | New variant: 'standard' | 'radius' prop; detached overlay mode |
prc-charting-library/src/lib/Components/ClickableLegend.tsx | Grouped vs. detached branching; per-item DraggableCore; LegendSwatch integration |
prc-charting-library/src/lib/Components/LegendSwatch.tsx | New — custom SVG legend shape |
prc-charting-library/src/lib/Components/BarHorizontal.tsx, BarVertical.tsx, DivergingBarHorizontal.tsx, DivergingBarVertical.tsx, ExplodedBar.tsx, DotPlot.tsx, Line.tsx, Scatter.tsx, Sankey.tsx | Adopt resolveCategoryColor / resolveCategoryOpacity helpers |
prc-charting-library/src/lib/Components/DiffColumn.tsx | Custom header / cell styling; click-to-edit pass-through |
prc-charting-library/src/lib/Components/DivergingBar*.tsx | Secondary ghost overlay rendering |
prc-chart-builder/.shared/variation-templates/map-usa-cbsa.js | New — CBSA variation template |
prc-chart-builder/.shared/variation-templates/index.js | Register CBSA variation export |
prc-chart-builder/src/controller/variations.js | Register cbUSACBSAMap variation + VARIATION_TO_LAYOUT_TYPE |
prc-chart-builder/src/controller/Edit.jsx | CHART_TYPE_TO_LAYOUT_TYPE for CBSA + bubble-mode default offsets |
prc-chart-builder/src/controller/icons.js | USACBSAMap icon |
prc-chart-builder/src/chart/utils/chart-types.js | New BUBBLE_MAP_CHART_TYPES, HIGHLIGHTABLE_CHART_TYPES, SUPPLEMENTAL_COLUMN_CHART_TYPES |
prc-chart-builder/src/chart/utils/get-available-legend-categories.js | Per-chart-type derivation map |
prc-chart-builder/src/chart/utils/merge-legend-category-order.js | New — custom-order-preserving merge |
prc-chart-builder/src/chart/utils/get-config.js | Runtime-merge new defaults |
prc-chart-builder/src/chart/utils/get-copyable-style-attributes.js | Include new style fields in copy/paste |
prc-chart-builder/src/chart/edit/color-controls.jsx | Highlighted categories section |
prc-chart-builder/src/chart/edit/data-controls.jsx | Map Style + bubble radius / legend controls |
prc-chart-builder/src/chart/edit/legend-controls.jsx | Grouped / detached layout toggle |
prc-chart-builder/src/chart/edit/diff-column-controls.jsx | Chart-level diff column styling defaults |
prc-chart-builder/src/chart/edit/diverging-bar-control.jsx | Secondary ghost overlay controls |
prc-chart-builder/src/chart/edit/map-controls.jsx | Bubble-specific styling controls |
prc-chart-builder/src/chart/edit/node-controls.jsx | Dot plot nodes.pointStroke control |
prc-chart-builder/src/chart/edit/popover/panels/DiffColumnHeaderPanel.jsx | New popover panel |
prc-chart-builder/src/chart/edit/popover/panels/DiffColumnLabelPanel.jsx | New popover panel |
prc-chart-builder/src/chart/edit/popover/panels/LegendItemPanel.jsx | Detached-mode offsets, font weight |
prc-chart-builder/src/chart/edit/popover/panels/LabelPanel.jsx, AnnotationPanel.jsx, TickLabelPanel.jsx, TextStyleControls.jsx | Adopt diff-column-aware shared text controls |
prc-chart-builder/src/chart/edit/popover/hooks/useDiffColumnHeaderCustomizations.js, useDiffColumnLabelCustomizations.js, useLegendItemCustomizations.js, useAnnotationCustomizations.js | New / updated customization hooks |
prc-chart-builder/src/chart/edit/wp-editor-functions.js | Diff column click handlers; legend-item drag handlers |
prc-chart-builder/src/chart/block.json | legend.variation, divergingBar.secondary, map.bubble, legend.bubbleLegend, highlightedCategories, diff column header fields |
prc-chart-builder/includes/utils/class-table-export.php | New — CSV export utility (UTF-8 BOM, hidden column filtering) |
prc-chart-builder/includes/class-plugin-bootstrap.php | Load Table_Export |
prc-chart-builder/includes/class-json-ld.php | UTF-8 BOM on CSV stream; filter hidden columns in JSON-LD Dataset |
prc-chart-builder/src/controller/utils/csv-export.js | New — frontend CSV encoder module |
prc-chart-builder/src/controller/view.js | Use new CSV encoder + BOM |
prc-chart-builder/src/controller/class-controller.php | Localize CBSA validation schema; localize new style fields |
prc-block-library/src/table/utils/validation.ts, block-attributes.ts, elements/context-menu.tsx | 'cbsa' column data type + CBSA code validation set |
prc-charting-utilities types (dataRender, map, legend, divergingBar, baseConfig) | New fields; new shared helpers (resolveCategoryColor et al.) |
prc-charting-library/class-prc-charting-library.php | Version sync |
prc-chart-builder/prc-chart-builder.php | Version header + PRC_CHART_BUILDER_VERSION → 3.9.0 |
prc-chart-builder/tests/unit/csv-export.test.js, merge-legend-category-order.test.js, resolve-category-color.test.js, tests/fixtures/csv-export-torture-test.csv, tests/test-table-export.php | New test coverage |
Upgrade Notes
- No database migrations. All new attributes are additive with safe defaults.
- No breaking changes to published content. Existing charts continue to render identically. New features are opt-in per chart.
- Bubble maps and CBSA map require a rebuild of
@prc/charting-libraryso the new lazy-loadedAlbersUSACBSAchunk and updated map renderers ship. Runnpx turbo build --filter=@prc/charting-library --filter=@prc/chart-builder --filter=@prc/block-librarybefore deploying. - CSV download responses now include a UTF-8 BOM. Downstream consumers that pipe CSVs through their own decoder (Excel, Google Sheets, Numbers, most BI tools) auto-detect the BOM and handle it correctly. Hand-rolled string-comparison parsers will need to strip a leading
\xEF\xBB\xBFbyte if they don't already. - Hidden columns no longer leak into CSV downloads or JSON-LD
Datasetrows. This is a deliberate content-correctness fix. If a downstream consumer was relying on hidden table columns being present in the CSV payload, surface the columns explicitly viacolumnMeta.hidden = falseinstead of relying on the legacy leak. - Highlighted categories require the chart to be in
HIGHLIGHTABLE_CHART_TYPES. Pie, treemap, and maps are excluded by design — those chart types use per-element overrides instead. - Bubble maps are gated to
map-usa,map-usa-counties,map-usa-cbsa, andmap-world.map-usa-blockandmap-usa-hexstay choropleth-only. - Detached legend is an opt-in mode (
legend.variation: 'detached'). Existing charts default to'grouped'and render unchanged. - Diverging bar secondary overlay is inactive by default (
secondary.active: false). Existing diverging-bar charts render unchanged. - Diff column header style separation. Charts with an existing customized diff column header may pick up slightly different defaults the first time the popover is opened (header now reads
headerFontWeight/headerFontStyle/headerFontFamilyseparately from cell defaults). Open and re-save the chart if a header drift is observed.
Tracked Issues
#3196— USA CBSA map + bubble-style geographic maps#3263— Highlighted category styling (chart-builder + charting-library + charting-utilities)#3108— Diff column custom styling & click-to-edit#3123— Detached legend mode (supersedes experimental#3120/#3119/#3121work)#3119/#3121/#3122— Diverging bar secondary ghost overlay#3279— CSV export UTF-8 BOM + hidden column filtering (Table_Exportutility)#3166— Legend order persistence on neutral column / secondary overlay / treemap / sankey toggles#3222— 3.9.0 tracking issue
Changesets
.changeset/cbsa-bubble-maps-3196.md.changeset/diff-column-styling-click-to-edit.md.changeset/highlighted-category-style-chart-builder.md.changeset/highlighted-category-style-charting-library.md.changeset/highlighted-category-style-charting-utilities.md.changeset/issue-3123-detached-legend.md.changeset/legend-order-merge-on-toggle.md