Map Component Reference¶
The BESSER Map component generates an interactive, multi-layer map in the produced React application. It is powered by Leaflet (used directly, no wrapper library) with OpenStreetMap tiles — no API key is required.
A single Map can stack multiple typed layers on top of the same tile base,
each fetching data from its own domain class:
A store-locator map combining point markers for each branch and a choropleth region layer to colour territories by sales volume is expressed as two layers on one map.
A heatmap of incidents overlaid on GeoJSON district boundaries is two layers.
On this page
Layer Types¶
Each layer has a layer_type that controls how its data is fetched and rendered.
|
Required columns |
Optional columns |
Renderer |
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Note
Column names above are the field names on the bound domain class. The column
does not have to be named exactly latitude — you map any Property to its
role via latitude_field=, geojson_field=, etc. on MapLayer.
Auto-detection¶
When layer_type is None (the default), effective_layer_type() infers it
from the configured fields and, as a fallback, from the attribute names and types on
the bound class:
Explicit
layer_typewins.geojson_fieldandvalue_fieldset →choropleth.geojson_fieldset (novalue_field) →geojson.weight_fieldset →heatmap.latitude_fieldandlongitude_fieldset →points.Inspect bound class attributes by name (exact, case-insensitive matches):
an attribute named
geometry→geojson(orchoroplethif an attribute namedvaluealso exists).attributes named
latitude/lat+longitude/lng/lon→points(orheatmapif an attribute namedweightalso exists).
Default:
points.
Metamodel¶
MapLayerType¶
from besser.BUML.metamodel.gui.dashboard import MapLayerType
MapLayerType.points # lat/lng markers
MapLayerType.geojson # GeoJSON polygons or lines
MapLayerType.choropleth # value-coloured GeoJSON + legend
MapLayerType.heatmap # density heat layer
MapLayer¶
A ViewComponent subclass that holds one data layer on a Map.
Parameter |
Default |
Description |
|---|---|---|
|
(required) |
Unique layer name. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
All field setters validate that the assigned value is a Property instance (or
None). Assigning a non-Property raises TypeError.
Map¶
Parameter |
Default |
Description |
|---|---|---|
|
(required) |
Unique component name. |
|
|
Optional heading displayed above the map. |
|
|
Initial map centre latitude (decimal degrees). |
|
|
Initial map centre longitude (decimal degrees). |
|
|
Initial zoom level (1 = world, 18 = street). |
|
|
List of |
WorldMap and LocationMap are thin subclasses of Map with no additional
parameters.
Worked Example¶
The following model produces a two-layer map: pin-drop markers for Luxembourg store locations and a choropleth colouring each canton by population.
from besser.BUML.metamodel.structural import (
Class, Property, DomainModel, FloatType, StringType,
)
from besser.BUML.metamodel.gui import GUIModel, Module, Screen, DataBinding
from besser.BUML.metamodel.gui.dashboard import Map, MapLayer, MapLayerType
# ── Domain model ────────────────────────────────────────────────────────────
# Table 1: Store locations — point markers
lat = Property(name="latitude", type=FloatType)
lng = Property(name="longitude", type=FloatType)
store_name = Property(name="name", type=StringType)
store = Class(name="Store", attributes={lat, lng, store_name})
# Table 2: Canton polygons — choropleth
geometry = Property(name="geometry", type=StringType) # GeoJSON string
population = Property(name="population", type=FloatType)
canton_name = Property(name="name", type=StringType)
canton = Class(name="Canton", attributes={geometry, population, canton_name})
domain = DomainModel(name="LuxembourgMap", types={store, canton})
# ── Layers ───────────────────────────────────────────────────────────────────
store_layer = MapLayer(
name="stores",
layer_type=MapLayerType.points,
data_binding=DataBinding(name="store_binding", domain_concept=store),
latitude_field=lat,
longitude_field=lng,
label_field=store_name,
)
canton_layer = MapLayer(
name="cantons",
layer_type=MapLayerType.choropleth,
data_binding=DataBinding(name="canton_binding", domain_concept=canton),
geojson_field=geometry,
value_field=population,
label_field=canton_name,
)
# ── Map component ────────────────────────────────────────────────────────────
lux_map = Map(
name="LuxMap",
title="Luxembourg — Stores & Cantons",
center_latitude=49.8153,
center_longitude=6.1296,
zoom=9,
layers=[store_layer, canton_layer],
)
# ── GUI model ────────────────────────────────────────────────────────────────
screen = Screen(name="MapScreen", description="", view_elements={lux_map},
is_main_page=True)
module = Module(name="AppModule", screens={screen})
gui = GUIModel(name="LuxApp", package="com.example.lux",
versionCode="1", versionName="1.0", modules={module})
Running WebAppGenerator(domain, gui, output_dir="output").generate() on this
model produces a React application with MapBlock.tsx that:
Fetches rows from
GET /store/and renders a pin for each store.Fetches rows from
GET /canton/and colours each polygon bypopulationwith a sequential white-to-red scale and an auto-generated legend.Both layers overlay the same OpenStreetMap tile base.
Generated MapBlock.tsx structure¶
The generated MapBlock.tsx drives Leaflet directly — there is no
react-leaflet wrapper. The map instance lives in a ref, layers are plain
Leaflet layer groups, and popup text is always HTML-escaped. Each layer type
has its own section-comment banner so you can extend an individual renderer
without reading BESSER documentation:
MapBlock.tsx
├── Types: LayerConfig, MapConfig, MapBlockProps
├── fetchRows() — robust fetch + row normaliser
├── escapeHtml() — every dynamic value in a popup goes through this
├── choroplethColor() — 5-step sequential white→red scale
│
├── // ===== POINTS LAYER =====
│ renderPointsLayer() — fetches rows, L.marker + bindPopup
│
├── // ===== GEOJSON LAYER =====
│ renderGeoJsonLayer() — fetches rows, L.geoJSON
│
├── // ===== CHOROPLETH LAYER =====
│ renderChoroplethLayer() — L.geoJSON + style fn + a legend added as
│ a new L.Control({position:"bottomright"})
│
├── // ===== HEATMAP LAYER =====
│ renderHeatLayer() — dynamic import("leaflet.heat") + L.heatLayer()
│
├── renderLayer() — switch on layer.type, dispatching to the above;
│ falls back to the points renderer with a console.warn
└── MapBlock (export) — loops layers, static fallback when layers empty
└── // ===== EXTENSION: CUSTOM MARKER ICONS / IMAGE POPUPS =====
└── commented-out examples: L.icon(), an <img> in bindPopup(),
and a per-layer colour override via L.divIcon()
Extending the generated MapBlock.tsx¶
Because section banners and commented extension blocks are inlined, you can make most common customisations without touching any BESSER source:
Custom marker icons — uncomment and fill in the L.icon block in the
extension section at the bottom of the file, then swap it into the
L.marker(...) call inside renderPointsLayer:
const myIcon = L.icon({
iconUrl: customIconPng,
iconSize: [32, 32],
iconAnchor: [16, 32],
popupAnchor: [0, -32],
});
// then, inside renderPointsLayer, replace
// L.marker([lat, lng], { icon: defaultIcon })
// with
// L.marker([lat, lng], { icon: myIcon })
The default pin is an L.divIcon built from an inline SVG, so recolouring
markers per layer needs no image asset at all — the extension section shows
that variant too.
Per-choropleth colour scale — replace choroplethColor() with your own
palette or a d3-scale interpolator.
Image popups — the commented bindPopup example in the extension section
shows how to display a thumbnail from a URL field. Run every dynamic value
through escapeHtml first: popups render HTML.
Additional layer type — add a new case 'mytype': to the switch in
renderLayer() and a corresponding renderMytypeLayer() function above it.
Graceful degradation¶
A
choroplethlayer with novalueFieldautomatically falls back to plain GeoJSON rendering and logs aconsole.warn.Any layer with a misconfigured
typefalls back torenderPointsLayerwith aconsole.warn.Parse errors in the geometry column are caught per-row and logged with a
console.warn, then skipped.No layer configuration → a static centre marker is shown.
npm Dependencies¶
The following packages are automatically added to the generated package.json
when the GUI model contains at least one Map component:
Package |
Version |
Purpose |
|---|---|---|
|
|
Core Leaflet mapping library (BSD-2-Clause), used directly. |
|
|
Heat-map plugin (used when any layer is |
|
|
TypeScript type definitions for Leaflet. |
|
|
TypeScript type definitions for leaflet.heat. |
No API key is required. OpenStreetMap tiles are served by the public OSM tile CDN.
Editor (No-Code)¶
In the BESSER Web Modeling Editor the Map block lives in the Charts palette.
After dropping a Map onto a screen, the right-hand sidebar shows:
Title, Latitude, Longitude, Zoom — static display settings.
Layers — a repeatable panel (one row per layer) with:
A name field.
A Type dropdown:
Points,GeoJSON,Choropleth,Heat map.A Data Source dropdown (populated from the current class diagram).
Conditional field selects driven by the chosen type (latitude/longitude for points, geometry for GeoJSON/choropleth, etc.).
Use the + button to add a layer and × to remove one. The layer list is
serialised as a JSON string in the map-layers component attribute, which the
BESSER backend parses into MapLayer instances on export.