Writing Custom PyBloqs#
Writing custom PyBloqs to embed your own HTML and visualisations is very easy and highly customisable. In this doucmentation we will build a custom PyBloq and show how to manage resources (CSS and JavaScript).
Show code cell content
from typing import Iterator, Optional
import pybloqs
import bs4
We will be writing a “metric block”. This is a block that will highlight display a single number, along with its change from a previous value.
To do this, we extend pybloqs.BaseBlock
. The only method we need to provide is _write_contents
which we can stub for now. We also create a constructor which stores the data we want to show.
class MetricBlock(pybloqs.BaseBlock):
def __init__(
self, metric_name: str, metric_value: float, metric_delta: float, **kwargs
) -> None:
super().__init__(**kwargs)
self.metric_name = metric_name
self.metric_value = metric_value
self.metric_delta = metric_delta
def _write_contents(
self,
container: bs4.Tag,
actual_cfg: pybloqs.util.Cfg,
id_gen: Iterator[str],
resource_deps=None,
static_output: Optional[bool] = None,
) -> None:
pass
Believe it or not, that is sufficient to render something to screen:
MetricBlock("Widgets made", 20, 3, title="Our metric", title_level=4)
Our metric
However, as you can see we haven’t rendered any acutal data. Lets fill in _write_contents
. The arguments to this function can mostly be ignored, apart from container
. This is a BeautifulSoup Tag
into which we must render our block.
PyBloqs provides pybloqs.html.append_to
as a helper function to add children to tags. Lets add three: one for the value, one for the name and one for the change.
from pybloqs.html import append_to
class MetricBlock(pybloqs.BaseBlock):
def __init__(
self, metric_name: str, metric_value: float, metric_delta: float, **kwargs
) -> None:
super().__init__(**kwargs)
self.metric_name = metric_name
self.metric_value = metric_value
self.metric_delta = metric_delta
def _write_contents(
self,
container: bs4.Tag,
actual_cfg: pybloqs.util.Cfg,
id_gen: Iterator[str],
resource_deps=None,
static_output: Optional[bool] = None,
) -> None:
metric_span = append_to(container, "span")
metric_span.string = f"{self.metric_value}"
name_span = append_to(container, "span")
name_span.string = self.metric_name
metric_delta = append_to(container, "span")
metric_delta.string = f"{self.metric_delta:+}"
MetricBlock("Widgets made", 20, 3, title="Our metric", title_level=4)
Our metric
20 Widgets made +3We can add tag attributes to these. Here we set the style
attribute to add some styling.
class MetricBlock(pybloqs.BaseBlock):
def __init__(
self, metric_name: str, metric_value: float, metric_delta: float, **kwargs
) -> None:
super().__init__(**kwargs)
self.metric_name = metric_name
self.metric_value = metric_value
self.metric_delta = metric_delta
def _write_contents(
self,
container: bs4.Tag,
actual_cfg: pybloqs.util.Cfg,
id_gen: Iterator[str],
resource_deps=None,
static_output: Optional[bool] = None,
) -> None:
metric_span = append_to(container, "span", style="font-size: 2rem;")
metric_span.string = f"{self.metric_value}"
name_span = append_to(container, "span", style="font-weight:bold")
name_span.string = self.metric_name
metric_delta = append_to(container, "span", style="color:green;")
metric_delta.string = f"({self.metric_delta:+})"
MetricBlock("Widgets made", 20, 3, title="Our metric", title_level=4)
Our metric
20 Widgets made (+3)We can go further though, and bundle some CSS with our block. Here we set the class
attribute on the tags to tie them to the style sheet.
Note
Because class
is a keyword in python, we can’t write append_to(container, "span", class="metric")
but we can abuse keyword arguments to pass this variable in.
We might also have set metric_span["class"] = "metric"
.
Warning
Note that resource_deps
is a tuple containing a single element.
from pybloqs.static import Css
CSS_STRING = """
.metric{
display: flex;
flex-direction: column;
border: 2px grey solid;
height: 10em;
width: 10em;
border-radius: 5em;
text-align: center;
}
.metric>.value {
font-size:2.5rem;
padding-top: 0.8em;
font-weight: bold;
}
.metric>.delta {
color: green;
}
"""
class MetricBlock(pybloqs.BaseBlock):
resource_deps = (Css(name="metrics_css", css_string=CSS_STRING),)
def __init__(
self, metric_name: str, metric_value: float, metric_delta: float, **kwargs
) -> None:
super().__init__(**kwargs)
self.metric_name = metric_name
self.metric_value = metric_value
self.metric_delta = metric_delta
def _write_contents(
self,
container: bs4.Tag,
actual_cfg: pybloqs.util.Cfg,
id_gen: Iterator[str],
resource_deps=None,
static_output: Optional[bool] = None,
) -> None:
metric_container = append_to(container, "span", **{"class": "metric"})
metric_span = append_to(metric_container, "span", **{"class": "value"})
metric_span.string = f"{self.metric_value}"
name_span = append_to(metric_container, "span", **{"class": "name"})
name_span.string = self.metric_name
metric_delta = append_to(metric_container, "span", **{"class": "delta"})
metric_delta.string = f"({self.metric_delta:+})"
MetricBlock("Widgets made", 20, 3, title="Our metric", title_level=4)
Our metric
20 Widgets made (+3)Note that pybloqs is smart and will only include the CSS once per document, no matter how many metric blocks are in the report!
Finally we can include some JavaScript. While the functionality here is easily do-able with html :hover
selectors, we use a small script to highlight some features.
from pybloqs.static import JScript
CSS_STRING = """
.metric{
display: flex;
flex-direction: column;
border: 2px grey solid;
height: 10em;
width: 10em;
border-radius: 5em;
text-align: center;
}
.metric>.value {
font-size:2.5rem;
padding-top: 0.8em;
font-weight: bold;
}
.metric>.delta {
color: green;
}
"""
JS_STRING = """
function add_shadow(event) {
document.getElementById(event).style.boxShadow = "0px 0px 5px grey";
}
function remove_shadow(event) {
document.getElementById(event).style.boxShadow = "none";
}
"""
class MetricBlock(pybloqs.BaseBlock):
resource_deps = (
Css(name="metrics_css", css_string=CSS_STRING),
JScript(name="metrics_js", script_string=JS_STRING),
)
def __init__(
self, metric_name: str, metric_value: float, metric_delta: float, **kwargs
) -> None:
super().__init__(**kwargs)
self.metric_name = metric_name
self.metric_value = metric_value
self.metric_delta = metric_delta
def _write_contents(
self,
container: bs4.Tag,
actual_cfg: pybloqs.util.Cfg,
id_gen: Iterator[str],
resource_deps=None,
static_output: Optional[bool] = None,
) -> None:
metric_container = append_to(
container, "span", id=next(id_gen), **{"class": "metric"}
)
metric_container["onmouseover"] = f'add_shadow("{metric_container["id"]}")'
metric_container["onmouseout"] = f'remove_shadow("{metric_container["id"]}")'
metric_span = append_to(metric_container, "span", **{"class": "value"})
metric_span.string = f"{self.metric_value}"
name_span = append_to(metric_container, "span", **{"class": "name"})
name_span.string = self.metric_name
metric_delta = append_to(metric_container, "span", **{"class": "delta"})
metric_delta.string = f"({self.metric_delta:+})"
MetricBlock("Widgets made", 20, 3, title="Our metric", title_level=4)
Our metric
20 Widgets made (+3)Again, JavaScript resources are included only once per document and as such resource_deps
is a class attribute of MetricBlock
, not a member of any instance. This is why we identify elements of the DOM by id
, here generated using the utility id_gen
.
The other arguments to _write_contents
can be passed to _write_block
of any child blocks of your custom block, if you have nesting.