# DATA SCAPE — Earth Language 1.0 migration

SORAH Design System / 2026.09 / System 1.7.0

[Foundations](https://sorah.io/sorah-pjt/design/) · [Earth Language](https://sorah.io/sorah-pjt/dataviz/) · [Scene Patterns](https://sorah.io/sorah-pjt/design/patterns/) · [Implementation](https://sorah.io/sorah-pjt/design/implementation/)

## What is ready

- 15 subjects, 30 colour profiles, 5 base-map types and 4 Crosspoint recipes.
- Earth Hues v5 exact displayed stops; Earth Grounds v1 exact approved assignments.
- Shared JS / Python samplers and 256-entry LUTs. Native STORM mm/h bins are not normalized.
- Proposed render contract, eight automatic configuration gates and four human-review gates.
- New reference sites with searchable profiles and downloadable definitions.

The development task “SORAH Dev” is adding the real GEOS-FP ingestion and artwork pipeline
concurrently. This package does not change its active working tree, existing published release,
rights ledger, deployment credentials or Supabase schema.

## Source of truth and release ownership

1. Edit `design-system/earth-language.json` in earth-engine for colour / ground assignments.
2. Edit `design-system/foundations.json` for brand / interface tokens.
3. Write visualization rules only in `engine/norm.py`, `RULES`, scope `scene`.
4. Run the sampler tests and build the documentation and kit together.
5. DATA SCAPE vendors a reviewed registry and adapter at a pinned commit + version.
6. Store the resolved contract with the immutable Scene release in Supabase.

Do not fetch the mutable documentation URL on each viewer load. A release must continue to use
its original registry even after the library changes. Increment the registry version when its
meaning, palette, interpolation or base-map mapping changes. Generated palettes are snapshots,
not a second editable source.

## Integration against the current Scene v1 schema

The current schema rejects unknown `render` fields. Keep the existing `palette` and `valueDomain`
for backward compatibility and introduce an optional `render.earthLanguage` extension in a schema
migration. New releases should require it once the viewer and Python renderer both support it.

```ts
type EarthLanguage = {
  registryVersion: "1.0.0";
  subject: "ICE" | "OCEAN" | "LANES" | "RIVER" | "HEAT" | "STORM" |
    "DUST" | "SMOKE" | "AIR" | "FOREST" | "DRYWET" | "SEASON" | "QUAKE" |
    "FIRE" | "LIGHTS";
  mode: "day" | "night";
  ground: "B0" | "B1" | "B2" | "B3" | "B4";
  interpolation: "srgb-channel-linear";
  transform: "linear"; // future transforms need explicit parameters and parity tests
  missing: "separate";
  clipping: "clamp-with-label";
};
```

At ingestion/compile time, resolve from the registry. Validate that the resolved palette matches
`render.palette`, then freeze both into a new release. Do not silently recolour an old published
Scene in place. Old Scenes remain on their prior palette until explicitly migrated.

### First adopter: DUST

- Preferred mode: `night`; base map: `B2`; no alternate ground.
- Ground: `#120f0b`; geography: `#221d17`.
- Stops: `#120f0b`, `#523826`, `#8c5e3a`, `#bf8452`, `#e7a973`.
- Existing data domain `[0,1]` AOD at 550 nm can be preserved; values above 1 remain explicitly saturated.
- The pre-migration viewer used a hand-written blue/orange palette and `sqrt` before colour lookup.
  Replace both with the resolved contract. If a non-linear transform is needed, first add it to the
  contract, legend, Python renderer and tests together. A hidden viewer-only `sqrt` breaks parity.
- Use the numeric value for colour lookup. Do not additionally vary opacity with that same value:
  this changes the visible colour relative to the legend. Use a numeric raster/LUT for quantitative
  fields; blurred overlapping point marks are only an explicitly labelled exploratory rendering.

```js
import registry from './earth-language.json';
import { renderContract, mapLibreExpression } from './earth-language.mjs';
const contract = renderContract(registry, 'DUST', 'night', [0,1]);
const expression = mapLibreExpression(registry, 'DUST', 'night', 'value', [0,1]);
// The layer's property must be the actual linear AOD value, not a sqrt intensity.
// expression works as a colour property; exclude null/no-data features explicitly.
```

```python
from engine.earth_language import render_contract, sample
contract = render_contract('DUST', 'night', (0, 1))
assert sample('DUST', 'night', .25) == '#523826'
# For images, vectorize the same stops or index the shared LUT with the documented quantizer.
```

JS/Python sampling is continuous between sRGB channel stops. When using the 256-entry LUT,
quantize `floor(clamp(t,0,1)*255 + .5)` in both renderers. The LUT is an explicit approximation;
compare LUT-rendered outputs with LUT-rendered outputs, not an unquantized renderer.

### Diverging data

For DRYWET, the middle stop is the ground. For HEAT, the 16 colours are identical in both modes.
Declare the baseline (including its reference period), center and numeric domain. A physical center
must map to `t=0.5`: either choose a symmetric domain or explicitly implement a piecewise transform
in both renderers. The generic sampler only implements a linear domain; it does not invent a center.
Do not apply anomaly colours to absolute values without a documented, semantically valid mapping.

### STORM

`sample(..., value)` receives mm/h directly. Thresholds are lower-inclusive: <1, 1–5, 5–10, 10–20,
20–30, 30–50, 50–80, ≥80. Below 1 uses the current ground. Never linearly interpolate the bins.
A storm track is a line with a relative black/white colour and a halo; rainfall is a separate field.

## Ground asset contract

Use Natural Earth 1:50m land/coastline/admin lines and ETOPO 2022 for the production terrain.
The new documentation previews use ETOPO1 at 10 arc-minute spacing, matching the reference's preview
status; these WebP files are not production terrain tiles. Do not label them ETOPO 2022.

The reference artifact says R2, while the active system uses Supabase Storage. Keep the active
storage architecture and put assets beneath immutable `releases/{release_id}/` prefixes. No new
storage provider is required for the design language. Register geography/terrain as context datasets
in the rights manifest. Store source version, resolution, projection, transform and content hash.

B1 shades both land and sea floor; B2 shades land only. The old terrain helper clips elevation to
zero before hillshade, so it cannot produce B1 without change. Build both masks explicitly. The
maximum overlay on the phenomenon is ±4 in CIELAB L*, not ±4 RGB bytes or a CSS opacity percentage.
Read values from the original data, not from terrain-modulated pixels. Validate the post-composite
result for each actual subject, projection and medium.

Line recipe: mix the underlying geography (or ground for B4) with black at 16% in day mode; mix
with white at 22% in night mode, in sRGB channels. Label political boundaries as the source's
representation, not SORAH's political position.

## Crosspoint contract

At most one spectrum field; other quantities use monochrome points or lines. The field owns the
base map. Orange `#eb8741` means contextual human activity over a natural phenomenon; standalone
LANES and LIGHTS use their own profiles. CP4 uses the HEAT ground B4 and a haloed black/white track.
CP2 restricts the displayed field to the pale dry side: day normalized palette range [.25,.5],
night [.375,.5], with numeric mapping and clipping declared in the Scene.

No subject pair is prohibited. Close pairs require explicit differentiation (default: outline +
opacity ≥.35), composited contrast checks, readable labels and human review. Do not multiply an
entire quantitative field by arbitrary alpha while claiming its colours still match the legend.

## Acceptance before switching a production Scene

1. A new immutable release pins the contract and assets, passes data-rights checks, and preserves
   the old release for rollback.
2. Select at least five fixed coordinates/times including a zero, a missing value, a threshold and
   a saturated value. Web/Python return the same raw value, class and RGBA before relief compositing.
3. Check actual viewport/print composites, CVD and grayscale. Confirm relief ΔL* ≤4 and distinct
   missing coverage. Geo/projection changes never change data values.
4. Check 390 / 1280 viewports, keyboard, 200% text scale and reduced motion. Time-series playback autostarts on display, with visible play/pause and time controls. Pause when hidden or reduced motion is requested; preserve the stopped time and require explicit resume after returning.
5. Human perceptual validation is recorded separately. The LLM association study remains a hypothesis.

The package's automated tests verify colour definitions and configuration rules. They do not claim
that ETOPO 2022 is deployed or that the real production MapLibre/Python integration is already complete.

## Configuration gate payload

The original six automatic gates require explicit mode and ground, `transform: "linear"`, `clipping: "clamp-with-label"`, and `missing: "separate"`. Diverging domains must be symmetric around the declared finite center; asymmetric domains must first be transformed outside this adapter and documented. Time-series playback requires `autoplay: true`, `playbackControls: true`, `timeControl: true`, `pauseWhenHidden: true`, and `reducedMotion: "stop"`. These settings concern time-series playback, not globe rotation or embedded editorial video examples. A reduced-motion preference overrides autoplay; time selection remains available without animation. S-10/S-11 additionally require the fieldRendering payload below (or the discrete equivalent). Production raster/legend parity remains a human acceptance check S-09.


## Continuous fields / System 1.5.0 (S-10–12)

P4 of “SORAH DATA SCAPE 構想と計画” is the visual target. The authored rules remain
`engine.norm.RULES`; the following is the implementation mapping.

```json
{"fieldRendering":{"kind":"continuous","spatial":"bilinear-scalar","missingPolicy":"preserve-nearest-mask","colorOrder":"scalar-then-lut","sourceResolution":"2° latitude × 2.5° longitude","readout":"source-and-display","temporal":{"method":"linear-scalar","maxGapSeconds":10800,"missing":"both-valid","loop":false,"label":"endpoints-and-fraction","sourceStatistic":"3-hourly model analysis"}}}
```

Use `engine.field_sampling.bilinear` on values in source-grid coordinates. Each output
pixel samples four source centres. If a contributing neighbour is missing, retain the
nearest cell's own value/missing state, never search for a more distant valid value.
The ICE geographic land mask is applied after sampling. Atmospheric fields can legitimately
cross coastlines. Convex weights do not create extrema; no RGB blur, fabricated eddies, or
categorical smoothing. Display interpolation does not increase scientific resolution.

Freeze the resulting scalar plane as little-endian uint16 (row-wise unsigned delta + gzip for transport; prefix-sum modulo 65536 per row on decode; 0–65534 normalized to the declared
domain; 65535 missing). Record quantization and dimensions. The PNG and GPU use the same plane,
fixed LUT and context overlays. For animation, mix adjacent scalar planes before LUT lookup;
intermediate missing values require both endpoints valid. Exact endpoints retain their own
masks. No end-to-start tween. An interval beyond maxGapSeconds uses a discrete step.

Monthly means may be interpolated only as a labelled transition between monthly means, not
as a new daily observation. The UI reports both original endpoint labels and the fraction,
and distinguishes original grid-centre values from the interpolated display value.
Preserve the stopped fraction. Prefetch bounded adjacent planes; pause on missing assets,
network failure, hidden tab and reduced motion. Keep a pre-rendered still and frame controls
if WebGL2 fails. Verify CPU/GPU output and frame pacing on actual devices; unit tests alone
are not browser/performance acceptance. Discrete points use kind=discrete, spatial=none,
temporal.method=step.

`engine.field_sampling` requires numpy; the colour registry and resolver remain dependency-free.

## Adopted interaction policies — System 1.5

`foundations.json` 6.3 owns `components.dataState`, `interpolatedReadout`, `asyncAction` and `typography.byMedium`.

- Render zero numerically; use distinct quiet text for missing, outside, loading and error. Provisional/estimated values carry quality metadata. Never turn an error into zero or absence.
- Keep display value, unit, location, time and interpolation/infill status visible. A native details control exposes source values/coordinates/resolution, source times, method and fraction. Hover-only access is insufficient.
- Keep previous values with their timestamps while a request runs. Prevent duplicate submission on that action only. Preserve selection on error. Discard responses whose request identity or selection revision is stale, even if the selection moves away and back. Keep live status outside an aria-busy subtree. Modal close/Escape returns focus to its opener.
- UI, articles, editorial cards, video and print use Poppins / Noto Sans JP. Full licensed font files for new engine rendering live in `font-sources/`; the site serves subsets with actual, separately declared weights. Original VI vectors and released image/video assets remain historical.

### Two continuous fields

Use `scene-comparison.json` as a layer fragment alongside the normal Scene contract. One coloured field owns the ground. The second quantity uses `geometry: contour`, `quantityKind: continuous`, `colourEncoding: single`, a nonempty strictly increasing finite `levels` list, `levelLabels: true`, `label` and `unit`. Both layers have a `period` containing timezone-aware `start`, `end` and `statistic`.

S-04 checks these declarations. `comparison.timeRelation` is `aligned` only when start, end and statistic match; otherwise use `different-periods` and a nonempty visible `mismatchLabel`. `periodsVisible: true` is required. This does not prove the actual map displays the labels correctly: visual acceptance is still required. Contours preserve missing coverage and do not interpolate categorical data. Existing point/line Crosspoints remain supported.

See `/design/ui/#ui-data-states`, `#ui-interpolated-readout`, `#ui-async-actions`, and `/design/patterns/#field-contours` for adopted examples. All example values are synthetic.

Registry 1.0.0 colour definitions remain fixed. Its historical AIR note about opacity is superseded for two continuous fields by System 1.5 S-04; use field + contour rather than two colour-filled layers.


## DATA SCAPE contributed visuals (System 1.6)

`data-scape/catalog.json` maps eight adopted subject references, eleven immutable JPEGs, nine source topics (cyclones unadopted), and exact SHA256 hashes for the 80 supplied files. The source archive is a working-directory snapshot, not a reproducible release. No input datasets or rendered videos are included. Builds never execute the contributed scripts; several perform writes and expensive reads at import time. `/dataviz/visuals/` exposes the complete specimens, quantities, periods, entrypoints and parameter tables. The kit includes the same reference source and catalog.

These are visual targets and historical renderer references, not overrides to Registry 1.0.0. Preserve published image bytes. Before implementing a new renderer, resolve exact inputs and calls, external paths, missing masks, aggregate periods, fonts, transforms and palette conversion. Some source operations replace missing values with zero or blend in Lab; do not inherit these as compliance with S-10–13 or the current sRGB registry. Code parameters in the catalog are extracted settings, not proof of the exact JPEG invocation.

- Arctic Z2 uses EPSG:3411, despite the package README's general equirectangular description. Ice and rivers were edited after their reference images.
- Forest N3 is implemented by `02_forest/nyt.py`; `forest1.py` supplies shared grid/projection functions.
- Temperature V4 is an anomaly using ColorBrewer; map and stripe limits differ. Check the dataset baseline.
- QG4 background generation needs the exact invocation across `bg.py`/`relief.py`; similarly named B4 is not evidence of parity.
- The global shipping grid is a fixed 2015-01–2021-02 aggregate. Do not label the layer reveal as annual change.
- AVHRR total AOD is not PM2.5, AIR or dust-only AOD. It remains without a dedicated registry profile.
- `ships_ice.jpg` is preserved as a supplementary Arctic shipping reference. `ships_years.py` explicitly hides ice. It is not evidence of a validated ice/shipping Crosspoint.
- Cyclones remain unadopted. Timestamp ordering in the supplied MANIFEST is not pixel-parity verification.

Upstream usage statements remain attributed to the supplied README, not newly certified by SDS. No raw geospatial input data is redistributed here.


## Editorial content production (System 1.7 / Foundations 6.4)

CP-01–07 in engine.norm.RULES are human-review rules for article-linked content, not automatic data validation. foundations.json owns contentEditorial typography, editorialNote colours/spacing, contentProjectState and editorialPoster motion guidance. The generated tokens.css and content.css demonstrate them in /design/patterns/content/. Keep card, content and interactive Scene playback scopes distinct.

The two freshwater specimens are unchanged approved revision-13 figures. Revision 14 added article notes and a photograph; it did not alter the figures. They are historical examples, not current measurements or replacements for Registry 1.0.0 palettes. No approval or publication status for another content project is inferred from these examples.
