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: GeoJsonLayer

Use for GeoDataFrame outputs or any DataFrame with a geometry column. Supports dynamic styling based on data properties.

Overture GeoJSON

Expected DataFrame structure:

geometry
POLYGON(...)
POLYGON(...)
POLYGON(...)
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": false,
"lineWidthMinPixels": 1,
"getFillColor": [100, 150, 200, 180],
"getLineColor": [50, 50, 50, 255]
}
}

Key Properties:

  • stroked: Show polygon/line outlines
  • filled: Fill polygon interiors
  • extruded: Enable 3D elevation effects (See 3D Visualization for more)
  • pickable: Enable hover tooltips and interaction

Example UDF

Vector: H3HexagonLayer

Use for DataFrame outputs with H3 hexagon indices (no geometry column required).

Expected DataFrame structure:

hexpopulationmetric
8a2a1072b59ffff125045.2
8a2a1072b5bffff89032.1
8a2a1072b5dffff210067.8
Expand to see Visualise code
{
"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]
}
}

Required Property:

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

Example UDF

Raster: BitmapLayer

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

Crops

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 code
{
"tileLayer": {
"@@type": "TileLayer",
"minZoom": 0,
"maxZoom": 19,
"tileSize": 256,
"pickable": true
},
"rasterLayer": {
"@@type": "BitmapLayer",
"pickable": true
},
"vectorLayer": {
"@@type": "GeoJsonLayer",
"pickable": true
}
}

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.

File

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 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,
"lineWidthMinPixels": 1,
"getFillColor": [20, 200, 200, 100],
"getLineColor": [50, 50, 50, 255]
}
}

When to use: Simple visualizations, proof of concepts, or when all features should look identical.

2. Property-Based Colors

Use data column values to calculate colors dynamically.

File

Expected DataFrame structure:

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

Note: Numerical columns are used directly in color calculations.

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": "@@=properties.value * 10",
"lineWidthMinPixels": 1,
"getFillColor": "@@=[properties.value * 50, properties.value * 30, properties.value * 2]",
"getLineColor": [50, 50, 50, 255]
}
}

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.

File

Expected DataFrame structure:

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

Note: Categorical columns with distinct values for color mapping.

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,
"lineWidthMinPixels": 1,
"getFillColor": {
"@@function": "colorCategories",
"attr": "building_type",
"domain": [
"residential",
"commercial",
"industrial",
"public"
],
"colors": "Bold"
}
}
}

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.

Color Continuous

Expected DataFrame structure:

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

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

Expand to see Visualise code
{
"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
}
}

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 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": "@@=properties.height_value * 5",
"elevationScale": 10,
"getFillColor": [100, 150, 200, 180]
}
}
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.