1Boot a map
mount() takes a container and a config, and resolves to a FimMap — the
per-map handle everything else hangs off. provider is required and has no
default: the two built-in backends differ in credentials and in capability, so the
engine refuses to pick one for you. Leaflet needs nothing; Google needs an apiKey.
// The engine ships no UI. This mounts a real, working, empty map — nothing else. import { FimViz, ColorScale, Stats } from "fimviz"; // Omit `provider` and this throws `config-invalid` naming both backends. // Omit the `apiKey` that 'google' requires and it throws the same error. const fim = await FimViz.mount("#map", { provider: "leaflet" }); fim.map; // the real L.Map (or google.maps.Map) — advisory, read-mostly fim.config; // this instance's config; the engine never reads an ambient one
Output
…
2Parse a file into a Dataset
addDataset() takes a File, Blob, ArrayBuffer or
URL string, detects the format from the name, and returns a lazy Dataset. It
reads the header and stops there: isMaterialized is false and no pixel has
been decoded. Note also that parsing never reprojects — ds.crs is whatever the
file itself declares. This sample happens to be EPSG:4326, so it can be drawn as-is; a raster in
any other CRS has to be warped first (notebook 2).
// A user upload would arrive as a File from an <input> or a drop; a fetched sample // is the same thing, so this page wraps the bytes in a File to keep the shape honest. const res = await fetch("../assets/SampleFiles/4326.tif"); const file = new File([await res.blob()], "4326.tif"); const ds = await fim.addDataset(file); // → Dataset, pushed onto fim.datasets ds.toJSON(); // the metadata view — no heavy `data` payload ds.isMaterialized; // false — the header was read, nothing was decoded
Output
…
3Render it, and fit the map to it
addLayer() constructs a layer and draws it. Passing the Dataset alone is enough — the
registry infers 'raster' or 'vector' from ds.kind. This is the
call that actually forces the chain: the file is decoded here, not in step 2.
Nothing said which colours to use, yet it draws. When a RasterLayer has no
ColorScale attached it resolves one and keeps it, so
layer.colorScale, getLegend() and getStats() always describe
what is actually on screen. The order is: an explicit scale you attached → a GDAL legend embedded in
the file → a continuous default ramp over the grid's own min/max. This file has no embedded legend,
so it lands on the default.
const layer = await fim.addLayer(ds); // kind-inferred; decodes, colorizes, draws layer.fit(); // move the camera to this layer's bounds layer.colorScale.toJSON(); // the auto-resolved default, ranged to the data fim.layers; // [layer] — the per-map registry, bottom-up
Output
…
4Recolour it live
layer.set() is the one knob writer. Scale keys (palette,
continuous, min, max, unit) route to the attached
ColorScale; layer keys (opacity, noData, hover)
apply to the layer. A batch is one write and one repaint, not one per key — and an
unknown palette name throws with the list of real ones rather than silently falling back.
ColorScale.palettes(); // → every ramp available: built-ins + anything registered // One call, one repaint. `set` is partial/best-effort: if one key fails the // others still apply, and a single aggregate error names which failed. await layer.set({ palette: "viridis", continuous: true, min: 0, max: 10, opacity: 0.85 }); // Registering your own ramp is two arguments — the library owns the mechanism, // the host owns the specifics. ColorScale.registerPalette("flood", ["#f7fbff", "#4292c6", "#08306b"]);
Output
…
5The two read-models: Legend and Stats
Both are derived, not stored. getLegend() is a view of the colour scale —
data-complete (toJSON()) with toHtml() as a convenience renderer.
getStats() is a single pass over the decoded pixels: min/max/mean/median/stddev, a
histogram, real ground area, and byClass counts bucketed by the same scale the map is
drawn with. Both re-derive after a recolour, which is why the panel below changes when you touch the
controls above.
const legend = layer.getLegend(); // Legend — { unit, kind, source, stops } legend.toHtml(); // a ready-made swatch list / gradient bar const stats = await layer.getStats(); // Stats — pure, terminal, serializable stats.describe(); // a one-line human summary stats.percentile(95); // p95 depth stats.toCSV(); // …or hand it straight to a download
Output
…
6Read a pixel under the cursor
valueAt(lat, lng) is the lookup; enableHover() wires it to the provider's
mousemove and emits a ready-to-display hover event. The engine names no element of
yours — it reports what happened and the page decides what that looks like, which is why
the readout below is three lines of page code.
Hit-testing is pixel-level, not bounding-box: hitTest() is true only over a real
non-noData pixel, so a click on a transparent part of the footprint falls through to whatever is
underneath. Move the pointer over the raster to see it.
layer.enableHover({ // A domain rule, not a noData rule: zero depth means "not flooded here". isEmpty: (v) => v === 0, formatValue: (v) => `${v.toFixed(2)} m`, }); layer.on("hover", ({ lat, lng, value, text }) => { readout.textContent = `${text} @ ${lat.toFixed(4)}, ${lng.toFixed(4)}`; });
7A vector layer on the same map
Same two calls, different kind. Vector styling uses a neutral vocabulary —
fillColor, fillOpacity, strokeColor,
strokeWidth, pointRadius — that each provider translates into its own SDK
names, so one style object works on Google and Leaflet alike. Any key the vocabulary doesn't name
passes through untouched to the provider.
This sample is 99 Iowa counties, and the raster is in Mississippi, so the two layers do not overlap
— which makes fit() worth having. Click a county to resolve the feature under the
pointer with featureAt().
const counties = await fim.addDataset(countyFile); // GeoJSON → a vector Dataset const vec = await fim.addLayer("vector", { source: counties, style: { fillColor: "#58a6ff", fillOpacity: 0.15, strokeColor: "#58a6ff", strokeWidth: 1 }, }); // Layer dispatch has to be turned on: hover/click are routed to layers by // hit-test and z-order, and a layer nobody listens to shouldn't pay for it. fim.enableMapEvents(["click", "hover"]); fim.on("map:click", ({ lat, lng }) => vec.featureAt(lat, lng));
Output
…
8Teardown
layer.remove() tears the overlay down, fires removed synchronously and
drops the layer from fim.layers. The Dataset survives: the registry records what
was parsed, the reference count records what is rendered, and only the latter is affected. Re-adding
a layer for the same Dataset re-forces it from bytes it already holds.
fim.on("layers:changed", ({ reason, layer }) => refreshMyList()); layer.remove(); // teardown + 'removed' + drop from fim.layers layer.remove({ purgeSource: true }); // …and unregister sources nothing else holds fim.removeDataset(ds); // throws while a layer still renders it fim.removeDataset(ds, { force: true }); fim.destroy(); // detach the map, release it from its app
Output
…