Guide

Chart Builder 3.9.0 — Release Notes

Release: Chart Builder 3.9.0 · PRC Platform 1.8 Work 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:

  1. 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.
  2. 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.
  3. 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.
  4. Detached legend mode — a new legend.variation: 'grouped' | 'detached' config that turns legend items into independently draggable annotations driven by underlying data.
  5. 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.
  6. CSV export hardening — UTF-8 BOM for Excel compatibility, hidden columns now filtered out of every export path, and a reusable Table_Export PHP utility shared across JSON_LD, the JSON-LD export controller, and view.js.
  7. 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.

  • Topologyplugins/prc-charting-library/src/lib/maps/usa-cbsa/topology.json, generated from the USDOT/Census CBSA boundaries and simplified to ~10% via mapshaper. Mirrors the existing usa-counties/topology.json layout (objects.cbsas keyed by 5-digit CBSA code).
  • Renderer — new AlbersUSACBSA.tsx component in prc-charting-library, structurally identical to AlbersUSACounties.tsx, registered via ChartBuilder.tsx's lazy-loaded map switch.
  • Variation templateplugins/prc-chart-builder/.shared/variation-templates/map-usa-cbsa.js ships ~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 to ALL_CHART_TYPES, MAP_CHART_TYPES, and BUBBLE_MAP_CHART_TYPES in chart-types.js, and to the Layout.type union 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, and map-world only. map-usa-block and map-usa-hex remain choropleth-only (their abstracted geometries don't have meaningful centroids).
  • Encoding — bubble radius is computed with d3.scaleSqrt so 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 with map.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.7 and 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), minRadius defaults to 4 so 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.format is a standard d3-format string (default ',.0f').
  • Inherits dragability — the new variant: 'radius' prop on StyledLegend skips the flex container but keeps the existing DraggableCore plumbing. Editors drag the radius legend like any other legend; offsets persist to legend.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:

HelperRole
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/:

PanelHookStorage
DiffColumnHeaderPaneluseDiffColumnHeaderCustomizationsdiffColumn.columnHeader, diffColumn.style.headerFill, diffColumn.style.headerFontWeight, diffColumn.style.headerFontStyle, diffColumn.style.headerFontFamily, diffColumn.style.headerTextOutline
DiffColumnLabelPaneluseDiffColumnLabelCustomizationsdiffColumn.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:

VariationBehavior
'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 — when legend.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 on legend.variation. Grouped mode is unchanged. Detached mode applies per-item style overrides (markerStyle, fontSize, color, fontWeight) and wraps each item in DraggableCore in the editor.
  • LegendSwatch — new custom SVG component (replacing the visx LegendShape) that renders true SVG rect / circle / line shapes 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.jsx to switch a chart's legend between grouped and detached.
  • Drag handlerswpEditorFunctions.legendItems.onItemDragStart / Move / End persist each item's offsets back to legend.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.detached field 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, markerStyle fields — 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:

  1. Excel was double-decoding UTF-8 because the CSV had no BOM, so non-ASCII characters in titles, sources, or cell text arrived garbled.
  2. 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 csvUrl payload 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_BOM constant"\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: prefer columnMeta[].hidden, fall back to legacy hiddenColumns.
  • 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 before Dataset JSON-LD is emitted (hasPart rows 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_BOM constant ('\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 for Table_Export::get_hidden_column_indices and Table_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 Dataset rows 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.js was 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 reorganizationchart-controls.jsx, color-controls.jsx, data-controls.jsx, and legend-controls.jsx were 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.js updated 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.js runtime-merges the new bubble / bubbleLegend / secondary / legend.variation defaults so charts authored before 3.9.0 pick up the new fields without an edit.
  • Charting library exportsLegendSwatch, BubbleRadiusLegend, and the four resolveCategoryColor / resolveCategoryOpacity / withCategoryOpacity / legendCategoryShapeStyle helpers are added to @prc/charting-library / @prc/charting-utilities public exports.
  • get-available-legend-categories.js refactored 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 bootstraputils/class-table-export.php is now loaded alongside the other includes in Plugin_Bootstrap::load_dependencies().
  • Version constants alignedPRC_CHART_BUILDER_VERSION bumped to 3.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)

AreaChange
prc-charting-library/src/lib/maps/usa-cbsa/topology.jsonNew — simplified USDOT/Census CBSA boundaries
prc-charting-library/src/lib/Components/maps/AlbersUSACBSA.tsxNew — CBSA map renderer
prc-charting-library/src/lib/Components/BubbleRadiusLegend.tsxNew — nested-circle radius legend for bubble maps
prc-charting-library/src/lib/Components/maps/AlbersUSA.tsx, AlbersUSACounties.tsx, World.tsxThree-layer SVG refactor (polygons → bubbles → labels); bubble mode rendering
prc-charting-library/src/lib/Components/Legend.tsxNew variant: 'standard' | 'radius' prop; detached overlay mode
prc-charting-library/src/lib/Components/ClickableLegend.tsxGrouped vs. detached branching; per-item DraggableCore; LegendSwatch integration
prc-charting-library/src/lib/Components/LegendSwatch.tsxNew — 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.tsxAdopt resolveCategoryColor / resolveCategoryOpacity helpers
prc-charting-library/src/lib/Components/DiffColumn.tsxCustom header / cell styling; click-to-edit pass-through
prc-charting-library/src/lib/Components/DivergingBar*.tsxSecondary ghost overlay rendering
prc-chart-builder/.shared/variation-templates/map-usa-cbsa.jsNew — CBSA variation template
prc-chart-builder/.shared/variation-templates/index.jsRegister CBSA variation export
prc-chart-builder/src/controller/variations.jsRegister cbUSACBSAMap variation + VARIATION_TO_LAYOUT_TYPE
prc-chart-builder/src/controller/Edit.jsxCHART_TYPE_TO_LAYOUT_TYPE for CBSA + bubble-mode default offsets
prc-chart-builder/src/controller/icons.jsUSACBSAMap icon
prc-chart-builder/src/chart/utils/chart-types.jsNew BUBBLE_MAP_CHART_TYPES, HIGHLIGHTABLE_CHART_TYPES, SUPPLEMENTAL_COLUMN_CHART_TYPES
prc-chart-builder/src/chart/utils/get-available-legend-categories.jsPer-chart-type derivation map
prc-chart-builder/src/chart/utils/merge-legend-category-order.jsNew — custom-order-preserving merge
prc-chart-builder/src/chart/utils/get-config.jsRuntime-merge new defaults
prc-chart-builder/src/chart/utils/get-copyable-style-attributes.jsInclude new style fields in copy/paste
prc-chart-builder/src/chart/edit/color-controls.jsxHighlighted categories section
prc-chart-builder/src/chart/edit/data-controls.jsxMap Style + bubble radius / legend controls
prc-chart-builder/src/chart/edit/legend-controls.jsxGrouped / detached layout toggle
prc-chart-builder/src/chart/edit/diff-column-controls.jsxChart-level diff column styling defaults
prc-chart-builder/src/chart/edit/diverging-bar-control.jsxSecondary ghost overlay controls
prc-chart-builder/src/chart/edit/map-controls.jsxBubble-specific styling controls
prc-chart-builder/src/chart/edit/node-controls.jsxDot plot nodes.pointStroke control
prc-chart-builder/src/chart/edit/popover/panels/DiffColumnHeaderPanel.jsxNew popover panel
prc-chart-builder/src/chart/edit/popover/panels/DiffColumnLabelPanel.jsxNew popover panel
prc-chart-builder/src/chart/edit/popover/panels/LegendItemPanel.jsxDetached-mode offsets, font weight
prc-chart-builder/src/chart/edit/popover/panels/LabelPanel.jsx, AnnotationPanel.jsx, TickLabelPanel.jsx, TextStyleControls.jsxAdopt diff-column-aware shared text controls
prc-chart-builder/src/chart/edit/popover/hooks/useDiffColumnHeaderCustomizations.js, useDiffColumnLabelCustomizations.js, useLegendItemCustomizations.js, useAnnotationCustomizations.jsNew / updated customization hooks
prc-chart-builder/src/chart/edit/wp-editor-functions.jsDiff column click handlers; legend-item drag handlers
prc-chart-builder/src/chart/block.jsonlegend.variation, divergingBar.secondary, map.bubble, legend.bubbleLegend, highlightedCategories, diff column header fields
prc-chart-builder/includes/utils/class-table-export.phpNew — CSV export utility (UTF-8 BOM, hidden column filtering)
prc-chart-builder/includes/class-plugin-bootstrap.phpLoad Table_Export
prc-chart-builder/includes/class-json-ld.phpUTF-8 BOM on CSV stream; filter hidden columns in JSON-LD Dataset
prc-chart-builder/src/controller/utils/csv-export.jsNew — frontend CSV encoder module
prc-chart-builder/src/controller/view.jsUse new CSV encoder + BOM
prc-chart-builder/src/controller/class-controller.phpLocalize 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.phpVersion sync
prc-chart-builder/prc-chart-builder.phpVersion header + PRC_CHART_BUILDER_VERSION3.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.phpNew 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-library so the new lazy-loaded AlbersUSACBSA chunk and updated map renderers ship. Run npx turbo build --filter=@prc/charting-library --filter=@prc/chart-builder --filter=@prc/block-library before 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\xBF byte if they don't already.
  • Hidden columns no longer leak into CSV downloads or JSON-LD Dataset rows. 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 via columnMeta.hidden = false instead 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, and map-world. map-usa-block and map-usa-hex stay 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 / headerFontFamily separately 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 / #3121 work)
  • #3119 / #3121 / #3122 — Diverging bar secondary ghost overlay
  • #3279 — CSV export UTF-8 BOM + hidden column filtering (Table_Export utility)
  • #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

Was this helpful?