How to Implement Virtual Fields in the GeoLibre Attribute Table: A Complete Developer Guide

Virtual fields in GeoLibre are expression-backed columns computed on-the-fly using MapLibre expressions, persisted in layer configuration, and integrated throughout the application from attribute tables to data-driven styling.

GeoLibre's virtual fields system provides a powerful way to derive computed columns without modifying underlying data sources. Similar to QGIS's "Create virtual field" feature, these fields evaluate MapLibre expressions for every feature and appear seamlessly across attribute tables, the Expression Builder, styling rules, labels, selection queries, and exports. This article walks through the complete implementation pipeline based on the opengeos/GeoLibre source code.

Virtual Field Architecture Overview

The virtual field system follows a four-stage pipeline designed for reliability and idempotency. According to the GeoLibre core implementation in packages/core/src/virtual-fields.ts, each stage serves a distinct purpose:

  1. Definition storage — Virtual fields live in LayerVirtualField[] arrays within layer configuration
  2. Stripping — Previous virtual columns are removed to ensure clean re-computation
  3. Evaluation — MapLibre expressions compile and execute against each feature
  4. Integration — Joins engine orchestrates the order of operations

This architecture ensures that virtual fields can reference joined columns and that repeated application produces consistent results.

Defining Virtual Fields in Layer Configuration

Virtual fields are declared using the LayerVirtualField interface defined in packages/core/src/types.ts. Each field requires a unique ID, display name, MapLibre expression, and enabled flag.

const populationDensityField: LayerVirtualField = {
  id: "vf-pop-density-001",
  name: "pop_per_km2",
  expression: `["/", ["get", "population"], ["get", "area_km2"]]`,
  enabled: true,
};

// Add to layer configuration
layer.virtualFields = [
  ...(layer.virtualFields ?? []),
  populationDensityField
];

Key properties:

  • id — Unique identifier for the field definition
  • name — Column header displayed in attribute tables
  • expression — Valid MapLibre expression string
  • enabled — Toggle for temporary deactivation without deletion

Stripping and Reapplying Virtual Fields

Before re-computing virtual fields, GeoLibre always strips previously added columns using stripVirtualFieldColumns. This function—implemented in packages/core/src/virtual-fields.ts—restores the original feature set by removing properties that match virtual field IDs.

import { stripVirtualFieldColumns } from "@geolibre/core/virtual-fields";

// Clean slate: remove all virtual field properties
const cleanFeatures = stripVirtualFieldColumns(
  featuresWithVirtuals,
  layer.virtualFields
);

This idempotent approach prevents duplicate columns and ensures that virtual field updates take full effect. The source code includes an explicit trade-off comment noting this behavior when layers receive replacement GeoJSON datasets.

Evaluating Expressions with applyLayerVirtualFields

The core computation happens in applyLayerVirtualFields, also from packages/core/src/virtual-fields.ts. This function:

  1. Compiles each enabled field's MapLibre expression
  2. Evaluates against every feature's properties
  3. Normalizes results (non-finite numbers → null)
  4. Adds computed values as addedField properties
  5. Captures errors per-field without aborting
import { applyLayerVirtualFields } from "@geolibre/core/virtual-fields";

const { features: withVirtuals, fields: updatedFields } =
  applyLayerVirtualFields(originalFeatures, layer.virtualFields);

// updatedFields contains errorCount for each field
const fieldsWithErrors = updatedFields.filter(f => f.errorCount > 0);

Error handling behavior:

  • Syntax errors surface immediately through expression compilation
  • Runtime failures (undefined references, division by zero) increment errorCount
  • Zero-feature layers still validate expression syntax
  • Individual field failures do not block other virtual fields

Integrating Virtual Fields with Table Joins

Virtual fields gain significant power from their integration with GeoLibre's joins system. In packages/core/src/joins.ts, the pipeline explicitly applies joins before virtual fields, allowing expressions to reference joined columns.

import { applyLayerVirtualFields, stripVirtualFieldColumns } from "./virtual-fields";
import { applyLayerJoins } from "./joins";

export function applyJoinsAndVirtuals(layer: Layer): ProcessedLayer {
  // Step 1: Execute table joins
  const joinedResult = applyLayerJoins(layer);
  
  // Step 2: Strip any stale virtual columns
  const strippedFeatures = stripVirtualFieldColumns(
    joinedResult.features,
    layer.virtualFields
  );
  
  // Step 3: Compute virtual fields with access to joined data
  const { features, fields } = applyLayerVirtualFields(
    strippedFeatures,
    layer.virtualFields
  );
  
  return { ...joinedResult, features, virtualFields: fields };
}

This ordering enables sophisticated workflows: join demographic data from a CSV, then compute per-capita metrics as virtual fields without permanently altering either source.

Handling Name Collisions and Edge Cases

The GeoLibre implementation enforces specific precedence rules for column names:

Scenario Behavior
Virtual field matches base column Virtual field skipped — base data wins
Virtual field matches joined column Virtual field skipped — joined data wins
Duplicate virtual field IDs Last defined field takes precedence
Expression returns non-finite number Stored as null
Expression references missing property Evaluates to null, may increment errorCount

These rules ensure predictable behavior when layer schemas evolve or joins introduce unexpected column names.

Persisting Virtual Field Configuration

Virtual field definitions survive project saves through .geolibre.json serialization. The layer library in packages/core/src/layer-library.ts handles persistence, while apps/geolibre-desktop/src/components/panels/VirtualFieldsSection.tsx provides the user interface for creating, editing, and deleting fields.

On project load, the same evaluation pipeline runs automatically, re-deriving computed columns from stored expressions. This separation of definition (persisted) from computed values (ephemeral) keeps project files compact while maintaining full analytical capability.

Complete Working Example

import { 
  applyLayerVirtualFields, 
  stripVirtualFieldColumns,
  LayerVirtualField
} from "@geolibre/core/virtual-fields";

// Configuration: define computed columns
const virtualFields: LayerVirtualField[] = [
  {
    id: "vf-revenue-per-customer",
    name: "revenue_per_customer",
    expression: `["/", ["get", "total_revenue"], ["get", "customer_count"]]`,
    enabled: true
  },
  {
    id: "vf-category-label",
    name: "display_category",
    expression: `["concat", ["get", "category"], ": ", ["get", "subcategory"]]`,
    enabled: true
  }
];

// Processing pipeline for updated source data
function refreshVirtualFields(
  newSourceFeatures: Feature[],
  existingVirtualFields: LayerVirtualField[]
) {
  // Always strip first for clean state
  const clean = stripVirtualFieldColumns(newSourceFeatures, existingVirtualFields);
  
  // Apply current virtual field definitions
  const { features, fields } = applyLayerVirtualFields(clean, existingVirtualFields);
  
  // Log any evaluation issues
  fields.forEach(f => {
    if (f.errorCount > 0) {
      console.warn(`Virtual field "${f.name}" has ${f.errorCount} errors`);
    }
  });
  
  return { features, virtualFieldStatus: fields };
}

Summary

  • Virtual fields are expression-backed columns defined in LayerVirtualField configuration and stored in packages/core/src/types.ts
  • Idempotent application requires stripVirtualFieldColumns before applyLayerVirtualFields to prevent duplicate columns
  • Join integration in packages/core/src/joins.ts sequences operations so virtual expressions can reference joined data
  • MapLibre expressions power all computations, with errors captured per-field without blocking other fields
  • Persistence saves definitions to .geolibre.json; values are recomputed on load

Frequently Asked Questions

What expression syntax do GeoLibre virtual fields use?

Virtual fields use MapLibre expression syntax—JSON arrays with operators like ["get", "propertyName"], ["/", ["get", "a"], ["get", "b"]], or ["concat", "prefix", ["get", "field"]]. These compile and execute through the same engine that powers data-driven styling. Invalid syntax surfaces during compilation; runtime issues like missing properties evaluate to null.

Can virtual fields reference columns from joined tables?

Yes. The joins engine in packages/core/src/joins.ts applies joins before virtual fields, making joined columns available in expressions. This enables workflows like joining external CSV data, then computing derived metrics without permanent schema changes. Name collisions with joined columns resolve in favor of the joined data.

How does GeoLibre handle errors in virtual field expressions?

Errors are isolated per field. Syntax errors prevent compilation and surface immediately. Runtime errors—undefined properties, non-finite math results—produce null values and increment the field's errorCount property. Other virtual fields continue evaluating normally. Check updatedFields from applyLayerVirtualFields to monitor field health programmatically.

Where do virtual field definitions persist in GeoLibre projects?

Definitions store in the .geolibre.json project file through packages/core/src/layer-library.ts. Only the configuration (ID, name, expression, enabled flag) persists—computed values are ephemeral and re-derived on project load. This keeps files compact while preserving analytical logic.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →