일단 커밋. 오랫동안 커밋을 안해서 꼬였다.

리팩토리 중.
This commit is contained in:
2025-11-15 15:59:49 +09:00
parent 5a47b792d6
commit d79c10b975
12909 changed files with 2070539 additions and 285 deletions

View File

@@ -0,0 +1,13 @@
import branca.colormap as colormap
import branca.element as element
try:
from ._version import __version__
except ImportError:
__version__ = "unknown"
__all__ = [
"colormap",
"element",
]

View File

@@ -0,0 +1 @@
{"indigo": "#4B0082", "gold": "#FFD700", "hotpink": "#FF69B4", "firebrick": "#B22222", "indianred": "#CD5C5C", "sage": "#87AE73", "yellow": "#FFFF00", "mistyrose": "#FFE4E1", "darkolivegreen": "#556B2F", "olive": "#808000", "darkseagreen": "#8FBC8F", "pink": "#FFC0CB", "tomato": "#FF6347", "lightcoral": "#F08080", "orangered": "#FF4500", "navajowhite": "#FFDEAD", "lime": "#00FF00", "palegreen": "#98FB98", "greenyellow": "#ADFF2F", "burlywood": "#DEB887", "seashell": "#FFF5EE", "mediumspringgreen": "#00FA9A", "fuchsia": "#FF00FF", "papayawhip": "#FFEFD5", "blanchedalmond": "#FFEBCD", "chartreuse": "#7FFF00", "dimgray": "#696969", "black": "#000000", "peachpuff": "#FFDAB9", "springgreen": "#00FF7F", "aquamarine": "#7FFFD4", "white": "#FFFFFF", "b": "#0000FF", "orange": "#FFA500", "lightsalmon": "#FFA07A", "darkslategray": "#2F4F4F", "brown": "#A52A2A", "ivory": "#FFFFF0", "dodgerblue": "#1E90FF", "peru": "#CD853F", "lawngreen": "#7CFC00", "chocolate": "#D2691E", "crimson": "#DC143C", "forestgreen": "#228B22", "slateblue": "#6A5ACD", "lightseagreen": "#20B2AA", "cyan": "#00FFFF", "mintcream": "#F5FFFA", "silver": "#C0C0C0", "antiquewhite": "#FAEBD7", "mediumorchid": "#BA55D3", "skyblue": "#87CEEB", "gray": "#808080", "darkturquoise": "#00CED1", "goldenrod": "#DAA520", "darkgreen": "#006400", "floralwhite": "#FFFAF0", "darkviolet": "#9400D3", "darkgray": "#A9A9A9", "moccasin": "#FFE4B5", "saddlebrown": "#8B4513", "darkslateblue": "#483D8B", "lightskyblue": "#87CEFA", "lightpink": "#FFB6C1", "mediumvioletred": "#C71585", "r": "#FF0000", "red": "#FF0000", "deeppink": "#FF1493", "limegreen": "#32CD32", "k": "#000000", "darkmagenta": "#8B008B", "palegoldenrod": "#EEE8AA", "plum": "#DDA0DD", "turquoise": "#40E0D0", "m": "#FF00FF", "lightgoldenrodyellow": "#FAFAD2", "darkgoldenrod": "#B8860B", "lavender": "#E6E6FA", "maroon": "#800000", "yellowgreen": "#9ACD32", "sandybrown": "#FAA460", "thistle": "#D8BFD8", "violet": "#EE82EE", "navy": "#000080", "magenta": "#FF00FF", "tan": "#D2B48C", "rosybrown": "#BC8F8F", "olivedrab": "#6B8E23", "blue": "#0000FF", "lightblue": "#ADD8E6", "ghostwhite": "#F8F8FF", "honeydew": "#F0FFF0", "cornflowerblue": "#6495ED", "linen": "#FAF0E6", "darkblue": "#00008B", "powderblue": "#B0E0E6", "seagreen": "#2E8B57", "darkkhaki": "#BDB76B", "snow": "#FFFAFA", "sienna": "#A0522D", "mediumblue": "#0000CD", "royalblue": "#4169E1", "lightcyan": "#E0FFFF", "green": "#008000", "mediumpurple": "#9370DB", "midnightblue": "#191970", "cornsilk": "#FFF8DC", "paleturquoise": "#AFEEEE", "bisque": "#FFE4C4", "slategray": "#708090", "darkcyan": "#008B8B", "khaki": "#F0E68C", "wheat": "#F5DEB3", "teal": "#008080", "darkorchid": "#9932CC", "deepskyblue": "#00BFFF", "salmon": "#FA8072", "y": "#FFFF00", "darkred": "#8B0000", "steelblue": "#4682B4", "g": "#008000", "palevioletred": "#DB7093", "lightslategray": "#778899", "aliceblue": "#F0F8FF", "lightgreen": "#90EE90", "orchid": "#DA70D6", "gainsboro": "#DCDCDC", "mediumseagreen": "#3CB371", "lightgray": "#D3D3D3", "c": "#00FFFF", "mediumturquoise": "#48D1CC", "darksage": "#598556", "lemonchiffon": "#FFFACD", "cadetblue": "#5F9EA0", "lightyellow": "#FFFFE0", "lavenderblush": "#FFF0F5", "coral": "#FF7F50", "purple": "#800080", "aqua": "#00FFFF", "lightsage": "#BCECAC", "whitesmoke": "#F5F5F5", "mediumslateblue": "#7B68EE", "darkorange": "#FF8C00", "mediumaquamarine": "#66CDAA", "darksalmon": "#E9967A", "beige": "#F5F5DC", "w": "#FFFFFF", "blueviolet": "#8A2BE2", "azure": "#F0FFFF", "lightsteelblue": "#B0C4DE", "oldlace": "#FDF5E6"}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
__version__ = "0.8.2"

View File

@@ -0,0 +1,674 @@
"""
Colormap
--------
Utility module for dealing with colormaps.
"""
import json
import math
import os
from typing import Dict, List, Optional, Sequence, Tuple, Union
from jinja2 import Template
from branca.element import ENV, Figure, JavascriptLink, MacroElement
from branca.utilities import legend_scaler
rootpath: str = os.path.abspath(os.path.dirname(__file__))
with open(os.path.join(rootpath, "_cnames.json")) as f:
_cnames: Dict[str, str] = json.loads(f.read())
with open(os.path.join(rootpath, "_schemes.json")) as f:
_schemes: Dict[str, List[str]] = json.loads(f.read())
TypeRGBInts = Tuple[int, int, int]
TypeRGBFloats = Tuple[float, float, float]
TypeRGBAInts = Tuple[int, int, int, int]
TypeRGBAFloats = Tuple[float, float, float, float]
TypeAnyColorType = Union[TypeRGBInts, TypeRGBFloats, TypeRGBAInts, TypeRGBAFloats, str]
def _is_hex(x: str) -> bool:
return x.startswith("#") and len(x) == 7
def _parse_hex(color_code: str) -> TypeRGBAFloats:
return (
_color_int_to_float(int(color_code[1:3], 16)),
_color_int_to_float(int(color_code[3:5], 16)),
_color_int_to_float(int(color_code[5:7], 16)),
1.0,
)
def _color_int_to_float(x: int) -> float:
"""Convert an integer between 0 and 255 to a float between 0. and 1.0"""
return x / 255.0
def _color_float_to_int(x: float) -> int:
"""Convert a float between 0. and 1.0 to an integer between 0 and 255"""
return int(x * 255.9999)
def _parse_color(x: Union[tuple, list, str]) -> TypeRGBAFloats:
if isinstance(x, (tuple, list)):
return tuple(tuple(x) + (1.0,))[:4] # type: ignore
elif isinstance(x, str) and _is_hex(x):
return _parse_hex(x)
elif isinstance(x, str):
cname = _cnames.get(x.lower(), None)
if cname is None:
raise ValueError(f"Unknown color {cname!r}.")
return _parse_hex(cname)
else:
raise ValueError(f"Unrecognized color code {x!r}")
def _base(x: float) -> float:
if x > 0:
base = pow(10, math.floor(math.log10(x)))
return round(x / base) * base
else:
return 0
class ColorMap(MacroElement):
"""A generic class for creating colormaps.
Parameters
----------
vmin: float
The left bound of the color scale.
vmax: float
The right bound of the color scale.
caption: str
A caption to draw with the colormap.
text_color: str, default "black"
The color for the text.
max_labels : int, default 10
Maximum number of legend tick labels
"""
_template: Template = ENV.get_template("color_scale.js")
def __init__(
self,
vmin: float = 0.0,
vmax: float = 1.0,
caption: str = "",
text_color: str = "black",
max_labels: int = 10,
):
super().__init__()
self._name = "ColorMap"
self.vmin = vmin
self.vmax = vmax
self.caption = caption
self.text_color = text_color
self.index: List[float] = [vmin, vmax]
self.max_labels = max_labels
self.tick_labels: Optional[Sequence[Union[float, str]]] = None
self.width = 450
self.height = 40
def render(self, **kwargs):
"""Renders the HTML representation of the element."""
self.color_domain = [
float(self.vmin + (self.vmax - self.vmin) * k / 499.0) for k in range(500)
]
self.color_range = [self.__call__(x) for x in self.color_domain]
# sanitize possible numpy floats to native python floats
self.index = [float(i) for i in self.index]
if self.tick_labels is None:
self.tick_labels = legend_scaler(self.index, self.max_labels)
super().render(**kwargs)
figure = self.get_root()
assert isinstance(figure, Figure), (
"You cannot render this Element " "if it is not in a Figure."
)
figure.header.add_child(
JavascriptLink("https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"),
name="d3",
) # noqa
def rgba_floats_tuple(self, x: float) -> TypeRGBAFloats:
"""
This class has to be implemented for each class inheriting from
Colormap. This has to be a function of the form float ->
(float, float, float, float) describing for each input float x,
the output color in RGBA format;
Each output value being between 0 and 1.
"""
raise NotImplementedError
def rgba_bytes_tuple(self, x: float) -> TypeRGBAInts:
"""Provides the color corresponding to value `x` in the
form of a tuple (R,G,B,A) with int values between 0 and 255.
"""
return tuple(_color_float_to_int(u) for u in self.rgba_floats_tuple(x)) # type: ignore
def rgb_bytes_tuple(self, x: float) -> TypeRGBInts:
"""Provides the color corresponding to value `x` in the
form of a tuple (R,G,B) with int values between 0 and 255.
"""
return self.rgba_bytes_tuple(x)[:3]
def rgb_hex_str(self, x: float) -> str:
"""Provides the color corresponding to value `x` in the
form of a string of hexadecimal values "#RRGGBB".
"""
return "#%02x%02x%02x" % self.rgb_bytes_tuple(x)
def rgba_hex_str(self, x: float) -> str:
"""Provides the color corresponding to value `x` in the
form of a string of hexadecimal values "#RRGGBBAA".
"""
return "#%02x%02x%02x%02x" % self.rgba_bytes_tuple(x)
def __call__(self, x: float) -> str:
"""Provides the color corresponding to value `x` in the
form of a string of hexadecimal values "#RRGGBBAA".
"""
return self.rgba_hex_str(x)
def _repr_html_(self) -> str:
"""Display the colormap in a Jupyter Notebook.
Does not support all the class arguments.
"""
nb_ticks = 7
delta_x = math.floor(self.width / (nb_ticks - 1))
x_ticks = [(i) * delta_x for i in range(0, nb_ticks)]
delta_val = delta_x * (self.vmax - self.vmin) / self.width
val_ticks = [round(self.vmin + (i) * delta_val, 1) for i in range(0, nb_ticks)]
return (
f'<svg height="40" width="{self.width}">'
+ "".join(
[
(
'<line x1="{i}" y1="15" x2="{i}" '
'y2="27" style="stroke:{color};stroke-width:2;" />'
).format(
i=i * 1,
color=self.rgba_hex_str(
self.vmin + (self.vmax - self.vmin) * i / (self.width - 1),
),
)
for i in range(self.width)
],
)
+ (
'<text x="0" y="38" style="text-anchor:start; font-size:11px;'
' font:Arial; fill:{}">{}</text>'
).format(
self.text_color,
self.vmin,
)
+ "".join(
[
(
'<text x="{}" y="38"; style="text-anchor:middle; font-size:11px;'
' font:Arial; fill:{}">{}</text>'
).format(x_ticks[i], self.text_color, val_ticks[i])
for i in range(1, nb_ticks - 1)
],
)
+ (
'<text x="{}" y="38" style="text-anchor:end; font-size:11px;'
' font:Arial; fill:{}">{}</text>'
).format(
self.width,
self.text_color,
self.vmax,
)
+ '<text x="0" y="12" style="font-size:11px; font:Arial; fill:{}">{}</text>'.format(
self.text_color,
self.caption,
)
+ "</svg>"
)
class LinearColormap(ColorMap):
"""Creates a ColorMap based on linear interpolation of a set of colors
over a given index.
Parameters
----------
colors : list-like object with at least two colors.
The set of colors to be used for interpolation.
Colors can be provided in the form:
* tuples of RGBA ints between 0 and 255 (e.g: `(255, 255, 0)` or
`(255, 255, 0, 255)`)
* tuples of RGBA floats between 0. and 1. (e.g: `(1.,1.,0.)` or
`(1., 1., 0., 1.)`)
* HTML-like string (e.g: `"#ffff00`)
* a color name or shortcut (e.g: `"y"` or `"yellow"`)
index : list of floats, default None
The values corresponding to each color.
It has to be sorted, and have the same length as `colors`.
If None, a regular grid between `vmin` and `vmax` is created.
vmin : float, default 0.
The minimal value for the colormap.
Values lower than `vmin` will be bound directly to `colors[0]`.
vmax : float, default 1.
The maximal value for the colormap.
Values higher than `vmax` will be bound directly to `colors[-1]`.
caption: str
A caption to draw with the colormap.
text_color: str, default "black"
The color for the text.
max_labels : int, default 10
Maximum number of legend tick labels
tick_labels: list of floats, default None
If given, used as the positions of ticks."""
def __init__(
self,
colors: Sequence[TypeAnyColorType],
index: Optional[Sequence[float]] = None,
vmin: float = 0.0,
vmax: float = 1.0,
caption: str = "",
text_color: str = "black",
max_labels: int = 10,
tick_labels: Optional[Sequence[float]] = None,
):
super().__init__(
vmin=vmin,
vmax=vmax,
caption=caption,
text_color=text_color,
max_labels=max_labels,
)
self.tick_labels: Optional[Sequence[float]] = tick_labels
n = len(colors)
if n < 2:
raise ValueError("You must provide at least 2 colors.")
if index is None:
self.index = [vmin + (vmax - vmin) * i * 1.0 / (n - 1) for i in range(n)]
else:
self.index = list(index)
self.colors: List[TypeRGBAFloats] = [_parse_color(x) for x in colors]
def rgba_floats_tuple(self, x: float) -> TypeRGBAFloats:
"""Provides the color corresponding to value `x` in the
form of a tuple (R,G,B,A) with float values between 0. and 1.
"""
if x <= self.index[0]:
return self.colors[0]
if x >= self.index[-1]:
return self.colors[-1]
i = len([u for u in self.index if u < x]) # 0 < i < n.
if self.index[i - 1] < self.index[i]:
p = (x - self.index[i - 1]) * 1.0 / (self.index[i] - self.index[i - 1])
elif self.index[i - 1] == self.index[i]:
p = 1.0
else:
raise ValueError("Thresholds are not sorted.")
return tuple( # type: ignore
(1.0 - p) * self.colors[i - 1][j] + p * self.colors[i][j] for j in range(4)
)
def to_step(
self,
n: Optional[int] = None,
index: Optional[Sequence[float]] = None,
data: Optional[Sequence[float]] = None,
method: str = "linear",
quantiles: Optional[Sequence[float]] = None,
round_method: Optional[str] = None,
max_labels: int = 10,
) -> "StepColormap":
"""Splits the LinearColormap into a StepColormap.
Parameters
----------
n : int, default None
The number of expected colors in the output StepColormap.
This will be ignored if `index` is provided.
index : list of floats, default None
The values corresponding to each color bounds.
It has to be sorted.
If None, a regular grid between `vmin` and `vmax` is created.
data : list of floats, default None
A sample of data to adapt the color map to.
method : str, default 'linear'
The method used to create data-based colormap.
It can be 'linear' for linear scale, 'log' for logarithmic,
or 'quant' for data's quantile-based scale.
quantiles : list of floats, default None
Alternatively, you can provide explicitly the quantiles you
want to use in the scale.
round_method : str, default None
The method used to round thresholds.
* If 'int', all values will be rounded to the nearest integer.
* If 'log10', all values will be rounded to the nearest
order-of-magnitude integer. For example, 2100 is rounded to
2000, 2790 to 3000.
max_labels : int, default 10
Maximum number of legend tick labels
Returns
-------
A StepColormap with `n=len(index)-1` colors.
Examples:
>> lc.to_step(n=12)
>> lc.to_step(index=[0, 2, 4, 6, 8, 10])
>> lc.to_step(data=some_list, n=12)
>> lc.to_step(data=some_list, n=12, method='linear')
>> lc.to_step(data=some_list, n=12, method='log')
>> lc.to_step(data=some_list, n=12, method='quantiles')
>> lc.to_step(data=some_list, quantiles=[0, 0.3, 0.7, 1])
>> lc.to_step(data=some_list, quantiles=[0, 0.3, 0.7, 1],
... round_method='log10')
"""
msg = "You must specify either `index` or `n`"
if index is None:
if data is None:
if n is None:
raise ValueError(msg)
else:
index = [
self.vmin + (self.vmax - self.vmin) * i * 1.0 / n
for i in range(1 + n)
]
scaled_cm = self
else:
max_ = max(data)
min_ = min(data)
scaled_cm = self.scale(vmin=min_, vmax=max_)
method = "quantiles" if quantiles is not None else method
if method.lower().startswith("lin"):
if n is None:
raise ValueError(msg)
index = [min_ + i * (max_ - min_) * 1.0 / n for i in range(1 + n)]
elif method.lower().startswith("log"):
if n is None:
raise ValueError(msg)
if min_ <= 0:
msg = "Log-scale works only with strictly " "positive values."
raise ValueError(msg)
index = [
math.exp(
math.log(min_)
+ i * (math.log(max_) - math.log(min_)) * 1.0 / n,
)
for i in range(1 + n)
]
elif method.lower().startswith("quant"):
if quantiles is None:
if n is None:
msg = (
"You must specify either `index`, `n` or" "`quantiles`."
)
raise ValueError(msg)
else:
quantiles = [i * 1.0 / n for i in range(1 + n)]
p = len(data) - 1
s = sorted(data)
index = [
s[int(q * p)] * (1.0 - (q * p) % 1)
+ s[min(int(q * p) + 1, p)] * ((q * p) % 1)
for q in quantiles
]
else:
raise ValueError(f"Unknown method {method}")
else:
scaled_cm = self.scale(vmin=min(index), vmax=max(index))
n = len(index) - 1
if round_method == "int":
index = [round(x) for x in index]
if round_method == "log10":
index = [_base(x) for x in index]
colors = [
scaled_cm.rgba_floats_tuple(
index[i] * (1.0 - i / (n - 1.0)) + index[i + 1] * i / (n - 1.0),
)
for i in range(n)
]
caption = self.caption
text_color = self.text_color
return StepColormap(
colors,
index=index,
vmin=index[0],
vmax=index[-1],
caption=caption,
text_color=text_color,
max_labels=max_labels,
tick_labels=self.tick_labels,
)
def scale(
self,
vmin: float = 0.0,
vmax: float = 1.0,
max_labels: int = 10,
) -> "LinearColormap":
"""Transforms the colorscale so that the minimal and maximal values
fit the given parameters.
"""
return LinearColormap(
self.colors,
index=[
vmin + (vmax - vmin) * (x - self.vmin) * 1.0 / (self.vmax - self.vmin)
for x in self.index
], # noqa
vmin=vmin,
vmax=vmax,
caption=self.caption,
text_color=self.text_color,
max_labels=max_labels,
)
class StepColormap(ColorMap):
"""Creates a ColorMap based on linear interpolation of a set of colors
over a given index.
Parameters
----------
colors : list-like object
The set of colors to be used for interpolation.
Colors can be provided in the form:
* tuples of int between 0 and 255 (e.g: `(255,255,0)` or
`(255, 255, 0, 255)`)
* tuples of floats between 0. and 1. (e.g: `(1.,1.,0.)` or
`(1., 1., 0., 1.)`)
* HTML-like string (e.g: `"#ffff00`)
* a color name or shortcut (e.g: `"y"` or `"yellow"`)
index : list of floats, default None
The bounds of the colors. The lower value is inclusive,
the upper value is exclusive.
It has to be sorted, and have the same length as `colors`.
If None, a regular grid between `vmin` and `vmax` is created.
vmin : float, default 0.
The minimal value for the colormap.
Values lower than `vmin` will be bound directly to `colors[0]`.
vmax : float, default 1.
The maximal value for the colormap.
Values higher than `vmax` will be bound directly to `colors[-1]`.
caption: str
A caption to draw with the colormap.
text_color: str, default "black"
The color for the text.
max_labels : int, default 10
Maximum number of legend tick labels
tick_labels: list of floats, default None
If given, used as the positions of ticks.
"""
def __init__(
self,
colors: Sequence[TypeAnyColorType],
index: Optional[Sequence[float]] = None,
vmin: float = 0.0,
vmax: float = 1.0,
caption: str = "",
text_color: str = "black",
max_labels: int = 10,
tick_labels: Optional[Sequence[float]] = None,
):
super().__init__(
vmin=vmin,
vmax=vmax,
caption=caption,
text_color=text_color,
max_labels=max_labels,
)
self.tick_labels = tick_labels
n = len(colors)
if n < 1:
raise ValueError("You must provide at least 1 colors.")
if index is None:
self.index = [vmin + (vmax - vmin) * i * 1.0 / n for i in range(n + 1)]
else:
self.index = list(index)
self.colors: List[TypeRGBAFloats] = [_parse_color(x) for x in colors]
def rgba_floats_tuple(self, x: float) -> TypeRGBAFloats:
"""
Provides the color corresponding to value `x` in the
form of a tuple (R,G,B,A) with float values between 0. and 1.
"""
if x <= self.index[0]:
return self.colors[0]
if x >= self.index[-1]:
return self.colors[-1]
i = len([u for u in self.index if u <= x]) # 0 < i < n.
return self.colors[i - 1]
def to_linear(
self,
index: Optional[Sequence[float]] = None,
max_labels: int = 10,
) -> LinearColormap:
"""
Transforms the StepColormap into a LinearColormap.
Parameters
----------
index : list of floats, default None
The values corresponding to each color in the output colormap.
It has to be sorted.
If None, a regular grid between `vmin` and `vmax` is created.
max_labels : int, default 10
Maximum number of legend tick labels
"""
if index is None:
n = len(self.index) - 1
index = [
self.index[i] * (1.0 - i / (n - 1.0))
+ self.index[i + 1] * i / (n - 1.0)
for i in range(n)
]
colors = [self.rgba_floats_tuple(x) for x in index]
return LinearColormap(
colors,
index=index,
vmin=self.vmin,
vmax=self.vmax,
caption=self.caption,
text_color=self.text_color,
max_labels=max_labels,
)
def scale(
self,
vmin: float = 0.0,
vmax: float = 1.0,
max_labels: int = 10,
) -> "StepColormap":
"""Transforms the colorscale so that the minimal and maximal values
fit the given parameters.
"""
return StepColormap(
self.colors,
index=[
vmin + (vmax - vmin) * (x - self.vmin) * 1.0 / (self.vmax - self.vmin)
for x in self.index
], # noqa
vmin=vmin,
vmax=vmax,
caption=self.caption,
text_color=self.text_color,
max_labels=max_labels,
)
class _LinearColormaps:
"""A class for hosting the list of built-in linear colormaps."""
def __init__(self):
self._schemes = _schemes.copy()
self._colormaps = {key: LinearColormap(val) for key, val in _schemes.items()}
for key, val in _schemes.items():
setattr(self, key, LinearColormap(val))
def _repr_html_(self) -> str:
return Template(
"""
<table>
{% for key,val in this._colormaps.items() %}
<tr><td>{{key}}</td><td>{{val._repr_html_()}}</td></tr>
{% endfor %}</table>
""",
).render(this=self)
linear = _LinearColormaps()
class _StepColormaps:
"""A class for hosting the list of built-in step colormaps."""
def __init__(self):
self._schemes = _schemes.copy()
self._colormaps = {key: StepColormap(val) for key, val in _schemes.items()}
for key, val in _schemes.items():
setattr(self, key, StepColormap(val))
def _repr_html_(self) -> str:
return Template(
"""
<table>
{% for key,val in this._colormaps.items() %}
<tr><td>{{key}}</td><td>{{val._repr_html_()}}</td></tr>
{% endfor %}</table>
""",
).render(this=self)
step = _StepColormaps()

View File

@@ -0,0 +1,740 @@
"""
Element
-------
A generic class for creating Elements.
"""
import base64
import json
import warnings
from binascii import hexlify
from collections import OrderedDict
from html import escape
from os import urandom
from pathlib import Path
from typing import BinaryIO, List, Optional, Tuple, Type, Union
from urllib.request import urlopen
from jinja2 import Environment, PackageLoader, Template
from .utilities import TypeParseSize, _camelify, _parse_size, none_max, none_min
ENV = Environment(loader=PackageLoader("branca", "templates"))
class Element:
"""Basic Element object that does nothing.
Other Elements may inherit from this one.
Parameters
----------
template : str, default None
A jinaj2-compatible template string for rendering the element.
If None, template will be:
.. code-block:: jinja
{% for name, element in this._children.items() %}
{{element.render(**kwargs)}}
{% endfor %}
so that all the element's children are rendered.
template_name : str, default None
If no template is provided, you can also provide a filename.
"""
_template: Template = Template(
"{% for name, element in this._children.items() %}\n"
" {{element.render(**kwargs)}}"
"{% endfor %}",
)
def __init__(
self,
template: Optional[str] = None,
template_name: Optional[str] = None,
):
self._name: str = "Element"
self._id: str = self._generate_id()
self._children: OrderedDict[str, Element] = OrderedDict()
self._parent: Optional[Element] = None
self._template_str: Optional[str] = template
self._template_name: Optional[str] = template_name
if template is not None:
self._template = Template(template)
elif template_name is not None:
self._template = ENV.get_template(template_name)
@classmethod
def _generate_id(cls) -> str:
return hexlify(urandom(16)).decode()
def __getstate__(self) -> dict:
"""Modify object state when pickling the object.
jinja2 Templates cannot be pickled, so remove the instance attribute
if it exists. It will be added back when unpickling (see __setstate__).
"""
state: dict = self.__dict__.copy()
state.pop("_template", None)
return state
def __setstate__(self, state: dict):
"""Re-add _template instance attribute when unpickling"""
if state["_template_str"] is not None:
state["_template"] = Template(state["_template_str"])
elif state["_template_name"] is not None:
state["_template"] = ENV.get_template(state["_template_name"])
self.__dict__.update(state)
def get_name(self) -> str:
"""Returns a string representation of the object.
This string has to be unique and to be a python and
javascript-compatible
variable name.
"""
return _camelify(self._name) + "_" + self._id
def _get_self_bounds(self) -> List[List[Optional[float]]]:
"""Computes the bounds of the object itself (not including it's children)
in the form [[lat_min, lon_min], [lat_max, lon_max]]
"""
return [[None, None], [None, None]]
def get_bounds(self) -> List[List[Optional[float]]]:
"""Computes the bounds of the object and all it's children
in the form [[lat_min, lon_min], [lat_max, lon_max]].
"""
bounds = self._get_self_bounds()
for child in self._children.values():
child_bounds = child.get_bounds()
bounds = [
[
none_min(bounds[0][0], child_bounds[0][0]),
none_min(bounds[0][1], child_bounds[0][1]),
],
[
none_max(bounds[1][0], child_bounds[1][0]),
none_max(bounds[1][1], child_bounds[1][1]),
],
]
return bounds
def add_children(
self,
child: "Element",
name: Optional[str] = None,
index: Optional[int] = None,
) -> "Element":
"""Add a child."""
warnings.warn(
"Method `add_children` is deprecated. Please use `add_child` instead.",
FutureWarning,
stacklevel=2,
)
return self.add_child(child, name=name, index=index)
def add_child(
self,
child: "Element",
name: Optional[str] = None,
index: Optional[int] = None,
) -> "Element":
"""Add a child."""
if name is None:
name = child.get_name()
if index is None:
self._children[name] = child
else:
items = [item for item in self._children.items() if item[0] != name]
items.insert(int(index), (name, child))
self._children = OrderedDict(items)
child._parent = self
return self
def add_to(
self,
parent: "Element",
name: Optional[str] = None,
index: Optional[int] = None,
) -> "Element":
"""Add element to a parent."""
parent.add_child(self, name=name, index=index)
return self
def to_dict(
self,
depth: int = -1,
ordered: bool = True,
**kwargs,
) -> Union[dict, OrderedDict]:
"""Returns a dict representation of the object."""
dict_fun: Type[Union[dict, OrderedDict]]
if ordered:
dict_fun = OrderedDict
else:
dict_fun = dict
out = dict_fun()
out["name"] = self._name
out["id"] = self._id
if depth != 0:
out["children"] = dict_fun(
[
(name, child.to_dict(depth=depth - 1))
for name, child in self._children.items()
],
)
return out
def to_json(self, depth: int = -1, **kwargs) -> str:
"""Returns a JSON representation of the object."""
return json.dumps(self.to_dict(depth=depth, ordered=True), **kwargs)
def get_root(self) -> "Element":
"""Returns the root of the elements tree."""
if self._parent is None:
return self
else:
return self._parent.get_root()
def render(self, **kwargs) -> str:
"""Renders the HTML representation of the element."""
return self._template.render(this=self, kwargs=kwargs)
def save(
self,
outfile: Union[str, bytes, Path, BinaryIO],
close_file: bool = True,
**kwargs,
):
"""Saves an Element into a file.
Parameters
----------
outfile : str or file object
The file (or filename) where you want to output the html.
close_file : bool, default True
Whether the file has to be closed after write.
"""
fid: BinaryIO
if isinstance(outfile, (str, bytes, Path)):
fid = open(outfile, "wb")
else:
fid = outfile
root = self.get_root()
html = root.render(**kwargs)
fid.write(html.encode("utf8"))
if close_file:
fid.close()
class Link(Element):
"""An abstract class for embedding a link in the HTML."""
def __init__(self, url: str, download: bool = False):
super().__init__()
self.url = url
self.code: Optional[bytes] = None
if download:
self.get_code()
def get_code(self) -> bytes:
"""Opens the link and returns the response's content."""
if self.code is None:
self.code = urlopen(self.url).read()
return self.code
def to_dict(
self,
depth: int = -1,
ordered: bool = True,
**kwargs,
) -> Union[dict, OrderedDict]:
"""Returns a dict representation of the object."""
out = super().to_dict(depth=depth, ordered=ordered, **kwargs)
out["url"] = self.url
return out
class JavascriptLink(Link):
"""Create a JavascriptLink object based on a url.
Parameters
----------
url : str
The url to be linked
download : bool, default False
Whether the target document shall be loaded right now.
"""
_template = Template(
'{% if kwargs.get("embedded",False) %}'
"<script>{{this.get_code()}}</script>"
"{% else %}"
'<script src="{{this.url}}"></script>'
"{% endif %}",
)
def __init__(self, url: str, download: bool = False):
super().__init__(url=url, download=download)
self._name = "JavascriptLink"
class CssLink(Link):
"""Create a CssLink object based on a url.
Parameters
----------
url : str
The url to be linked
download : bool, default False
Whether the target document shall be loaded right now.
"""
_template = Template(
'{% if kwargs.get("embedded",False) %}'
"<style>{{this.get_code()}}</style>"
"{% else %}"
'<link rel="stylesheet" href="{{this.url}}"/>'
"{% endif %}",
)
def __init__(self, url: str, download: bool = False):
super().__init__(url=url, download=download)
self._name = "CssLink"
class Figure(Element):
"""Create a Figure object, to plot things into it.
Parameters
----------
width : str, default "100%"
The width of the Figure.
It may be a percentage or pixel value (like "300px").
height : str, default None
The height of the Figure.
It may be a percentage or a pixel value (like "300px").
ratio : str, default "60%"
A percentage defining the aspect ratio of the Figure.
It will be ignored if height is not None.
title : str, default None
Figure title.
figsize : tuple of two int, default None
If you're a matplotlib addict, you can overwrite width and
height. Values will be converted into pixels in using 60 dpi.
For example figsize=(10, 5) will result in
width="600px", height="300px".
"""
_template = Template(
"<!DOCTYPE html>\n"
"<html>\n"
"<head>\n"
"{% if this.title %}<title>{{this.title}}</title>{% endif %}"
" {{this.header.render(**kwargs)}}\n"
"</head>\n"
"<body>\n"
" {{this.html.render(**kwargs)}}\n"
"</body>\n"
"<script>\n"
" {{this.script.render(**kwargs)}}\n"
"</script>\n"
"</html>\n",
)
def __init__(
self,
width: str = "100%",
height: Optional[str] = None,
ratio: str = "60%",
title: Optional[str] = None,
figsize: Optional[Tuple[int, int]] = None,
):
super().__init__()
self._name = "Figure"
self.header = Element()
self.html = Element()
self.script = Element()
self.header._parent = self
self.html._parent = self
self.script._parent = self
self.width = width
self.height = height
self.ratio = ratio
self.title = title
if figsize is not None:
self.width = str(60 * figsize[0]) + "px"
self.height = str(60 * figsize[1]) + "px"
# Create the meta tag.
self.header.add_child(
Element(
'<meta http-equiv="content-type" content="text/html; charset=UTF-8" />',
), # noqa
name="meta_http",
)
def to_dict(
self,
depth: int = -1,
ordered: bool = True,
**kwargs,
) -> Union[dict, OrderedDict]:
"""Returns a dict representation of the object."""
out = super().to_dict(depth=depth, **kwargs)
out["header"] = self.header.to_dict(depth=depth - 1, **kwargs)
out["html"] = self.html.to_dict(depth=depth - 1, **kwargs)
out["script"] = self.script.to_dict(depth=depth - 1, **kwargs)
return out
def get_root(self) -> "Figure":
"""Returns the root of the elements tree."""
return self
def render(self, **kwargs) -> str:
"""Renders the HTML representation of the element."""
for name, child in self._children.items():
child.render(**kwargs)
return self._template.render(this=self, kwargs=kwargs)
def _repr_html_(self, **kwargs) -> str:
"""Displays the Figure in a Jupyter notebook."""
html = escape(self.render(**kwargs))
if self.height is None:
iframe = (
'<div style="width:{width};">'
'<div style="position:relative;width:100%;height:0;padding-bottom:{ratio};">' # noqa
'<span style="color:#565656">Make this Notebook Trusted to load map: File -> Trust Notebook</span>' # noqa
'<iframe srcdoc="{html}" style="position:absolute;width:100%;height:100%;left:0;top:0;' # noqa
'border:none !important;" '
"allowfullscreen webkitallowfullscreen mozallowfullscreen>"
"</iframe>"
"</div></div>"
).format(html=html, width=self.width, ratio=self.ratio)
else:
iframe = (
'<iframe srcdoc="{html}" width="{width}" height="{height}"'
'style="border:none !important;" '
'"allowfullscreen" "webkitallowfullscreen" "mozallowfullscreen">'
"</iframe>"
).format(html=html, width=self.width, height=self.height)
return iframe
def add_subplot(self, x: int, y: int, n: int, margin: float = 0.05) -> "Div":
"""Creates a div child subplot in a matplotlib.figure.add_subplot style.
Parameters
----------
x : int
The number of rows in the grid.
y : int
The number of columns in the grid.
n : int
The cell number in the grid, counted from 1 to x*y.
margin : float, default 0.05
Factor to add to the left, top, width and height parameters.
Example
-------
>>> fig.add_subplot(3, 2, 5)
# Create a div in the 5th cell of a 3rows x 2columns
grid(bottom-left corner).
"""
width = 1.0 / y
height = 1.0 / x
left = ((n - 1) % y) * width
top = ((n - 1) // y) * height
left = left + width * margin
top = top + height * margin
width = width * (1 - 2.0 * margin)
height = height * (1 - 2.0 * margin)
div = Div(
position="absolute",
width=f"{100.0 * width}%",
height=f"{100.0 * height}%",
left=f"{100.0 * left}%",
top=f"{100.0 * top}%",
)
self.add_child(div)
return div
class Html(Element):
"""Create an HTML div object for embedding data.
Parameters
----------
data : str
The HTML data to be embedded.
script : bool
If True, data will be embedded without escaping
(suitable for embedding html-ready code)
width : int or str, default '100%'
The width of the output div element.
Ex: 120 , '80%'
height : int or str, default '100%'
The height of the output div element.
Ex: 120 , '80%'
"""
_template = Template(
'<div id="{{this.get_name()}}" '
'style="width: {{this.width[0]}}{{this.width[1]}}; height: {{this.height[0]}}{{this.height[1]}};">' # noqa
"{% if this.script %}{{this.data}}{% else %}{{this.data|e}}{% endif %}</div>",
)
def __init__(
self,
data: str,
script: bool = False,
width: TypeParseSize = "100%",
height: TypeParseSize = "100%",
):
super().__init__()
self._name = "Html"
self.script = script
self.data = data
self.width = _parse_size(width)
self.height = _parse_size(height)
class Div(Figure):
"""Create a Div to be embedded in a Figure.
Parameters
----------
width: int or str, default '100%'
The width of the div in pixels (int) or percentage (str).
height: int or str, default '100%'
The height of the div in pixels (int) or percentage (str).
left: int or str, default '0%'
The left-position of the div in pixels (int) or percentage (str).
top: int or str, default '0%'
The top-position of the div in pixels (int) or percentage (str).
position: str, default 'relative'
The position policy of the div.
Usual values are 'relative', 'absolute', 'fixed', 'static'.
"""
_template = Template(
"{% macro header(this, kwargs) %}"
"<style> #{{this.get_name()}} {\n"
" position : {{this.position}};\n"
" width : {{this.width[0]}}{{this.width[1]}};\n"
" height: {{this.height[0]}}{{this.height[1]}};\n"
" left: {{this.left[0]}}{{this.left[1]}};\n"
" top: {{this.top[0]}}{{this.top[1]}};\n"
" </style>"
"{% endmacro %}"
"{% macro html(this, kwargs) %}"
'<div id="{{this.get_name()}}">{{this.html.render(**kwargs)}}</div>'
"{% endmacro %}",
)
def __init__(
self,
width: TypeParseSize = "100%",
height: TypeParseSize = "100%",
left: TypeParseSize = "0%",
top: TypeParseSize = "0%",
position: str = "relative",
):
super(Figure, self).__init__()
self._name = "Div"
# Size Parameters.
self.width = _parse_size(width) # type: ignore
self.height = _parse_size(height) # type: ignore
self.left = _parse_size(left)
self.top = _parse_size(top)
self.position = position
self.header = Element()
self.html = Element(
"{% for name, element in this._children.items() %}"
"{{element.render(**kwargs)}}"
"{% endfor %}",
)
self.script = Element()
self.header._parent = self
self.html._parent = self
self.script._parent = self
def get_root(self) -> "Div":
"""Returns the root of the elements tree."""
return self
def render(self, **kwargs):
"""Renders the HTML representation of the element."""
figure = self._parent
assert isinstance(figure, Figure), (
"You cannot render this Element " "if it is not in a Figure."
)
for name, element in self._children.items():
element.render(**kwargs)
for name, element in self.header._children.items():
figure.header.add_child(element, name=name)
for name, element in self.script._children.items():
figure.script.add_child(element, name=name)
header = self._template.module.__dict__.get("header", None)
if header is not None:
figure.header.add_child(Element(header(self, kwargs)), name=self.get_name())
html = self._template.module.__dict__.get("html", None)
if html is not None:
figure.html.add_child(Element(html(self, kwargs)), name=self.get_name())
script = self._template.module.__dict__.get("script", None)
if script is not None:
figure.script.add_child(Element(script(self, kwargs)), name=self.get_name())
def _repr_html_(self, **kwargs) -> str:
"""Displays the Div in a Jupyter notebook."""
if self._parent is None:
self.add_to(Figure())
out = self._parent._repr_html_(**kwargs) # type: ignore
self._parent = None
else:
out = self._parent._repr_html_(**kwargs) # type: ignore
return out
class IFrame(Element):
"""Create a Figure object, to plot things into it.
Parameters
----------
html : str, default None
Eventual HTML code that you want to put in the frame.
width : str, default "100%"
The width of the Figure.
It may be a percentage or pixel value (like "300px").
height : str, default None
The height of the Figure.
It may be a percentage or a pixel value (like "300px").
ratio : str, default "60%"
A percentage defining the aspect ratio of the Figure.
It will be ignored if height is not None.
figsize : tuple of two int, default None
If you're a matplotlib addict, you can overwrite width and
height. Values will be converted into pixels in using 60 dpi.
For example figsize=(10, 5) will result in
width="600px", height="300px".
"""
def __init__(
self,
html: Optional[Union[str, Element]] = None,
width: str = "100%",
height: Optional[str] = None,
ratio: str = "60%",
figsize: Optional[Tuple[int, int]] = None,
):
super().__init__()
self._name = "IFrame"
self.width = width
self.height = height
self.ratio = ratio
if figsize is not None:
self.width = str(60 * figsize[0]) + "px"
self.height = str(60 * figsize[1]) + "px"
if isinstance(html, str):
self.add_child(Element(html))
elif html is not None:
self.add_child(html)
def render(self, **kwargs) -> str:
"""Renders the HTML representation of the element."""
html = super().render(**kwargs)
html = "data:text/html;charset=utf-8;base64," + base64.b64encode(
html.encode("utf8"),
).decode("utf8")
if self.height is None:
iframe = (
'<div style="width:{width};">'
'<div style="position:relative;width:100%;height:0;padding-bottom:{ratio};">' # noqa
'<iframe src="{html}" style="position:absolute;width:100%;height:100%;left:0;top:0;' # noqa
'border:none !important;">'
"</iframe>"
"</div></div>"
).format(html=html, width=self.width, ratio=self.ratio)
else:
iframe = (
'<iframe src="{html}" width="{width}" style="border:none !important;" '
'height="{height}"></iframe>'
).format(html=html, width=self.width, height=self.height)
return iframe
class MacroElement(Element):
"""This is a parent class for Elements defined by a macro template.
To compute your own element, all you have to do is:
* To inherit from this class
* Overwrite the '_name' attribute
* Overwrite the '_template' attribute with something of the form::
{% macro header(this, kwargs) %}
...
{% endmacro %}
{% macro html(this, kwargs) %}
...
{% endmacro %}
{% macro script(this, kwargs) %}
...
{% endmacro %}
"""
_template = Template("")
def __init__(self):
super().__init__()
self._name = "MacroElement"
def render(self, **kwargs):
"""Renders the HTML representation of the element."""
figure = self.get_root()
assert isinstance(figure, Figure), (
"You cannot render this Element " "if it is not in a Figure."
)
header = self._template.module.__dict__.get("header", None)
if header is not None:
figure.header.add_child(Element(header(self, kwargs)), name=self.get_name())
html = self._template.module.__dict__.get("html", None)
if html is not None:
figure.html.add_child(Element(html(self, kwargs)), name=self.get_name())
script = self._template.module.__dict__.get("script", None)
if script is not None:
figure.script.add_child(Element(script(self, kwargs)), name=self.get_name())
for name, element in self._children.items():
element.render(**kwargs)

View File

@@ -0,0 +1 @@
{"codes": ["viridis", "plasma", "inferno", "magma", "Spectral", "RdYlGn", "PuBu", "Accent", "OrRd", "Set1", "Set2", "Set3", "BuPu", "Dark2", "RdBu", "Oranges", "BuGn", "PiYG", "YlOrBr", "YlGn", "Pastel2", "RdPu", "Greens", "PRGn", "YlGnBu", "RdYlBu", "Paired", "BrBG", "Purples", "Reds", "Pastel1", "GnBu", "Greys", "RdGy", "YlOrRd", "PuOr", "PuRd", "Blues", "PuBuGn"]}

View File

@@ -0,0 +1 @@
{"Spectral": "Diverging", "RdYlGn": "Diverging", "Set2": "Qualitative", "Accent": "Qualitative", "OrRd": "Sequential", "Set1": "Qualitative", "PuBu": "Sequential", "Set3": "Qualitative", "BuPu": "Sequential", "Dark2": "Qualitative", "RdBu": "Diverging", "BuGn": "Sequential", "PiYG": "Diverging", "YlOrBr": "Sequential", "YlGn": "Sequential", "RdPu": "Sequential", "PRGn": "Diverging", "YlGnBu": "Sequential", "RdYlBu": "Diverging", "Paired": "Qualitative", "Pastel2": "Qualitative", "Pastel1": "Qualitative", "GnBu": "Sequential", "RdGy": "Diverging", "YlOrRd": "Sequential", "PuOr": "Diverging", "PuRd": "Sequential", "BrBG": "Diverging", "PuBuGn": "Sequential", "Greens": "Sequential", "viridis": "Sequential", "plasma": "Sequential", "inferno": "Sequential", "magma": "Sequential", "Oranges": "Sequential", "Blues": "Sequential", "Greys": "Sequential", "Reds": "Sequential", "Purples": "Sequential"}

View File

@@ -0,0 +1,57 @@
{% macro script(this, kwargs) %}
var {{this.get_name()}} = {};
{%if this.color_range %}
{{this.get_name()}}.color = d3.scale.threshold()
.domain({{this.color_domain}})
.range({{this.color_range}});
{%else%}
{{this.get_name()}}.color = d3.scale.threshold()
.domain([{{ this.color_domain[0] }}, {{ this.color_domain[-1] }}])
.range(['{{ this.fill_color }}', '{{ this.fill_color }}']);
{%endif%}
{{this.get_name()}}.x = d3.scale.linear()
.domain([{{ this.color_domain[0] }}, {{ this.color_domain[-1] }}])
.range([0, {{ this.width }} - 50]);
{{this.get_name()}}.legend = L.control({position: 'topright'});
{{this.get_name()}}.legend.onAdd = function (map) {var div = L.DomUtil.create('div', 'legend'); return div};
{{this.get_name()}}.legend.addTo({{this._parent.get_name()}});
{{this.get_name()}}.xAxis = d3.svg.axis()
.scale({{this.get_name()}}.x)
.orient("top")
.tickSize(1)
.tickValues({{ this.tick_labels }});
{{this.get_name()}}.svg = d3.select(".legend.leaflet-control").append("svg")
.attr("id", 'legend')
.attr("width", {{ this.width }})
.attr("height", {{ this.height }});
{{this.get_name()}}.g = {{this.get_name()}}.svg.append("g")
.attr("class", "key")
.attr("fill", {{ this.text_color | tojson }})
.attr("transform", "translate(25,16)");
{{this.get_name()}}.g.selectAll("rect")
.data({{this.get_name()}}.color.range().map(function(d, i) {
return {
x0: i ? {{this.get_name()}}.x({{this.get_name()}}.color.domain()[i - 1]) : {{this.get_name()}}.x.range()[0],
x1: i < {{this.get_name()}}.color.domain().length ? {{this.get_name()}}.x({{this.get_name()}}.color.domain()[i]) : {{this.get_name()}}.x.range()[1],
z: d
};
}))
.enter().append("rect")
.attr("height", {{ this.height }} - 30)
.attr("x", function(d) { return d.x0; })
.attr("width", function(d) { return d.x1 - d.x0; })
.style("fill", function(d) { return d.z; });
{{this.get_name()}}.g.call({{this.get_name()}}.xAxis).append("text")
.attr("class", "caption")
.attr("y", 21)
.attr("fill", {{ this.text_color | tojson }})
.text({{ this.caption|tojson }});
{% endmacro %}

View File

@@ -0,0 +1,460 @@
"""
Utilities
-------
Utility module for Folium helper functions.
"""
import base64
import json
import math
import os
import re
import struct
import typing
import zlib
from typing import Any, Callable, List, Optional, Sequence, Tuple, Union
from jinja2 import Environment, PackageLoader
try:
import numpy as np
except ImportError:
np = None # type: ignore
if typing.TYPE_CHECKING:
from branca.colormap import ColorMap
rootpath: str = os.path.abspath(os.path.dirname(__file__))
TypeParseSize = Union[int, float, str, Tuple[float, str]]
def get_templates() -> Environment:
"""Get Jinja templates."""
return Environment(loader=PackageLoader("branca", "templates"))
def legend_scaler(
legend_values: Sequence[float],
max_labels: int = 10,
) -> List[Union[float, str]]:
"""
Downsamples the number of legend values so that there isn't a collision
of text on the legend colorbar (within reason). The colorbar seems to
support ~10 entries as a maximum.
"""
legend_ticks: List[Union[float, str]]
if len(legend_values) < max_labels:
legend_ticks = list(legend_values)
else:
spacer = int(math.ceil(len(legend_values) / max_labels))
legend_ticks = []
for i in legend_values[::spacer]:
legend_ticks += [i]
legend_ticks += [""] * (spacer - 1)
return legend_ticks
def linear_gradient(hexList: List[str], nColors: int) -> List[str]:
"""
Given a list of hexcode values, will return a list of length
nColors where the colors are linearly interpolated between the
(r, g, b) tuples that are given.
"""
def _scale(start, finish, length, i):
"""
Return the value correct value of a number that is in between start
and finish, for use in a loop of length *length*.
"""
base = 16
fraction = float(i) / (length - 1)
raynge = int(finish, base) - int(start, base)
thex = hex(int(int(start, base) + fraction * raynge)).split("x")[-1]
if len(thex) != 2:
thex = "0" + thex
return thex
allColors: List[str] = []
# Separate (R, G, B) pairs.
for start, end in zip(hexList[:-1], hexList[1:]):
# Linearly interpolate between pair of hex ###### values and
# add to list.
nInterpolate = 765
for index in range(nInterpolate):
r = _scale(start[1:3], end[1:3], nInterpolate, index)
g = _scale(start[3:5], end[3:5], nInterpolate, index)
b = _scale(start[5:7], end[5:7], nInterpolate, index)
allColors.append("".join(["#", r, g, b]))
# Pick only nColors colors from the total list.
result: List[str] = []
for counter in range(nColors):
fraction = float(counter) / (nColors - 1)
index = int(fraction * (len(allColors) - 1))
result.append(allColors[index])
return result
def color_brewer(color_code: str, n: int = 6) -> List[str]:
"""
Generate a colorbrewer color scheme of length 'len', type 'scheme.
Live examples can be seen at http://colorbrewer2.org/
"""
maximum_n = 253
minimum_n = 3
if not isinstance(n, int):
raise TypeError("n has to be an int, not a %s" % type(n))
# Raise an error if the n requested is greater than the maximum.
if n > maximum_n:
raise ValueError(
"The maximum number of colors in a"
" ColorBrewer sequential color series is 253",
)
if n < minimum_n:
raise ValueError(
"The minimum number of colors in a"
" ColorBrewer sequential color series is 3",
)
if not isinstance(color_code, str):
raise ValueError(f"color should be a string, not a {type(color_code)}.")
if color_code[-2:] == "_r":
base_code = color_code[:-2]
core_color_code = base_code + "_" + str(n).zfill(2)
color_reverse = True
else:
base_code = color_code
core_color_code = base_code + "_" + str(n).zfill(2)
color_reverse = False
with open(os.path.join(rootpath, "_schemes.json")) as f:
schemes = json.loads(f.read())
with open(os.path.join(rootpath, "scheme_info.json")) as f:
scheme_info = json.loads(f.read())
with open(os.path.join(rootpath, "scheme_base_codes.json")) as f:
core_schemes = json.loads(f.read())["codes"]
if base_code not in core_schemes:
raise ValueError(base_code + " is not a valid ColorBrewer code")
explicit_scheme = True
if schemes.get(core_color_code) is None:
explicit_scheme = False
# Only if n is greater than the scheme length do we interpolate values.
if not explicit_scheme:
# Check to make sure that it is not a qualitative scheme.
if scheme_info[base_code] == "Qualitative":
matching_quals = []
for key in schemes:
if base_code + "_" in key:
matching_quals.append(int(key.split("_")[1]))
raise ValueError(
"Expanded color support is not available"
" for Qualitative schemes; restrict the"
" number of colors for the "
+ base_code
+ " code to between "
+ str(min(matching_quals))
+ " and "
+ str(max(matching_quals)),
)
else:
longest_scheme_name = base_code
longest_scheme_n = 0
for sn_name in schemes.keys():
if "_" not in sn_name:
continue
if sn_name.split("_")[0] != base_code:
continue
if int(sn_name.split("_")[1]) > longest_scheme_n:
longest_scheme_name = sn_name
longest_scheme_n = int(sn_name.split("_")[1])
if not color_reverse:
color_scheme = linear_gradient(schemes.get(longest_scheme_name), n)
else:
color_scheme = linear_gradient(
schemes.get(longest_scheme_name)[::-1],
n,
)
else:
if not color_reverse:
color_scheme = schemes.get(core_color_code, None)
else:
color_scheme = schemes.get(core_color_code, None)[::-1]
return color_scheme
def image_to_url(
image: Any,
colormap: Union["ColorMap", Callable, None] = None,
origin: str = "upper",
) -> str:
"""Infers the type of an image argument and transforms it into a URL.
Parameters
----------
image: string, file or array-like object
* If string, it will be written directly in the output file.
* If file, it's content will be converted as embedded in the
output file.
* If array-like, it will be converted to PNG base64 string and
embedded in the output.
origin : ['upper' | 'lower'], optional, default 'upper'
Place the [0, 0] index of the array in the upper left or
lower left corner of the axes.
colormap : ColorMap or callable, used only for `mono` image.
Function of the form [x -> (r,g,b)] or [x -> (r,g,b,a)]
for transforming a mono image into RGB.
It must output iterables of length 3 or 4, with values between
0. and 1. Hint : you can use colormaps from `matplotlib.cm`.
"""
if hasattr(image, "read"):
# We got an image file.
if hasattr(image, "name"):
# We try to get the image format from the file name.
fileformat = image.name.lower().split(".")[-1]
else:
fileformat = "png"
url = "data:image/{};base64,{}".format(
fileformat,
base64.b64encode(image.read()).decode("utf-8"),
)
elif (not (isinstance(image, str) or isinstance(image, bytes))) and hasattr(
image,
"__iter__",
):
# We got an array-like object.
png = write_png(image, origin=origin, colormap=colormap)
url = "data:image/png;base64," + base64.b64encode(png).decode("utf-8")
else:
# We got an URL.
url = json.loads(json.dumps(image))
return url.replace("\n", " ")
def write_png(
data: Any,
origin: str = "upper",
colormap: Union["ColorMap", Callable, None] = None,
) -> bytes:
"""
Transform an array of data into a PNG string.
This can be written to disk using binary I/O, or encoded using base64
for an inline PNG like this:
>>> png_str = write_png(array)
>>> "data:image/png;base64," + png_str.encode("base64")
Inspired from
http://stackoverflow.com/questions/902761/saving-a-numpy-array-as-an-image
Parameters
----------
data: numpy array or equivalent list-like object.
Must be NxM (mono), NxMx3 (RGB) or NxMx4 (RGBA)
origin : ['upper' | 'lower'], optional, default 'upper'
Place the [0,0] index of the array in the upper left or lower left
corner of the axes.
colormap : ColorMap subclass or callable, optional
Only needed to transform mono images into RGB. You have three options:
- use a subclass of `ColorMap` like `LinearColorMap`
- use a colormap from `matplotlib.cm`
- use a custom function of the form [x -> (r,g,b)] or [x -> (r,g,b,a)].
It must output iterables of length 3 or 4 with values between 0 and 1.
Returns
-------
PNG formatted byte string
"""
from branca.colormap import ColorMap
if np is None:
raise ImportError("The NumPy package is required" " for this functionality")
if isinstance(colormap, ColorMap):
colormap_callable = colormap.rgba_floats_tuple
elif callable(colormap):
colormap_callable = colormap
else:
colormap_callable = lambda x: (x, x, x, 1) # noqa E731
array = np.atleast_3d(data)
height, width, nblayers = array.shape
if nblayers not in [1, 3, 4]:
raise ValueError("Data must be NxM (mono), " "NxMx3 (RGB), or NxMx4 (RGBA)")
assert array.shape == (height, width, nblayers)
if nblayers == 1:
array = np.array(list(map(colormap_callable, array.ravel())))
nblayers = array.shape[1]
if nblayers not in [3, 4]:
raise ValueError(
"colormap must provide colors of" "length 3 (RGB) or 4 (RGBA)",
)
array = array.reshape((height, width, nblayers))
assert array.shape == (height, width, nblayers)
if nblayers == 3:
array = np.concatenate((array, np.ones((height, width, 1))), axis=2)
nblayers = 4
assert array.shape == (height, width, nblayers)
assert nblayers == 4
# Normalize to uint8 if it isn't already.
if array.dtype != "uint8":
with np.errstate(divide="ignore", invalid="ignore"):
array = array * 255.0 / array.max(axis=(0, 1)).reshape((1, 1, 4))
array[~np.isfinite(array)] = 0
array = array.astype("uint8")
# Eventually flip the image.
if origin == "lower":
array = array[::-1, :, :]
# Transform the array to bytes.
raw_data = b"".join([b"\x00" + array[i, :, :].tobytes() for i in range(height)])
def png_pack(png_tag, data):
chunk_head = png_tag + data
return (
struct.pack("!I", len(data))
+ chunk_head
+ struct.pack("!I", 0xFFFFFFFF & zlib.crc32(chunk_head))
)
return b"".join(
[
b"\x89PNG\r\n\x1a\n",
png_pack(b"IHDR", struct.pack("!2I5B", width, height, 8, 6, 0, 0, 0)),
png_pack(b"IDAT", zlib.compress(raw_data, 9)),
png_pack(b"IEND", b""),
],
)
def _camelify(out: str) -> str:
return (
(
"".join(
[
(
"_" + x.lower()
if i < len(out) - 1 and x.isupper() and out[i + 1].islower()
else (
x.lower() + "_"
if i < len(out) - 1 and x.islower() and out[i + 1].isupper()
else x.lower()
)
)
for i, x in enumerate(list(out))
],
)
)
.lstrip("_")
.replace("__", "_")
)
def _parse_size(value: TypeParseSize) -> Tuple[float, str]:
if isinstance(value, (int, float)):
return float(value), "px"
elif isinstance(value, str):
# match digits or a point, possibly followed by a space,
# followed by a unit: either 1 to 5 letters or a percent sign
match = re.fullmatch(r"([\d.]+)\s?(\w{1,5}|%)", value.strip())
if match:
return float(match.group(1)), match.group(2)
else:
raise ValueError(
f"Cannot parse {value!r}, it should be a number followed by a unit.",
)
elif (
isinstance(value, tuple)
and isinstance(value[0], (int, float))
and isinstance(value[1], str)
):
# value had been already parsed
return (float(value[0]), value[1])
else:
raise TypeError(
f"Cannot parse {value!r}, it should be a number or a string containing a number and a unit.",
)
def _locations_mirror(x):
"""Mirrors the points in a list-of-list-of-...-of-list-of-points.
For example:
>>> _locations_mirror([[[1, 2], [3, 4]], [5, 6], [7, 8]])
[[[2, 1], [4, 3]], [6, 5], [8, 7]]
"""
if hasattr(x, "__iter__"):
if hasattr(x[0], "__iter__"):
return list(map(_locations_mirror, x))
else:
return list(x[::-1])
else:
return x
def _locations_tolist(x):
"""Transforms recursively a list of iterables into a list of list."""
if hasattr(x, "__iter__"):
return list(map(_locations_tolist, x))
else:
return x
def none_min(x: Optional[float], y: Optional[float]) -> Optional[float]:
if x is None:
return y
elif y is None:
return x
else:
return min(x, y)
def none_max(x: Optional[float], y: Optional[float]) -> Optional[float]:
if x is None:
return y
elif y is None:
return x
else:
return max(x, y)
def iter_points(x: Union[List, Tuple]) -> list:
"""Iterates over a list representing a feature, and returns a list of points,
whatever the shape of the array (Point, MultiPolyline, etc).
"""
if isinstance(x, (list, tuple)):
if len(x):
if isinstance(x[0], (list, tuple)):
out = []
for y in x:
out += iter_points(y)
return out
else:
return [x]
else:
return []
else:
raise ValueError(f"List/tuple type expected. Got {x!r}.")