Skip to content

Commit

Permalink
Merge pull request #97 from eoda-dev/feature/color-utils
Browse files Browse the repository at this point in the history
color and PMTiles utils
  • Loading branch information
crazycapivara authored Dec 14, 2024
2 parents e0ae296 + 93d1ef1 commit a3ca99f
Show file tree
Hide file tree
Showing 71 changed files with 5,053 additions and 292 deletions.
72 changes: 43 additions & 29 deletions _experimental/color_utils.py → _deprecated/color_utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# TODO: DEPRECATED: use maplibre.colors instead
from __future__ import annotations

from itertools import chain
Expand All @@ -9,26 +10,25 @@
print(e)
branca_color_brewer = None

# TODO> Move to options
CMAPS_JSON = "https://raw.githubusercontent.com/python-visualization/branca/main/branca/_schemes.json"

# TODO: Move to options
FALLBACK_COLOR = "#000000"


def color_brewer(cmap: str, n: int) -> list:
n = int(n)
if n == 2:
colors = branca_color_brewer(cmap)
return [colors[i] for i in [1, -1]]
return [colors[i] for i in [0, -1]]

return branca_color_brewer(cmap, n)


def create_categorical_color_expression(
values: Any, column_name: str, cmap: str = "viridis"
) -> list | None:
if not color_brewer:
return

unique_values = list(set(values))
) -> tuple:
unique_values = sorted(list(set(values)))
colors = color_brewer(cmap, len(unique_values))
expression = (
["match", ["get", column_name]]
Expand All @@ -39,40 +39,54 @@ def create_categorical_color_expression(
)
+ [FALLBACK_COLOR]
)
return expression
return expression, unique_values, colors


# TODO: Allow to pass colors
def create_numeric_color_expression_from_steps(
column_name: str, steps: list, cmap="viridis"
) -> list | None:
colors = color_brewer(cmap, len(steps))
# TODO: Extract this step to helper function
def create_numeric_color_expression_from_breaks(
column_name: str, breaks: list, cmap="viridis"
) -> tuple:
n = len(breaks)
colors = color_brewer(cmap, n + 1)
expression = (
["step", ["get", column_name]]
+ list(
chain.from_iterable([[color, step] for color, step in zip(colors, steps)])
chain.from_iterable(
[[color, step] for color, step in zip(colors[0:n], breaks)]
)
)
+ [FALLBACK_COLOR]
+ [colors[-1]]
)
return expression
return expression, breaks, colors


def create_numeric_color_expression(
values: Any, n: int, column_name: str, cmap: str = "viridis"
) -> tuple | None:
) -> tuple:
step = (max(values) - min(values)) / n
breaks = [min(values) + i * step for i in range(n)]
colors = color_brewer(cmap, n + 1)
# breaks = [min(values) + i * step for i in range(n)]
breaks = [min(values) + step + i * step for i in range(n - 1)]
return create_numeric_color_expression_from_breaks(column_name, breaks, cmap)

expression = (
["step", ["get", column_name]]
+ list(
chain.from_iterable(
[[color, step] for color, step in zip(colors[0:n], breaks)]
)
)
+ [colors[-1]]
)

return expression, breaks, colors
def create_numeric_color_expression_from_quantiles(
values: Any, q: list, column_name: str, cmap: str = "viridis"
) -> tuple | None:
try:
import numpy as np
except ImportError as e:
print(e)
return

breaks = np.quantile(values, q)
return create_numeric_color_expression_from_breaks(column_name, breaks, cmap)


def list_cmaps() -> list | None:
try:
import requests
except ImportError as e:
print(e)
return

return list(requests.get(CMAPS_JSON).json().keys())
199 changes: 199 additions & 0 deletions _deprecated/express.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
# DEPRECATED:
# See maplibre.__future__.express for new version

from __future__ import annotations

try:
import pandas as pd
from geopandas import GeoDataFrame, read_file
except ImportError as e:
print(e)
pd = None
GeoDataFrame = None
read_file = None

from maplibre.controls import *
from maplibre.layer import Layer, LayerType
from maplibre.map import Map, MapOptions
from maplibre.sources import GeoJSONSource
from maplibre.utils import geopandas_to_geojson

from _deprecated.color_utils import *

CRS = "EPSG:4326"
DEFAULT_COLOR = "darkred"

if pd is not None:

@pd.api.extensions.register_dataframe_accessor("maplibre")
class MapLibreAccessor(object):
def __init__(self, gdf: GeoDataFrame):
self._gdf = gdf

def to_source(self) -> GeoJSONSource:
return GeoJSONSource(data=geopandas_to_geojson(self._gdf))


class GeoJSON(object):
def __init__(
self,
data: GeoDataFrame | str,
layer_type: LayerType | str,
color_column: str = None,
cmap: str = "viridis",
n: int = None,
q: list = None,
breaks: list = None,
**kwargs,
):
if isinstance(data, str):
data = read_file(data)

if str(data.crs) != CRS:
data = data.to_crs(CRS)

self.bounds = data.total_bounds

# Create layer
layer_type = LayerType(layer_type).value
kwargs["type"] = layer_type
if "paint" not in kwargs:
kwargs["paint"] = {f"{layer_type}-color": DEFAULT_COLOR}

self._layer = Layer(**kwargs)

# Set color expression
# TODO: Extract this step to separate function
if color_column:
_breaks = None
_categories = None
if n is not None:
color_expression, _breaks, _colors = create_numeric_color_expression(
values=data[color_column], n=n, column_name=color_column, cmap=cmap
)
elif breaks is not None:
color_expression, _breaks, _colors = (
create_numeric_color_expression_from_breaks(
column_name=color_column, breaks=breaks, cmap=cmap
)
)
elif q is not None:
color_expression, _breaks, _colors = (
create_numeric_color_expression_from_quantiles(
values=data[color_column],
q=q,
column_name=color_column,
cmap=cmap,
)
)
else:
color_expression, _categories, _colors = (
create_categorical_color_expression(
values=data[color_column], column_name=color_column, cmap=cmap
)
)

self._layer.paint[f"{layer_type}-color"] = color_expression

self._layer.source = GeoJSONSource(data=geopandas_to_geojson(data))

@property
def layer(self):
return self._layer

def to_map(
self,
fit_bounds: bool = True,
tooltip: bool = True,
controls: list = [NavigationControl()],
before_id: str = None,
**kwargs,
):
map_options = MapOptions(**kwargs)
if fit_bounds:
map_options.bounds = self.bounds

m = Map(map_options)
for control in controls:
m.add_control(control)

m.add_layer(self._layer, before_id)
if tooltip:
m.add_tooltip(self._layer.id)

return m


class Circle(GeoJSON):
def __init__(
self,
data: GeoDataFrame | str,
color_column: str = None,
cmap: str = "viridis",
n: int = None,
q: list = None,
breaks: list = None,
**kwargs,
):
super().__init__(
data, LayerType.CIRCLE, color_column, cmap, n, q, breaks, **kwargs
)


class Fill(GeoJSON):
def __init__(
self,
data: GeoDataFrame | str,
color_column: str = None,
cmap: str = "viridis",
n: int = None,
q: list = None,
breaks: list = None,
fill_outline_color: str = None,
**kwargs,
):
if "paint" not in kwargs and fill_outline_color is not None:
kwargs["paint"] = {"fill-outline-color": fill_outline_color}

super().__init__(
data, LayerType.FILL, color_column, cmap, n, q, breaks, **kwargs
)


class Line(GeoJSON):
def __init__(
self,
data: GeoDataFrame | str,
color_column: str = None,
cmap: str = "viridis",
n: int = None,
q: list = None,
breaks: list = None,
**kwargs,
):
super().__init__(
data, LayerType.LINE, color_column, cmap, n, q, breaks, **kwargs
)


class FillExtrusion(GeoJSON):
def __init__(
self,
data: GeoDataFrame | str,
color_column: str = None,
cmap: str = "viridis",
n: int = None,
q: list = None,
breaks: list = None,
fill_extrusion_height: Any = None,
# fill_extrusion_base: Any = None
**kwargs,
):
super().__init__(
data, LayerType.FILL_EXTRUSION, color_column, cmap, n, q, breaks, **kwargs
)
if fill_extrusion_height:
if isinstance(fill_extrusion_height, str):
fill_extrusion_height = ["get", fill_extrusion_height]

self._layer.paint["fill-extrusion-height"] = fill_extrusion_height
Loading

0 comments on commit a3ca99f

Please sign in to comment.