"""
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"))
[docs]
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 = hexlify(urandom(16)).decode()
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)
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)
[docs]
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]]
[docs]
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
[docs]
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)
[docs]
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
[docs]
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
[docs]
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
[docs]
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)
[docs]
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()
[docs]
def render(self, **kwargs) -> str:
"""Renders the HTML representation of the element."""
return self._template.render(this=self, kwargs=kwargs)
[docs]
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()
[docs]
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()
[docs]
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
[docs]
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
[docs]
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"
[docs]
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"
[docs]
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)
[docs]
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
[docs]
def get_root(self) -> "Div":
"""Returns the root of the elements tree."""
return self
[docs]
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
[docs]
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)
[docs]
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
[docs]
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"
[docs]
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)