Skip to content

feat(chart): establish the official Kumo globe - #769

Merged
stritt merged 17 commits into
mainfrom
the-globe
Sep 14, 2026
Merged

stritt merged 17 commits into
mainfrom
the-globe

Conversation

@stritt

@stritt stritt commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

What changed

Adds a reusable GlobeMap to Kumo. The scope is the hatched globe without choropleth boundaries.

  • hatched Natural Earth land with no country borders
  • geographic markers with tooltips and optional click interactions
  • pointer and keyboard rotation plus optional auto-rotation
  • markers render above the outline and fade at the horizon
  • Kumo colors and light/dark mode support
  • no WebGL or ECharts required

The docs example uses Cloudflare availability locations and matches the globe style from the marketing site.

Review updates

  • updated the branch to current main
  • made informational markers non-interactive and exposed their data as an accessible list
  • added keyboard rotation instructions and arrow-key controls
  • paused auto-rotation while focus is within the globe so focused markers stay mounted
  • renamed one-time rotation to defaultRotation
  • renamed landDotSpacing to landHatchSpacing
  • renamed the callback to onUserRotationChange to clarify that it reports pointer and keyboard changes
  • hardened primary-pointer capture, cancellation, and lost-capture handling
  • positioned focus tooltips from rendered marker bounds
  • clamped unsafe rendering inputs
  • moved markers above the globe outline and faded them near the horizon

Performance

  • memoizes the projection, sphere, graticule, and expensive land hatch path
  • auto-rotation is throttled to 30 FPS and pauses offscreen, during drag, and during focus
  • pointer movement is coalesced to one update per animation frame
  • respects reduced motion
  • no new dependencies

Testing

  • added rendering, density, drag, clipping, marker layering and fading, marker activation, informational-marker semantics, and keyboard rotation coverage

  • package typecheck, lint, full unit suite, production build, publint, and ATTW pass locally

  • Reviews

  • bonk has reviewed the change

  • automated review not possible because: the globe needs final interactive and visual review

  • Tests

  • Tests included/updated

  • Automated tests not possible - manual testing has been completed as follows: not applicable

  • Additional testing not necessary because: not applicable

@pkg-pr-new

pkg-pr-new Bot commented Sep 1, 2026

Copy link
Copy Markdown
npm i https://pkg.pr.new/@cloudflare/kumo@769

commit: a83e99d

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Docs Preview

View docs preview

Commit: 096afbd

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor
Visual Regression Report — 31 changed, 14 unchanged

31 screenshot(s) with visual changes:

Button / Variant: Primary

473 px (0.53%) changed

Before After Diff
Before After Diff

Button / Variant: Secondary Destructive

1,056 px (1.19%) changed

Before After Diff
Before After Diff

Button / Loading State

15 px (0.02%) changed

Before After Diff
Before After Diff

Button / Title

1,136 px (1.28%) changed

Before After Diff
Before After Diff

Button / Link as Button

449 px (0.51%) changed

Before After Diff
Before After Diff

Button / Link with Tooltip

579 px (0.65%) changed

Before After Diff
Before After Diff

Dialog / Dialog With Actions

352 px (0.4%) changed

Before After Diff
Before After Diff

Dialog / Dialog Basic

404 px (0.46%) changed

Before After Diff
Before After Diff

Dialog / Dialog Alert

914 px (1.03%) changed

Before After Diff
Before After Diff

Dialog / Dialog Confirmation

1,228 px (1.39%) changed

Before After Diff
Before After Diff

Dialog / Custom Max Width

1,043 px (1.18%) changed

Before After Diff
Before After Diff

Dialog / Dialog With Dropdown

480 px (0.54%) changed

Before After Diff
Before After Diff

Dialog (Open)

0 px (0%) changed

Before After Diff
Before After Diff

Select / Select Basic

91 px (0.1%) changed

Before After Diff
Before After Diff

Select / Select Placement

4,084 px (2.82%) changed

Before After Diff
Before After Diff

Select / Select Dynamic Placement

2,420 px (2.33%) changed

Before After Diff
Before After Diff

Select / Select Without Label

664 px (0.75%) changed

Before After Diff
Before After Diff

Select / Select With Description

1,688 px (1.62%) changed

Before After Diff
Before After Diff

Select / Select With Error

1,927 px (1.85%) changed

Before After Diff
Before After Diff

Select / Select Placeholder

1,328 px (1.5%) changed

Before After Diff
Before After Diff

Select / Select With Tooltip

982 px (1.11%) changed

Before After Diff
Before After Diff

Select / Select Custom Rendering

1,209 px (1.37%) changed

Before After Diff
Before After Diff

Select / Select Loading

2,119 px (1.2%) changed

Before After Diff
Before After Diff

Select / Select Multiple

1,548 px (1.75%) changed

Before After Diff
Before After Diff

Select / Select Complex

1,788 px (1.72%) changed

Before After Diff
Before After Diff

Select / Select Disabled Options

1,555 px (1.76%) changed

Before After Diff
Before After Diff

Select / Select Disabled Items

793 px (0.9%) changed

Before After Diff
Before After Diff

Select / Select Grouped

813 px (0.92%) changed

Before After Diff
Before After Diff

Select / Select Grouped With Disabled

1,189 px (1.34%) changed

Before After Diff
Before After Diff

Select / Select Long List

1,649 px (1.58%) changed

Before After Diff
Before After Diff

Select (Open)

98 px (0%) changed

Before After Diff
Before After Diff
14 screenshot(s) unchanged
  • Button / Basic
  • Button / Variant: Secondary
  • Button / Variant: Ghost
  • Button / Variant: Destructive
  • Button / Variant: Outline
  • Button / Sizes
  • Button / With Icon
  • Button / Icon Only
  • Button / Disabled State
  • Button / Disabled Link
  • Dialog / Dialog Sizes
  • Dialog / Dialog With Select
  • Dialog / Dialog With Combobox
  • Select / Select Sizes

Generated by Kumo Visual Regression

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

📐 Bundle Size

Consumer fixtures bundled against packages/kumo/dist (peers external, minified ESM).

Fixture Raw Gzip Brotli
Button (root) 174.6 KB 49.9 KB 43.1 KB
Button (components/button) 174.5 KB 49.9 KB 43.0 KB
Button + Dialog + Select (root) 299.5 KB 85.8 KB 71.8 KB
Button + Dialog + Select (component subpaths) 299.5 KB 85.9 KB 71.8 KB
Chart (root) 247.0 KB 71.1 KB 60.9 KB
Chart (components/chart) 246.9 KB 71.1 KB 60.9 KB
Badge (components/badge) 44.7 KB 11.7 KB 10.0 KB
Flow (components/flow) 208.9 KB 58.3 KB 50.5 KB
Button (primitives/button) 12.2 KB 4.4 KB 3.9 KB
Primitives barrel 635.8 KB 176.9 KB 139.3 KB
Code highlighting (code) 2.08 MB 467.9 KB 349.2 KB

npm tarball: 540 files, 1.48 MB packed, 6.91 MB unpacked.

⚠️ 23 flagged files in tarball (tests / raw scripts)
  • dist/blocks-source/resource-list/resource-list.test.tsx
  • scripts/component-registry/cache.ts
  • scripts/component-registry/discovery.ts
  • scripts/component-registry/example-cleanup.ts
  • scripts/component-registry/index.test.ts
  • scripts/component-registry/index.ts
  • scripts/component-registry/markdown-generator.ts
  • scripts/component-registry/metadata.ts
  • scripts/component-registry/props-filter.ts
  • scripts/component-registry/schema-generator.ts
  • scripts/component-registry/sub-components.ts
  • scripts/component-registry/types.ts
  • scripts/component-registry/utils.ts
  • scripts/component-registry/variant-parser.ts
  • scripts/css-build.ts
  • scripts/generate-globe-land-mask.mjs
  • scripts/generate-primitives.ts
  • scripts/theme-generator/config.ts
  • scripts/theme-generator/generate-css.test.ts
  • scripts/theme-generator/generate-css.ts
  • scripts/theme-generator/index.ts
  • scripts/theme-generator/migrate.ts
  • scripts/theme-generator/types.ts

Report-only: no size thresholds are enforced yet.

Commit: a83e99d5

@stritt

stritt commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator Author

/bonk review this

@ask-bonk

ask-bonk Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

After reviewing the changes, I found 2 actionable issues introduced by this PR.

1. Missing keyboard activation for focusable SVG paths (WCAG violation)

The interactive region and marker <path> elements have tabIndex={0} but no onKeyDown handlers to map Enter/Space to their onClick behavior.

  • Region paths (packages/kumo/src/components/chart/Maps.tsx lines ~1501–1551): tabIndex={entry ? 0 : undefined} is set when data is present, but keyboard users can focus the region and see a tooltip without being able to activate it.
  • Marker paths (packages/kumo/src/components/chart/Maps.tsx lines ~1566–1600): tabIndex={0} is set, but the same gap exists.

Fix: Add onKeyDown handlers that dispatch to the same logic as onClick when event.key === "Enter" || event.key === " ".

2. role="img" on an SVG containing interactive children is an ARIA anti-pattern

The <svg> element declares role="img", which by ARIA specification makes its children presentational. However, the SVG contains focusable, interactive child <path> elements (tabIndex, onClick, onPointerEnter, etc.). Screen readers may flatten or fail to expose these interactive descendants.

  • Location: packages/kumo/src/components/chart/Maps.tsx lines ~1450–1453.

Fix: Remove role="img" (the SVG's implicit role better supports interactive children) while keeping the aria-label. Note that the unit test currently queries via getByRole("img"), so it should be updated to use getByLabelText("Traffic globe") or container.querySelector("svg") instead.

These are the only material issues I found. The implementation is otherwise solid: the dot cache, WeakMap, compound path, and frame-coalesced drag updates are well done, semantic tokens are used correctly, and the performance claims in the description are reflected in the code.

github run

@stritt

stritt commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator Author

/bonk i think those are fixed can you give it another once over?

@ask-bonk

ask-bonk Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

The previous two issues have been addressed:

  1. Keyboard activation — markers now have an onKeyDown handler (lines 643‑647) that activates the marker on Enter and Space, with preventDefault to avoid page scrolling.
  2. role="img" removed — the <svg> no longer has role="img", and the unit test verifies it (expect(globe.getAttribute("role")).toBeNull()).

I also verified:

  • The test now queries the marker via getByRole("button", { name: "London: Availability location" }) and asserts the keyboard event triggers onMarkerClick.
  • GlobeMap.displayName is set correctly.
  • Semantic tokens are used throughout (no raw Tailwind colors, no dark: variants).
  • The visual regression diffs are all on unrelated components (Button, Dialog, Select) and are under ~3%, which is consistent with baseline/environment noise rather than regressions introduced by this PR.

No new material issues found. LGTM!

github run

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 3, 2026

Copy link
Copy Markdown

🚀 Deploying Preview to Cloudflare 🚀

Preview URL: https://the-globe-kumo-docs.design-engineering.workers.dev (commit a83e99d)

This URL reflects your latest Preview deployment

Preview Deployments by commit

Status Deployment URL Commit Updated (UTC) See this deployment's details
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://885b1b70-kumo-docs.design-engineering.workers.dev a83e99d 2026-09-14T16:09:49.487Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://4d5f032b-kumo-docs.design-engineering.workers.dev a86fc5b 2026-09-14T15:52:35.831Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://9151229c-kumo-docs.design-engineering.workers.dev dcac81d 2026-09-03T23:43:18.436Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://8733e091-kumo-docs.design-engineering.workers.dev 67033ed 2026-09-03T19:35:06.150Z Visit the dashboard ↗
  • Build: In progress 🔵

View logs ↗
2edcece 2026-09-03T19:20:46.217Z View logs ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://a5caddd2-kumo-docs.design-engineering.workers.dev 49a55fc 2026-09-03T19:13:21.387Z Visit the dashboard ↗
  • Build: In progress 🔵

View logs ↗
40e5564 2026-09-03T19:03:07.453Z View logs ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://7a87a509-kumo-docs.design-engineering.workers.dev 9fe4fa3 2026-09-03T19:00:01.829Z Visit the dashboard ↗
  • Build: In progress 🔵

View logs ↗
1ea39f9 2026-09-03T18:54:15.819Z View logs ↗

@mattrothenberg

Copy link
Copy Markdown
Collaborator

/bonk review the new implementation that uses d3-geo

@ask-bonk

ask-bonk Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

After reviewing the d3-geo-based GlobeMap implementation, I found 3 actionable issues introduced by this change:

1. Pointer capture on the SVG breaks marker click events

handlePointerDown unconditionally calls setPointerCapture on the SVG element (packages/kumo/src/components/chart/GlobeMap.tsx line ~237). When a user clicks a marker <circle>, the parent SVG captures the pointer. Per the Pointer Events spec, the resulting click event targets the pointerup target—which becomes the SVG rather than the marker—so React's synthetic onClick on the circle never fires. This makes onMarkerClick unreachable via mouse/touch. The existing test suite only asserts keyboard activation (fireEvent.keyDown); there is no test coverage for pointer clicks.

Fix: Skip capture when the pointerdown target is a marker:

const target = event.target as Element;
if (target.closest("circle[role='button']")) return;

Add a test that clicks a marker (e.g., userEvent.click) to guard against regression.

2. GlobeMap is missing forwardRef

All other chart components (BubbleMap, ChoroplethMap, Chart, TimeseriesChart) use forwardRef. GlobeMap is currently a plain function, so consumers have no way to obtain a DOM reference to the outer wrapper. This breaks the Kumo convention that interactive components expose refs.

Fix: Wrap the component with forwardRef<HTMLDivElement, GlobeMapProps> and attach the ref to the root <div>.

3. Focus tooltip is centered on the SVG instead of on the focused marker

In the marker's onFocus handler (lines ~344–354), the tooltip x/y are hard-coded to bounds.width / 2 and bounds.height / 2, which places the tooltip at the center of the globe regardless of which marker is focused. The marker's projected position is already computed in scope via projection([marker.longitude, marker.latitude]) and provides a much more accurate anchor.

Fix: Use the marker's projected position[0]/position[1] (scaled to the SVG bounding rect) for the focus tooltip coordinates instead of the SVG center.

Minor suggestion (non-blocking):
The land hatch <path> on line ~303 does not set strokeLinecap="round". Since the PR calls this a "dotted" globe, adding strokeLinecap="round" (and optionally strokeLinejoin="round") makes the short diagonal hatch segments read as rounded dots rather than small dashes.

github run

@mattrothenberg

Copy link
Copy Markdown
Collaborator

/bonk updated, please re-review.

@ask-bonk

ask-bonk Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

All three issues from the previous review have been fixed, and the non-blocking suggestion was applied as well. I don't see any new material issues introduced by these changes.

Verified fixes:

  1. Pointer capture no longer intercepts marker clickshandlePointerDown now returns early when the target is a marker (circle[role="button"]), skipping setPointerCapture. A new test ("calls onMarkerClick when a marker is clicked") guards this with userEvent.click.

  2. forwardRef added — The component is properly wrapped with forwardRef<HTMLDivElement, GlobeMapProps> and the ref is forwarded to the root <div>.

  3. Focus tooltip now anchors to the focused marker — The onFocus handler uses the marker's projected position scaled to the SVG bounding rect instead of the SVG center.

  4. Hatch stroke stylingstrokeLinecap="round" and strokeLinejoin="round" are now set on the land hatch <path>, giving the dotted appearance described in the PR.

Also confirmed:

  • GlobeMap.displayName = "GlobeMap" is set.
  • The SVG no longer has role="img", and the unit test asserts getAttribute("role") is null.
  • Markers use role="button" with onKeyDown handling Enter and Space, plus preventDefault.
  • Semantic tokens are used throughout (stroke-kumo-line, bg-kumo-base, etc.) with no raw Tailwind colors or dark: variants.

LGTM!

github run

@stritt
stritt merged commit d4a3e71 into main Sep 14, 2026
18 of 19 checks passed
@stritt
stritt deleted the the-globe branch September 14, 2026 16:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants