Skip to content

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

Open
stritt wants to merge 14 commits into
mainfrom
the-globe
Open

feat(chart): establish the official Kumo globe#769
stritt wants to merge 14 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. I kept the scope to the dotted globe for now and removed the choropleth/boundary version.

  • dotted Natural Earth land with no country borders
  • geographic markers with tooltips and keyboard support
  • drag and optional auto-rotation
  • smooth fading 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.

Perf

I reworked the land rendering around a fixed dot lattice and a rotating land mask. The dots stay in place and land membership is updated as the globe rotates, which keeps the pattern dense and avoids pushing rotation through React every frame.

  • one compound SVG path for all land dots
  • generated 8 KB Natural Earth land mask
  • auto-rotation runs at 30 FPS and pauses offscreen
  • dragging is frame-coalesced at up to 60 FPS
  • respects reduced motion
  • around 16 KB gzip when tree-shaken on its own
  • no new dependencies

Testing

  • added rendering, density, drag, horizon fade, and keyboard interaction coverage

  • typecheck, focused tests, lint, package build, publint, ATTW, and docs typecheck pass

  • manually checked the globe while rotating in the docs

  • Reviews

  • bonk has reviewed the change

  • automated review not possible because: the globe needed 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: dcac81d

@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 — 19 changed, 26 unchanged

19 screenshot(s) with visual changes:

Button / Loading State

12 px (0.01%) changed

Before After Diff
Before After Diff

Dialog / Dialog With Actions

298 px (0.34%) changed

Before After Diff
Before After Diff

Dialog / Dialog Basic

401 px (0.45%) changed

Before After Diff
Before After Diff

Dialog / Dialog With Select

493 px (0.56%) changed

Before After Diff
Before After Diff

Dialog / Dialog With Dropdown

253 px (0.29%) changed

Before After Diff
Before After Diff

Dialog (Open)

0 px (0%) changed

Before After Diff
Before After Diff

Select / Select Basic

1,066 px (1.2%) changed

Before After Diff
Before After Diff

Select / Select Sizes

3,228 px (1.99%) changed

Before After Diff
Before After Diff

Select / Select Without Label

806 px (0.91%) changed

Before After Diff
Before After Diff

Select / Select With Description

1,687 px (1.62%) changed

Before After Diff
Before After Diff

Select / Select Loading

0 px (0%) changed

Before After Diff
Before After Diff

Select / Select Multiple

938 px (1.06%) changed

Before After Diff
Before After Diff

Select / Select Complex

181 px (0.17%) changed

Before After Diff
Before After Diff

Select / Select Disabled Options

1,217 px (1.37%) changed

Before After Diff
Before After Diff

Select / Select Disabled Items

839 px (0.95%) changed

Before After Diff
Before After Diff

Select / Select Grouped

705 px (0.8%) changed

Before After Diff
Before After Diff

Select / Select Grouped With Disabled

1,478 px (1.67%) changed

Before After Diff
Before After Diff

Select / Select Long List

163 px (0.16%) changed

Before After Diff
Before After Diff

Select (Open)

1,183 px (0%) changed

Before After Diff
Before After Diff
26 screenshot(s) unchanged
  • Button / Basic
  • Button / Variant: Primary
  • Button / Variant: Secondary
  • Button / Variant: Ghost
  • Button / Variant: Destructive
  • Button / Variant: Outline
  • Button / Variant: Secondary Destructive
  • Button / Sizes
  • Button / With Icon
  • Button / Icon Only
  • Button / Disabled State
  • Button / Title
  • Button / Link as Button
  • Button / Link with Tooltip
  • Button / Disabled Link
  • Dialog / Dialog Sizes
  • Dialog / Dialog Alert
  • Dialog / Dialog Confirmation
  • Dialog / Custom Max Width
  • Dialog / Dialog With Combobox
  • Select / Select Placement
  • Select / Select Dynamic Placement
  • Select / Select With Error
  • Select / Select Placeholder
  • Select / Select With Tooltip
  • Select / Select Custom Rendering

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) 177.6 KB 50.5 KB 43.5 KB
Button (components/button) 177.5 KB 50.4 KB 43.6 KB
Button + Dialog + Select (root) 300.5 KB 86.1 KB 72.1 KB
Button + Dialog + Select (component subpaths) 300.5 KB 86.1 KB 71.9 KB
Chart (root) 244.2 KB 70.0 KB 59.9 KB
Chart (components/chart) 244.2 KB 70.0 KB 59.9 KB
Badge (components/badge) 44.7 KB 11.7 KB 10.1 KB
Flow (components/flow) 208.9 KB 58.3 KB 50.6 KB
Button (primitives/button) 11.8 KB 4.3 KB 3.8 KB
Primitives barrel 650.8 KB 178.6 KB 139.9 KB
Code highlighting (code) 2.08 MB 468.3 KB 349.5 KB

npm tarball: 518 files, 1.45 MB packed, 6.80 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: dcac81df

@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 dcac81d)

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

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