Add mapbox-heatmap-small-dataset-crash skill
This commit is contained in:
parent
dc73ebb535
commit
4d0b752fa3
1 changed files with 127 additions and 0 deletions
127
dot_claude/skills/mapbox-heatmap-small-dataset-crash/SKILL.md
Normal file
127
dot_claude/skills/mapbox-heatmap-small-dataset-crash/SKILL.md
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
---
|
||||
name: mapbox-heatmap-small-dataset-crash
|
||||
description: |
|
||||
Fix for Mapbox GL JS "ExpressionEvaluationError: Input is not a number" when using
|
||||
data-driven paint properties (legacy `{ property, stops }` format or interpolate
|
||||
expressions). Use when: (1) heatmap or fill layer crashes after filtering reduces
|
||||
the dataset to fewer than ~20 features, (2) switching metrics or filter parameters
|
||||
triggers the error intermittently, (3) color stops produce NaN values from percentile
|
||||
calculations. Root causes: out-of-bounds percentile index on small arrays, non-monotonic
|
||||
stops when min === max, and stale color stops from a previous metric remaining active
|
||||
when no valid values exist for the new metric.
|
||||
author: Claude Code
|
||||
version: 1.0.0
|
||||
date: 2026-02-08
|
||||
---
|
||||
|
||||
# Mapbox Heatmap Crash on Small Datasets
|
||||
|
||||
## Problem
|
||||
|
||||
Mapbox GL JS throws `ExpressionEvaluationError: Input is not a number` when a data-driven
|
||||
paint property (e.g., `fill-color` based on a feature property) encounters non-numeric
|
||||
values in color stops or feature properties. This commonly surfaces when dynamic filtering
|
||||
reduces the visible dataset to a small number of features.
|
||||
|
||||
## Context / Trigger Conditions
|
||||
|
||||
- Using legacy Mapbox property functions: `{ property: "count", stops: [[val, color], ...] }`
|
||||
- Mapbox GL JS v2+ internally converts legacy format to `["interpolate", ["linear"], ["number", ["get", "count"]], ...]`
|
||||
- The `["number", ...]` assertion throws if the input is not a number (including `NaN`)
|
||||
- Error appears in stack as: `vi → zs → evaluate → populatePaintArray → ... → reloadTile`
|
||||
|
||||
Common triggers:
|
||||
1. Filtering data down to 1-20 features (percentile index goes out of bounds)
|
||||
2. Switching metrics where the new metric has sparse/missing data
|
||||
3. Color stops from a previous metric remaining active for new data
|
||||
|
||||
## Solution
|
||||
|
||||
### 1. Clamp percentile indices to array bounds
|
||||
|
||||
The most common cause: `Math.round(values.length * 0.95)` exceeds `values.length - 1`
|
||||
for small arrays.
|
||||
|
||||
```typescript
|
||||
// BAD: out-of-bounds when values.length < 20
|
||||
const maxIndex = Math.round(values.length * 0.95);
|
||||
const max = values[maxIndex]; // undefined!
|
||||
|
||||
// GOOD: clamp to valid range
|
||||
const maxIndex = Math.min(
|
||||
Math.round(values.length * PERCENTILE_MAX),
|
||||
values.length - 1
|
||||
);
|
||||
const max = values[maxIndex]; // always valid
|
||||
```
|
||||
|
||||
Why: `Math.round(1 * 0.95) = 1` but a 1-element array's max index is 0. The returned
|
||||
`undefined` cascades: `Math.max(undefined, x)` = `NaN`, then `calculateColorStops(scheme, min, NaN)`
|
||||
produces `[[NaN, color], ...]`, and Mapbox's expression evaluator throws.
|
||||
|
||||
### 2. Ensure strictly monotonic stops (guard min === max)
|
||||
|
||||
```typescript
|
||||
const max = Math.max(values[maxIndex], min + 1);
|
||||
```
|
||||
|
||||
When all features have the same metric value, `min === max`. The legacy format's internal
|
||||
conversion to `["interpolate", ...]` requires strictly increasing stop inputs.
|
||||
|
||||
### 3. Always set color stops (never leave stale stops)
|
||||
|
||||
```typescript
|
||||
if (values.length > 0) {
|
||||
const stops = calculateColorStops(scheme, min, max);
|
||||
heatmap.setColorStops(stops);
|
||||
} else {
|
||||
// Don't skip — stale stops from a previous metric cause range mismatches
|
||||
const stops = calculateColorStops(scheme, 0, 1);
|
||||
heatmap.setColorStops(stops);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Filter Infinity from metric values
|
||||
|
||||
```typescript
|
||||
.filter(v => typeof v === 'number' && isFinite(v) && v > 0)
|
||||
```
|
||||
|
||||
`isNaN(Infinity)` is `false`, so `Infinity` passes NaN checks but can produce
|
||||
non-monotonic or degenerate stops.
|
||||
|
||||
### 5. Don't inject undefined into feature properties
|
||||
|
||||
When injecting synthetic properties for heatmap coloring, skip features without data
|
||||
rather than setting the property to `undefined`:
|
||||
|
||||
```typescript
|
||||
// BAD: Mapbox sees { poi_travel_7_WALK: undefined }
|
||||
return { ...feature, properties: { ...feature.properties, [prop]: match?.value } };
|
||||
|
||||
// GOOD: feature unchanged, property simply absent
|
||||
if (!match) return feature;
|
||||
return { ...feature, properties: { ...feature.properties, [prop]: match.value } };
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
1. Set a tight filter that reduces data to 1-5 features — no crash
|
||||
2. Switch between metrics rapidly — no crash
|
||||
3. Select a metric with no data (e.g., uncomputed travel mode) — graceful empty map
|
||||
4. All features have identical metric value — colors still render
|
||||
|
||||
## Notes
|
||||
|
||||
- The legacy `{ property, stops }` format still works for colors in Mapbox GL JS v2+
|
||||
but is internally converted to expressions. Don't "fix" it by switching to expression
|
||||
format manually — color interpolation with `rgba()` strings works in legacy format
|
||||
but can break if the expression is constructed incorrectly.
|
||||
- The `["number", value, fallback]` syntax is NOT a valid Mapbox expression. `"number"`
|
||||
is a type assertion that throws on non-numbers. For fallbacks, use
|
||||
`["coalesce", ["to-number", ["get", "prop"]], 0]` instead.
|
||||
- HexgridHeatmap's reduce function handles `undefined` and `NaN` via `isNaN()` checks,
|
||||
but `isNaN(null) === false`, so null values pass through as 0.
|
||||
|
||||
See also: react-hooks-order-early-return (hooks after early returns cause similar
|
||||
intermittent crashes during UI interactions)
|
||||
Loading…
Add table
Add a link
Reference in a new issue