GeoLeaf.Table — Table Module Documentation
WARNING
The data table is no longer part of @geoleaf/core: it is provided by the MIT plugin @geoleaf-plugins/table. Install it with npm install @geoleaf-plugins/table, then load its script after geoleaf.esm.js and before GeoLeaf.boot().
The GeoLeaf.Table module provides a tabular view of map data, complementing the display on the MapLibre GL JS map.
It can:
- Display the attributes of GeoJSON features in a table
- Sort data by column (ascending → descending → none cycle)
- Select features and synchronize with the map
- Zoom to the selected features
- Highlight the selection on the map
- Export the selection (through an event)
1. Functional role of the Table
GeoLeaf.Table has five main responsibilities:
- Display the data of GeoJSON layers in tabular form
- Synchronize the selection between table and map
- Allow multi-column sorting with cycles (asc → desc → null)
- Handle multiple selection with export and zoom
- Integrate with the GeoJSON, Filters and Core modules
Important: the Table module requires GeoLeaf.GeoJSON to be loaded and configured.
2. Public API of GeoLeaf.Table
2.1 Initialization
The plugin initializes itself: loading it mounts GeoLeaf.Table, and the panel is wired when the map is ready. There is no init call to make, and GeoLeaf.Table has no init member.
The panel is built at startup when modules.table.defaultVisible is true, otherwise at the first click on the toolbar button.
Configuration — the modules.table block of the profile:
enabled:boolean— enable the module (default:true)showButton:boolean— toolbar button (default:true)defaultVisible:boolean— visible on startup (default:false)maxRowsPerLayer:number— row limit (default:30000)enableExportButton:boolean— export button (default:true)defaultHeight:string— default height (default:'40%')minHeight:string— minimum height (default:'20%')maxHeight:string— maximum height (default:'60%')resizable:boolean— resizable (default:true)
Minimal example
// Nothing to initialize: pick a layer and show the panel
GeoLeaf.Table.setLayer("restaurants");
GeoLeaf.Table.show();Example with configuration
{
"modules": {
"table": {
"defaultVisible": true,
"maxRowsPerLayer": 2000,
"defaultHeight": "50%",
"resizable": true
}
}
}2.2 GeoLeaf.Table.show()
Shows the table.
GeoLeaf.Table.show();Events emitted: geoleaf:table:opened
Example
document.getElementById("show-table-btn").addEventListener("click", () => {
GeoLeaf.Table.show();
});2.3 GeoLeaf.Table.hide()
Hides the table. Also turns off the active highlight.
GeoLeaf.Table.hide();Events emitted: geoleaf:table:closed
Example
document.getElementById("hide-table-btn").addEventListener("click", () => {
GeoLeaf.Table.hide();
});2.4 GeoLeaf.Table.toggle()
Toggles table visibility (shows it if hidden, hides it if shown).
GeoLeaf.Table.toggle();Example
document.getElementById("toggle-table-btn").addEventListener("click", () => {
GeoLeaf.Table.toggle();
});2.5 GeoLeaf.Table.setLayer(layerId)
Sets the GeoJSON layer to display in the table.
GeoLeaf.Table.setLayer(layerId);Parameters:
layerId:string— ID of the GeoJSON layer (ornullto clear)
Events emitted: geoleaf:table:layerChanged with { layerId }
Note: only layers declared in the GeoJSON configuration can be displayed.
setLayerresets the selection and the sort.
Example
document.getElementById("layer-select").addEventListener("change", (e) => {
GeoLeaf.Table.setLayer(e.target.value);
});Layer configuration for Table
In geoleaf.config.json:
{
"geojson": {
"layers": [
{
"id": "restaurants",
"url": "data/restaurants.geojson",
"table": {
"enabled": true,
"columns": [
{
"field": "properties.name",
"label": "Nom",
"width": "30%",
"sortable": true
},
{
"field": "properties.category",
"label": "Catégorie",
"width": "20%",
"sortable": true
},
{
"field": "properties.rating",
"label": "Note",
"width": "15%",
"sortable": true
}
],
"defaultSort": {
"field": "properties.name",
"direction": "asc"
}
}
}
]
}
}2.6 GeoLeaf.Table.refresh()
Refreshes the data displayed in the table.
GeoLeaf.Table.refresh();Retrieves the features of the current layer through GeoLeaf.GeoJSON.getLayerData(), reapplies the sort and re-renders the table.
Example
// Refresh after a filter change
map.on("geoleaf:filters:changed", () => {
GeoLeaf.Table.refresh();
});2.7 GeoLeaf.Table.sortByField(field)
Changes the sort on a given column.
GeoLeaf.Table.sortByField(field);Parameters:
field:string— field path (dot notation:properties.name)
Behavior: sort cycle on the same column:
- First call: ascending sort
- Second call: descending sort
- Third call: no sort (original order)
Events emitted: geoleaf:table:sortChanged with { field, direction }
Example
document.querySelectorAll(".table-header").forEach((header) => {
header.addEventListener("click", () => {
GeoLeaf.Table.sortByField(header.dataset.field);
});
});2.8 GeoLeaf.Table.setSelection(ids, add)
Selects or deselects features.
GeoLeaf.Table.setSelection(ids, add);Parameters:
ids:Array<string>— IDs of the features to selectadd:boolean— add to the existing selection (true) or replace it (false, default)
Events emitted: geoleaf:table:selectionChanged with { layerId, selectedIds }
Example
// Select specific features
GeoLeaf.Table.setSelection(["poi-1", "poi-5", "poi-12"]);
// Add to the existing selection
GeoLeaf.Table.setSelection(["poi-20"], true);2.9 GeoLeaf.Table.getSelectedIds()
Returns the IDs of the selected features.
const selectedIds = GeoLeaf.Table.getSelectedIds();Returns: Array<string> — list of selected IDs
Example
const selected = GeoLeaf.Table.getSelectedIds();
console.log(`${selected.length} selected features:`, selected);2.10 GeoLeaf.Table.clearSelection()
Clears the whole selection.
GeoLeaf.Table.clearSelection();Events emitted: geoleaf:table:selectionChanged with { layerId, selectedIds: [] }
Example
document.getElementById("clear-selection-btn").addEventListener("click", () => {
GeoLeaf.Table.clearSelection();
});2.11 GeoLeaf.Table.zoomToSelection()
Zooms to the selected features on the map.
GeoLeaf.Table.zoomToSelection();Events emitted: geoleaf:table:zoomToSelection with { layerId, selectedIds }
Behavior: computes the bounds of the selected features and adjusts the MapLibre GL JS map view.
Example
document.getElementById("zoom-selection-btn").addEventListener("click", () => {
const selected = GeoLeaf.Table.getSelectedIds();
if (selected.length === 0) return;
GeoLeaf.Table.zoomToSelection();
});2.12 GeoLeaf.Table.highlightSelection(active)
Enables or disables the highlighting of the selected features on the map.
GeoLeaf.Table.highlightSelection(active);Parameters:
active:boolean— enable (true) or disable (false)
Events emitted: geoleaf:table:highlightSelection with { layerId, selectedIds, active }
Example
let highlighted = false;
document.getElementById("highlight-btn").addEventListener("click", () => {
highlighted = !highlighted;
GeoLeaf.Table.highlightSelection(highlighted);
});2.13 GeoLeaf.Table.exportSelection(format?, options?)
Exports the selected features to a file the browser downloads, then emits an event. With no selection it does nothing.
GeoLeaf.Table.exportSelection(); // GeoJSON
GeoLeaf.Table.exportSelection("csv", { csvSeparator: ";" });format is "geojson" (default), "csv", "kml", "gpx" or "excel". options takes csvSeparator ("," or ";") and csvIncludeGeometry.
Events emitted: geoleaf:table:exportSelection with { layerId, format, selectedIds, rows } — rows are the exported features, as they go to the file.
The table writes the file itself: the event is there for a host that wants to follow the export, not to implement it. GeoLeaf.Table.exportLayer(format?, options?) does the same for the whole layer and emits geoleaf:table:exportLayer with { layerId, format, count }.
Example
document.getElementById("export-btn").addEventListener("click", () => {
GeoLeaf.Table.exportSelection("csv");
});
// Follow the export
document.addEventListener("geoleaf:table:exportSelection", (e) => {
console.log(`${e.detail.rows.length} features exported as ${e.detail.format}`);
});3. Configuration in geoleaf.config.json
3.1 Global module configuration
The plugin reads the modules.table block of the profile. The values below are its defaults: a profile declares only what it changes.
{
"modules": {
"table": {
"enabled": true,
"defaultVisible": false,
"maxRowsPerLayer": 30000,
"enableExportButton": true,
"defaultHeight": "40%",
"minHeight": "20%",
"maxHeight": "60%",
"resizable": true
}
}
}pageSize and virtualScrolling are not in this block: both are deprecated and read by nothing. The panel does not paginate, and it windows its rows by itself past a row-count threshold.
3.2 Per-layer configuration
{
"geojson": {
"layers": [
{
"id": "restaurants",
"url": "data/restaurants.geojson",
"table": {
"enabled": true,
"columns": [
{
"field": "properties.name",
"label": "Nom",
"width": "30%",
"sortable": true
},
{
"field": "properties.category",
"label": "Catégorie",
"width": "20%",
"sortable": true
},
{
"field": "properties.address",
"label": "Adresse",
"width": "35%",
"sortable": false
},
{
"field": "properties.rating",
"label": "Note",
"width": "15%",
"sortable": true
}
],
"defaultSort": {
"field": "properties.rating",
"direction": "desc"
},
"searchFields": ["properties.name", "properties.category"]
}
}
]
}
}4. Events
The Table module emits the following events on the MapLibre GL JS map, and on document — subscribe to one of the two, not both:
| Event | Detail | Description |
|---|---|---|
geoleaf:table:opened | — | Table shown |
geoleaf:table:closed | — | Table hidden |
geoleaf:table:layerChanged | { layerId } | Displayed layer changed |
geoleaf:table:sortChanged | { field, direction } | Sort changed |
geoleaf:table:selectionChanged | { layerId, selectedIds } | Selection changed |
geoleaf:table:zoomToSelection | { layerId, selectedIds } | Zoom to selection triggered |
geoleaf:table:highlightSelection | { layerId, selectedIds, active } | Highlight enabled/disabled |
geoleaf:table:exportSelection | { layerId, format, selectedIds, rows } | Selection exported |
geoleaf:table:exportLayer | { layerId, format, count } | Whole layer exported |
geoleaf:table:rowActivated | { layerId, featureId, properties } | Row double-clicked, or Enter pressed on the focused row |
geoleaf:table:rowActivated states an intention and opens nothing: the subscriber decides what an activation means. featureId is the identifier selectionChanged lists in selectedIds (a string, synthetic — __gl_row_<n> — when the feature has no identifier of its own), and properties are the feature's complete properties. A double-click leaves the selection as it was: its first click toggles it, the activation restores it, so two selectionChanged surround the gesture. The checkbox cell and the sort header never activate.
Listening example
const map = GeoLeaf.Core.getMap();
// Listen to selection changes
map.on("geoleaf:table:selectionChanged", (e) => {
console.log(`${e.selectedIds.length} selected features`);
highlightFeaturesOnMap(e.selectedIds);
});
// Listen to layer changes
map.on("geoleaf:table:layerChanged", (e) => {
console.log(`Displayed layer: ${e.layerId}`);
updateUIControls(e.layerId);
});5. Integration with other modules
5.1 Integration with GeoJSON
The Table module displays the data of the GeoJSON layers loaded by the core:
// GeoJSON layers are configured in geoleaf.config.json
// and loaded automatically by GeoLeaf.GeoJSON at startup.
// Display a layer in the table:
GeoLeaf.Table.setLayer("restaurants");
GeoLeaf.Table.show();5.2 Integration with Filters
The table synchronizes with filters automatically by listening to events:
// Apply a filter — a state replaces the whole panel selection
GeoLeaf.Filter.applyFilter({
fields: [{ id: "categories", kind: "taxonomy", values: ["restaurant"] }],
});
// The table updates itself automatically
// (it listens to the geoleaf:filters:applied event internally)5.3 Two-way map ↔ table synchronization
In MapLibre GL JS, synchronization relies on GeoLeaf events:
// Selection in the table → highlight on the map
map.on("geoleaf:table:selectionChanged", (e) => {
const { selectedIds } = e;
// Update the MapLibre style through setFeatureState or a paint expression
// (the built-in highlighting uses highlightSelection())
GeoLeaf.Table.highlightSelection(selectedIds.length > 0);
});
// Click on the map → selection in the table
map.on("click", (e) => {
const features = map.queryRenderedFeatures(e.point);
if (features.length > 0) {
const featureId = features[0].id;
GeoLeaf.Table.setSelection([String(featureId)], true);
}
});6. Practical use cases
6.1 Table with sorting
GeoLeaf.Table.setLayer("restaurants");
GeoLeaf.Table.show();
// Trigger a sort programmatically
GeoLeaf.Table.sortByField("properties.rating");6.2 Multi-format export
map.on("geoleaf:table:exportSelection", (e) => {
const { rows, layerId } = e;
const format = prompt("Export format (csv/json/geojson):", "csv");
switch (format) {
case "csv":
exportCSV(rows, `${layerId}.csv`);
break;
case "json":
exportJSON(rows, `${layerId}.json`);
break;
case "geojson":
exportGeoJSON(rows, `${layerId}.geojson`);
break;
}
});6.3 Configuration for large tables
{
"modules": {
"table": {
"maxRowsPerLayer": 5000
}
}
}7. Internal architecture
7.1 Component modules
GeoLeaf.Table (public API — table-api.ts)
├── table-state.ts (shared state: _map, _config, _currentLayerId, _selectedIds, etc.)
├── table-layer.ts (feature retrieval, layer handling, attachMapEvents)
├── table-highlight.ts (MapLibre highlighting, bounds computation)
├── table-selection.ts (setSelection, clearSelection, zoomToSelection, exportSelection)
├── sort.ts (sortInPlace, nextSortState — sort cycle)
├── export.ts (resolveFeatureId)
├── panel.ts (TablePanel — DOM container creation)
└── renderer.ts (TableRenderer — HTML table rendering)7.2 Data flow
1. Initialization
The plugin wires itself when the map is ready
└── Reads the `modules.table` block, merged over the built-in defaults
└── Creates the DOM container through TablePanel.create()
└── Attaches map event listeners
2. Layer change
GeoLeaf.Table.setLayer('restaurants')
└── Resets selection, highlight and sort
└── Applies defaultSort from the layer config
└── Calls refresh()
3. Refresh
GeoLeaf.Table.refresh()
└── Retrieves features through GeoLeaf.GeoJSON.getLayerData()
└── Builds _featureIdMap
└── Applies the sort if defined (sortInPlace)
└── Calls TableRenderer.render()
4. Sorting
GeoLeaf.Table.sortByField('properties.name')
└── nextSortState() → cycle asc → desc → null
└── Calls refresh()
└── Emits: geoleaf:table:sortChanged
5. Selection
GeoLeaf.Table.setSelection(['id1', 'id2'])
└── Updates _selectedIds (Set)
└── Calls TableRenderer.updateSelection()
└── Emits: geoleaf:table:selectionChanged8. Best practices
Do
- Limit the number of rows through
maxRowsPerLayer(default: 30000). The cut warns the user once per layer and per session;0means zero - Virtual scrolling needs no key: past a row-count threshold the renderer windows automatically. ⚠️
virtualScrollingis deprecated and inert — it never commanded the switch - Declare only the relevant columns in
columns - Use
defaultSortfor a better initial experience - Debounce frequent calls to
refresh()
Avoid
- Displaying every property without narrowing the columns
- Loading more than 5000 rows without pagination or virtual scrolling
- Calling
refresh()too often without a debounce
9. Performance
Built-in optimizations
- Automatic capping:
maxRowsPerLayerprevents overload, and says so — a silently truncated table looks exactly like a complete one - Virtual scrolling: only the visible rows are rendered
- Data caching:
_cachedDataavoids repeated queries - Optimized sorting: uses
localeComparefor strings
Recommendations for very large tables (10k+ rows)
{
"modules": {
"table": {
"maxRowsPerLayer": 5000
}
}
}10. Troubleshooting
Issue: "Container not initialized"
Cause: TablePanel not loaded, or the module was not initialized. Solution: check that the plugin is loaded and that modules.table.enabled is not false — there is no init call to make.
Issue: empty columns
Cause: field does not match the GeoJSON structure. Solution: check the property paths (dot notation).
// GeoJSON structure
{ "properties": { "name": "Restaurant X", "info": { "category": "Italian" } } }
// Correct column configuration
{ "columns": [
{ "field": "properties.name", "label": "Nom" }, // OK
{ "field": "properties.info.category", "label": "Cat" }, // OK
{ "field": "category", "label": "Cat" } // Incorrect
] }Issue: sorting does not work
Cause: the column is not marked sortable: true. Solution:
{ "columns": [{ "field": "properties.name", "label": "Nom", "sortable": true }] }