Source code for branca.element

"""
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 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", )
[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, **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
[docs] def get_root(self) -> "Figure": """Returns the root of the elements tree.""" return self
[docs] 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
[docs] 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
[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)