Skip to content

Charts: add HeatmapChart (CHARTS-218)#49855

Draft
adamwoodnz wants to merge 18 commits into
trunkfrom
charts-218-heatmap-chart
Draft

Charts: add HeatmapChart (CHARTS-218)#49855
adamwoodnz wants to merge 18 commits into
trunkfrom
charts-218-heatmap-chart

Conversation

@adamwoodnz

@adamwoodnz adamwoodnz commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Fixes CHARTS-218

Why

@automattic/charts had no way to visualize a two-dimensional matrix of values on a color scale. CHARTS-218 needs a heatmap — in particular a GitHub-contribution-style "Posting Activity" calendar — for product widgets, including a compact variant for short widgets. This adds that chart type using the same theming, accessibility, and composition patterns as the rest of the library, so it drops into existing consumers without new conventions.

Proposed changes

  • Add a new responsive HeatmapChart (custom-SVG over @visx/heatmap's HeatmapRect).
  • Generic matrix modeldata: HeatmapColumn[] (columns of cells) plus optional rowLabels; null values render as empty cells.
  • Color scale — mirrors GeoChart's approach: a scaleLinear from a lightened theme primary (lightenHexColor(primary, 0.8)) to the full theme primary across the value extent, with the primary resolved to hex via getElementStyles + normalizeColorToHex/resolveCssVariable. Empty cells use the grey var(--jp-gray-0, #f6f7f7) fill.
  • compact mode — square cells, hides in-cell values, tightens the cell gap, and thins axis labels.
  • Composition legend<HeatmapChart.Legend /> renders a Less … More color-scale swatch row (composition-only; no showLegend/legend props).
  • buildCalendarHeatmapData() — a pure helper that buckets a flat DataPointDate[] series into weekday-row × week-column data with month labels, for the contribution-calendar layout.
  • Accessibilityrole="grid" with a grid › row › gridcell hierarchy, per-cell aria-label accessible names, full 2D arrow-key navigation, and hover tooltips (@visx/tooltip).
  • Empty state uses the Center layout primitive (CHARTS-212).
  • Stories (.stories.tsx + .docs.mdx + .api.mdx), the library Introduction catalog entry, and the README import example.

Screenshots

Default — matrix with month/quarter top labels, weekday rows, value-scaled cells, in-cell numbers.

heatmap-default

Compact — square cells with values hidden, for short widgets.

heatmap-compact

Calendar — GitHub-contribution-style layout via buildCalendarHeatmapData.

heatmap-calendar

With composition legend — the Less … More scale renders below the chart.

heatmap-legend

Related product discussion/links

Does this pull request change what data or activity we track or use?

No. This adds a presentational chart component; it collects and tracks nothing.

Testing instructions

  • From projects/js-packages/charts, run pnpm run typecheck (clean) and pnpm run test (911 passing — chart render/labels/values/compact, tooltips + a11y, legend, color utilities, calendar helper).
  • Run pnpm run storybook and open JS Packages / Charts Library / Charts / Heatmap Chart:
    • Default — matrix with month/quarter top labels, weekday left labels, light→primary cells by value, in-cell numbers, grey empties.
    • Compact — square cells, values hidden, tighter cells, thinned labels.
    • Calendar — contribution-style layout from a flat date series.
    • WithCompositionLegend — the Less … More legend renders below the chart.
    • Change the theme control and confirm cell color follows the selected primary; ErrorStates shows the centered empty message.

@github-actions

github-actions Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.

  • To test on WoA, go to the Plugins menu on a WoA dev site. Click on the "Upload" button and follow the upgrade flow to be able to upload, install, and activate the Jetpack Beta plugin. Once the plugin is active, go to Jetpack > Jetpack Beta, select your plugin (Jetpack or WordPress.com Site Helper), and enable the charts-218-heatmap-chart branch.
  • To test on Simple, run the following command on your sandbox:
bin/jetpack-downloader test jetpack charts-218-heatmap-chart
bin/jetpack-downloader test jetpack-mu-wpcom-plugin charts-218-heatmap-chart

Interested in more tips and information?

  • In your local development environment, use the jetpack rsync command to sync your changes to a WoA dev blog.
  • Read more about our development workflow here: PCYsg-eg0-p2
  • Figure out when your changes will be shipped to customers here: PCYsg-eg5-p2

@github-actions

github-actions Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Thank you for your PR!

When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:

  • ✅ Include a description of your PR changes.
  • ✅ Add a "[Status]" label (In Progress, Needs Review, ...).
  • ✅ Add testing instructions.
  • ✅ Specify whether this PR includes any changes to data or privacy.
  • ✅ Add changelog entries to affected projects

This comment will be updated as you work on your PR and make changes. If you think that some of those checks are not needed for your PR, please explain why you think so. Thanks for cooperation 🤖


Follow this PR Review Process:

  1. Ensure all required checks appearing at the bottom of this PR are passing.
  2. Make sure to test your changes on all platforms that it applies to. You're responsible for the quality of the code you ship.
  3. You can use GitHub's Reviewers functionality to request a review.
  4. When it's reviewed and merged, you will be pinged in Slack to deploy the changes to WordPress.com simple once the build is done.

If you have questions about anything, reach out in #jetpack-developers for guidance!

@github-actions github-actions Bot added the [Status] Needs Author Reply We need more details from you. This label will be auto-added until the PR meets all requirements. label Jun 23, 2026
@adamwoodnz adamwoodnz added [Status] In Progress and removed [Status] Needs Author Reply We need more details from you. This label will be auto-added until the PR meets all requirements. labels Jun 23, 2026
@adamwoodnz adamwoodnz self-assigned this Jun 23, 2026
@adamwoodnz adamwoodnz marked this pull request as draft June 23, 2026 10:21
@adamwoodnz

Copy link
Copy Markdown
Contributor Author

@copilot review this PR. Leave your feedback as review comments only — do NOT push commits, apply suggestions, or modify the branch in any way. I will make all code changes myself.

@jp-launch-control

jp-launch-control Bot commented Jun 23, 2026

Copy link
Copy Markdown

Code Coverage Summary

7 files are newly checked for coverage. Only the first 5 are listed here.

File Coverage
projects/js-packages/charts/src/charts/heatmap-chart/private/heatmap-legend.tsx 8/9 (88.89%) 💚
projects/js-packages/charts/src/charts/heatmap-chart/heatmap-chart.tsx 107/117 (91.45%) 💚
projects/js-packages/charts/src/charts/heatmap-chart/private/build-calendar-data.ts 40/43 (93.02%) 💚
projects/js-packages/charts/src/charts/heatmap-chart/private/use-heatmap-colors.ts 35/36 (97.22%) 💚
projects/js-packages/charts/src/charts/heatmap-chart/index.ts 0/0 (—%) 🤷

Full summary · PHP report · JS report

Copilot AI commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

@copilot review this PR. Leave your feedback as review comments only — do NOT push commits, apply suggestions, or modify the branch in any way. I will make all code changes myself.

Here's my review of the HeatmapChart PR. All feedback is purely observational — no code changes made.


🔴 Missing public type exports

HeatmapTooltipData and CalendarHeatmapResult are exported from src/charts/heatmap-chart/index.ts but not re-exported from the package root src/index.ts.

Any TypeScript consumer who uses renderTooltip needs to type their callback, but can't import type { HeatmapTooltipData } from '@automattic/charts' — they'd have to deep-import from @automattic/charts/src/charts/heatmap-chart. Same problem for CalendarHeatmapResult. Both should be added to the root src/index.ts export block alongside HeatmapChartProps.


🔴 Keyboard navigation is 1D on a role="grid"

The heatmap reuses useKeyboardNavigation, which only handles ArrowLeft / ArrowRight. However, WAI-ARIA 1.2 §6.8 specifies that role="grid" must support all four arrow keys: ArrowUp / ArrowDown move between rows, ArrowLeft / ArrowRight move between columns (or cells within a row).

Currently, pressing ArrowDown on a focused heatmap scrolls the page instead of moving to the next row. For a calendar layout with 7 rows × 52 columns this makes keyboard-only navigation impractical.

The flat selectedIndex = column * rows + row encoding is already in place; the hook just needs ArrowUp / ArrowDown cases added (or the heatmap could use a bespoke 2D keyboard handler).


🟡 ARIA grid hierarchy missing role="row"

The structure is:

<div role="grid">            ← outer div
  <svg>
    <g>                      ← HeatmapRect render group
      <g role="gridcell">    ← individual cells

WAI-ARIA requires gridcell elements to be owned by row elements, which are owned by the grid. Without role="row" wrappers, screen readers may not present the grid/row/cell relationship correctly. Rows are natural here — one <g role="row"> per row index would satisfy the requirement. Alternatively, aria-owns on the row containers can fulfill the ownership requirement, but explicit DOM structure is preferred.


🟡 Duplicate isPresent predicate

isPresent is defined identically in both heatmap-chart.tsx (line 49) and use-heatmap-colors.ts (line 7). The version in use-heatmap-colors.ts could be exported and imported in heatmap-chart.tsx to avoid the duplication.


🟡 Compact mode may suppress month labels in calendar layout

The label visibility filter ! compact || columnIndex % 2 === 0 hides odd-indexed column labels in compact mode. When using buildCalendarHeatmapData, month labels only appear on the first column of each month — but if that column happens to be at an odd index, the month name gets silently dropped in compact mode. For example, February starting at week 5 (index 4, even — shown) vs. March starting at week 9 (index 8, even — shown) works fine, but a calendar starting mid-week could push a month to an odd-indexed column. Filtering based on the label content (e.g., only suppress blank labels) or on a minimum pixel spacing would be more robust.


🟡 getCount returns 0 for null cells

const getCount = (cell: HeatmapCell): number =>
    isPresent(cell.value) ? (cell.value as number) : 0;

@visx/heatmap uses count() internally to compute the color fill of each bin via its own scale. By mapping null0, the internal visx color scale treats null cells as minimum-valued cells rather than absent ones. The fill override in the <rect> (present ? colorFor(value) : emptyColorHex) correctly overrides this visually, but it means the visx-internal color scale is computing colors for null cells that are never used. A value of NaN or keeping the null would be a clearer signal, though in practice this causes no visible bug.


🔵 Hardcoded focus-ring color

stroke={selectedIndex === cellFlatIndex ? '#005fcc' : 'none'}

The focus ring uses a hardcoded hex. Other charts in the library use #005fcc too (e.g., bar-chart), so this is consistent — but it would be worth tracking as a token when the design system exposes a focus-color variable (e.g., var(--wp-admin-theme-color, #005fcc)).


🔵 Type cast via anonymous shape instead of HeatmapCell

const value = (cell.bin as { value: number | null }).value;

cell.bin is the original HeatmapCell passed into @visx/heatmap, so (cell.bin as HeatmapCell).value would be more type-safe and use the already-imported type.


Summary: The two highest-priority items are the missing HeatmapTooltipData export (breaks TS consumers) and the 1D keyboard navigation on a 2D role="grid" (accessibility gap). The ARIA role="row" issue is the other meaningful correctness concern. Everything else is low-risk.

@adamwoodnz adamwoodnz force-pushed the charts-218-heatmap-chart branch from 573c65d to 401c416 Compare June 23, 2026 10:45
@adamwoodnz

Copy link
Copy Markdown
Contributor Author

@copilot review this PR. Leave your feedback as review comments only — do NOT push commits, apply suggestions, or modify the branch in any way. I will make all code changes myself.

Copilot AI commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

@copilot review this PR. Leave your feedback as review comments only — do NOT push commits, apply suggestions, or modify the branch in any way. I will make all code changes myself.

The only change since the previous review is the pnpm-lock.yaml regeneration (401c416) — no tarball: entries remain, so the CI "Lock files are up to date" failure is resolved.

The heatmap source is otherwise unchanged. All items from the previous review still stand:

🔴 Still open:

  • HeatmapTooltipData and CalendarHeatmapResult not exported from the package root src/index.ts (line 9 exports only HeatmapChartProps, HeatmapColumn, HeatmapCell)
  • Keyboard navigation is 1D — useKeyboardNavigation handles only ArrowLeft/ArrowRight; ArrowUp/ArrowDown scroll the page rather than moving between rows

🟡 Still open:

  • role="gridcell" elements not wrapped in role="row" intermediaries as required by WAI-ARIA
  • isPresent duplicated in heatmap-chart.tsx (line 49) and use-heatmap-colors.ts (line 7)
  • Compact mode skips odd-indexed column labels (columnIndex % 2 === 0), which can silently drop month labels in calendar layout
  • getCount maps null0, treating empty cells as zero-valued in the visx internal scale

🔵 Still open:

  • Hardcoded focus-ring #005fcc (consistent with other charts, low priority)
  • cell.bin as { value: number | null } could use the already-imported HeatmapCell type

@adamwoodnz

Copy link
Copy Markdown
Contributor Author

Thanks @copilot — assessed all eight. Addressed in f2036c2ce1:

  • 🔴 Missing public type exports — Fixed. HeatmapTooltipData and CalendarHeatmapResult are now re-exported from the package root src/index.ts.
  • 🔴 1D keyboard nav on role="grid" — Fixed. Replaced the shared useKeyboardNavigation (Left/Right only) with a bespoke 2D handler: ArrowUp/Down move by row, ArrowLeft/Right by column, all four preventDefault() (no page scroll), with clamping at edges; Escape clears, Tab bubbles out.
  • 🟡 Missing role="row" hierarchy — Fixed. Cells are now rendered row-major inside <g role="row" aria-rowindex> under the role="grid" container, with per-cell id + aria-colindex, and aria-rowcount/aria-colcount/aria-activedescendant on the grid.
  • 🟡 Duplicate isPresent — Fixed. Now exported from use-heatmap-colors.ts and imported in the chart; the local copy is gone.
  • 🟡 Compact suppressing month labels — Fixed. Dropped the index-parity thinning for column labels (they're already sparse — only month/quarter starts have text), so a month label can't be hidden by landing on an odd column. Row-label thinning (dense weekdays) is kept.
  • 🔵 cell.bin cast — Fixed. Now ( cell.bin as HeatmapCell ).value.

Two I'm intentionally leaving, with reasoning:

  • 🟡 getCount returns 0 for null cells — The chart does not pass a colorScale/opacityScale to HeatmapRect; fill is computed entirely in the cell renderer (present ? colorFor(value) : emptyColorHex). So visx's internal count() result is never consumed for color — the 0 is inert. Switching to NaN risks visx's geometry math; keeping 0 is the safe no-op. No visible or latent bug.
  • 🔵 Hardcoded focus-ring #005fcc — Intentionally consistent with the rest of the library (e.g. bar-chart uses the same focus hex). I'd rather keep it uniform than introduce a one-off token here; worth a library-wide change when the DS exposes a focus-color variable.

Re-requesting your review of the above.

@adamwoodnz

Copy link
Copy Markdown
Contributor Author

@copilot review this PR. Leave your feedback as review comments only — do NOT push commits, apply suggestions, or modify the branch in any way. I will make all code changes myself.

Copilot AI commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

@copilot review this PR. Leave your feedback as review comments only — do NOT push commits, apply suggestions, or modify the branch in any way. I will make all code changes myself.

All items from the previous two rounds are resolved in f2036c2c. Three new observations below, all low priority.


✅ Previously raised items — all resolved

Item Status
HeatmapTooltipData / CalendarHeatmapResult missing from root src/index.ts ✅ Fixed
1D keyboard nav on role="grid" ✅ Fixed — bespoke 2D handler, all four arrows + Escape + Tab
role="gridcell" not owned by role="row" ✅ Fixed — row-major <g role="row" aria-rowindex> + per-cell id/aria-colindex + aria-activedescendant
Duplicate isPresent ✅ Fixed — exported from use-heatmap-colors.ts, local copy removed
Compact mode silently dropping month labels on odd columns ✅ Fixed — index-parity filter on column labels removed
cell.bin cast to anonymous shape ✅ Fixed — now cell.bin as HeatmapCell

🟡 Dead isNavigating state

const [ , setIsNavigating ] = useState( false );

The reader is discarded at declaration; the setter is called four times but has no consumer. The state transitions are no-ops. This is harmless but leaves confusing dead code — the state can be removed entirely along with the four setIsNavigating(...) calls and the setIsNavigating dependency in the useCallback dep-arrays.


🟡 aria-activedescendant persists after blur

onChartBlur resets isNavigating (which is dead) but does not clear selectedIndex, so aria-activedescendant still references a cell element when the grid has lost focus. The WAI-ARIA authoring practices recommend omitting or clearing aria-activedescendant when the composite widget is not focused, to avoid AT announcing a focused descendant for a blurred container. Adding setSelectedIndex( undefined ) to onChartBlur would fix this.


🔵 First arrow-key lands on row 1, not row 0

On fresh focus selectedIndex is undefined, so currentIndex = selectedIndex ?? 0 = 0 and the key handler computes the next cell from index 0. This means:

  • ArrowDown → row 1, col 0 (skips cell 0 entirely)
  • ArrowRight → row 0, col 1 (skips cell 0 entirely)
  • ArrowUp → clamps at row 0, col 0 (only way to land on the first cell)

Conventional grid behaviour is that the first arrow press selects the first cell (index 0) and subsequent presses move from there. The fix is to initialise selectedIndex to 0 in useState, or to treat the "no selection" state as "about to select first cell" rather than "already at first cell" — e.g. set selectedIndex to 0 in onChartFocus before the keydown logic runs.

Copilot AI commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

@copilot review this PR. Leave your feedback as review comments only — do NOT push commits, apply suggestions, or modify the branch in any way. I will make all code changes myself.

All six items from the previous review are addressed in f2036c2c. A few minor observations from this pass — nothing blocking:


🟡 isNavigating is dead state

const [ , setIsNavigating ] = useState( false );

The state value is always discarded, so the three setIsNavigating(...) calls in onChartKeyDown, onChartBlur, and the Tab branch only trigger unnecessary re-renders. The visual effect that drove this state (likely a CSS class for the navigating state) is gone. Safe to remove useState and all three call sites.


🔵 onChartFocus is a no-op

const onChartFocus = useCallback( () => {
    // leave selectedIndex undefined until first arrow key press
}, [] );

The desired behaviour (do nothing) is already achieved by the initial useState<number | undefined>(). The handler allocates a stable function reference for no reason. Could be removed from onFocus (or the attribute omitted entirely).


🔵 Test coverage gap for ArrowUp / ArrowLeft

The new keyboard test covers ArrowDown then ArrowRight. ArrowUp, ArrowLeft, edge-clamping (e.g. left at col 0 stays at col 0), and Escape clearing aria-activedescendant are untested. Low risk given the implementation is symmetric, but worth a line or two.


Summary: The structural ARIA fix (role="row" wrappers, aria-rowcount/aria-colcount/aria-activedescendant), 2D keyboard nav, type exports, and isPresent deduplication all look correct. The above are cleanup-level items only.

@adamwoodnz

Copy link
Copy Markdown
Contributor Author

@claude please review this PR. Copilot's rounds are complete — all its findings were addressed (2D keyboard nav + ARIA grid/row hierarchy, root type exports, isPresent dedupe, compact label fix, and cleanup of dead nav state). Full suite is green (26 heatmap tests) and typecheck is clean. Note: the branch is a few commits behind trunk but MERGEABLE — an automated rebase isn't possible in this worktree, so a manual rebase may be wanted before merge.

@adamwoodnz adamwoodnz added the [Status] Needs Review This PR is ready for review. label Jun 23, 2026
@adamwoodnz adamwoodnz marked this pull request as ready for review June 23, 2026 20:29
@claude

claude Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Claude finished @adamwoodnz's task in 37s —— View job


Re-review: HeatmapChart (CHARTS-218) — ad959056

  • Verify first-keypress lands on (0,0)
  • Verify legend spans data colors
  • Verify ragged-column guard
  • Verify chartRef removed

All four follow-ups are confirmed resolved in the code (read statically; taking the reported "26/26 green + clean typecheck" at face value).

Item Verified
🟡 First arrow press lands on (0,0) heatmap-chart.tsx:135-138if ( selectedIndex === undefined ) { setSelectedIndex( 0 ); return; } short-circuits before the move logic, so the first press enters onto cell 0 and subsequent presses move from it (WAI-ARIA grid entry).
🟡 Legend spans the data colors heatmap-legend.tsx:36 — every swatch is now colorFor( value ); the index === 0 ? emptyColorHex special-case is gone, so the gradient runs colorFor(min)colorFor(max) and the lightest data color (lightColorHex) is the first swatch. Matches the cells.
🔵 Ragged-column guard heatmap-chart.tsx:347-349if ( ! cell ) { return null; } guards before the cell.bin as HeatmapCell access, so unequal-length columns can't throw on undefined.
🔵 Unused chartRef ✅ Gone — declaration, attribute, and the useRef import all removed (heatmap-chart.tsx:7 now imports only createContext, useCallback, useContext, useMemo, useState).

Two small consistency notes while I was in the file — both pre-existing, neither blocking:

  • The flat-index encoding is consistent end-to-end now: keyboard nav uses col * rows + row (:153), aria-activedescendant decodes with the same rows divisor (:279-284), and the cell renderer computes cell.column * rows + cell.row (:352) — all keyed off the global rows = Math.max(0, …) (:105), so the ragged-column guard and the nav math agree.
  • getCount still maps null0 (:51-52), which remains inert since no colorScale/opacityScale is passed to HeatmapRect — exactly as you reasoned earlier. No action needed.

Nothing further from me — this is ship-ready. Re: rebase, agreed it's the clean final step; I can't perform branch operations from here, so a manual rebase onto trunk before merge is the path.
· charts-218-heatmap-chart

@adamwoodnz

Copy link
Copy Markdown
Contributor Author

Review cycle complete ✅

Final reviewer: @claude (Copilot rounds complete first).

Addressed across the cycle:

  • CI: regenerated pnpm-lock.yaml without registry-proxy tarball: URLs (fixed "Lock files are up to date").
  • Copilot (all resolved): root type exports (HeatmapTooltipData, CalendarHeatmapResult); full 2D keyboard navigation on role="grid"; ARIA grid › row › gridcell hierarchy + aria-rowcount/aria-colcount/aria-activedescendant; deduped isPresent; compact no longer drops month labels; tightened cell.bin cast; removed dead isNavigating state and no-op focus handler; added arrow-key tests.
  • @claude (all resolved): first arrow press lands on cell (0,0); legend gradient spans the data colors (colorFor(min)…colorFor(max)); ragged-column null guard; removed unused chartRef.

CI: green / pending (no failures); the one earlier failure (lock files) is fixed.
Resolved review items: all Copilot + @claude findings addressed (0 open addressable comments).
Unaddressed human comments: 0.

Status: clean — ready for human review and the merge decision.

Note: the branch is MERGEABLE but a few commits behind trunk; an automated rebase isn't possible in this worktree (Jetpack's hooks / git's redundant-commit detection corrupt the multi-commit replay), so a manual rebase onto trunk before merge is recommended. Tests (26) green, typecheck clean, and all stories verified in Storybook.

@claude

claude Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Claude finished @adamwoodnz's task in 46s —— View job


Final verification ✅

I re-read the current branch and confirmed every item from both review cycles is genuinely resolved in the code — not just claimed.

  • Read core component, legend, color hook, root exports
  • Cross-check each resolved item against the source
  • Post final sign-off
Item Verified in code
Root type exports (HeatmapTooltipData, CalendarHeatmapResult) src/index.ts:9-15
2D keyboard nav (4 arrows + Esc + Tab, clamped, preventDefault) heatmap-chart.tsx:113-156
First arrow press lands on (0,0) ✅ early-return at :135-138 selects index 0
ARIA grid › row › gridcell + aria-rowcount/colcount/activedescendant :277-285, :344, :366-368
isPresent dedupe ✅ exported use-heatmap-colors.ts:7, imported :28
cell.bin typed cast :350 cell.bin as HeatmapCell
Legend gradient spans data colors (no grey-as-low-end) heatmap-legend.tsx:36 colorFor( value ), special-case gone
Ragged-column null guard :347 if ( ! cell ) return null
Unused chartRef removed ✅ no chartRef/useRef remain

One side-effect worth flagging (not a blocker): dropping the index === 0 grey from the legend means the empty-cell color is no longer represented in the legend at all. That's the right call for a Less … More value gradient — the legend now matches the cells honestly — but if a future design wants to communicate "no data = grey" to readers, that'd need a separate, explicitly-labelled swatch rather than reintroducing it into the gradient.

Status: I concur — clean and ready for human review + the merge decision. No open addressable items from either review round.

Rebase: Confirmed I can't perform branch operations (merge/rebase) from here. The branch is MERGEABLE but ~5 commits behind trunk, so a manual rebase onto trunk before merge remains the recommended path. CI being green/pending with the lock-file fix in place, nothing else stands in the way.
· charts-218-heatmap-chart

@adamwoodnz adamwoodnz force-pushed the charts-218-heatmap-chart branch from 6f25ce1 to 5c59dc6 Compare June 23, 2026 23:32
@adamwoodnz adamwoodnz changed the base branch from trunk to charts-212-add-a-center-layout-primitive June 23, 2026 23:32
@adamwoodnz adamwoodnz removed the [Status] Needs Review This PR is ready for review. label Jun 23, 2026
@adamwoodnz adamwoodnz marked this pull request as draft June 23, 2026 23:37
Base automatically changed from charts-212-add-a-center-layout-primitive to trunk June 24, 2026 03:31
adamwoodnz and others added 18 commits June 24, 2026 17:58
…CHARTS-218)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…legend steps guard, docs (CHARTS-218)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…sition legend fits below it (CHARTS-218)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…CHARTS-218)

The lockfile committed earlier was generated through a local registry proxy,
which records explicit tarball: URLs that a default-registry install (CI) does
not, failing the 'Lock files are up to date' check. Regenerated against the
npmjs registry so only the @visx/heatmap entry is added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…hierarchy, root type exports, dedupe (CHARTS-218)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s handler, add arrow-key tests (CHARTS-218)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…0,0), legend spans data colors, ragged-column guard, drop unused ref (CHARTS-218)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Compact mode now sizes cells to the smaller of the per-column/per-row fit so
they render as squares (matching the contribution-graph design) instead of
stretching to fill. Non-compact still fills the area.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e title tooltip (CHARTS-218)

The hover tooltip used raw clientX/clientY, but TooltipInPortal re-adds the
container offset, placing it far from the cursor — subtract containerBounds so
it lands at the pointer. Replace the per-cell SVG <title> (which also triggered
a native browser tooltip) with aria-label, keeping the accessible name without
the duplicate tooltip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…RTS-218)

Compact used a 14px top margin, too small for the 12px column label rendered at
y=-6, clipping it at the SVG top edge. The label band is the same size in
compact (only the cells shrink), so use a 20px top margin for both.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…CHARTS-218)

Replaces the ad-hoc empty-state div with the Center layout primitive (from
CHARTS-212 / #49164) so the 'No data available' message is centred on both axes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@adamwoodnz adamwoodnz force-pushed the charts-218-heatmap-chart branch from 5c59dc6 to ddba57e Compare June 24, 2026 06:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants