Skip to main content

Dynamic Tile to H3

Dynamically tile data into H3 hexagons in a given viewport. Hex resolution is calculated based on the bounds of the viewport.

The following examples are available in a dedicated Fused Canvas

Set your UDF to Tile UDF Mode

The examples in this page all use the Tile UDF Mode to dynamically compute H3 based on the viewport bounds.

Read more about the differences between Tile UDF Mode and Single UDF Mode.

Polygon to Hex​

The following example uses a simplified Census Block Group dataset of the state of California, showing a heatmap of the population density per hex 9 cell:

Link to UDF in Fused Catalog

Code
@fused.udf
def udf(
bounds: fused.types.Bounds = [-125.0, 24.0, -66.9, 49.0],
path: str = "s3://fused-asset/demos/catchment_analysis/simplified_acs_bg_ca_2022.parquet",
min_hex_cell_res: int= 11, # Increase this if working with high res local data
max_hex_cell_res: int= 4,
):
import pandas as pd
import geopandas as gpd

common = fused.load("https://github.com/fusedio/udfs/tree/f430c25/public/common/")

# Dynamic H3 resolution
def dynamic_h3_res(b):
z = common.estimate_zoom(b)
return max(min(int(2 + z / 1.5),min_hex_cell_res), max_hex_cell_res)

parent_res = max(dynamic_h3_res(bounds) - 1, 0)

# Load and clip data
gdf = gpd.read_parquet(path)
tile = common.get_tiles(bounds, clip=True)
gdf = gdf.to_crs(4326).clip(tile)

# Early exit if empty
if len(gdf) == 0:
return pd.DataFrame(columns=["hex", "POP", "pct"])

# Hexify
con = common.duckdb_connect()
df_hex = common.gdf_to_hex(gdf, res=parent_res, add_latlng_cols=None)
con.register("df_hex", df_hex)

# Aggregate to parent hexagons and calculate percentages
# In this case we're aggregating by sum
query = f"""
WITH agg AS (
SELECT
h3_cell_to_parent(hex, {parent_res}) AS hex,
SUM(POP) AS POP
FROM df_hex
GROUP BY hex
)
SELECT
hex,
POP,
POP * 100.0 / SUM(POP) OVER () AS pct
FROM agg
ORDER BY POP DESC
"""

return con.sql(query).df()

Requirements​

  • Polygon Dataset
          GEOID  POP  ...                                           geometry
0 060376500012 82 ... POLYGON ((-118.32649 33.88282, -118.32206 33.8...
1 060376500014 315 ... POLYGON ((-118.32647 33.87978, -118.32214 33.8...
2 060376503002 145 ... POLYGON ((-118.3525 33.87249, -118.34388 33.87...
bounds: fused.types.Bounds = [-118.32649, 33.87249, -118.32206, 33.88282]
  • Field to hexagonify & Aggregation function. In this example we're summing the population: POP

Logic​

  1. Get tile & hex resolution
common = fused.load("https://github.com/fusedio/udfs/tree/f430c25/public/common/")

def dynamic_h3_res(b):
z = common.estimate_zoom(b)
return max(min(int(2 + z / 1.5),min_hex_cell_res), max_hex_cell_res)

parent_res = max(dynamic_h3_res(bounds) - 1, 0)
  1. Load data & clip to tile
gdf = gpd.read_parquet(path)
tile = common.get_tiles(bounds, clip=True)
gdf = gdf.to_crs(4326).clip(tile)
  1. Hexify
# We'll use functions from the common UDFs
df_hex = common.gdf_to_hex(gdf, res=parent_res, add_latlng_cols=None)
  1. Aggregate
query = f"""
WITH agg AS (
SELECT
h3_cell_to_parent(hex, {parent_res}) AS hex,
SUM(POP) AS POP
FROM df_hex
GROUP BY hex
)
SELECT
hex,
POP,
POP * 100.0 / SUM(POP) OVER () AS pct
FROM agg
ORDER BY POP DESC
"""

hex_df = con.sql(query).df()

Raster to Hex​

This example uses AWS's Terrain Tiles tiled GeoTiff data. Hexagons show elevation data:

Link to UDF in Fused Catalog

Code Details
@fused.udf
def udf(bounds: fused.types.Bounds = [-90.691,-21.719,-21.300,75.897], stats_type="mean", type='hex', color_scale:float=1):
import pandas as pd
import rioxarray

common = fused.load("https://github.com/fusedio/udfs/tree/b7637ee/public/common/")
# convert bounds to tile
tile = common.get_tiles(bounds, clip=True)

# 1. Initial parameters
x, y, z = tile.iloc[0][["x", "y", "z"]]
url = f"https://s3.amazonaws.com/elevation-tiles-prod/geotiff/{z}/{x}/{y}.tif"
if type=='png':
return url_to_plasma(url, min_max=(-1000,2000/color_scale**0.5), colormap='plasma')
else:

res_offset = 1 # lower makes the hex finer
h3_size = max(min(int(3 + z / 2), 12) - res_offset, 2)
print(h3_size)

# 2. Read tiff
da_tiff = rioxarray.open_rasterio(url).squeeze(drop=True).rio.reproject("EPSG:4326")
df_tiff = da_tiff.to_dataframe("data").reset_index()[["y", "x", "data"]]

# 3. Hexagonify & aggregate
df = aggregate_df_hex(
df_tiff, h3_size, latlng_cols=["y", "x"], stats_type=stats_type
)
df["elev_scale"] = int((15 - z) * 1)
df["metric"]=df["metric"]*color_scale
df = df[df["metric"] > 0]
return df



def url_to_plasma(url, min_max=None, colormap='plasma'):
common = fused.load("https://github.com/fusedio/udfs/tree/b7637ee/public/common/")

return common.arr_to_plasma(common.url_to_arr(url).squeeze(), min_max=min_max, colormap=colormap, reverse=False)


@fused.cache
def df_to_hex(df, res, latlng_cols=("lat", "lng")):
common = fused.load("https://github.com/fusedio/udfs/tree/b7637ee/public/common/")
qr = f"""
SELECT h3_latlng_to_cell({latlng_cols[0]}, {latlng_cols[1]}, {res}) AS hex, ARRAY_AGG(data) as agg_data
FROM df
group by 1
-- order by 1
"""
con = common.duckdb_connect()
return con.query(qr).df()


@fused.cache
def aggregate_df_hex(df, res, latlng_cols=("lat", "lng"), stats_type="mean"):
import pandas as pd

df = df_to_hex(df, res=res, latlng_cols=latlng_cols)
if stats_type == "sum":
fn = lambda x: pd.Series(x).sum()
elif stats_type == "mean":
fn = lambda x: pd.Series(x).mean()
else:
fn = lambda x: pd.Series(x).mean()
df["metric"] = df.agg_data.map(fn)
return df

Requirements​

                y           x  data
0 84.776183 -179.725474 -2191
1 84.776183 -179.176421 -2176
2 84.776183 -178.627369 -2176
bounds: fused.types.Bounds = [-90.691,-21.719,-21.300,75.897]

Logic​

  1. Get tile & hex resolution
x, y, z = tile.iloc[0][["x", "y", "z"]]
url = f"https://s3.amazonaws.com/elevation-tiles-prod/geotiff/{z}/{x}/{y}.tif"

res_offset = 1 # lower makes the hex finer
h3_size = max(min(int(3 + z / 2), 12) - res_offset, 2)
  1. Read array & return as dataframe
da_tiff = rioxarray.open_rasterio(url).squeeze(drop=True).rio.reproject("EPSG:4326")
df_tiff = da_tiff.to_dataframe("data").reset_index()[["y", "x", "data"]]
  1. Hexagonify & aggregate
df = df_tiff
res = h3_size
latlng_cols=("lat", "lng")

# duckdb query
qr = f"""
SELECT h3_latlng_to_cell({latlng_cols[0]}, {latlng_cols[1]}, {res}) AS hex, ARRAY_AGG(data) as agg_data
FROM df
group by 1
"""
  1. Aggregate
@fused.cache
def aggregate_df_hex(df, res, latlng_cols=("lat", "lng"), stats_type="mean"):
import pandas as pd

df = df_to_hex(df, res=res, latlng_cols=latlng_cols)
if stats_type == "sum":
fn = lambda x: pd.Series(x).sum()
elif stats_type == "mean":
fn = lambda x: pd.Series(x).mean()
else:
fn = lambda x: pd.Series(x).mean()
df["metric"] = df.agg_data.map(fn)
return df

Visualize H3​

Custom Map UDF​

We built a custom Map UDF template that takes in H3 hexagons and renders them on a map.

Using this template you simply need to pass your data & a custom config to render a custom HTML map:

Code
@fused.udf
def udf():
"""
Visualize the California census block-group hex tiles on a map using the
`hex_tile_map_template_with_tooltip_v2` template.

The template is called via its shared token (or name) and is provided with:
• tile_url_template – points to the vector-tile UDF `census_hex_all_california_bg`
• config_json – a purple colour scale for the POP attribute (0-10 000)
• tooltip_columns – columns to show when hovering (POP, GEOID, NAME)
"""
# URL that serves the vector tiles for the census hex data.
tile_url_template = (
"https://www.fused.io/server/v1/realtime-shared/"
"fsh_3jeg2MDjozUl8kXqSAxFGE/run/tiles/{z}/{x}/{y}?dtype_out_vector=json"
)

# Configuration for the H3HexagonLayer – purple colour ramp on POP (0-10 000)
config_json = r"""{
"tileLayer": {
"@@type": "TileLayer",
"maxZoom": 12
},
"hexLayer": {
"@@type": "H3HexagonLayer",
"filled": true,
"pickable": true,
"extruded": false,
"getHexagon": "@@=properties.hex",
"getFillColor": {
"@@function": "colorContinuous",
"attr": "POP",
"domain": [0, 10000],
"steps": 20,
"colors": "Purp",
"nullColor": [184,184,184]
}
}
}"""

# Columns we want to show in the tooltip when hovering over a hex.
tooltip_columns = ["POP", "GEOID", "NAME"]

return fused.run(
"UDF_Hex_Tile_Map_Template",
tile_url_template=tile_url_template,
config_json=config_json,
center_lng=-119.4179,
center_lat=36.7783,
zoom=5,
tooltip_columns=tooltip_columns,
)

In Workbench Map Viewer​