utils module¶
Utility functions for anymap-ts.
build_step_expression(column, breaks, colors)
¶
Build a MapLibre step expression for choropleth styling.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
column |
str |
Property name to style by. |
required |
breaks |
List[float] |
Break values (k+1 values for k classes). |
required |
colors |
List[str] |
List of k colors for each class. |
required |
Returns:
| Type | Description |
|---|---|
List |
MapLibre step expression as a list. |
Source code in anymap_ts/utils.py
def build_step_expression(column: str, breaks: List[float], colors: List[str]) -> List:
"""Build a MapLibre step expression for choropleth styling.
Args:
column: Property name to style by.
breaks: Break values (k+1 values for k classes).
colors: List of k colors for each class.
Returns:
MapLibre step expression as a list.
"""
# MapLibre step expression format:
# ["step", ["get", "property"], color0, break1, color1, break2, color2, ...]
expr = ["step", ["get", column], colors[0]]
# Add breaks and colors (skip the first break which is the minimum)
for i in range(1, len(breaks) - 1):
expr.append(breaks[i])
expr.append(colors[i])
# Handle the last class
if len(colors) > len(breaks) - 1:
expr.append(breaks[-1])
expr.append(colors[-1])
return expr
compute_breaks(values, classification, k, manual_breaks=None)
¶
Compute classification breaks for choropleth maps.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
values |
List[float] |
List of numeric values to classify. |
required |
classification |
str |
Classification method ('quantile', 'equal_interval', 'natural_breaks', 'manual'). |
required |
k |
int |
Number of classes. |
required |
manual_breaks |
Optional[List[float]] |
Custom break values for 'manual' classification. |
None |
Returns:
| Type | Description |
|---|---|
List[float] |
List of break values (k+1 values defining class boundaries). |
Exceptions:
| Type | Description |
|---|---|
ValueError |
If classification method is invalid or breaks are incorrect. |
Source code in anymap_ts/utils.py
def compute_breaks(
values: List[float],
classification: str,
k: int,
manual_breaks: Optional[List[float]] = None,
) -> List[float]:
"""Compute classification breaks for choropleth maps.
Args:
values: List of numeric values to classify.
classification: Classification method ('quantile', 'equal_interval',
'natural_breaks', 'manual').
k: Number of classes.
manual_breaks: Custom break values for 'manual' classification.
Returns:
List of break values (k+1 values defining class boundaries).
Raises:
ValueError: If classification method is invalid or breaks are incorrect.
"""
if classification == "manual":
if manual_breaks is None:
raise ValueError("manual_breaks required for 'manual' classification")
if len(manual_breaks) != k + 1:
raise ValueError(f"manual_breaks must have {k + 1} values for {k} classes")
return manual_breaks
sorted_values = sorted(values)
min_val = sorted_values[0]
max_val = sorted_values[-1]
if classification == "quantile":
# Equal number of features per class
breaks = [min_val]
for i in range(1, k):
idx = int(len(sorted_values) * i / k)
breaks.append(sorted_values[idx])
breaks.append(max_val)
return breaks
elif classification == "equal_interval":
# Equal value ranges
interval = (max_val - min_val) / k
breaks = [min_val + i * interval for i in range(k + 1)]
return breaks
elif classification == "natural_breaks":
# Jenks natural breaks - requires jenkspy
try:
import jenkspy
breaks = jenkspy.jenks_breaks(values, n_classes=k)
return breaks
except ImportError:
raise ImportError(
"jenkspy is required for natural_breaks classification. "
"Install with: pip install jenkspy"
)
else:
raise ValueError(
f"Unknown classification method '{classification}'. "
"Options: 'quantile', 'equal_interval', 'natural_breaks', 'manual'"
)
fetch_geojson(url)
¶
Fetch GeoJSON data from a URL.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
url |
str |
URL to fetch GeoJSON from |
required |
Returns:
| Type | Description |
|---|---|
Dict |
GeoJSON dict |
Exceptions:
| Type | Description |
|---|---|
ValueError |
If the URL cannot be fetched or parsed |
Source code in anymap_ts/utils.py
def fetch_geojson(url: str) -> Dict:
"""Fetch GeoJSON data from a URL.
Args:
url: URL to fetch GeoJSON from
Returns:
GeoJSON dict
Raises:
ValueError: If the URL cannot be fetched or parsed
"""
try:
with urlopen(url, timeout=30) as response:
charset = response.headers.get_content_charset() or "utf-8"
data = response.read().decode(charset)
return json.loads(data)
except URLError as e:
raise ValueError(f"Failed to fetch GeoJSON from URL: {e}") from e
except UnicodeDecodeError as e:
raise ValueError(f"Failed to decode response as UTF-8: {e}") from e
except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON at URL: {e}") from e
get_bounds(data)
¶
Calculate bounds from GeoJSON or GeoDataFrame.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data |
Any |
GeoJSON dict or GeoDataFrame |
required |
Returns:
| Type | Description |
|---|---|
Optional[List[float]] |
[west, south, east, north] bounds or None |
Source code in anymap_ts/utils.py
def get_bounds(data: Any) -> Optional[List[float]]:
"""Calculate bounds from GeoJSON or GeoDataFrame.
Args:
data: GeoJSON dict or GeoDataFrame
Returns:
[west, south, east, north] bounds or None
"""
if HAS_GEOPANDAS and isinstance(data, gpd.GeoDataFrame):
bounds = data.total_bounds
return [bounds[0], bounds[1], bounds[2], bounds[3]]
if isinstance(data, dict):
if HAS_SHAPELY:
return _get_geojson_bounds_shapely(data)
return _get_geojson_bounds_simple(data)
return None
get_choropleth_colors(cmap, k)
¶
Get colors for a choropleth map using matplotlib colormaps.
Uses matplotlib colormaps when available, falling back to a small set of built-in colormaps if matplotlib is not installed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cmap |
str |
Colormap name. Any matplotlib colormap is supported when matplotlib is installed. Common options include: - Sequential: 'viridis', 'plasma', 'inferno', 'magma', 'cividis', 'Blues', 'Greens', 'Reds', 'Oranges', 'Purples', 'Greys' - Diverging: 'RdBu', 'RdYlGn', 'RdYlBu', 'Spectral', 'coolwarm', 'bwr', 'seismic' - Qualitative: 'Set1', 'Set2', 'Set3', 'Paired', 'tab10', 'tab20' - Perceptually uniform: 'viridis', 'plasma', 'inferno', 'magma' |
required |
k |
int |
Number of classes/colors to generate. |
required |
Returns:
| Type | Description |
|---|---|
List[str] |
List of k hex color strings. |
Exceptions:
| Type | Description |
|---|---|
ValueError |
If colormap is not found. |
Examples:
>>> colors = get_choropleth_colors('viridis', 5)
>>> colors
['#440154', '#3b528b', '#21918c', '#5ec962', '#fde725']
Source code in anymap_ts/utils.py
def get_choropleth_colors(cmap: str, k: int) -> List[str]:
"""Get colors for a choropleth map using matplotlib colormaps.
Uses matplotlib colormaps when available, falling back to a small
set of built-in colormaps if matplotlib is not installed.
Args:
cmap: Colormap name. Any matplotlib colormap is supported when
matplotlib is installed. Common options include:
- Sequential: 'viridis', 'plasma', 'inferno', 'magma', 'cividis',
'Blues', 'Greens', 'Reds', 'Oranges', 'Purples', 'Greys'
- Diverging: 'RdBu', 'RdYlGn', 'RdYlBu', 'Spectral', 'coolwarm',
'bwr', 'seismic'
- Qualitative: 'Set1', 'Set2', 'Set3', 'Paired', 'tab10', 'tab20'
- Perceptually uniform: 'viridis', 'plasma', 'inferno', 'magma'
k: Number of classes/colors to generate.
Returns:
List of k hex color strings.
Raises:
ValueError: If colormap is not found.
Example:
>>> colors = get_choropleth_colors('viridis', 5)
>>> colors
['#440154', '#3b528b', '#21918c', '#5ec962', '#fde725']
"""
if HAS_MATPLOTLIB:
try:
# Get the colormap from matplotlib
colormap = plt.get_cmap(cmap)
# Sample k colors evenly from the colormap
colors = []
for i in range(k):
# Sample at evenly spaced points
position = i / (k - 1) if k > 1 else 0.5
rgba = colormap(position)
colors.append(_rgb_to_hex(rgba))
return colors
except ValueError:
raise ValueError(
f"Unknown colormap '{cmap}'. See matplotlib colormap documentation "
"for available options: https://matplotlib.org/stable/gallery/color/colormap_reference.html"
)
else:
# Fallback to built-in colormaps
if cmap not in _FALLBACK_COLORMAPS:
available = ", ".join(sorted(_FALLBACK_COLORMAPS.keys()))
raise ValueError(
f"Colormap '{cmap}' not available. Without matplotlib, only these "
f"colormaps are available: {available}. "
"Install matplotlib for full colormap support: pip install matplotlib"
)
full_colors = _FALLBACK_COLORMAPS[cmap]
if k <= len(full_colors):
# Sample evenly from the colormap
step = len(full_colors) / k
indices = [int(i * step) for i in range(k)]
return [full_colors[i] for i in indices]
else:
# Interpolate if we need more colors than available
# For simplicity, just repeat the last colors
colors = full_colors[:]
while len(colors) < k:
colors.append(colors[-1])
return colors[:k]
get_default_paint(layer_type)
¶
Get default paint properties for a layer type.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
layer_type |
str |
MapLibre layer type |
required |
Returns:
| Type | Description |
|---|---|
Dict[str, Any] |
Paint properties dict |
Source code in anymap_ts/utils.py
def get_default_paint(layer_type: str) -> Dict[str, Any]:
"""Get default paint properties for a layer type.
Args:
layer_type: MapLibre layer type
Returns:
Paint properties dict
"""
defaults = {
"circle": {
"circle-radius": 5,
"circle-color": "#3388ff",
"circle-opacity": 0.8,
"circle-stroke-width": 1,
"circle-stroke-color": "#ffffff",
},
"line": {
"line-color": "#3388ff",
"line-width": 2,
"line-opacity": 0.8,
},
"fill": {
"fill-color": "#3388ff",
"fill-opacity": 0.5,
"fill-outline-color": "#0000ff",
},
"fill-extrusion": {
"fill-extrusion-color": "#3388ff",
"fill-extrusion-opacity": 0.6,
"fill-extrusion-height": 100,
},
"raster": {
"raster-opacity": 1,
},
"heatmap": {
"heatmap-opacity": 0.8,
},
}
return defaults.get(layer_type, {})
infer_layer_type(geojson)
¶
Infer MapLibre layer type from GeoJSON geometry.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
geojson |
Dict |
GeoJSON dict |
required |
Returns:
| Type | Description |
|---|---|
str |
Layer type ('circle', 'line', 'fill') |
Source code in anymap_ts/utils.py
def infer_layer_type(geojson: Dict) -> str:
"""Infer MapLibre layer type from GeoJSON geometry.
Args:
geojson: GeoJSON dict
Returns:
Layer type ('circle', 'line', 'fill')
"""
geometry_type = None
if geojson.get("type") == "FeatureCollection":
features = geojson.get("features", [])
if features:
geometry_type = features[0].get("geometry", {}).get("type")
elif geojson.get("type") == "Feature":
geometry_type = geojson.get("geometry", {}).get("type")
else:
geometry_type = geojson.get("type")
type_map = {
"Point": "circle",
"MultiPoint": "circle",
"LineString": "line",
"MultiLineString": "line",
"Polygon": "fill",
"MultiPolygon": "fill",
"GeometryCollection": "fill",
}
return type_map.get(geometry_type, "circle")
to_geojson(data)
¶
Convert various data formats to GeoJSON.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data |
Any |
GeoJSON dict, GeoDataFrame, file path, or URL |
required |
Returns:
| Type | Description |
|---|---|
Dict |
GeoJSON dict |
Exceptions:
| Type | Description |
|---|---|
ValueError |
If data cannot be converted |
ImportError |
If geopandas is required but not installed |
Source code in anymap_ts/utils.py
def to_geojson(data: Any) -> Dict:
"""Convert various data formats to GeoJSON.
Args:
data: GeoJSON dict, GeoDataFrame, file path, or URL
Returns:
GeoJSON dict
Raises:
ValueError: If data cannot be converted
ImportError: If geopandas is required but not installed
"""
# Already a dict (GeoJSON)
if isinstance(data, dict):
return data
# GeoDataFrame
if HAS_GEOPANDAS and isinstance(data, gpd.GeoDataFrame):
return json.loads(data.to_json())
# File path or URL
if isinstance(data, (str, Path)):
path_str = str(data)
# If it's a URL, return as-is (will be handled by JS)
if path_str.startswith(("http://", "https://")):
return {"type": "url", "url": path_str}
# Read file with geopandas
if not HAS_GEOPANDAS:
raise ImportError(
"geopandas is required to read vector files. "
"Install with: pip install anymap-ts[vector]"
)
gdf = gpd.read_file(path_str)
return json.loads(gdf.to_json())
# Has __geo_interface__ (shapely geometry, etc.)
if hasattr(data, "__geo_interface__"):
geo = data.__geo_interface__
if geo.get("type") in (
"Point",
"LineString",
"Polygon",
"MultiPoint",
"MultiLineString",
"MultiPolygon",
"GeometryCollection",
):
return {"type": "Feature", "geometry": geo, "properties": {}}
return geo
raise ValueError(f"Cannot convert {type(data)} to GeoJSON")