Python package (Jupyter)
Meratyn ships a Python package, meratyn, that embeds the full Meratyn
app inside a Jupyter notebook cell as an anywidget,
with a leafmap-style API.
The widget loads the complete Meratyn app (menus, panels, processing tools) in
an iframe. State syncs both ways through a single .meratyn.json project, so
data you add from Python appears in the UI, and edits you make in the UI
(panning, zooming, adding layers) are readable back from Python.
Install
pip install -e ./python
The package is currently installed from this checkout; this repository does not publish it to PyPI or conda.
Optional extras for add_geojson() / add_gdf() from a GeoDataFrame, for
reading local vector files with add_vector() / add_geoparquet() /
add_flatgeobuf() / add_shp() / add_kml() / add_gpkg(), and for
in-memory xarray raster inputs:
pip install "meratyn[all]" # vectors + xarray/rioxarray/rasterio
pip install "meratyn[raster]" # only xarray/rioxarray/rasterio
For a Plotly Dash layout, install the separate meratyn-dash adapter from
this checkout. The core meratyn package remains usable without Dash:
pip install -e ./python -e ./python/dash
The optional [all] extra is pip-only. If you installed via conda, add it with
pip install "meratyn[all]" inside the same environment.
Earth Engine layers
Install the Earth Engine SDK separately and authenticate in the Python kernel:
pip install earthengine-api
import ee
from meratyn import Map
ee.Authenticate()
ee.Initialize(project="your-cloud-project")
m = Map()
m.add_ee_layer(
ee.Image("COPERNICUS/S2_SR_HARMONIZED/20240101T000000_20240106T000000"),
{"bands": ["B4", "B3", "B2"], "min": 0, "max": 3000},
name="Sentinel-2",
)
add_ee_layer() accepts an ee.Image, ee.ImageCollection (mosaicked before
rendering), ee.FeatureCollection, ee.Feature, or ee.Geometry. Vector
objects are styled with the collection’s supported style keys; image
visualization accepts Earth Engine bands, min, max, palette, and related
parameters. Earth Engine evaluates the object in the kernel and passes only a
normal XYZ tile layer to the browser.
Authentication and quota remain Earth Engine responsibilities. If the SDK is
missing, the client is not initialized, or a request exceeds its Cloud project
quota, Meratyn returns a privacy-safe error without retaining request bodies,
credentials, or raw service URLs. Earth Engine map IDs and tile URLs expire, so
regenerate the layer when a reopened project’s tiles are no longer valid. The
live, co-located widget retains the runtime tile token only for rendering; the
project file stores the layer reference only. to_project() and
save_project() always redact Earth Engine tile URL tokens, including when
keep_credentials=True; that option can preserve unrelated credential fields
for a trusted local artifact. to_html() and external embeds are always
redacted, and add_ee_layer() never persists credentials.
Quickstart
from meratyn import Map
m = Map(center=(-100, 40), zoom=4)
m.add_geojson("https://example.com/data.geojson", name="Data")
m
The full Meratyn UI renders in the cell. Add more data and drive the view:
m.add_tile_layer(
"https://tile.openstreetmap.org/{z}/{x}/{y}.png",
name="OpenStreetMap",
attribution="(c) OpenStreetMap contributors",
)
m.add_cog("https://example.com/dem.tif", name="DEM", colormap="terrain")
m.add_polyline(
"_p~iF~ps|U_ulLnnqC_mqNvxq`@",
name="Route",
precision=5, # use precision=6 for Valhalla/Mapbox values
)
m.add_basemap("dark")
m.set_center(-120, 47, zoom=8)
Add point tables and optional data-driven styling:
m.add_csv("cities.csv", x="longitude", y="latitude", name="Cities")
m.add_heatmap([(-75.5636, 6.2442), (-75.5, 6.3)], radius=35)
m.add_gdf(cities, name="Cities", column="population", colormap="blues")
add_polyline accepts a Google/OSRM precision-5 or Valhalla/Mapbox
precision-6 string, a newline-delimited batch, or a sequence of strings. Pass
delimiter="|" (or another non-empty separator) for a custom batch and
unescape=True for values copied from JSON/source literals. Invalid, truncated,
out-of-range, and one-point values are skipped; as_multi_line=True combines
the valid batch into one MultiLineString feature. The codec is available
without optional GeoPandas or raster dependencies through
meratyn.encode_polyline, meratyn.decode_polyline, and
meratyn.decode_polyline_detailed (which reports complete/truncated input),
and meratyn.polyline_to_geojson.
add_raster also accepts an xarray DataArray or Dataset when the raster
extra is installed. The object is written to a temporary Cloud-Optimized
GeoTIFF for the browser and removed when the map closes; pass array_args such
as variable=, isel=, x_dim=, y_dim=, crs=, or nodata= when needed.
add_raster / add_cog also accept a local GeoTIFF path on the kernel host:
the file is served by the bundled localhost server so the app can read it. This
only works where the browser can reach the kernel’s localhost (local Jupyter,
VS Code); on remote/browser-separated setups (Colab, JupyterHub, remote servers)
the localhost route is unreachable, so pass a hosted URL there. The served URL is
also session-scoped, so a project saved with a local raster will not restore it
when reopened later — pass a hosted URL for durable projects.
Add markers and data-driven symbology without precomputing styles:
m.add_marker(-76.522, 3.4372, properties={"name": "Cali"})
m.add_marker_cluster([(-75.5144, 10.391), (-74.7813, 10.9685), (-74.1982, 11.2408)])
m.add_choropleth(
"https://example.com/counties.geojson",
column="population",
colormap="blues",
scheme="quantile",
)
Add a legend, a colorbar, and a swipe (split-map) comparison:
# A built-in land-cover legend, or your own {label: color} dict.
m.add_legend(builtin="nlcd")
m.add_legend(legend_dict={"Water": "#0000ff", "Land": "#00ff00"})
# A colorbar for a continuous raster.
m.add_colorbar(colormap="terrain", vmin=0, vmax=4000, label="Elevation", units="m")
# Compare two layers (or a layer against the basemap) with a swipe slider.
before = m.add_cog("https://example.com/before.tif", name="Before")
after = m.add_cog("https://example.com/after.tif", name="After")
m.split_map(before, after)
Two-way sync
Because the project syncs both ways, you can pan or zoom the map in the UI and then read the live state back from Python:
proj = m.to_project()
proj["mapView"]["center"] # reflects the live UI view
[layer["name"] for layer in proj["layers"]]
to_project(), save_project(), and to_html() redact credentials:
service URL tokens, headers, environment variables, geocoder keys, and opaque
plugin settings are removed from their detached/exported project copies.
to_project() and save_project() accept keyword-only keep_credentials=True
when writing a trusted local artifact. That option preserves unrelated
credential fields only; Earth Engine tile URL tokens remain redacted. Standalone
HTML and external embeds always remain redacted; the live co-located anywidget
retains full project fidelity.
The top-level meratyn.save_project(path, project) authoring primitive is
different: it writes the supplied project verbatim, credentials included, so
editing a project in place cannot strip the owner’s API keys. Before sending
that file anywhere untrusted, pass the project through
meratyn.project.redact_project_credentials; Map.save_project performs that
redaction by default.
Standalone HTML exports frame the viewer with a restrictive iframe sandbox. The viewer receives only script execution, same-origin storage/handshake, form submits, and anchor downloads; popups and top-level navigation stay disabled.
Save and reload projects, fully interchangeable with the desktop and web apps:
m.save_project("my-map.meratyn.json")
m2 = Map()
m2.load_project("my-map.meratyn.json")
m2
Dash integration
The meratyn-dash DashMap component embeds the Meratyn app in a Plotly Dash layout while retaining the
same project and iframe lifecycle as the anywidget. The integration is
optional, so regular notebook imports do not require Dash. It supports
meratyn>=2.8.0,<3.0:
from dash import Dash, Input, Output, html
from meratyn_dash import DashMap
app = Dash(__name__)
m = DashMap(center=(-119.0, 39.8), zoom=5, basemap="dark", height="800px")
app.layout = html.Div([m, html.Pre(id="click-output")])
@app.callback(Output("click-output", "children"), Input(m, "clickData"))
def show_click(click_data):
return "Click the map" if click_data is None else str(click_data)
clickData contains lngLat as an {lng, lat} object plus an array of
identified GeoJSON features. selectionData contains the selected layer and
feature ids. viewData is reserved for a future camera-event payload. The
existing Map.on_click() and Map.on_selection_change() APIs are unchanged.
Each DashMap owns its iframe and message listener; unmounting a component
removes that listener, and maps created without an id receive independent
generated ids so multiple maps can share one Dash layout.
Map options
Map(
center=(-100, 40), # [lng, lat]
zoom=4,
basemap="dark", # a basemap name or a MapLibre style URL
height="800px",
layout="embed", # "embed" (compact UI), "full" (desktop UI), or "maponly"
theme="light", # "light" or "dark"
)
Interactive scripting
Beyond adding data, the widget can query the live app and react to it — the
same surface as the in-app Python Console. These
calls round-trip to the running map, so the map must be displayed first (show
m in a cell), then run the queries in a later cell.
m.get_center() # live [lng, lat], reflecting UI pans/zooms
m.get_bounds() # [west, south, east, north]
m.fly_to(-75.6961, 4.8143, zoom=10)
m.identify(-75.6961, 4.8143) # features at a point, like clicking the map
# Layer objects: read and mutate layers, read their features
for layer in m.layers:
layer.opacity = 0.6
features = m.layers[0].get_features() # list of GeoJSON Feature objects
# Run a processing algorithm; result layers are added to the map
m.list_algorithms()
m.run_algorithm("buffer", {"layer": layer_id, "distance": 1000})
m.list_whitebox_tools()
m.run_whitebox_tool("slope", {"dem": layer_id})
# Read back what the user selected or drew on the map
m.get_selected_features() # the clicked feature(s) as Feature objects
m.get_drawn_features() # features sketched with the Geo Editor
m.user_rois # the drawn ROIs as a GeoJSON FeatureCollection
m.get_drawn_features(as_gdf=True) # the same, as a GeoDataFrame (needs GeoPandas)
png = m.to_image() # PNG bytes (or m.to_image("map.png"))
m.to_html("map.html") # standalone HTML embedding the live project
React to user interaction with event callbacks:
m.on_click(lambda e: print("clicked", e["lngLat"]))
m.on_selection_change(lambda e: print("selected", e))
m.on_layer_change(lambda e: print("layers", e["layerIds"]))
Blocking queries Interactive queries block the kernel until the app replies (via
jupyter_ui_poll, installed automatically). Passtimeout=for slow calls, e.g.m.run_algorithm(..., timeout=300).
API reference
Interactive queries, events & processing
| Method | Description |
|---|---|
get_view() / get_center() / get_bounds() | Read the live camera / center / viewport bounds. |
fly_to(lng, lat, zoom=, bearing=, pitch=, duration=) | Animate the camera. |
fit_bounds([w, s, e, n]) | Fit the camera to a bounding box. |
zoom_to_bounds(bounds) / zoom_to_layer(layer) | Fit the view to bounds or a layer id, name, or Layer handle. |
identify(lng, lat, layer_id=None) | Query rendered features at a point. |
get_features(layer_id) | A layer’s features as Feature objects. |
get_selected_features(as_gdf=False) | The feature(s) selected in the app, as Feature objects (or a GeoDataFrame). |
get_drawn_features(as_gdf=False) / user_rois | Features drawn with the Geo Editor; user_rois returns them as a FeatureCollection. |
layers / get_layer(id) | Layer handles (read state; set name/visible/opacity, set_style, get_features, zoom_to, remove). |
list_algorithms() | Available processing algorithms (id, parameters, …). |
run_algorithm(id, parameters=None, timeout=) | Run an algorithm; returns {logs, resultLayerIds}. |
list_whitebox_tools() / run_whitebox_tool(id, parameters=None, timeout=) | Discover and run bundled Whitebox/WASM tools; layer inputs accept ids or Layer handles. |
to_image(path=None, timeout=) | Capture the map as PNG bytes, or write to path. |
to_html(path=None, title=, width=, height=, app_url=) | Export a standalone HTML page with a redacted project; returns the HTML or writes to path. |
on(event, cb) / on_click / on_selection_change / on_layer_change | Register event callbacks; returns an unsubscribe function. |
request(method, params=None, timeout=) | Low-level command primitive behind the methods above. |
Data, view & projects
| Method | Description |
|---|---|
Map(center, zoom, basemap=, height=, layout=, theme=) | Create a map. |
add_geojson(data, name=, **style) | Add GeoJSON from a dict, file path, URL, JSON string, or GeoDataFrame. |
add_gdf(gdf, name=, column=None, **style) | Add a GeoDataFrame, optionally as a graduated choropleth. |
add_polyline(polyline, name=, precision=, delimiter=, unescape=, as_multi_line=, **style) | Add encoded Google/OSRM (precision 5) or Valhalla/Mapbox (precision 6) lines; malformed values are skipped. |
add_csv / add_xy_data (data, x=, y=, name=, **style) | Add WGS84 points from CSV, a DataFrame, or row mappings; remaining fields become properties. |
add_marker(lng, lat, name=, properties=, **style) | Add a single point marker (shown as a circle; properties appear on click). |
add_markers(points, name=, **style) | Add point markers from (lng, lat) pairs, {lng/lon/x, lat/y, …} dicts, GeoJSON, or a GeoDataFrame. |
add_circle_markers(points, name=, radius=, **style) | Add circle markers with an explicit radius. |
add_marker_cluster(points, name=, cluster_radius=, cluster_max_zoom=, **style) | Add clustered point markers. |
add_heatmap(points, name=, radius=, intensity=, **style) | Add point data using the density heatmap renderer. |
add_choropleth(data, column, name=, class_count=, colormap=, scheme=, **style) | Add a GeoJSON layer with graduated symbology computed from a numeric column. |
add_data(data, column=None, name=, **kwargs) | Add data; a choropleth when column is given, else a plain GeoJSON layer (leafmap parity). |
add_vector(data, name=, render_mode=, data_format=, source_layer=, **style) | Add a vector dataset from a URL (GeoParquet, FlatGeobuf, zipped Shapefile, KML/KMZ, GeoPackage, GeoJSON, …) or a local file (read via GeoPandas and inlined). |
add_geoparquet(data, name=, **style) | Add a GeoParquet dataset (URL or local file). |
add_flatgeobuf(data, name=, **style) | Add a FlatGeobuf dataset (URL or local file). |
add_shp(data, name=, **style) | Add a Shapefile (zipped URL or local .shp). |
add_kml(data, name=, **style) / add_gpkg(data, name=, layer=, **style) | Add KML/KMZ or GeoPackage data; local GeoPackage layer selects a table. |
add_vector_tiles(url, name=, source_layers=, source_layer=, **style) | Add a vector tile layer from a TileJSON endpoint. |
add_pmtiles(url, name=, tile_type=, source_layers=, **style) | Add a PMTiles archive (vector or raster). |
add_tile_layer(url, name=, tile_size=, attribution=) | Add a raster XYZ tile layer. |
add_wms(endpoint, layers, name=, styles=, image_format=, transparent=, tile_size=, **style) | Add a WMS layer (GetMap, tiled raster). |
add_wmts(url, name=, tile_size=, **style) | Add a WMTS layer from a tile URL template. |
add_wfs(endpoint, type_name, name=, version=, output_format=, srs_name=, max_features=, **style) | Add a WFS layer (GetFeature GeoJSON, fetched and inlined). |
add_cog(url, name=, bands=, colormap=, rescale=, **style) | Add a Cloud Optimized GeoTIFF (URL or a kernel-side local GeoTIFF path). |
add_raster(url, name=, bands=, colormap=, rescale=, **style) | Add a raster (COG/GeoTIFF), URL or local path; alias of add_cog. |
add_ee_layer(ee_object, vis_params=None, name=, shown=, opacity=) | Add an authenticated Earth Engine image, collection, feature, or geometry as a normal XYZ layer; tile URLs expire and are not persisted with credentials. |
add_3d_tiles(url, name=, altitude_offset=, request_headers=, **style) | Add a 3D Tiles tileset.json. |
add_video(urls, coordinates, name=, **style) | Add a georeferenced video (four [lng, lat] corners). |
add_basemap(basemap) | Set the background basemap. |
split_map(left_layers=None, right_layers=None, orientation=, position=, control_position=) | Add a swipe (split-map) comparison slider between two layer sets. |
add_legend(title=None, legend_dict=, labels=, colors=, builtin=, position=, shape=) | Add a legend from a {label: color} dict, parallel labels/colors, or a builtin preset ("nlcd", "esa_worldcover"). |
add_colorbar(colormap=, vmin=, vmax=, label=, units=, colors=, orientation=, position=) | Add a colorbar for a continuous raster, from a named colormap or custom colors. |
add_colormap(colormap, vmin=, vmax=, label=, **kwargs) | Add a colorbar from a named colormap (leafmap-style alias of add_colorbar). |
set_center(lng, lat, zoom=None) | Center (and optionally zoom) the map. |
set_center_zoom(lng, lat, zoom=None) | Alias of set_center (leafmap compatibility). |
layer_names / find_layer(name) / find_layer_index(name) | Inspect layers by display name. |
set_layer_visibility(layer, visible) / set_layer_opacity(layer, opacity) | Change a layer by id, name, or Layer handle; opacity is constrained to [0, 1]. |
set_zoom(zoom) / set_bearing(bearing) / set_pitch(pitch) / fit_project_bounds(bounds) | Persist camera changes without requiring the widget to be displayed. |
center / zoom / bearing / pitch / basemap / name | Read persisted project and camera state; name is writable. |
rename_layer(layer, name) / move_layer(layer, index) / duplicate_layer(layer, name=) / show_layer(layer) / hide_layer(layer) | Manage layers by id, name, or Layer handle. |
layer_properties(layer) / column_values(layer, column) / describe() | Inspect inlined data and summarize a project without a browser round trip. |
remove_layer(layer_id) / clear_layers() | Remove one layer by id, name, or handle, or remove all layers. |
to_project(keep_credentials=False) | Return a detached, redacted project dict by default. |
load_project(src) | Replace the project from a dict, JSON string, or .meratyn.json path. |
save_project(path, keep_credentials=False) | Write a redacted project by default. |
Style keyword arguments (for example fillColor, strokeColor, strokeWidth,
circleRadius) map to the Meratyn layer style fields.
How it works
The wheel bundles the Meratyn web build. At import time the package starts a
small localhost static server that serves the bundled app; the widget renders
that app in an iframe and exchanges the project over window.postMessage.
Adding data from Python rewrites the synced project and pushes it into the app;
UI edits flow back the same way.
Environment support The interactive widget works in local Jupyter, VS Code, Google Colab, and JupyterHub / remote servers:
- Local Jupyter / VS Code - the app is served directly from localhost.
- Google Colab - routes through Colab’s built-in port proxy (
google.colab.kernel.proxyPort) automatically.- JupyterHub (including managed/shared hubs, detected at runtime via
JUPYTERHUB_SERVICE_PREFIX) - the front-end probes two same-origin routes and uses whichever is live, so a host needs only one of them:
- the Jupyter Server extension bundled with
meratyn, mounted at{base_url}mappview/app/on the notebook server’s own origin. It is enabled automatically onpip install meratynand needs nojupyter-server-proxyand no extra port, so it works on locked-down hubs that block raw-port proxying — but it only registers after the Jupyter server restarts, since it loads from a startup config drop-in.jupyter-server-proxyat{base_url}proxy/{port}/, which reaches the kernel’s localhost bundle in the running server with no restart, whereverjupyter-server-proxyis installed.- Other remote servers (Binder, remote JupyterLab over SSH/network) - pass
Map(server_proxy=True)to use that same dual-route remote path.Set
Map(server_proxy=False)to force the direct localhost path. If the app fails to load on a hub, either installjupyter-server-proxy, or confirm the extension is enabled withjupyter server extension list(look formeratyn; runjupyter server extension enable meratynif absent) and restart the Jupyter server so the extension loads.
URL fetching
add_csv(url)/add_xy_data(url)andadd_wfs()fetch data from the kernel with redirect re-validation and a 50 MB response bound. Hosts resolving to private, loopback, or link-local addresses (including cloud metadata endpoints) are refused.add_geojson(url), tile layers, and service layers are fetched by the browser instead. Local GeoJSON paths are served through the widget’s session file endpoint. In-memory GeoJSON retains a 50 MiB guard; collections above 4 MiB use a temporary file reference instead of copying the data through the notebook message channel. Do not load untrusted.meratyn.jsonprojects or URLs on a shared/multi-tenant kernel.
Building from source
The package lives in python/.
The bundled app is produced from the monorepo with:
bun run build:embed # builds the app and stages it into the wheel
python -m build # builds the wheel
python -m twine upload dist/* # upload to PyPI
pip install -e python # editable install for development
Changes to the Python code are picked up on kernel restart. Changes to the app
(TypeScript) require re-running bun run build:embed and restarting the kernel.