Skip to main content

Layer Styling: Visualization

The UDF builder displays data from your UDF on the map. You can customize the visual representation using the visualization icon located on the map.

Overview

Fused's visualization system is built on DeckGL, a powerful JavaScript framework for large-scale geospatial data visualizations. The system uses a three-layer architecture that automatically selects the appropriate rendering method based on your UDF output.

Layer Architecture

Every Visualization uses this JSON structure:

{
"tileLayer": {
"@@type": "TileLayer", // For viewport-optimized rendering
"minZoom": 0,
"maxZoom": 19,
"tileSize": 256,
"pickable": true
},
"rasterLayer": {
"@@type": "BitmapLayer", // For raster data (images, arrays)
"pickable": true
},
"vectorLayer": {
"@@type": "GeoJsonLayer", // For vector data (GeoDataFrames)
"stroked": true,
"filled": true,
"pickable": true
}
}

How Fused Selects Visualization Layers

Fused automatically determines which layer to use based on your UDF output:

UDF OutputLayer UsedLayer TypeUse Case
GeoDataFramevectorLayerGeoJsonLayerPolygons, lines, points with geometry
DataFrame with H3 columnvectorLayerH3HexagonLayerHexagonal grid data
PNG/array/imagerasterLayerBitmapLayerSatellite imagery, heat maps

Key Points:

  • You define all three layers in your JSON, but Fused uses only the appropriate one based on your data type
  • Unused layers are ignored automatically
  • Visualizations use JSON configuration, not Python code
{
"tileLayer": { "@@type": "TileLayer", ... }, // Always defined
"rasterLayer": { "@@type": "BitmapLayer", ... }, // Used for images/arrays
"vectorLayer": { "@@type": "GeoJsonLayer", ... } // Used for GeoDataFrames
}

Default Map View

You can set a default map view that automatically centers on your data:

Default map view settings

Presets

Use the "Preset" button to quickly generate styling configurations. You can always undo changes with Ctrl + Z.

Preset button

Layer Types

Vector: Custom Map (Mapbox GL)

Use when your UDF produces vector tiles (MVT) and you want full control using Mapbox GL styling and interactions.

Open demo ↗

Expand to see Visualise JSON
{
"tileLayer": {
"@@type": "TileLayer",
"minZoom": 0,
"maxZoom": 19,
"tileSize": 256,
"pickable": true
},
"rasterLayer": {
"@@type": "BitmapLayer",
"pickable": true
},
"vectorLayer": {
"@@type": "GeoJsonLayer",
"stroked": true,
"filled": true,
"pickable": true,
"lineWidthMinPixels": 1,
"getFillColor": [20, 200, 200, 100],
"getLineColor": [50, 50, 50, 255]
}
}
Expand to see standalone UDF code
@fused.udf(cache_max_age=0)
def udf(
token: str = "UDF_Overture_Maps_Example",
host: str = "https://www.fused.io",
mapbox_token: str = "pk.eyJ1IjoiaXNhYWNmdXNlZGxhYnMiLCJhIjoiY2xicGdwdHljMHQ1bzN4cWhtNThvbzdqcSJ9.73fb6zHMeO_c8eAXpZVNrA",
style_url: str = "mapbox://styles/mapbox/dark-v10",
center_lng: float = -122.4194,
center_lat: float = 37.7749,
zoom: float = 16,
pitch: float = 30,
bearing: float = 20,
minzoom: int = 6,
maxzoom: int = 14,
layer_id: str = "fused-vector-layer",
source_layer: str = "udf"
):
"""
Minimal Mapbox map (no input UI) that loads Fused XYZ vector tiles (MVT) from `token`.
Tiles URL: {host}/server/v1/realtime-shared/{token}/run/tiles/{z}/{x}/{y}?dtype_out_vector=mvt
"""
from jinja2 import Template
html = Template(r"""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Simple XYZ (MVT) Loader</title>
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no"/>
<link href="https://api.mapbox.com/mapbox-gl-js/v3.2.0/mapbox-gl.css" rel="stylesheet"/>
<script src="https://api.mapbox.com/mapbox-gl-js/v3.2.0/mapbox-gl.js"></script>
<style>
html, body { margin:0; height:100%; }
#map { position:absolute; inset:0; }
</style>
</head>
<body>
<div id="map"></div>

<script>
const MAPBOX_TOKEN = {{ mapbox_token | tojson }};
const STYLE_URL = {{ style_url | tojson }};
const HOST = {{ host | tojson }};
const TOKEN = {{ token | tojson }};
const CENTER = [{{ center_lng }}, {{ center_lat }}];
const ZOOM = {{ zoom }};
const PITCH = {{ pitch }};
const BEARING = {{ bearing }};
const MINZOOM = {{ minzoom }};
const MAXZOOM = {{ maxzoom }};
const LAYER_ID = {{ layer_id | tojson }};
const SOURCE_LAYER = {{ source_layer | tojson }};

function tilesFromToken(tok) {
return `${HOST}/server/v1/realtime-shared/${tok}/run/tiles/{z}/{x}/{y}?dtype_out_vector=mvt`;
}

mapboxgl.accessToken = MAPBOX_TOKEN;
const map = new mapboxgl.Map({
container: 'map',
style: STYLE_URL,
center: CENTER,
zoom: ZOOM,
pitch: PITCH,
bearing: BEARING
});

function addVectorTiles(tok) {
const tilesUrl = tilesFromToken(tok);
if (map.getLayer(LAYER_ID)) map.removeLayer(LAYER_ID);
if (map.getSource('xyz')) map.removeSource('xyz');

map.addSource('xyz', {
type: 'vector',
tiles: [tilesUrl],
minzoom: MINZOOM,
maxzoom: MAXZOOM
});

map.addLayer({
id: LAYER_ID,
type: 'line',
source: 'xyz',
'source-layer': SOURCE_LAYER,
layout: { 'line-join': 'round', 'line-cap': 'round' },
paint: { 'line-color': '#00FF0A', 'line-width': 2, 'line-opacity': 0.8 }
});
}

map.on('load', () => {
addVectorTiles(TOKEN);
});
</script>
</body>
</html>
""").render(
token=token,
host=host,
mapbox_token=mapbox_token,
style_url=style_url,
center_lng=center_lng,
center_lat=center_lat,
zoom=zoom,
pitch=pitch,
bearing=bearing,
minzoom=minzoom,
maxzoom=maxzoom,
layer_id=layer_id,
source_layer=source_layer,
)

common = fused.load("https://github.com/fusedio/udfs/tree/351515e/public/common/")
return common.html_to_obj(html)

Key Mapbox GL properties:

  • source: { type: 'vector', tiles: [url], minzoom, maxzoom }
  • source-layer: Name of the tile layer (e.g., udf)
  • layer.type: 'line' (or 'fill' for polygons, 'circle' for points)
  • paint: For lines use line-color, line-width, line-opacity (for polygons use fill-color and an outline line layer)

Example UDF

Vector: H3HexagonLayer

Use a custom map UDF that renders deck.gl H3HexagonLayer over Mapbox GL. The UDF consumes JSON with H3 indices and values and returns an HTML map.

Live example rendered from a UDF (HTML output):

Open demo ↗

Expected data schema:

  • hex: H3 index string (e.g., 8a2a1072b59ffff)
  • count: numeric value used for color/elevation
Expand to see Visualise JSON
{
"tileLayer": {
"@@type": "TileLayer",
"minZoom": 0,
"maxZoom": 19,
"tileSize": 256,
"pickable": true
},
"rasterLayer": {
"@@type": "BitmapLayer",
"pickable": true
},
"vectorLayer": {
"@@type": "H3HexagonLayer",
"stroked": true,
"filled": true,
"pickable": true,
"extruded": false,
"opacity": 1,
"coverage": 0.9,
"lineWidthMinPixels": 5,
"getHexagon": "@@=properties.hex",
"getFillColor": [255, 165, 0, 180],
"getLineColor": [200, 200, 200, 255]
}
}
Expand to see standalone UDF code
@fused.udf(cache_max_age=0)
def udf(
# H3 data (each row: {hex: '8f...', count: number})
data_url: str = "https://www.fused.io/server/v1/realtime-shared/UDF_DuckDB_H3_SF/run/file?format=json",

# Mapbox + camera
mapbox_token: str = "pk.eyJ1IjoiaXNhYWNmdXNlZGxhYnMiLCJhIjoiY2xicGdwdHljMHQ1bzN4cWhtNThvbzdqcSJ9.73fb6zHMeO_c8eAXpZVNrA",
style_url: str = "mapbox://styles/mapbox/dark-v10",
center_lng: float = -122.417759,
center_lat: float = 37.776452,
zoom: float = 11.0,
pitch: float = 50.0,
bearing: float = -10.0,

# Layer tuning
elevation_scale: float = 20.0, # count * elevation_scale
max_count_for_color: float = 500.0, # for [255, (1 - count/max)*255, 0]
wireframe: bool = False,
):

from jinja2 import Template

"""
Deck.gl 3D H3HexagonLayer over Mapbox, using your JSON (hex,count) data.
- Elevation: count * elevation_scale
- Color: [255, (1 - count/max_count_for_color)*255, 0]
- Popup shows hex & count
"""
html = Template(r"""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Deck.gl 3D H3 (Fixed)</title>
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no"/>
<link href="https://api.mapbox.com/mapbox-gl-js/v3.2.0/mapbox-gl.css" rel="stylesheet"/>
<script src="https://api.mapbox.com/mapbox-gl-js/v3.2.0/mapbox-gl.js"></script>

<!-- Load h3-js BEFORE deck.gl -->
<script src="https://unpkg.com/h3-js@latest/dist/h3-js.umd.js"></script>
<!-- Then load deck.gl -->
<script src="https://unpkg.com/deck.gl@9.0.0/dist.min.js"></script>

<style>
html, body { margin:0; height:100%; }
#map { position:absolute; inset:0; }
.deck-tooltip {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
font-size: 12px;
padding: 6px 8px;
background: rgba(0,0,0,0.7);
color: #fff;
border-radius: 6px;
max-width: 260px;
pointer-events: none;
}
</style>
</head>
<body>
<div id="map"></div>
<script>
// Verify h3-js is loaded properly
if (typeof h3 === 'undefined') {
console.error('h3-js library not loaded!');
throw new Error('h3-js library must be loaded before deck.gl');
}

const MAPBOX_TOKEN = {{ mapbox_token | tojson }};
const STYLE_URL = {{ style_url | tojson }};
const DATA_URL = {{ data_url | tojson }};

const INITIAL_VIEW_STATE = {
longitude: {{ center_lng }},
latitude: {{ center_lat }},
zoom: {{ zoom }},
pitch: {{ pitch }},
bearing: {{ bearing }}
};

const ELEVATION_SCALE = {{ elevation_scale }};
const MAX_COUNT_FOR_COLOR = {{ max_count_for_color }};
const WIREFRAME = {{ wireframe | tojson }};

mapboxgl.accessToken = MAPBOX_TOKEN;
if (mapboxgl.setTelemetryEnabled) { try { mapboxgl.setTelemetryEnabled(false); } catch(e){} }

const map = new mapboxgl.Map({
container: 'map',
style: STYLE_URL,
center: [INITIAL_VIEW_STATE.longitude, INITIAL_VIEW_STATE.latitude],
zoom: INITIAL_VIEW_STATE.zoom,
pitch: INITIAL_VIEW_STATE.pitch,
bearing: INITIAL_VIEW_STATE.bearing
});

const overlay = new deck.MapboxOverlay({
interleaved: true,
layers: []
});

map.on('load', () => {
map.addControl(overlay);

const ambient = new deck.AmbientLight({intensity: 1.0});
const dir = new deck.DirectionalLight({intensity: 0.9, direction: [-1, -2, -1]});
const effects = [ new deck.LightingEffect({ambientLight: ambient, dirLight: dir}) ];

const h3Layer = new deck.H3HexagonLayer({
id: 'h3-3d',
data: DATA_URL,
pickable: true,
extruded: true,
wireframe: WIREFRAME,
material: { ambient: 0.35, diffuse: 0.6, shininess: 32, specularColor: [255,255,255] },

getHexagon: d => d.hex,
getElevation: d => (d && Number.isFinite(d.count) ? d.count : 0) * ELEVATION_SCALE,
getFillColor: d => {
const count = (d && Number.isFinite(d.count)) ? d.count : 0;
const g = Math.max(0, Math.min(255, (1 - (count / MAX_COUNT_FOR_COLOR)) * 255));
return [255, g, 0];
},
filled: true,
stroked: true,
getLineColor: [255, 255, 255],
getLineWidth: 2,
lineWidthUnits: 'pixels',

onClick: info => {
if (!info || !info.coordinate || !info.object) return;
const props = { hex: info.object.hex, count: info.object.count };
new mapboxgl.Popup({closeButton:true})
.setLngLat(info.coordinate)
.setHTML('<pre style="margin:0;white-space:pre-wrap;">' +
JSON.stringify(props, null, 2) + '</pre>')
.addTo(map);
}
});

overlay.setProps({ layers: [h3Layer], effects });
});
</script>
</body>
</html>
""").render(
data_url=data_url,
mapbox_token=mapbox_token,
style_url=style_url,
center_lng=center_lng,
center_lat=center_lat,
zoom=zoom,
pitch=pitch,
bearing=bearing,
elevation_scale=elevation_scale,
max_count_for_color=max_count_for_color,
wireframe=wireframe,
)

common = fused.load("https://github.com/fusedio/udfs/tree/351515e/public/common/")
return common.html_to_obj(html)

Required accessor:

  • getHexagon: d => d.hex (returns H3 index string)

Key layer properties:

  • getElevation: d => (d.count || 0) * elevation_scale
  • getFillColor: color ramp derived from count
  • extruded: true for 3D columns; wireframe toggles outlines
  • pickable: enable interactivity; show Popup on click
  • material: basic lighting parameters

Note: Load h3-js before deck.gl. Ensure H3 IDs are strings in JS (convert numeric/BigInt to hex if needed).

Required Property:

  • getHexagon: Specify the DataFrame column containing H3 indices (e.g., "@@=properties.hex")

Example UDF

Vector: H3HexagonLayer with Tiles

Use a custom map UDF that renders deck.gl H3HexagonLayer with tile-based data loading. The UDF consumes JSON tiles with H3 indices and values and returns an HTML map with configurable styling.

Live example rendered from a UDF (HTML output):

Open demo ↗

Expected data schema:

  • hex: H3 index string (e.g., 8a2a1072b59ffff)
  • metric: numeric value used for color/elevation

Key features:

  • Tile-based loading: Efficiently loads H3 data as map tiles
  • Configurable styling: JSON-based configuration for colors, elevation, and styling
  • Interactive: Hover to see hex details and metric values
  • Performance optimized: Only loads visible tiles
Expand to see Visualize JSON
{
"tileLayer": {
"@@type": "TileLayer",
"minZoom": 0,
"maxZoom": 19,
"tileSize": 256,
"pickable": true
},
"hexLayer": {
"@@type": "H3HexagonLayer",
"stroked": false,
"filled": true,
"pickable": true,
"extruded": false,
"opacity": 1,
"coverage": 0.9,
"lineWidthMinPixels": 10,
"getHexagon": "@@=properties.hex",
"getFillColor": {
"@@function": "colorContinuous",
"attr": "metric",
"domain": [100000, 0],
"colors": "TealGrn",
"nullColor": [184, 184, 184]
},
"getLineColor": [200, 200, 200],
"getElevation": {
"@@function": "hasProp",
"property": "metric",
"present": "@@=properties.metric",
"absent": 1
},
"elevationScale": 2
}
}
Expand to see standalone UDF code
DEFAULT_CONFIG = r"""{
"tileLayer": {
"@@type": "TileLayer",
"minZoom": 0,
"maxZoom": 19,
"tileSize": 256,
"pickable": true
},
"hexLayer": {
"@@type": "H3HexagonLayer",
"stroked": false,
"filled": true,
"pickable": true,
"extruded": false,
"opacity": 1,
"coverage": 0.9,
"lineWidthMinPixels": 10,
"getHexagon": "@@=properties.hex",
"getFillColor": {
"@@function": "colorContinuous",
"attr": "metric",
"domain": [100000, 0],
"colors": "TealGrn",
"nullColor": [184, 184, 184]
},
"getLineColor": [200, 200, 200],
"getElevation": {
"@@function": "hasProp",
"property": "metric",
"present": "@@=properties.metric",
"absent": 1
},
"elevationScale": 2
}
}"""

@fused.udf(cache_max_age=0)
def udf(
tile_url_template: str = "https://www.fused.io/server/v1/realtime-shared/UDF_Ookla_Download_Speeds/run/tiles/{z}/{x}/{y}?dtype_out_vector=json",
config_json: str = DEFAULT_CONFIG,
mapbox_token: str = "pk.eyJ1IjoiaXNhYWNmdXNlZGxhYnMiLCJhIjoiY2xicGdwdHljMHQ1bzN4cWhtNThvbzdqcSJ9.73fb6zHMeO_c8eAXpZVNrA",
center_lng: float = -98.5,
center_lat: float = 39.5,
zoom: float = 3
):
from jinja2 import Template

html = Template(r"""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>H3 XYZ Workbench-style Viewer</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />

<!-- Mapbox GL -->
<link href="https://api.mapbox.com/mapbox-gl-js/v2.15.0/mapbox-gl.css" rel="stylesheet" />
<script src="https://api.mapbox.com/mapbox-gl-js/v2.15.0/mapbox-gl.js"></script>

<!-- Load h3-js FIRST, then deck.gl + geo-layers (+ carto for color ramps) -->
<script src="https://unpkg.com/h3-js@4.1.0/dist/h3-js.umd.js"></script>
<script src="https://unpkg.com/deck.gl@9.1.3/dist.min.js"></script>
<script src="https://unpkg.com/@deck.gl/geo-layers@9.1.3/dist.min.js"></script>
<script src="https://unpkg.com/@deck.gl/carto@9.1.3/dist.min.js"></script>

<style>
html, body, #map { margin: 0; height: 100%; width: 100%; }
#hud { position: absolute; top: 8px; left: 8px; z-index: 5; color: #fff; background: rgba(0,0,0,.65);
padding: 8px 12px; border-radius: 6px; font: 12px/1.5 system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial; }
#hud b { color: #4fc3f7; }
</style>
</head>
<body>
<div id="map"></div>
<div id="hud"><b>Tiles:</b> <span id="note">ready</span></div>

<script>
const MAPBOX_TOKEN = {{ mapbox_token | tojson }};
const STYLE_URL = "mapbox://styles/mapbox/dark-v10";
const TPL = {{ tile_url_template | tojson }};
const CONFIG = JSON.parse({{ config_json | tojson }});

const { TileLayer, PolygonLayer, MapboxOverlay } = deck;
const H3HexagonLayer = deck.H3HexagonLayer || (deck.GeoLayers && deck.GeoLayers.H3HexagonLayer);
const { colorContinuous } = deck.carto;

const $note = () => document.getElementById('note');
function setNote(t){ const n=$note(); if(n) n.textContent=t; }

// ----- helpers for @@function / @@= support -----
function evalExpression(expr, object) {
if (typeof expr === 'string' && expr.startsWith('@@=')) {
const code = expr.slice(3);
try {
const fn = new Function('object', `
const properties = object?.properties || object || {};
return (${code});
`);
return fn(object);
} catch (e) { console.error('@@= eval error:', expr, e); return null; }
}
return expr;
}

function hasProp({ property, present, absent }) {
return (object) => {
const props = object?.properties || object || {};
if (property in props && props[property] !== null && props[property] !== undefined) {
return typeof present === 'function' ? present(object)
: (typeof present === 'string' && present.startsWith('@@=')) ? evalExpression(present, object)
: present;
}
return typeof absent === 'function' ? absent(object) : absent;
};
}

function processColorContinuous(cfg) {
// accept 2-element domain or full array
return {
attr: cfg.attr,
domain: cfg.domain,
colors: cfg.colors || 'TealGrn',
nullColor: cfg.nullColor || [184,184,184]
};
}

function parseHexLayerConfig(config) {
const out = {};
for (const [k, v] of Object.entries(config || {})) {
if (k === '@@type') continue;
if (v && typeof v === 'object' && !Array.isArray(v)) {
if (v['@@function'] === 'colorContinuous') {
out[k] = colorContinuous(processColorContinuous(v));
} else if (v['@@function'] === 'hasProp') {
out[k] = hasProp(v);
} else {
out[k] = v;
}
} else if (typeof v === 'string' && v.startsWith('@@=')) {
out[k] = (obj) => evalExpression(v, obj);
} else {
out[k] = v;
}
}
return out;
}

// ----- H3 ID safety (handles string/number/bigint/[hi,lo]) -----
function toH3String(hex) {
try {
if (hex == null) return null;
if (typeof hex === 'string') {
const s = hex.startsWith('0x') ? hex.slice(2) : hex;
// if decimal digits, convert to hex via BigInt
return (/^\d+$/.test(s) ? BigInt(s).toString(16) : s.toLowerCase());
}
if (typeof hex === 'number') return BigInt(Math.trunc(hex)).toString(16);
if (typeof hex === 'bigint') return hex.toString(16);
if (Array.isArray(hex) && hex.length === 2) {
const a = (BigInt(hex[0]) << 32n) | BigInt(hex[1]);
const b = (BigInt(hex[1]) << 32n) | BigInt(hex[0]);
const sa = a.toString(16), sb = b.toString(16);
if (h3.isValidCell?.(sa)) return sa;
if (h3.isValidCell?.(sb)) return sb;
return sa;
}
} catch(_) {}
return null;
}

// Normalize any tile JSON into data rows, and add both top-level + properties for compatibility
function normalize(raw){
const arr = Array.isArray(raw)
? raw
: (Array.isArray(raw?.data) ? raw.data
: (Array.isArray(raw?.features) ? raw.features : []));
const rows = arr.map(d => d?.properties ? {...d.properties} : {...d});
return rows.map(p => {
const hexRaw = p.hex ?? p.h3 ?? p.index ?? p.id;
const metric = p.metric ?? p.value ?? p.count ?? p.pct ?? p.area ?? p.total ?? p.val;
const hex = toH3String(hexRaw);
if (!hex) return null;
const props = { ...p, hex, metric };
return { ...props, properties: { ...props } }; // both locations
}).filter(Boolean);
}

// Mapbox init
mapboxgl.accessToken = MAPBOX_TOKEN;
const map = new mapboxgl.Map({ container:'map', style:STYLE_URL, center:[{{ center_lng }}, {{ center_lat }}], zoom: {{ zoom }} });

// Fetch & parse tile with big-int protection for "hex|h3|index"
async function getTileData({ index, signal }) {
const {x,y,z} = index;
const url = TPL.replace('{z}', z).replace('{x}', x).replace('{y}', y);
try {
const res = await fetch(url, { cache:'no-cache', signal });
if (!res.ok) throw new Error(String(res.status));
let text = await res.text();
text = text.replace(/"(hex|h3|index)"\s*:\s*(\d+)/gi, (_m, k, d) => `"${k}":"${d}"`);
const data = JSON.parse(text);
const out = normalize(data);
setNote(`z${z} (${x},${y}) → ${out.length}`);
return out;
} catch (e) {
// silence aborts (happen on pan/zoom); show others
const s = String(e?.name||e);
if (!/Abort/i.test(s)) {
console.error('tile error', e);
setNote(`error z${z} (${x},${y})`);
}
return [];
}
}

// Build layers from CONFIG
const tileCfg = CONFIG.tileLayer || {};
const hexCfg = parseHexLayerConfig(CONFIG.hexLayer || {});

const overlay = new MapboxOverlay({
interleaved: false,
layers: [
new TileLayer({
id: 'hex-tiles',
data: TPL,
tileSize: tileCfg.tileSize ?? 256,
minZoom: tileCfg.minZoom ?? 0,
maxZoom: tileCfg.maxZoom ?? 19,
pickable: tileCfg.pickable ?? true,
getTileData,
renderSubLayers: (props) => {
const data = props.data || [];
if (!data.length) return null;

if (H3HexagonLayer) {
return new H3HexagonLayer({
id: `${props.id}-h3`,
data,
// defaults
pickable: true, stroked: false, filled: true, extruded: false,
coverage: 0.9, lineWidthMinPixels: 1,
getHexagon: d => d.hex,
// apply config overrides
...hexCfg
});
}

// Fallback: draw boundaries with PolygonLayer if geo-layers not present
const polys = data.map(d => {
const ring = h3.cellToBoundary(d.hex, true).map(([lat,lng]) => [lng, lat]);
return { ...d, polygon: ring };
});
return new PolygonLayer({
id: `${props.id}-poly-fallback`,
data: polys,
pickable: true, stroked: true, filled: true, extruded: false,
getPolygon: d => d.polygon,
getFillColor: [184,184,184,220],
getLineColor: [200,200,200,255],
lineWidthMinPixels: 1
});
}
})
]
});
map.addControl(overlay);

// HUD hover
map.on('mousemove', (e)=>{
const info = overlay.pickObject({x:e.point.x, y:e.point.y, radius:4});
if(info?.object){
map.getCanvas().style.cursor='pointer';
const p = info.object;
const val = p.metric;
setNote(`hex ${p.hex}${val!=null?` • ${Number(val).toFixed(2)}`:''}`);
} else {
map.getCanvas().style.cursor='';
}
});
</script>
</body>
</html>
""").render(
tile_url_template=tile_url_template,
config_json=config_json,
mapbox_token=mapbox_token,
center_lng=center_lng,
center_lat=center_lat,
zoom=zoom,
)

common = fused.load("https://github.com/fusedio/udfs/tree/abf9c87/public/common/")
return common.html_to_obj(html)

Example UDF

Raster: BitmapLayer

Use for raster outputs like satellite imagery, elevation models, or PNG files.

Live example rendered from a UDF (HTML output):

Open demo ↗

Expected UDF output:

Output TypeDescriptionExample
numpy array3D array (height, width, channels)(256, 256, 3) RGB image
PIL ImagePNG/JPEG image objectImage.open('satellite.png')
bytesEncoded image dataPNG/JPEG bytes
Expand to see Visualise JSON
{
"tileLayer": {
"@@type": "TileLayer",
"minZoom": 0,
"maxZoom": 19,
"tileSize": 256,
"pickable": true
},
"rasterLayer": {
"@@type": "BitmapLayer",
"pickable": true
}
}
Expand to see standalone UDF code

@fused.udf(cache_max_age=0)
def udf(
token: str = "UDF_CDLs_Tile_Example",
host: str = "https://www.fused.io",
mapbox_token: str = "pk.eyJ1IjoiaXNhYWNmdXNlZGxhYnMiLCJhIjoiY2xicGdwdHljMHQ1bzN4cWhtNThvbzdqcSJ9.73fb6zHMeO_c8eAXpZVNrA",
style_url: str = "mapbox://styles/mapbox/dark-v10",
center_lng: float = -121.16450354933122,
center_lat: float = 38.44272969483187,
zoom: float = 8.59,
minzoom: int = 6,
maxzoom: int = 14,
layer_id_raster: str = "fused-raster-layer",
raster_tile_size: int = 256,
raster_opacity: float = 0.95
):
from jinja2 import Template

"""
Mapbox map that loads **raster** XYZ tiles from a Fused UDF token.

Tiles URL:
{host}/server/v1/realtime-shared/{token}/run/tiles/{z}/{x}/{y}?dtype_out_raster=png
"""
html = Template(r"""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Raster XYZ Loader</title>
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no"/>
<link href="https://api.mapbox.com/mapbox-gl-js/v3.2.0/mapbox-gl.css" rel="stylesheet"/>
<script src="https://api.mapbox.com/mapbox-gl-js/v3.2.0/mapbox-gl.js"></script>
<style>
html, body { margin:0; height:100%; }
#map { position:absolute; inset:0; }
</style>
</head>
<body>
<div id="map"></div>

<script>
const MAPBOX_TOKEN = {{ mapbox_token | tojson }};
const STYLE_URL = {{ style_url | tojson }};
const HOST = {{ host | tojson }};
const TOKEN = {{ token | tojson }};
const CENTER = [{{ center_lng }}, {{ center_lat }}];
const ZOOM = {{ zoom }};
const MINZOOM = {{ minzoom }};
const MAXZOOM = {{ maxzoom }};
const LID_RAS = {{ layer_id_raster | tojson }};
const R_TILE_SIZE = {{ raster_tile_size }};
const R_OPACITY = {{ raster_opacity }};

function buildRaster(host, tok){
return host.replace(/\/+$/,'') + `/server/v1/realtime-shared/${tok}/run/tiles/{z}/{x}/{y}?dtype_out_raster=png`;
}

mapboxgl.accessToken = MAPBOX_TOKEN;
const map = new mapboxgl.Map({
container: 'map',
style: STYLE_URL,
center: CENTER,
zoom: ZOOM
});

function addRaster(url){
if (map.getLayer(LID_RAS)) map.removeLayer(LID_RAS);
if (map.getSource('xyz')) map.removeSource('xyz');

map.addSource('xyz', {
type: 'raster',
tiles: [url],
tileSize: R_TILE_SIZE,
minzoom: MINZOOM,
maxzoom: MAXZOOM
});

map.addLayer({
id: LID_RAS,
type: 'raster',
source: 'xyz',
paint: {
'raster-opacity': R_OPACITY,
'raster-fade-duration': 0
}
});
}

map.on('load', () => {
const url = buildRaster(HOST, TOKEN);
addRaster(url);
});
</script>
</body>
</html>
""").render(
token=token,
host=host,
mapbox_token=mapbox_token,
style_url=style_url,
center_lng=center_lng,
center_lat=center_lat,
zoom=zoom,
minzoom=minzoom,
maxzoom=maxzoom,
layer_id_raster=layer_id_raster,
raster_tile_size=raster_tile_size,
raster_opacity=raster_opacity,
)

common = fused.load("https://github.com/fusedio/udfs/tree/351515e/public/common/")
return common.html_to_obj(html)


Example UDF

Color Styling Methods (getFillColor)

There are four ways to style colors in Fused. Each method serves different use cases:

1. Hardcoded Colors

Use fixed RGB/RGBA values for uniform styling.

Live example rendered from a UDF (HTML output):

Open demo ↗

Expected DataFrame structure:

geometryany_column
POLYGON(...)any_value
POLYGON(...)any_value

Note: Column values don't affect styling - all features get the same color.

Expand to see Visualize JSON
{
"tileLayer": {
"@@type": "TileLayer",
"minZoom": 0,
"maxZoom": 19,
"tileSize": 256,
"pickable": true
},
"rasterLayer": {
"@@type": "BitmapLayer",
"pickable": true
},
"vectorLayer": {
"@@type": "GeoJsonLayer",
"stroked": true,
"filled": true,
"pickable": true,
"lineWidthMinPixels": 1,
"getFillColor": [20, 200, 200, 100],
"getLineColor": [50, 50, 50, 255]
}
}
Expand to see Standalone UDF code
@fused.udf(cache_max_age=0)
def udf(
token: str = "UDF_Overture_Maps_Example",
host: str = "https://www.fused.io",
mapbox_token: str = "pk.eyJ1IjoiaXNhYWNmdXNlZGxhYnMiLCJhIjoiY2xicGdwdHljMHQ1bzN4cWhtNThvbzdqcSJ9.73fb6zHMeO_c8eAXpZVNrA",
style_url: str = "mapbox://styles/mapbox/dark-v10",
center_lng: float = -122.4194,
center_lat: float = 37.7749,
zoom: float = 12,
minzoom: int = 0,
maxzoom: int = 15,
layer_id: str = "vector-fill",
source_layer: str = "udf",
fill_color: str = "#35AF6D",
fill_opacity: float = 0.55,
outline_color: str = "#0b0b0b",
):
from jinja2 import Template
html = Template(r"""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Vector Fill - Hardcoded Color</title>
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=false"/>
<link href="https://api.mapbox.com/mapbox-gl-js/v3.2.0/mapbox-gl.css" rel="stylesheet"/>
<script src="https://api.mapbox.com/mapbox-gl-js/v3.2.0/mapbox-gl.js"></script>
<style>html,body{margin:0;height:100%}#map{position:absolute;inset:0}</style>
</head>
<body>
<div id="map"></div>
<script>
const MAPBOX_TOKEN = {{ mapbox_token | tojson }};
const STYLE_URL = {{ style_url | tojson }};
const HOST = {{ host | tojson }};
const TOKEN = {{ token | tojson }};
const CENTER = [{{ center_lng }}, {{ center_lat }}];
const ZOOM = {{ zoom }};
const MINZOOM = {{ minzoom }};
const MAXZOOM = {{ maxzoom }};
const LAYER_ID = {{ layer_id | tojson }};
const SOURCE_LAYER = {{ source_layer | tojson }};
const FILL_COLOR = {{ fill_color | tojson }};
const FILL_OPAC = {{ fill_opacity }};
const OUTLINE_COL = {{ outline_color | tojson }};

function tilesFromToken(tok) {
return `${HOST.replace(/\/+$/,'')}/server/v1/realtime-shared/${tok}/run/tiles/{z}/{x}/{y}?dtype_out_vector=mvt`;
}

mapboxgl.accessToken = MAPBOX_TOKEN;
const map = new mapboxgl.Map({ container:'map', style:STYLE_URL, center:CENTER, zoom:ZOOM });

map.on('load', () => {
map.addSource('xyz', { type:'vector', tiles:[tilesFromToken(TOKEN)], minzoom:MINZOOM, maxzoom:MAXZOOM });

map.addLayer({
id: LAYER_ID,
type: 'fill',
source: 'xyz',
'source-layer': SOURCE_LAYER,
filter: ['==', ['geometry-type'], 'Polygon'],
paint: { 'fill-color': FILL_COLOR, 'fill-opacity': FILL_OPAC }
});

map.addLayer({
id: LAYER_ID + '-outline',
type: 'line',
source: 'xyz',
'source-layer': SOURCE_LAYER,
filter: ['==', ['geometry-type'], 'Polygon'],
paint: { 'line-color': OUTLINE_COL, 'line-width': 0.5, 'line-opacity': 0.9 }
});
});
</script>
</body>
</html>
""").render(
token=token, host=host, mapbox_token=mapbox_token, style_url=style_url,
center_lng=center_lng, center_lat=center_lat, zoom=zoom,
minzoom=minzoom, maxzoom=maxzoom, layer_id=layer_id,
source_layer=source_layer, fill_color=fill_color, fill_opacity=fill_opacity,
outline_color=outline_color,
)
common = fused.load("https://github.com/fusedio/udfs/tree/351515e/public/common/")
return common.html_to_obj(html)

Expected DataFrame structure:

geometryany_column
POLYGON(...)any_value
POLYGON(...)any_value

Note: Column values don't affect styling - all features get the same color.

2. Property-Based Colors

Use data column values to calculate colors dynamically.

Live example rendered from a UDF (HTML output):

Open demo ↗

Expected DataFrame structure:

geometryvalueheight
POLYGON(...)525
POLYGON(...)840
POLYGON(...)315

Note: Numerical columns are used directly in color calculations.

Expand to see Visualise JSON
{
"tileLayer": {
"@@type": "TileLayer",
"minZoom": 0,
"maxZoom": 19,
"tileSize": 256,
"pickable": true
},
"rasterLayer": {
"@@type": "BitmapLayer",
"pickable": true
},
"vectorLayer": {
"@@type": "GeoJsonLayer",
"stroked": true,
"filled": true,
"pickable": true,
"extruded": true,
"getElevation": "@@=properties.value * 10",
"lineWidthMinPixels": 1,
"getFillColor": "@@=[properties.value * 50, properties.value * 30, properties.value * 2]",
"getLineColor": [50, 50, 50, 255]
}
}
Expand to see standalone UDF code
@fused.udf(cache_max_age=0)
def udf(
token: str = "UDF_DSM_Zonal_Stats",
host: str = "https://www.fused.io",
mapbox_token: str = "pk.eyJ1IjoiaXNhYWNmdXNlZGxhYnMiLCJhIjoiY2xicGdwdHljMHQ1bzN4cWhtNThvbzdqcSJ9.73fb6zHMeO_c8eAXpZVNrA",
style_url: str = "mapbox://styles/mapbox/dark-v10",
center_lng: float = -122.4194,
center_lat: float = 37.7749,
zoom: float = 12,
minzoom: int = 0,
maxzoom: int = 15,
layer_id: str = "vector-choro",
source_layer: str = "udf",
value_attr: str = "stats",
domain_min: float = 0.0,
domain_mid: float = 50.0,
domain_max: float = 100.0,
color_min: str = "#2b65a0",
color_mid: str = "#35af6d",
color_max: str = "#e8ff59",
fill_opacity: float = 0.65,
outline_color: str = "#111111",
):
from jinja2 import Template
html = Template(r"""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Vector Fill - Property Based</title>
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=false"/>
<link href="https://api.mapbox.com/mapbox-gl-js/v3.2.0/mapbox-gl.css" rel="stylesheet"/>
<script src="https://api.mapbox.com/mapbox-gl-js/v3.2.0/mapbox-gl.js"></script>
<style>html,body{margin:0;height:100%}#map{position:absolute;inset:0}</style>
</head>
<body>
<div id="map"></div>
<script>
const MAPBOX_TOKEN = {{ mapbox_token | tojson }};
const STYLE_URL = {{ style_url | tojson }};
const HOST = {{ host | tojson }};
const TOKEN = {{ token | tojson }};
const CENTER = [{{ center_lng }}, {{ center_lat }}];
const ZOOM = {{ zoom }};
const MINZOOM = {{ minzoom }};
const MAXZOOM = {{ maxzoom }};
const LAYER_ID = {{ layer_id | tojson }};
const SOURCE_LAYER = {{ source_layer | tojson }};
const ATTR = {{ value_attr | tojson }};
const DMIN = {{ domain_min }};
const DMID = {{ domain_mid }};
const DMAX = {{ domain_max }};
const CMIN = {{ color_min | tojson }};
const CMID = {{ color_mid | tojson }};
const CMAX = {{ color_max | tojson }};
const FILL_OPAC = {{ fill_opacity }};
const OUTLINE_COL = {{ outline_color | tojson }};

function tilesFromToken(tok) {
return `${HOST.replace(/\/+$/,'')}/server/v1/realtime-shared/${tok}/run/tiles/{z}/{x}/{y}?dtype_out_vector=mvt`;
}

mapboxgl.accessToken = MAPBOX_TOKEN;
const map = new mapboxgl.Map({ container:'map', style:STYLE_URL, center:CENTER, zoom:ZOOM });

function valueExpr() {
return ['to-number', ['get', ATTR]];
}

map.on('load', () => {
map.addSource('xyz', { type:'vector', tiles:[tilesFromToken(TOKEN)], minzoom:MINZOOM, maxzoom:MAXZOOM });

map.addLayer({
id: LAYER_ID,
type: 'fill',
source: 'xyz',
'source-layer': SOURCE_LAYER,
filter: ['==', ['geometry-type'], 'Polygon'],
paint: {
'fill-color': [
'interpolate', ['linear'], ['coalesce', valueExpr(), DMIN],
DMIN, CMIN,
DMID, CMID,
DMAX, CMAX
],
'fill-opacity': FILL_OPAC
}
});

map.addLayer({
id: LAYER_ID + '-outline',
type: 'line',
source: 'xyz',
'source-layer': SOURCE_LAYER,
filter: ['==', ['geometry-type'], 'Polygon'],
paint: { 'line-color': OUTLINE_COL, 'line-width': 0.5, 'line-opacity': 0.9 }
});
});
</script>
</body>
</html>
""").render(
token=token, host=host, mapbox_token=mapbox_token, style_url=style_url,
center_lng=center_lng, center_lat=center_lat, zoom=zoom,
minzoom=minzoom, maxzoom=maxzoom, layer_id=layer_id, source_layer=source_layer,
value_attr=value_attr, domain_min=domain_min, domain_mid=domain_mid, domain_max=domain_max,
color_min=color_min, color_mid=color_mid, color_max=color_max,
fill_opacity=fill_opacity, outline_color=outline_color,
)
common = fused.load("https://github.com/fusedio/udfs/tree/351515e/public/common/")
return common.html_to_obj(html)

When to use: When you want colors to directly reflect data values with custom calculations.

Example UDF

3. Conditional Colors with hasProp

Handle missing data gracefully with fallback colors.

Expected DataFrame structure:

geometryvaluestatus
POLYGON(...)25active
POLYGON(...)NaNinactive
POLYGON(...)15active

Note: Can handle missing/null values with fallback styling.

Expand to see Visualise code
{
"tileLayer": {
"@@type": "TileLayer",
"minZoom": 0,
"maxZoom": 19,
"tileSize": 256,
"pickable": true
},
"rasterLayer": {
"@@type": "BitmapLayer",
"pickable": true
},
"vectorLayer": {
"@@type": "GeoJsonLayer",
"stroked": true,
"filled": true,
"pickable": true,
"extruded": true,
"getElevation": {
"@@function": "hasProp",
"property": "value",
"present": "@@=properties.value",
"absent": 1
},
"lineWidthMinPixels": 1,
"getFillColor": {
"@@function": "hasProp",
"property": "value",
"present": "@@=[properties.value * 50, properties.value * 3, properties.value * 2]",
"absent": [255, 0, 255]
}
}
}

When to use: When your data might have missing values and you need reliable fallbacks.

Example UDF

Understanding hasProp Function

The hasProp function is a core Fused utility for conditional styling based on data property presence. It's the foundation for handling missing or null values gracefully in visualizations.

How it works:

"getFillColor": {
"@@function": "hasProp",
"property": "metric",
"present": "@@=[255, (1 - properties.metric/500) * 255, 0]",
"absent": [220, 255, 100]
}

Key Properties:

  • "@@function": "hasProp": Declares the conditional function
  • "property": "column_name": Specifies which DataFrame column to check
  • "present": expression: Styling when the property exists and is not null
  • "absent": value: Fallback styling when the property is missing or null

Common Use Cases:

  • Missing data: Handle null/NaN values in datasets
  • Optional columns: Style features that may not have certain attributes
  • Data validation: Provide visual feedback for incomplete records
  • Progressive enhancement: Add styling layers based on data availability

Example with elevation:

"getElevation": {
"@@function": "hasProp",
"property": "height",
"present": "@@=properties.height * 10",
"absent": 1
}

This ensures buildings without height data still appear with a default elevation of 1 unit.

4. Category Colors

Use predefined color palettes for categorical data.

Live example rendered from a UDF (HTML output):

Open demo ↗

Expected DataFrame structure:

geometrybuilding_typezone
POLYGON(...)residentialA
POLYGON(...)commercialB
POLYGON(...)industrialA

Note: Categorical columns with distinct values for color mapping.

Expand to see Visualise JSON
{
"tileLayer": {
"@@type": "TileLayer",
"minZoom": 0,
"maxZoom": 19,
"tileSize": 256,
"pickable": true
},
"rasterLayer": {
"@@type": "BitmapLayer",
"pickable": true
},
"vectorLayer": {
"@@type": "GeoJsonLayer",
"stroked": true,
"filled": true,
"pickable": true,
"lineWidthMinPixels": 1,
"getFillColor": {
"@@function": "colorCategories",
"attr": "building_type",
"domain": [
"residential",
"commercial",
"industrial",
"public"
],
"colors": "Bold"
}
}
}
Expand to see the Standalone UDF
@fused.udf(cache_max_age=0)
def udf(
token: str = "UDF_FEMA_Buildings_US",
host: str = "https://www.fused.io",
mapbox_token: str = "pk.eyJ1IjoiaXNhYWNmdXNlZGxhYnMiLCJhIjoiY2xicGdwdHljMHQ1bzN4cWhtNThvbzdqcSJ9.73fb6zHMeO_c8eAXpZVNrA",
style_url: str = "mapbox://styles/mapbox/dark-v10",
center_lng: float = -74.0110, # longitude
center_lat: float = 40.7133,
zoom: float = 14,
minzoom: int = 0,
maxzoom: int = 15,
source_layer: str = "udf",
layer_id: str = "fema-categories",
attr_name: str = "OCC_CLS",
domain: list = None,
# CARTOColors Bold palette (7 colors)
colors: list = None,
fill_opacity: float = 0.65,
outline_color: str = "#0b0b0b"
):
from jinja2 import Template

if domain is None:
domain = [
"Assembly",
"Commercial",
"Utility and Misc",
"Residential",
"Industrial",
"Education",
"Government",
]
if colors is None:
colors = ["#7F3C8D", "#11A579", "#3969AC", "#F2B701", "#E73F74", "#80BA5A", "#E68310"]

html = Template(r"""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Category Colors (Mapbox GL)</title>
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no"/>
<link href="https://api.mapbox.com/mapbox-gl-js/v3.2.0/mapbox-gl.css" rel="stylesheet"/>
<script src="https://api.mapbox.com/mapbox-gl-js/v3.2.0/mapbox-gl.js"></script>
<style>html,body{margin:0;height:100%}#map{position:absolute;inset:0}</style>
</head>
<body>
<div id="map"></div>
<script>
const MAPBOX_TOKEN = {{ mapbox_token | tojson }};
const STYLE_URL = {{ style_url | tojson }};
const HOST = {{ host | tojson }};
const TOKEN = {{ token | tojson }};
const CENTER = [{{ center_lng }}, {{ center_lat }}];
const ZOOM = {{ zoom }};
const MINZOOM = {{ minzoom }};
const MAXZOOM = {{ maxzoom }};
const SOURCE_LAYER = {{ source_layer | tojson }};
const LAYER_ID = {{ layer_id | tojson }};
const ATTR_NAME = {{ attr_name | tojson }};
const DOMAIN = {{ domain | tojson }};
const COLORS = {{ colors | tojson }};
const FILL_OPAC = {{ fill_opacity }};
const OUTLINE_COL = {{ outline_color | tojson }};

function tilesFromToken(h, tok) {
return h.replace(/\/+$/,'') + `/server/v1/realtime-shared/${tok}/run/tiles/{z}/{x}/{y}?dtype_out_vector=mvt`;
}

function matchByCategory(attr, domain, colors, fallbackColor) {
// ['match', ['get', attr], domain[0], colors[0], domain[1], colors[1], ..., fallback]
const expr = ['match', ['get', attr]];
for (let i = 0; i < Math.min(domain.length, colors.length); i++) {
expr.push(domain[i], colors[i]);
}
expr.push(fallbackColor);
return expr;
}

mapboxgl.accessToken = MAPBOX_TOKEN;
const map = new mapboxgl.Map({
container: 'map',
style: STYLE_URL,
center: CENTER,
zoom: ZOOM
});

map.on('load', () => {
// Vector MVT from the FEMA UDF token
map.addSource('fema', {
type: 'vector',
tiles: [tilesFromToken(HOST, TOKEN)],
minzoom: MINZOOM,
maxzoom: MAXZOOM
});

// Polygons fill by category
map.addLayer({
id: LAYER_ID,
type: 'fill',
source: 'fema',
'source-layer': SOURCE_LAYER,
filter: ['==', ['geometry-type'], 'Polygon'],
paint: {
'fill-color': matchByCategory(ATTR_NAME, DOMAIN, COLORS, '#BBBBBB'),
'fill-opacity': FILL_OPAC
}
});

// Thin polygon outline
map.addLayer({
id: LAYER_ID + '-outline',
type: 'line',
source: 'fema',
'source-layer': SOURCE_LAYER,
filter: ['==', ['geometry-type'], 'Polygon'],
paint: { 'line-color': OUTLINE_COL, 'line-width': 0.5, 'line-opacity': 0.9 }
});

// Optional: hover cursor over polygons
map.on('mouseenter', LAYER_ID, () => map.getCanvas().style.cursor = 'pointer');
map.on('mouseleave', LAYER_ID, () => map.getCanvas().style.cursor = '');
});
</script>
</body>
</html>
""").render(
token=token,
host=host,
mapbox_token=mapbox_token,
style_url=style_url,
center_lng=center_lng,
center_lat=center_lat,
zoom=zoom,
minzoom=minzoom,
maxzoom=maxzoom,
source_layer=source_layer,
layer_id=layer_id,
attr_name=attr_name,
domain=domain,
colors=colors,
fill_opacity=fill_opacity,
outline_color=outline_color,
)

common = fused.load("https://github.com/fusedio/udfs/tree/351515e/public/common/")
return common.html_to_obj(html)

When to use: Categorical data like building types, land use classifications, or administrative regions.

warning

Note that unexpected behaviors may arise if too many domains are used.

Example UDF

5. Continuous Color Scales

Use gradient color scales for continuous numerical data.

Live example rendered from a UDF (HTML output):

Open demo ↗

Expected DataFrame structure:

hexpopulationdensity
8a2a1072b59ffff125045.2
8a2a1072b5bffff89032.1
8a2a1072b5dffff210067.8

Note: Numerical columns with values within the specified domain range.

Expand to see Visualise JSON
{
"tileLayer": {
"@@type": "TileLayer",
"minZoom": 0,
"maxZoom": 19,
"tileSize": 256,
"pickable": true
},
"rasterLayer": {
"@@type": "BitmapLayer",
"pickable": true
},
"vectorLayer": {
"@@type": "H3HexagonLayer",
"stroked": true,
"filled": true,
"pickable": true,
"extruded": true,
"opacity": 1,
"coverage": 0.9,
"lineWidthMinPixels": 5,
"getHexagon": "@@=properties.hex",
"getFillColor": {
"@@function": "colorContinuous",
"attr": "population",
"domain": [0, 10000],
"steps": 15,
"colors": "SunsetDark",
"nullColor": [184, 184, 184]
},
"getElevation": {
"@@function": "hasProp",
"property": "population",
"present": "@@=properties.population / 100",
"absent": 1
},
"elevationScale": 10
}
}
Expand to see the Standalone UDF
@fused.udf(cache_max_age=0)
def udf(
data_url: str = "https://www.fused.io/server/v1/realtime-shared/UDF_DuckDB_H3_SF/run/file?format=json",
mapbox_token: str = "pk.eyJ1IjoiaXNhYWNmdXNlZGxhYnMiLCJhIjoiY2xicGdwdHljMHQ1bzN4cWhtNThvbzdqcSJ9.73fb6zHMeO_c8eAXpZVNrA",
style_url: str = "mapbox://styles/mapbox/dark-v10",
center_lng: float = -122.417759,
center_lat: float = 37.776452,
zoom: float = 11.0,
pitch: float = 45.0,
bearing: float = 0.0,
elevation_scale: float = 10.0,
wireframe: bool = False,
domain_min: float = 0.0,
domain_max: float = 1000.0,
null_color: tuple = (184, 184, 184),
):
from jinja2 import Template
html = Template(r"""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>H3 Continuous Color Scale</title>
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no"/>
<link href="https://api.mapbox.com/mapbox-gl-js/v3.2.0/mapbox-gl.css" rel="stylesheet"/>
<script src="https://api.mapbox.com/mapbox-gl-js/v3.2.0/mapbox-gl.js"></script>
<script src="https://unpkg.com/h3-js@latest/dist/h3-js.umd.js"></script>
<script src="https://unpkg.com/deck.gl@9.0.0/dist.min.js"></script>
<style>html, body { margin:0; height:100%; } #map { position:absolute; inset:0; }</style>
</head>
<body>
<div id="map"></div>
<script>
if (typeof h3 === 'undefined') throw new Error('h3-js must load before deck.gl');

const MAPBOX_TOKEN = {{ mapbox_token | tojson }};
const STYLE_URL = {{ style_url | tojson }};
const DATA_URL = {{ data_url | tojson }};

const INITIAL_VIEW_STATE = {
longitude: {{ center_lng }}, latitude: {{ center_lat }}, zoom: {{ zoom }}, pitch: {{ pitch }}, bearing: {{ bearing }}
};

const ELEVATION_SCALE = {{ elevation_scale }};
const WIREFRAME = {{ wireframe | tojson }};
const DOMAIN_MIN = {{ domain_min }};
const DOMAIN_MAX = {{ domain_max }};
const NULL_COLOR = {{ null_color | tojson }};

function ramp(t){ t=Math.max(0,Math.min(1,t)); const s=[[52,18,88],[226,83,18],[248,213,72]]; const n=s.length-1; const p=t*n; const i=Math.floor(p); const f=p-i; const a=s[i],b=s[Math.min(i+1,n)]; return [Math.round(a[0]+(b[0]-a[0])*f),Math.round(a[1]+(b[1]-a[1])*f),Math.round(a[2]+(b[2]-a[2])*f)]; }
function colorForCount(c){ if(!Number.isFinite(c)) return [NULL_COLOR[0],NULL_COLOR[1],NULL_COLOR[2]]; if(DOMAIN_MAX===DOMAIN_MIN) return ramp(1); const t=(c-DOMAIN_MIN)/(DOMAIN_MAX-DOMAIN_MIN); return ramp(t); }

mapboxgl.accessToken = MAPBOX_TOKEN;
if (mapboxgl.setTelemetryEnabled) { try { mapboxgl.setTelemetryEnabled(false); } catch(e){} }
const map = new mapboxgl.Map({ container:'map', style:STYLE_URL, center:[INITIAL_VIEW_STATE.longitude, INITIAL_VIEW_STATE.latitude], zoom:INITIAL_VIEW_STATE.zoom, pitch:INITIAL_VIEW_STATE.pitch, bearing:INITIAL_VIEW_STATE.bearing });
const overlay = new deck.MapboxOverlay({ interleaved:true, layers:[] });

map.on('load', () => {
map.addControl(overlay);
const layer = new deck.H3HexagonLayer({
id:'h3-continuous', data:DATA_URL, pickable:true, extruded:true, wireframe:WIREFRAME,
material:{ ambient:0.35, diffuse:0.6, shininess:32, specularColor:[255,255,255] },
getHexagon:d=>d.hex,
getElevation:d=>{ const c=(d&&Number.isFinite(d.count))?d.count:0; return c*ELEVATION_SCALE; },
getFillColor:d=>{ const c=(d&&Number.isFinite(d.count))?d.count:NaN; const rgb=colorForCount(c); return [rgb[0],rgb[1],rgb[2],210]; },
stroked:true, getLineColor:[30,30,30], getLineWidth:1, lineWidthUnits:'pixels',
});
overlay.setProps({ layers:[layer] });
});
</script>
</body>
</html>
""").render(
data_url=data_url, mapbox_token=mapbox_token, style_url=style_url,
center_lng=center_lng, center_lat=center_lat, zoom=zoom,
pitch=pitch, bearing=bearing, elevation_scale=elevation_scale,
wireframe=wireframe, domain_min=domain_min, domain_max=domain_max,
null_color=list(null_color),
)
common = fused.load("https://github.com/fusedio/udfs/tree/351515e/public/common/")
return common.html_to_obj(html)

When to use: Continuous numerical data like population density, temperature, or elevation values.

Example UDF

6. Transparency & Opacity

Control the transparency of your visualizations for layering effects and visual hierarchy.

Vector Color Transparency:

"getFillColor": [255, 100, 100, 200],  // RGBA: Red, Green, Blue, Alpha
"getLineColor": [50, 50, 50, 255] // Fully opaque outline

Opacity Guidelines:

  • Good visibility: Use 180-255 for clearly visible features
  • Semi-transparent: Use 100-179 for overlay effects
  • Avoid very low opacity: Values below 100 make features barely visible
  • Solid colors: Omit alpha channel for full opacity: [255, 100, 100]

Examples:

// Good examples
"getFillColor": [255, 100, 100, 200], // Clearly visible red
"getFillColor": [100, 150, 200], // Solid blue (alpha defaults to 255)

// Poor examples
"getFillColor": [255, 255, 255, 25], // Too transparent, barely visible
"getFillColor": [100, 150, 200, 50] // Hard to see on most backgrounds

Raster Transparency:

  • RGB images: Black pixels (0,0,0) are automatically transparent
  • RGBA images: Use the 4th channel (alpha) for transparency control

Advanced Features

3D Visualization

Enable 3D effects by setting extruded: true and using getElevation:

Expected DataFrame structure:

geometryheight_valuefloors
POLYGON(...)4515
POLYGON(...)12040
POLYGON(...)3010

Note: Numerical columns used for building height/elevation in 3D view. Use "@@=properties.column_name" syntax to reference height data.

Expand to see Visualise JSON
{
"tileLayer": {
"@@type": "TileLayer",
"minZoom": 0,
"maxZoom": 19,
"tileSize": 256,
"pickable": true
},
"rasterLayer": {
"@@type": "BitmapLayer",
"pickable": true
},
"vectorLayer": {
"@@type": "GeoJsonLayer",
"stroked": true,
"filled": true,
"pickable": true,
"extruded": true,
"getElevation": "@@=properties.height_value * 5",
"elevationScale": 10,
"getFillColor": [100, 150, 200, 180]
}
}

Live example rendered from a UDF (HTML output):

Open demo ↗

Expand to see the Standalone UDF
@fused.udf(cache_max_age=0)
def udf(
token: str = "UDF_DSM_Zonal_Stats",
host: str = "https://www.fused.io", # or https://staging.fused.io
mapbox_token: str = "pk.eyJ1IjoiaXNhYWNmdXNlZGxhYnMiLCJhIjoiY2xicGdwdHljMHQ1bzN4cWhtNThvbzdqcSJ9.73fb6zHMeO_c8eAXpZVNrA",
style_url: str = "mapbox://styles/mapbox/dark-v10",
center_lng: float = -122.4194, # SF
center_lat: float = 37.7749,
zoom: float = 16.0,
pitch: float = 60.0,
bearing: float = -20.0,
minzoom: int = 0,
maxzoom: int = 15,
elevation_scale: float = 1.0, # stats -> height multiplier
):
from jinja2 import Template

"""
Minimal Deck.gl 3D vector map (no raster). Requires server to serve MVT:
{host}/.../{token}/run/tiles/{z}/{x}/{y}?dtype_out_vector=mvt
"""
html = Template(r"""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Deck.gl 3D Vector (Minimal)</title>
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no"/>
<link href="https://api.mapbox.com/mapbox-gl-js/v3.2.0/mapbox-gl.css" rel="stylesheet"/>
<script src="https://api.mapbox.com/mapbox-gl-js/v3.2.0/mapbox-gl.js"></script>
<script src="https://unpkg.com/deck.gl@latest/dist.min.js"></script>
<style>
html,body{margin:0;height:100%} #map{position:absolute;inset:0}
</style>
</head>
<body>
<div id="map"></div>
<script>
const MAPBOX_TOKEN = {{ mapbox_token | tojson }};
const STYLE_URL = {{ style_url | tojson }};
const HOST = {{ host | tojson }};
const TOKEN = {{ token | tojson }};
const CENTER = [{{ center_lng }}, {{ center_lat }}];
const ZOOM = {{ zoom }};
const PITCH = {{ pitch }};
const BEARING = {{ bearing }};
const MIN_ZOOM = {{ minzoom }};
const MAX_ZOOM = {{ maxzoom }};
const ELEV_SCALE = {{ elevation_scale }};

const mvtURL = (h,t) => h.replace(/\/+$/,'') +
`/server/v1/realtime-shared/${t}/run/tiles/{z}/{x}/{y}?dtype_out_vector=mvt`;

mapboxgl.accessToken = MAPBOX_TOKEN;
if (mapboxgl.setTelemetryEnabled) { try { mapboxgl.setTelemetryEnabled(false); } catch(e){} }

const map = new mapboxgl.Map({
container: 'map',
style: STYLE_URL,
center: CENTER,
zoom: ZOOM,
pitch: PITCH,
bearing: BEARING
});

const overlay = new deck.MapboxOverlay({
layers: []
});

map.on('load', () => {
map.addControl(overlay);

const vector3d = new deck.MVTLayer({
id: 'vector-3d',
data: mvtURL(HOST, TOKEN),
minZoom: MIN_ZOOM,
maxZoom: MAX_ZOOM,
pickable: true,
extruded: true,
wireframe: false,
getElevation: f => {
const s = f.properties && f.properties.stats;
return (Number.isFinite(s) ? Number(s) : 0) * ELEV_SCALE;
},
getFillColor: f => {
const s = f.properties && f.properties.stats;
if (!Number.isFinite(s)) return [160,160,160,60];
const t = Math.max(0, Math.min(1, s/100));
return [Math.floor(30+200*t), Math.floor(120+60*(1-t)), Math.floor(90+30*(1-t)), 200];
},
getLineColor: [0,0,0,120],
lineWidthMinPixels: 1,
onClick: info => {
if (!info || !info.coordinate || !info.object) return;
new mapboxgl.Popup({closeButton:true})
.setLngLat(info.coordinate)
.setHTML('<pre style="margin:0;white-space:pre-wrap;">' +
JSON.stringify(info.object.properties||{}, null, 2) + '</pre>')
.addTo(map);
}
});

// Simple lighting so the extrusion looks 3D
const ambient = new deck.AmbientLight({intensity: 1.0});
const dir = new deck.DirectionalLight({intensity: 1.0, direction: [-1,-2,-1]});
overlay.setProps({ layers: [vector3d], effects: [new deck.LightingEffect({ambientLight: ambient, dirLight: dir})]});
});
</script>
</body>
</html>
""").render(
token=token,
host=host,
mapbox_token=mapbox_token,
style_url=style_url,
center_lng=center_lng,
center_lat=center_lat,
zoom=zoom,
pitch=pitch,
bearing=bearing,
minzoom=minzoom,
maxzoom=maxzoom,
elevation_scale=elevation_scale,
)

common = fused.load("https://github.com/fusedio/udfs/tree/351515e/public/common/")
return common.html_to_obj(html)
tip

Hold Cmd (macOS) or Ctrl (Windows/Linux) while dragging to tilt the map view and see 3D effects.

Example UDF

Debug Layers (DebugTileLayer)

Use DebugTileLayer with custom loading and error states for development:

Expand to see Visualise code
{
"tileLayer": {
"@@type": "DebugTileLayer",
"minZoom": 0,
"maxZoom": 15,
"tileSize": 256,
"pickable": true
},
"rasterLayer": {
"@@type": "BitmapLayer",
"pickable": true
},
"vectorLayer": {
"@@type": "GeoJsonLayer",
"stroked": true,
"filled": true,
"pickable": true,
"extruded": true,
"lineWidthMinPixels": 1
},
"loadingLayer": {
"@@type": "GeoJsonLayer",
"stroked": true,
"filled": false,
"pickable": true,
"lineWidthMinPixels": 10,
"getLineColor": [25, 55, 0, 255]
},
"errorLayer": {
"@@type": "GeoJsonLayer",
"stroked": true,
"filled": true,
"pickable": true,
"lineWidthMinPixels": 10,
"getLineColor": [255, 255, 0, 255],
"getFillColor": [255, 20, 255, 40]
}
}

Performance Optimization

  • Zoom levels: Set appropriate minZoom and maxZoom to control data loading
  • Line width units: Use lineWidthUnits: "pixels" for consistent appearance across zoom levels
  • Tile size: Adjust tileSize for optimal performance with your data density

FAQ: Debugging your visualizations

Why is my vector layer not colorized?

Let's take the example of a UDF that returns a GeoDataFrame with hex values:

@fused.udf()
def udf(
bounds: fused.types.Bounds = None,
):
# get_hex() is a non-important function for this demo that gives us US counties
df_hex = get_hex(gdf, hex_res)
df_hex['state_id'] = [id[:2] for id in df_hex["GEOID"]]

return df_hex

And our visualization JSON looks like this:

  {
"hexLayer": {
"@@type": "H3HexagonLayer",
"filled": true,
"pickable": true,
"extruded": false,
"getHexagon": "@@=properties.hex",
"getFillColor": {
"@@function": "colorContinuous",
"attr": "state_id",
"domain": [
0,
50
],
"steps": 2,
"colors": "Magenta"
}
}
}

You should make sure:

  1. hexLayer > getFillColor > attr is set to a column that exists in the GeoDataFrame (in this case state_id)
  2. Make sure your attr column is in either int or float type, not in str. In this case we should cast state_id to int:
@fused.udf()
def udf(
bounds: fused.types.Bounds = None,
):
df_hex = get_hex(gdf, hex_res)
df_hex['state_id'] = [id[:2] for id in df_hex["GEOID"]]
df_hex['state_id'] = df_hex['state_id'].astype(int)

return df_hex
  1. Making sure your values are within the correct domain (hexLayer > getFillColor > domain). In our case, we're showing US States, so the domain should be [0, 50].
Why are my 3D buildings not showing?

Common 3D visualization issues:

  1. Wrong elevation function: Don't use distance functions like haversine for elevation

    // ❌ Wrong - haversine is for distances
    "getElevation": {
    "@@function": "haversine",
    "attr": "HEIGHT"
    }

    // ✅ Correct - direct property reference
    "getElevation": "@@=properties.HEIGHT"
  2. Missing required properties: Ensure you have both extruded: true and a height column

    "vectorLayer": {
    "@@type": "GeoJsonLayer",
    "extruded": true,
    "getElevation": "@@=properties.height_value",
    "elevationScale": 10
    }
  3. Low transparency: Avoid very low alpha values that make buildings invisible

    // ❌ Too transparent (barely visible)
    "getFillColor": [255, 255, 255, 25]

    // ✅ Good visibility
    "getFillColor": [100, 150, 200, 180]
Why is my layer not appearing at all?

Common visibility issues:

  • Pickable setting: Check that pickable: true is set for interactivity
  • Data type mismatch: Verify your UDF returns the expected data type (GeoDataFrame for vector, array for raster)
  • Zoom level issues: Ensure zoom levels (minZoom, maxZoom) include your current map zoom level
  • Missing geometry: GeoJsonLayer requires a geometry column in your GeoDataFrame
  • Incorrect layer type: Make sure you're using the right @@type for your data (GeoJsonLayer vs H3HexagonLayer)

For more details on DeckGL properties, see the DeckGL documentation.