Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/atlas-city-map.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@tanstack/react-charts': minor
'@tanstack/charts': minor
---

Add an optional interactive 3D city map for React, with categorical districts, metric-driven buildings, selection, camera controls, weather, and customizable CSS themes. Import the city entry and install the optional Three.js peer to use it.
23 changes: 23 additions & 0 deletions API-FRICTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,7 @@ Each entry records:
| F-312 | Tick stubs could not be styled independently | API | resolved |
| F-313 | Initial observer precision cancelled entrance motion | Tooling | resolved |
| F-314 | Framework ticks and immediate focus layers were unclear | Documentation | resolved |
| F-315 | Optional React city assets needed packaging support | Tooling | monitoring |

## Findings

Expand Down Expand Up @@ -9354,3 +9355,25 @@ Each entry records:
direction changes. A focus dot appears during entrance while the data path
and its running animation remain intact. Concurrent inline states remain a
separate feature; this addresses the documented alternatives requested.

### F-315 - Optional React renderers could not ship JavaScript and CSS assets

- Status: monitoring
- Severity: medium
- Owner: Tooling
- Observed in: porting the Atlas city renderer to the React package
- Friction: the packed consumer builder accepted only TypeScript runtime files,
required declarations for stylesheet exports, and required sideEffects to be
false even for explicitly imported stylesheets.
- Decision: keep the renderer behind an optional entry and Three.js behind an
optional peer. Own scoped styles in the renderer, as existing React examples
do, and retain the package's sideEffects: false contract. JavaScript runtime
modules currently use an explicit TypeScript public contract.
- Verification: city integration tests and the packed package gate pass.
- Application correction: the first port added a standalone `/city/` route and
browser-check script. The repository already has source-backed catalog cases,
React mounting helpers, definition-only previews, and interaction scenarios.
Move the city example into that existing flow, colocate its regressions with
the package, and remove the separate route, runner, and stylesheet exports.
- Follow-up: consider migrating the private JavaScript engine to TypeScript
independently of the public React API.
11 changes: 6 additions & 5 deletions benchmarks/conformance/DEFINITION-COVERAGE-AUDIT.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Date: 2026-08-10

Scope: all 188 catalog directories. The 67 cases previously classified as
Scope: all 189 catalog directories. The 67 cases previously classified as
strict custom authoring, preparation review, or shell-only are reviewed beside
the former 42-case definition-native control group from
[the custom authoring audit](./CUSTOM-AUTHORING-AUDIT.md). That audit remains
Expand Down Expand Up @@ -31,12 +31,12 @@ work:

| Disposition | Cases | Meaning |
| --------------------- | ------: | ------------------------------------------------------------------------ |
| Definition now | 135 | Current marks and eager transforms are sufficient |
| Definition now | 136 | Current marks and eager transforms are sufficient |
| First-party primitive | 35 | Add a reusable mark, transform, layout, guide, or controlled signal |
| Optional primitive | 15 | Keep a heavy dependency granular, but hide its layout DTOs and lifecycle |
| Application boundary | 2 | The remaining work is product state, DOM layout, or data arrival |
| Inline custom mark | 1 | The geometry is intentionally case-specific |
| **Total** | **188** | |
| **Total** | **189** | |

A normal definition does not require every implementation algorithm to live
in Charts core. D3 may implement an optional `sankeyDiagram`, `densityContour`,
Expand Down Expand Up @@ -161,6 +161,7 @@ ownership boundary.
| [Labeled heatmap](./cases/heatmap-labeled/example.tsx) | Definition now | Native cells and text share explicit category and color scales. |
| [Histogram](./cases/histogram/example.tsx) | Definition now | Public `binX` output feeds native semantic rectangle intervals. |
| [Bubble scatter](./cases/scatter-bubble/example.tsx) | Definition now | Native dot color and radius channels consume model rows directly. |
| [3D city map](./cases/city-map/example.tsx) | Definition now | City marks retain a semantic SVG scene while the React renderer paints the interactive WebGL surface. |

## Statistical and prepared-data cases

Expand Down Expand Up @@ -353,8 +354,8 @@ bundle.

## Delivery result

All 188 catalog directories now have one roadmap record and case-local
evidence. All 185 normal-definition cases are verified against their current
All 189 catalog directories now have one roadmap record and case-local
evidence. All 186 normal-definition cases are verified against their current
boundary; only cases 85, 86, and 116 retain accepted application or bespoke
geometry work. The roadmap validator compares its IDs with the live catalog
directories so a new case cannot silently remain outside this review.
9 changes: 5 additions & 4 deletions benchmarks/conformance/DEFINITION-COVERAGE-OVERVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@

Date: 2026-08-10

The catalog has 188 case directories. 185 visualizations now use
The catalog has 189 case directories. 186 visualizations now use
normal chart definitions. Cases 85 and 86 retain application shells, and Case
116 retains one deliberately bespoke inline mark. No other case-owned layout
or renderer is an accepted endpoint.

| Disposition | Cases | Result |
| --------------------- | ------: | ------------------------------------------------- |
| Definition now | 135 | Normal definition |
| Definition now | 136 | Normal definition |
| First-party primitive | 35 | Normal definition |
| Optional primitive | 15 | Normal definition |
| Application boundary | 2 | Accepted boundary |
| Inline custom mark | 1 | Accepted boundary |
| **Total** | **188** | **185 normal definitions; 3 accepted boundaries** |
| **Total** | **189** | **186 normal definitions; 3 accepted boundaries** |

## Shared decisions

Expand Down Expand Up @@ -63,7 +63,7 @@ presentation policy.

## Verification

- The roadmap validator compares all 188 roadmap and audit IDs with the live
- The roadmap validator compares all 189 roadmap and audit IDs with the live
catalog directories, requires unique IDs, validates the capability DAG, and
requires every verified or accepted case to cite evidence under its own
directory.
Expand Down Expand Up @@ -268,3 +268,4 @@ presentation policy.
| [heatmap-labeled — Labeled ordinal heatmap](./cases/heatmap-labeled/example.tsx) | Definition now | `current-definition-api` | `example.tsx` |
| [histogram — Histogram](./cases/histogram/example.tsx) | Definition now | `current-definition-api` | `example.tsx` |
| [scatter-bubble — Bubble scatterplot](./cases/scatter-bubble/example.tsx) | Definition now | `current-definition-api` | `example.tsx` |
| [city-map — 3D city map](./cases/city-map/example.tsx) | Definition now | `current-definition-api` | `example.tsx`, `case.json`, `tanstack.test.ts` |
34 changes: 34 additions & 0 deletions benchmarks/conformance/cases/city-map/case.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"schemaVersion": 1,
"order": 1990,
"id": "city-map",
"title": "3D city map",
"family": "geography",
"height": 720,
"intent": "Represent store metrics as building heights and category intersections as districts, with an interactive 3D surface and a semantic top-view preview.",
"support": "composed",
"features": [
"custom renderer",
"Three.js city",
"metric-driven building height",
"category intersections",
"camera controls",
"selection",
"lighting and weather"
],
"geometry": [
{
"role": "rect",
"count": 140
}
],
"source": {
"title": "Three.js WebGL renderer",
"url": "https://threejs.org/docs/#api/en/renderers/WebGLRenderer"
},
"ai": {
"create": "Render store metrics as buildings in category-intersection districts using the city renderer and the existing React chart contracts.",
"maintain": "Preserve datum identity, metric heights, category districts, keyboard selection, camera controls, renderer disposal, and the source-backed catalog entry."
},
"collections": ["shadcn"]
}
39 changes: 39 additions & 0 deletions benchmarks/conformance/cases/city-map/data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { CityDatum } from '@tanstack/charts/react/city'

export type District = 1 | 2 | 3 | 4 | 5 | 6 | 7
export interface Store extends CityDatum {
mask: District
}
export const districtMasks: District[] = [1, 2, 4, 3, 5, 6, 7]
export const districtColors = [
'#4b91ef',
'#e281af',
'#ecc456',
'#ab7cdd',
'#47b8b2',
'#e99b55',
'#7777ca',
]
const categories = [
{ value: 'retail', label: 'Retail' },
{ value: 'online', label: 'Online' },
{ value: 'wholesale', label: 'Wholesale' },
]

export function storesForRevision(revision: number): Store[] {
return Array.from({ length: 140 }, (_, index) => {
const mask = ((index % 7) + 1) as District
return {
id: `store-${index}`,
name: `Store ${String(index + 1).padStart(3, '0')}`,
metric: {
label: 'Orders',
value: ((index * 397 + revision * 71) % 5000) + 100,
},
categories: categories.filter((_, bit) => mask & (1 << bit)),
mask,
status:
index % 9 === 0 ? 'critical' : index % 5 === 0 ? 'warning' : 'healthy',
}
})
}
79 changes: 79 additions & 0 deletions benchmarks/conformance/cases/city-map/example.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { useMemo, type CSSProperties } from 'react'
import { defineChart } from '@tanstack/charts'
import { scaleLinear } from '@tanstack/charts/scales/linear'
import { CityMap, cityMark, cityFocus } from '@tanstack/charts/react/city'
import { districtMasks, districtColors, storesForRevision } from './data'
import './styles.css'

export interface ChartOptions {
revision: number
}
export interface ExampleProps {
width?: number
height?: number
revision?: number
}

export function createExampleChart(input: ChartOptions) {
return defineChart({
marks: [cityMark(storesForRevision(input.revision))],
scales: {
x: { scale: scaleLinear, axis: false },
y: { scale: scaleLinear, axis: false },
},
color: { domain: districtMasks, range: districtColors },
margin: 0,
guides: false,
focus: cityFocus,
})
}

export const exampleAriaLabel = 'Store health city'
export const chart = createExampleChart({ revision: 0 })

export default function Example({
width = 960,
height = 720,
revision = 0,
}: ExampleProps) {
const records = useMemo(() => storesForRevision(revision), [revision])
return (
<div className="sc-example sc-city-map" style={{ width }}>
<article
className="sc-card sc-default"
style={
{
width,
'--atlas-city-height': `${Math.max(520, height - 160)}px`,
} as CSSProperties
}
>
<header className="sc-card-header">
<div className="sc-card-heading">
<h2>City Map - Store Health</h2>
<p>Orders and store health across category districts</p>
</div>
</header>
<div className="sc-card-content">
<CityMap
data={records}
title="Store health"
ariaLabel={exampleAriaLabel}
showMonitor={false}
showEnvironmentControls={false}
showCompass={false}
showInstructions={false}
showSearch={false}
lightingMode="day"
/>
</div>
<footer className="sc-card-footer">
<span>{records.length} stores across 7 districts</span>
<span className="sc-footer-note">
Building heights represent orders. Select a building for details.
</span>
</footer>
</article>
</div>
)
}
34 changes: 34 additions & 0 deletions benchmarks/conformance/cases/city-map/plot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import * as Plot from '@observablehq/plot'
import { mountObservablePlot } from '../../shared/mount'
import { districtMasks, districtColors, storesForRevision } from './data'
import { referenceLots } from './transform'
import type { ConformanceInput } from '../../types'

// Plot compares the semantic district layout. The interactive example paints
// that same data as a Three.js city; Plot does not implement its WebGL surface.
function render(input: ConformanceInput) {
const rows = referenceLots(storesForRevision(input.revision))
return Plot.plot({
ariaLabel: 'Store health city',
width: input.width,
height: input.height,
margin: 0,
x: { axis: null, nice: false },
y: { axis: null, nice: false },
color: { domain: districtMasks, range: districtColors },
marks: [
Plot.rect(rows, {
x1: 'x',
x2: 'x',
y1: 'z',
y2: 'z',
fill: 'mask',
inset: -3,
rx: 1,
}),
],
})
}

export const mount = (container: HTMLElement, input: ConformanceInput) =>
mountObservablePlot(container, input, render)
Loading