Skip to content

feat: contour mark#523

Merged
gka merged 13 commits intomainfrom
feat/contour-mark
Mar 18, 2026
Merged

feat: contour mark#523
gka merged 13 commits intomainfrom
feat/contour-mark

Conversation

@gka
Copy link
Copy Markdown
Contributor

@gka gka commented Mar 16, 2026

resolves #83

Summary

  • Adds a new <Contour> mark that renders iso-contour lines and filled contour bands from a scalar field using the marching-squares algorithm (d3-contour)
  • Fixes aspect ratio distortion when axes add auto-margins

Contour mark

Supports the same three input modes as <Raster>:

  • Dense grid: flat row-major array + width/height
  • Function sampling: value={(x, y) => number} evaluated on a pixel grid
  • Scatter interpolation: point cloud with x/y/value channels, spatially interpolated via nearest/barycentric/random-walk (reuses existing rasterInterpolate helpers)

Key design decisions:

  • fill="value" / stroke="value" map each contour level's threshold through the plot's color scale; any other string is a constant CSS color
  • When fill is active, stroke defaults to "none"; otherwise "currentColor"
  • For sampler mode, a cheap 20×20 coarse sample registers the color scale domain while the full-resolution grid re-evaluates on every render — this lets impure functions (closures over reactive state, performance.now(), etc.) work correctly

Aspect ratio fix

heightFromAspect was called with plotWidthForAspectRatio (full outer width, auto-margins zeroed to avoid a feedback loop) instead of plotWidth (the actual inner plot area). This made plotHeight larger than plotWidth by marginLeft + marginRight, distorting the data-space aspect ratio whenever axes were present.

Fix: pass plotWidth to heightFromAspect. The feedback loop (height → tick density → label widths → autoMarginLeft → plotWidth → height) converges in at most one reactive update in practice. plotWidthForAspectRatio is retained for geo-projection aspect-ratio computation.

Test plan

  • pnpm test — 750 tests passing
  • pnpm lint — no errors
  • pnpm check — 0 errors, pre-existing warnings only
  • Verify contour examples render correctly in the docs app (pnpm dev)
  • Verify aspect ratio is preserved correctly with and without axes

🤖 Generated with Claude Code

@netlify
Copy link
Copy Markdown

netlify bot commented Mar 16, 2026

Deploy Preview for svelteplot ready!

Name Link
🔨 Latest commit d919522
🔍 Latest deploy log https://app.netlify.com/projects/svelteplot/deploys/69ba96b534fcf7000852b742
😎 Deploy Preview https://deploy-preview-523--svelteplot.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@gka gka added this to the 1.0 milestone Mar 16, 2026
@github-actions
Copy link
Copy Markdown

📦 Preview package for this PR is published!

Version: 0.12.0-pr-523.0

Install it with:

npm install svelteplot@pr-523
# or install the specific version
npm install svelteplot@0.12.0-pr-523.0

@github-actions
Copy link
Copy Markdown

📦 Preview package for this PR is published!

Version: 0.12.0-pr-523.1

Install it with:

npm install svelteplot@pr-523
# or install the specific version
npm install svelteplot@0.12.0-pr-523.1

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ecb8917dc9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/lib/marks/Contour.svelte Outdated
Comment thread src/lib/marks/Contour.svelte Outdated
Comment on lines +484 to +487
const markChannels = ['x', 'y', 'fill'] as const;

const markFill = $derived(
isDenseGridMode || isSamplerMode ? (RAW_VALUE as any) : (value as any)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid forcing contour values into the global color scale

This always registers a fill channel backed by scalar field values, even when the contour is rendered with constant styles (fill="none", stroke="currentColor"). In mixed plots, that injects numeric contour values into the shared color scale domain and can change type/domain inference for other marks (for example, categorical color encodings become mixed with numeric categories), producing incorrect colors and legends outside the contour itself.

Useful? React with 👍 / 👎.

@github-actions
Copy link
Copy Markdown

📦 Preview package for this PR is published!

Version: 0.12.0-pr-523.2

Install it with:

npm install svelteplot@pr-523
# or install the specific version
npm install svelteplot@0.12.0-pr-523.2

@github-actions
Copy link
Copy Markdown

📦 Preview package for this PR is published!

Version: 0.12.0-pr-523.3

Install it with:

npm install svelteplot@pr-523
# or install the specific version
npm install svelteplot@0.12.0-pr-523.3

@github-actions
Copy link
Copy Markdown

📦 Preview package for this PR is published!

Version: 0.12.0-pr-523.4

Install it with:

npm install svelteplot@pr-523
# or install the specific version
npm install svelteplot@0.12.0-pr-523.4

@github-actions
Copy link
Copy Markdown

📦 Preview package for this PR is published!

Version: 0.12.0-pr-523.5

Install it with:

npm install svelteplot@pr-523
# or install the specific version
npm install svelteplot@0.12.0-pr-523.5

@github-actions
Copy link
Copy Markdown

📦 Preview package for this PR is published!

Version: 0.12.0-pr-523.6

Install it with:

npm install svelteplot@pr-523
# or install the specific version
npm install svelteplot@0.12.0-pr-523.6

@github-actions
Copy link
Copy Markdown

📦 Preview package for this PR is published!

Version: 0.12.0-pr-523.7

Install it with:

npm install svelteplot@pr-523
# or install the specific version
npm install svelteplot@0.12.0-pr-523.7

gka and others added 10 commits March 17, 2026 20:29
Adds a new `<Contour>` mark that renders iso-contour lines and filled
contour bands from a scalar field using the marching-squares algorithm
(d3-contour).

Supports the same three input modes as `<Raster>`:
- **Dense grid**: flat row-major array + `width`/`height`
- **Function sampling**: `value={(x, y) => number}` evaluated on a pixel grid
- **Scatter interpolation**: point cloud with x/y/value channels, spatially
  interpolated via nearest/barycentric/random-walk (reuses rasterInterpolate helpers)

Key design decisions:
- `fill="value"` / `stroke="value"` are special keywords that map each
  contour level's threshold through the plot color scale; any other string
  is a constant CSS color
- When `fill` is active, `stroke` defaults to `"none"`; otherwise `"currentColor"`
- Color scale domain is registered via `<Mark>`'s fill channel so all modes
  (including function-sampling) correctly infer domain and type
- For sampler mode, a cheap 20×20 coarse sample registers the color domain
  while the full-resolution grid is (re-)evaluated on every render, allowing
  impure functions (closures over reactive state, `performance.now()`) to work

Includes:
- `src/lib/marks/Contour.svelte` — the mark component
- Docs page at `/marks/contour` with interactive examples
- Six example plots (volcano, filled bands, sampled, weather, quantile, interactive swirl)
- 15 unit tests covering all three modes and style defaults
- Visual regression snapshots

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add key `(contourGeom.value)` to `{#each}` block (svelte/require-each-key)
- Run Prettier on four new files (quantile.svelte, volcano.svelte,
  weather.svelte, contour.test.svelte.ts)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove spurious "and" in fill="value" description
- Fix "four interpolation strategies" to list all four (add `none`)
- Fix broken Example link to point to /examples/contour/volcano
- Replace mismatched live example in explicit-thresholds section with
  one that actually demonstrates an explicit thresholds array
- Fix "Pass a function to thresholds" inaccuracy in quantile section
  (the example passes a precomputed array, not a function)
- Fix wrong band-count comment (7 → produces 9 bands, not 10)
- Reword quantile section intro for clarity

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the manual floor/accumulate loop with d3-array's range(), which
handles floating-point precision correctly and is more idiomatic. Also
simplify the control flow: resolve the interval case with an early return
rather than converting to an intermediate floor/range object.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…contours

Previously the fill channel was always registered with scalar field values,
which polluted the shared color scale domain even when fill/stroke were
plain CSS colors. This corrupted type/domain inference for sibling marks
(e.g. categorical color encodings gained unexpected numeric entries).

Now scalar values are only registered when fill="value" or stroke="value"
is active (markUsesColorScale). The three modes are handled separately:

- Dense grid / sampler: fill channel omitted entirely when not coloring by value
- Scatter: fill channel is always registered (it's the delivery mechanism for
  scalar values into computeContours via scaledData[].resolved.fill), but
  passes { value, scale: false } when color scale registration is not needed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When `value` is omitted and `fill` or `stroke` is a data accessor
(field name string or function) rather than a CSS color, it is
automatically promoted to the `value` channel and replaced with the
`"value"` keyword — matching Observable Plot's behaviour.

Examples:
  <Contour data={pts} x="x" y="y" fill="temp" />
  -- equivalent to: value="temp" fill="value"

  <Contour data={pts} x="x" y="y" stroke={(d) => d.temp} />
  -- equivalent to: value={(d) => d.temp} stroke="value"

Detection uses the existing isColorOrNull helper (d3-color backed) plus
explicit checks for SVG/SveltePlot constants ("none", "value",
"inherit") that d3-color does not recognise as CSS colors. CSS color
strings (named colors, hex, functional notation) and the "value" /
"none" keywords are never promoted.

Throws when both fill and stroke are accessors (ambiguous).

Adds 4 unit tests covering: field-name promotion, function promotion,
no promotion when value is explicit, no promotion for CSS color strings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both marks now read from getPlotDefaults().contour / .raster at
component initialisation, so project-wide defaults (e.g. blur,
pixelSize, interpolate, smooth) can be set with setPlotDefaults():

  setPlotDefaults({ contour: { blur: 1, pixelSize: 3 } });
  setPlotDefaults({ raster: { pixelSize: 2, imageRendering: 'pixelated' } });

PlotDefaults.contour and .raster were already declared in the type
system; this adds the missing runtime wiring following the same
DEFAULTS pattern used by Dot, Link, Sphere, and other marks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a new example plot derived from the existing interpolated raster
example, using the CA55 aeromagnetic survey dataset. The contour mark
overlays the interpolated field with Dot markers at the original
measurement positions, demonstrating scatter-to-contour interpolation
with the fill shorthand (fill="MAG_IGRF90" auto-promotes to value).

Also removes accidental debug console.log calls left in Contour.svelte.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@gka gka force-pushed the feat/contour-mark branch from 74ca6d0 to 9370822 Compare March 17, 2026 20:29
@github-actions
Copy link
Copy Markdown

📦 Preview package for this PR is published!

Version: 0.12.0-pr-523.8

Install it with:

npm install svelteplot@pr-523
# or install the specific version
npm install svelteplot@0.12.0-pr-523.8

@github-actions
Copy link
Copy Markdown

📦 Preview package for this PR is published!

Version: 0.12.0-pr-523.9

Install it with:

npm install svelteplot@pr-523
# or install the specific version
npm install svelteplot@0.12.0-pr-523.9

@github-actions
Copy link
Copy Markdown

📦 Preview package for this PR is published!

Version: 0.12.0-pr-523.10

Install it with:

npm install svelteplot@pr-523
# or install the specific version
npm install svelteplot@0.12.0-pr-523.10

x1/x2/y1/y2 in ContourMarkProps are typed as number (sampler bounds),
so casting Symbol values directly to that type fails svelte-check.
Using `as unknown as ContourMarkProps['x1']` satisfies TypeScript.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

📦 Preview package for this PR is published!

Version: 0.12.0-pr-523.11

Install it with:

npm install svelteplot@pr-523
# or install the specific version
npm install svelteplot@0.12.0-pr-523.11

@gka gka merged commit 0182c09 into main Mar 18, 2026
8 checks passed
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.

feature: add contour mark

1 participant