Source code for k1lib.viz

# AUTOGENERATED FILE! PLEASE DON'T EDIT HERE. EDIT THE SOURCE NOTEBOOKS INSTEAD
"""
This module is for nice visualization tools. This is exposed automatically with::

   from k1lib.imports import *
   viz.mask # exposed
"""
import k1lib, base64, io, os, matplotlib as mpl, warnings
import k1lib.cli as cli
plt = k1lib.dep.plt; import numpy as np
from typing import Callable, List, Union
from functools import partial, update_wrapper
try: import torch; import torch.nn as nn; hasTorch = True
except:
    torch = k1lib.Object().withAutoDeclare(lambda: type("RandomClass", (object, ), {}))
    nn = k1lib.Object().withAutoDeclare(lambda: type("RandomClass", (object, ), {})); hasTorch = False
try: import PIL; hasPIL = True
except: hasPIL = False
__all__ = ["SliceablePlot", "plotSegments", "Carousel", "Toggle", "ToggleImage",
           "Scroll", "confusionMatrix", "FAnim", "mask", "PDF", "Html", "onload"]
class _PlotDecorator:                                                            # _PlotDecorator
    """The idea with decorators is that you can do something like this::

    sp = k1lib.viz.SliceablePlot()
    sp.yscale("log") # will format every plot as if ``plt.yscale("log")`` has been called

This class is not expected to be used by end users though."""                    # _PlotDecorator
    def __init__(self, sliceablePlot:"SliceablePlot", name:str):                 # _PlotDecorator
        """
:param sliceablePlot: the parent plot
:param name: the decorator's name, like "yscale" """                             # _PlotDecorator
        self.sliceablePlot = sliceablePlot                                       # _PlotDecorator
        self.name = name; self.args, self.kwargs = None, None                    # _PlotDecorator
    def __call__(self, *args, **kwargs):                                         # _PlotDecorator
        """Stores all args, then return the parent :class:`SliceablePlot`"""     # _PlotDecorator
        self.args = args; self.kwargs = kwargs; return self.sliceablePlot        # _PlotDecorator
    def run(self): getattr(plt, self.name)(*self.args, **self.kwargs)            # _PlotDecorator
[docs]class SliceablePlot: # SliceablePlot """This is a plot that is "sliceable", meaning you can focus into a particular region of the plot quickly. A minimal example looks something like this:: import numpy as np, matplotlib.pyplot as plt, k1lib x = np.linspace(-2, 2, 100) def normalF(): plt.plot(x, x**2) @k1lib.viz.SliceablePlot.decorate def plotF(_slice): plt.plot(x[_slice], (x**2)[_slice]) plotF()[70:] # plots x^2 equation with x in [0.8, 2] So, ``normalF`` plots the equation :math:`x^2` with x going from -2 to 2. You can convert this into a :class:`SliceablePlot` by adding a term of type :class:`slice` to the args, and decorate with :meth:`decorate`. Now, every time you slice the :class:`SliceablePlot` with a specific range, ``plotF`` will receive it. How intuitive everything is depends on how you slice your data. ``[70:]`` results in x in [0.8, 2] is rather unintuitive. You can change it into something like this:: @k1lib.viz.SliceablePlot.decorate def niceF(_slice): n = 100; r = k1lib.Range(-2, 2) x = np.linspace(*r, n) _slice = r.toRange(k1lib.Range(n), r.bound(_slice)).slice_ plt.plot(x[_slice], (x**2)[_slice]) # this works without a decorator too btw: k1lib.viz.SliceablePlot(niceF) niceF()[0.3:0.7] # plots x^2 equation with x in [0.3, 0.7] niceF()[0.3:] # plots x^2 equation with x in [0.3, 2] The idea is to just take the input :class:`slice`, put some bounds on its parts, then convert that slice from [-2, 2] to [0, 100]. Check out :class:`k1lib.Range` if it's not obvious how this works. A really cool feature of :class:`SliceablePlot` looks like this:: niceF().legend(["A"])[-1:].grid(True).yscale("log") This will plot :math:`x^2` with range in [-1, 2] with a nice grid, and with y axis's scale set to log. Essentially, undefined method calls on a :class:`SliceablePlot` will translate into ``plt`` calls. So the above is roughly equivalent to this:: x = np.linspace(-2, 2, 100) plt.plot(x, x**2) plt.legend(["A"]) plt.grid(True) plt.yscale("log") .. image:: images/SliceablePlot.png This works even if you have multiple axes inside your figure. It's wonderful, isn't it?""" # SliceablePlot def __init__(self, plotF:Callable[[slice], None], slices:Union[slice, List[slice]]=slice(None), plotDecorators:List[_PlotDecorator]=[], docs=""): # SliceablePlot """Creates a new SliceablePlot. Only use params listed below: :param plotF: function that takes in a :class:`slice` or tuple of :class:`slice`s :param docs: optional docs for the function that will be displayed in :meth:`__repr__`""" # SliceablePlot self.plotF = plotF # SliceablePlot self.slices = [slices] if isinstance(slices, slice) else slices # SliceablePlot self.docs = docs; self.plotDecorators = list(plotDecorators) # SliceablePlot
[docs] @staticmethod # SliceablePlot def decorate(f): # SliceablePlot """Decorates a plotting function so that it becomes a SliceablePlot.""" # SliceablePlot answer = partial(SliceablePlot, plotF=f) # SliceablePlot update_wrapper(answer, f) # SliceablePlot return answer # SliceablePlot
@property # SliceablePlot def squeezedSlices(self) -> Union[List[slice], slice]: # SliceablePlot """If :attr:`slices` only has 1 element, then return that element, else return the entire list.""" # SliceablePlot return k1lib.squeeze(self.slices) # SliceablePlot def __getattr__(self, attr): # SliceablePlot if attr.startswith("_"): raise AttributeError() # SliceablePlot # automatically assume the attribute is a plt.attr method # SliceablePlot dec = _PlotDecorator(self, attr) # SliceablePlot self.plotDecorators.append(dec); return dec # SliceablePlot def __getitem__(self, idx): # SliceablePlot if type(idx) == slice: # SliceablePlot return SliceablePlot(self.plotF, [idx], self.plotDecorators, self.docs) # SliceablePlot if type(idx) == tuple and all([isinstance(elem, slice) for elem in idx]): # SliceablePlot return SliceablePlot(self.plotF, idx, self.plotDecorators, self.docs) # SliceablePlot raise Exception(f"Don't understand {idx}") # SliceablePlot def __repr__(self, show=True): # SliceablePlot self.plotF(self.squeezedSlices) # SliceablePlot for ax in plt.gcf().get_axes(): # SliceablePlot plt.sca(ax) # SliceablePlot for decorator in self.plotDecorators: decorator.run() # SliceablePlot if show: plt.show() # SliceablePlot return f"""Sliceable plot. Can... - p[a:b]: to focus on a specific range of the plot - p.yscale("log"): to perform operation as if you're using plt{self.docs}""" # SliceablePlot
[docs]def plotSegments(x:List[float], y:List[float], states:List[int], colors:List[str]=None): # plotSegments """Plots a line graph, with multiple segments with different colors. Idea is, you have a normal line graph, but you want to color parts of the graph red, other parts blue. Then, you can pass a "state" array, with the same length as your data, filled with ints, like this:: y = np.array([ 460800, 921600, 921600, 1445888, 1970176, 1970176, 2301952, 2633728, 2633728, 3043328, 3452928, 3452928, 3457024, 3461120, 3463680, 3463680, 3470336, 3470336, 3467776, 3869184, 3865088, 3865088, 3046400, 2972672, 2972672, 2309632, 2504192, 2504192, 1456128, 1393664, 1393664, 472576]) s = np.array([1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1]) plotSegments(None, y, s, colors=["tab:blue", "tab:red"]) .. image:: images/plotSegments.png :param x: (nullable) list of x coordinate at each point :param y: list of y coordinates at each point :param states: list of color at each point :param colors: string colors (matplotlib color strings) to display for each states""" # plotSegments if x is None: x = range(len(y)) # plotSegments if colors is None: colors = ["tab:blue", "tab:red", "tab:green", "tab:orange", "tab:purple", "tab:brown"][:len(x)] # plotSegments _x = []; _y = []; state = -1; count = -1 # stretchs, and bookkeeping nums # plotSegments lx = None; ly = None # last x and y from last stretch, for plot autocompletion # plotSegments while count + 1 < len(x): # plotSegments count += 1 # plotSegments if state != states[count]: # plotSegments if len(_x) > 0 and state >= 0: # plotSegments if lx != None: _x = [lx] + _x; _y = [ly] + _y # plotSegments plt.plot(_x, _y, colors[state]); lx = _x[-1]; ly = _y[-1] # plotSegments _x = [x[count]]; _y = [y[count]]; state = states[count] # plotSegments else: _x.append(x[count]); _y.append(y[count]) # plotSegments if len(_x) > 0 and state >= 0: # plotSegments if lx != None: _x = [lx] + _x; _y = [ly] + _y # plotSegments plt.plot(_x, _y, colors[state]) # plotSegments
class _Carousel: # _Carousel def __init__(self, searchMode, imgs, titles): # _Carousel self.searchMode = searchMode # _Carousel self.imgs:List[Tuple[str, str]] = imgs # Tuple[format, base64 img] # _Carousel self.titles = titles # _Carousel def _repr_html_(self): # _Carousel idx = Carousel._idx(); pre = f"k1c_{idx}"; searchMode = self.searchMode # _Carousel imgs = self.imgs | cli.apply(lambda x: f"`{x}`") | cli.deref(); n = len(imgs) # _Carousel titles = self.titles | cli.apply(lambda x: f"`{x}`") | cli.deref() # _Carousel if searchMode > 0: searchBar = f"<input type='text' value='' id='{pre}_search' placeholder='Search in {'content' if searchMode == 1 else 'header'}' style='padding: 4px 4px'>" # _Carousel else: searchBar = "" # _Carousel if n > 0: contents = imgs | cli.apply(k1lib.decode) | cli.insertIdColumn() | ~cli.apply(lambda idx, html: f"<div id='{pre}_content{idx}'>{html}</div>") | cli.deref() | cli.join('\n') # _Carousel else: contents = "(no pages or images are found)" # _Carousel #imgs = [f"\"<img alt='' src='data:image/{fmt};base64, {img}' />\"" for fmt, img in self.imgs] # _Carousel html = f"""<!-- k1lib.Carousel start --> <style> .{pre}_btn {{ cursor: pointer; padding: 6px 12px; /*background: #9e9e9e;*/ background-color: #eee; margin-right: 8px; color: #000; box-shadow: 0 3px 5px rgb(0,0,0,0.3); border-radius: 18px; user-select: none; -webkit-user-select: none; /* Safari */ -ms-user-select: none; /* IE 10+ */ }} .{pre}_btn:hover {{ box-shadow: box-shadow: 0 3px 10px rgb(0,0,0,0.6); background: #4caf50; color: #fff; }} </style> {searchBar} <div> <div style="display: flex; flex-direction: row; padding: 8px"> <div id="{pre}_prevBtn_10" class="{pre}_btn">&#60;10</div> <div id="{pre}_prevBtn" class="{pre}_btn">Prev</div> <div id="{pre}_nextBtn" class="{pre}_btn">Next</div> <div id="{pre}_nextBtn_10" class="{pre}_btn">10&#62;</div> </div> <div id="{pre}_status" style="padding: 10px"></div> </div> <div id="{pre}_imgContainer"> {contents} </div> <script> {pre}_allImgs = [{','.join(imgs)}]; {pre}_imgs = [...Array({pre}_allImgs.length).keys()]; // index of all available images. If searching for something then it will be a subset of allImgs {pre}_titles = [{','.join(titles)}]; {pre}_imgIdx = 0; // n-th element of pre_imgs, not of pre_allImgs {pre}_searchMode = {searchMode}; function {pre}_show(i) {{ // i here is allImgs index, not of imgs document.querySelector(`#{pre}_content${{i}}`).style.display = "block"; }} function {pre}_hide(i) {{ // i here is allImgs index, not of imgs document.querySelector(`#{pre}_content${{i}}`).style.display = "none"; }} function {pre}_updatePageCount() {{ let n = {pre}_imgs.length; if (n > 0) document.querySelector("#{pre}_status").innerHTML = "Page: " + ({pre}_imgIdx + 1) + "/" + n; else document.querySelector("#{pre}_status").innerHTML = "Page: 0/0" }} function {pre}_display() {{ let n = {pre}_imgs.length; for (let i = 0; i < {n}; i++) {pre}_hide(i); if (n > 0) {pre}_show({pre}_imgs[{pre}_imgIdx]); {pre}_updatePageCount(); }}; document.querySelector("#{pre}_prevBtn") .onclick = () => {{ {pre}_imgIdx -= 1; {pre}_imgIdx = Math.max({pre}_imgIdx, 0); {pre}_display(); }}; document.querySelector("#{pre}_prevBtn_10").onclick = () => {{ {pre}_imgIdx -= 10; {pre}_imgIdx = Math.max({pre}_imgIdx, 0); {pre}_display(); }}; document.querySelector("#{pre}_nextBtn") .onclick = () => {{ {pre}_imgIdx += 1; {pre}_imgIdx = Math.min({pre}_imgIdx, {pre}_imgs.length - 1); {pre}_display(); }}; document.querySelector("#{pre}_nextBtn_10").onclick = () => {{ {pre}_imgIdx += 10; {pre}_imgIdx = Math.min({pre}_imgIdx, {pre}_imgs.length - 1); {pre}_display(); }}; if ({pre}_searchMode > 0) {{ {pre}_searchInp = document.querySelector("#{pre}_search"); {pre}_searchInp.oninput = (value) => {{ const val = {pre}_searchInp.value; {pre}_imgs = ({pre}_searchMode === 1 ? {pre}_allImgs : {pre}_titles).map((e, i) => [window.atob(e).includes(val), i]).filter(e => e[0]).map(e => e[1]); {pre}_imgIdx = 0;; {pre}_display(); }} }} {pre}_display(); </script> <!-- k1lib.Carousel end -->""" # _Carousel return html # _Carousel k1lib.cli.init.addAtomic(Carousel) # Carousel
[docs]class Toggle(cli.BaseCli): # Toggle _idx = k1lib.AutoIncrement.random() # Toggle
[docs] def __init__(self): # Toggle """Button to toggle whether the content is displayed or not. Useful if the html content is very big in size. Example:: x = np.linspace(-2, 2); plt.plot(x, x ** 2) plt.gcf() | toImg() | toHtml() | viz.Toggle() This will plot a graph, then create a button where you can toggle the image's visibility""" # Toggle self.content:str = "" # html string # Toggle self._enteredRor = False # Toggle
[docs] def __ror__(self, it): self._enteredRor = True; self.content = it if isinstance(it, str) else it | cli.toHtml(); return self # Toggle
def __or__(self, it): # see discussion on Carousel() # Toggle if self._enteredRor: return it.__ror__(self) # Toggle else: return super().__or__(it) # Toggle def _repr_html_(self): # Toggle pre = f"k1t_{Toggle._idx()}" # Toggle html = f"""<!-- k1lib.Toggle start --> <style> #{pre}_btn {{ cursor: pointer; padding: 6px 12px; background: #eee; margin-right: 5px; color: #000; user-select: none; -webkit-user-select: none; /* Safari */ -ms-user-select: none; /* IE 10+ */ box-shadow: 0 3px 5px rgb(0,0,0,0.3); border-radius: 18px; }} #{pre}_btn:hover {{ box-shadow: 0 3px 5px rgb(0,0,0,0.6); background: #4caf50; color: #fff; }} </style> <div> <div style="display: flex; flex-direction: row; padding: 4px"> <div id="{pre}_btn">Show content</div> <div style="flex: 1"></div> </div> <div id="{pre}_content" style="display: none; margin-top: 12px">{self.content}</div> </div> <script> console.log("setup script ran for {pre}"); {pre}_btn = document.querySelector("#{pre}_btn"); {pre}_content = document.querySelector("#{pre}_content"); {pre}_displayed = false; {pre}_btn.onclick = () => {{ {pre}_displayed = !{pre}_displayed; {pre}_btn.innerHTML = {pre}_displayed ? "Hide content" : "Show content"; {pre}_content.style.display = {pre}_displayed ? "block" : "none"; }}; </script> <!-- k1lib.Toggle end -->""" # Toggle return html # Toggle def _jsF(self, meta): # Toggle fIdx = cli.init._jsFAuto(); dataIdx = cli.init._jsDAuto(); pre = cli.init._jsDAuto() # Toggle return f"""{fIdx} = ({dataIdx}) => {{ return unescape(` <!-- k1lib.Toggle start --> <style> #{pre}_btn {{ cursor: pointer; padding: 6px 12px; background: #eee; margin-right: 5px; color: #000; user-select: none; -webkit-user-select: none; /* Safari */ -ms-user-select: none; /* IE 10+ */ box-shadow: 0 3px 5px rgb(0,0,0,0.3); border-radius: 18px; }} #{pre}_btn:hover {{ box-shadow: 0 3px 5px rgb(0,0,0,0.6); background: #4caf50; color: #fff; }} </style> <div> <div style="display: flex; flex-direction: row; padding: 4px"> <div id="{pre}_btn">Show content</div> <div style="flex: 1"></div> </div> <div id="{pre}_content" style="display: none; margin-top: 12px">${{{dataIdx}}}</div> </div> %3Cscript%3E (async () => {{ console.log("setup script ran for {pre}"); {pre}_btn = document.querySelector("#{pre}_btn"); {pre}_content = document.querySelector("#{pre}_content"); {pre}_displayed = false; {pre}_btn.onclick = () => {{ {pre}_displayed = !{pre}_displayed; {pre}_btn.innerHTML = {pre}_displayed ? "Hide content" : "Show content"; {pre}_content.style.display = {pre}_displayed ? "block" : "none"; }}; }})(); %3C/script%3E`) }} <!-- k1lib.Toggle end -->""", fIdx # Toggle
k1lib.cli.init.addAtomic(Toggle) # Toggle
[docs]def ToggleImage(): # ToggleImage """This function is sort of legacy. It's just ``img | toHtml() | viz.Toggle()`` really""" # ToggleImage return cli.toHtml() | Toggle() # ToggleImage
[docs]class Scroll(cli.BaseCli): # Scroll _idx = k1lib.AutoIncrement.random() # Scroll
[docs] def __init__(self, height=300): # Scroll """Creates a new preview html component. If content is too long, then it will only show the first 500px, then have a button to expand and view the rest. Example:: x = np.linspace(-2, 2); plt.plot(x, x ** 2) plt.gcf() | toImg() | toHtml() | viz.Scroll() This will plot a preview of a graph :param height: height of the parent container""" # Scroll self.content:str = "" # html string # Scroll self.height = height; self._enteredRor = False # Scroll
[docs] def __ror__(self, it): self._enteredRor = True; self.content = it; return self # Scroll
def __or__(self, it): # see discussion on Carousel() # Scroll if self._enteredRor: return it.__ror__(self) # Scroll else: return super().__or__(it) # Scroll def _repr_html_(self): # Scroll pre = f"k1scr_{Scroll._idx()}" # Scroll html = f"""<!-- k1lib.Scroll start --> <div style="max-height: {self.height}px; overflow-y: auto">{self.content}</div> <!-- k1lib.Scroll end -->""" # Scroll return html # Scroll def _jsF(self, meta): # Scroll fIdx = cli.init._jsFAuto(); dataIdx = cli.init._jsDAuto(); pre = cli.init._jsDAuto() # Scroll return f"""{fIdx} = ({dataIdx}) => {{ return unescape(`<!-- k1lib.Scroll start --> <div style="max-height: {self.height}px; overflow-y: auto">${{{dataIdx}}}</div> <!-- k1lib.Scroll end -->`) }}""", fIdx # Scroll
k1lib.cli.init.addAtomic(Scroll) # Scroll
[docs]def confusionMatrix(matrix:torch.Tensor, categories:List[str]=None, **kwargs): # confusionMatrix """Plots a confusion matrix. Example:: k1lib.viz.confusionMatrix(torch.rand(5, 5), ["a", "b", "c", "d", "e"]) .. image:: images/confusionMatrix.png :param matrix: 2d matrix of shape (n, n) :param categories: list of string categories :param kwargs: keyword args passed into :meth:`plt.figure`""" # confusionMatrix if isinstance(matrix, torch.Tensor): matrix = matrix.numpy() # confusionMatrix if categories is None: categories = [f"{e}" for e in range(len(matrix))] # confusionMatrix fig = plt.figure(**{"dpi":100, **kwargs}); ax = fig.add_subplot(111) # confusionMatrix cax = ax.matshow(matrix); fig.colorbar(cax) # confusionMatrix # confusionMatrix with k1lib.ignoreWarnings(): # confusionMatrix ax.set_xticklabels([''] + categories, rotation=90) # confusionMatrix ax.set_yticklabels([''] + categories) # confusionMatrix # confusionMatrix # Force label at every tick # confusionMatrix ax.xaxis.set_major_locator(mpl.ticker.MultipleLocator(1)) # confusionMatrix ax.yaxis.set_major_locator(mpl.ticker.MultipleLocator(1)) # confusionMatrix ax.xaxis.set_label_position('top') # confusionMatrix plt.xlabel("Predictions"); plt.ylabel("Ground truth") # confusionMatrix
[docs]def FAnim(fig, f, frames, *args, **kwargs): # FAnim """Matplotlib function animation, 60fps. Example:: # line below so that the animation is displayed in the notebook. Included in :mod:`k1lib.imports` already, so you don't really have to do this! plt.rcParams["animation.html"] = "jshtml" x = np.linspace(-2, 2); y = x**2 fig, ax = plt.subplots() plt.close() # close cause it'll display 1 animation, 1 static if we don't do this def f(frame): ax.clear() ax.set_ylim(0, 4); ax.set_xlim(-2, 2) ax.plot(x[:frame], y[:frame]) k1lib.FAnim(fig, f, len(x)) # plays animation in cell :param fig: figure object from `plt.figure(...)` command :param f: function that accepts 1 frame from `frames`. :param frames: number of frames, or iterator, to pass into function""" # FAnim return partial(mpl.animation.FuncAnimation, interval=1000/30)(fig, f, frames, *args, **kwargs) # FAnim
from k1lib.cli import op # FAnim
[docs]def mask(img:torch.Tensor, act:torch.Tensor) -> torch.Tensor: # mask """Shows which part of the image the network is focusing on. :param img: the image, expected to have dimension of (3, h, w) :param act: the activation, expected to have dimension of (x, y), and with elements from 0 to 1.""" # mask *_, h, w = img.shape # mask mask = act[None,] | nn.AdaptiveAvgPool2d([h//16, w//16]) | nn.AdaptiveAvgPool2d([h//8, w//8]) | nn.AdaptiveAvgPool2d([h, w]) # mask return mask * img | op().permute(1, 2, 0) # mask
class PDF(object): # PDF def __init__(self, pdf:str=None, size=(700,500)): # PDF """Displays pdf in the notebook. Example:: viz.PDF("a.pdf") "a.pdf" | viz.PDF() viz.PDF("a.pdf", (700, 500)) "a.pdf" | viz.PDF(size=(700, 500)) If you're exporting this notebook as html, then you have to make sure you place the generated html file in the correct directory so that it can reference those pdf files. :param pdf: relative path to pdf file""" # PDF self.pdf = pdf; self.size = size # PDF def __ror__(self, pdf): self.pdf = pdf; return self # PDF def _repr_html_(self): return '<iframe src={0} width={1[0]} height={1[1]}></iframe>'.format(self.pdf, self.size) # PDF def _repr_latex_(self): return r'\includegraphics[width=1.0\textwidth]{{{0}}}'.format(self.pdf) # PDF class Html(str): # Html """A string that will display rich html to a notebook""" # Html def _repr_html_(self): return self # Html class onload(cli.BaseCli): # onload def __init__(self): # onload """Returns html code that will run the captured clis when that html is loaded. Example:: 3 | (toJsFunc() | (viz.onload() | aS("x+3"))) | op().interface() This displays html that will execute "x+3", then inject it into the main body. At first glance, this seems useless. A much simpler solution exists:: 3 | (toJsFunc() | aS("x+3")) | op().interface() This would pretty much do the exact same thing. But there's a subtle difference. The jsFunc output of the first line (the one with onload()) is some complex html, and of the second line (without onload()) is just a single number. This is useful in cases where you don't want to render something right away (as that can take time/space), but want to defer rendering till later. This is roughly what the first line generates this html: .. code-block:: html <div id="content">(Loading...)</div> <script> const data = 3; const customFunction = (x) => x+3; function onload() { document.querySelector("#content").innerHTML = customFunction(data); } onload(); </script> While the second line generates this html: .. code-block:: html 6 These html is returned by the blocks ``(viz.onload() | aS("x+3"))`` and ``aS("x+3")`` in JS, respectively. The value of this is that your custom function might do something that generates a whole bunch of html (like fetching an image somewhere, then return the base64-encoded image tag). If you were to do it the normal way, then you would execute your function, return the giant html to display. Meanwhile, if you use this cli, then when you're moving html around, it's relatively lightweight, and only when that html is embedded into a page will your custom function captured by :class:`onload` execute, and display outwards. Honestly this feels like another implementation of :class:`~k1lib.cli.kjs.toJsFunc` """ # onload super().__init__(capture=True) # onload def __ror__(self, it:str): # onload jsF = it | (cli.toJsFunc() | self.capturedSerial); pre = cli.init._jsDAuto() # this impl below doesn't quite do what this cli claims to do, so throws error for now # onload return Html(f""" <div id='{pre}_div'>(Loading...)</div> <script> {jsF.fn} setTimeout(async () => {{ document.querySelector('#{pre}_div').innerHTML = await {jsF.fIdx}(); }}, 10); </script>""") # onload def _jsF(self, meta): # onload fIdx = cli.init._jsFAuto(); dataIdx = cli.init._jsDAuto(); pre = cli.init._jsDAuto(); # onload header, fn, _async = k1lib.kast.asyncGuard(self.capturedSerial._jsF(meta)) # onload return f"""//k1_moveOutStart{header}\n{dataIdx} = null;//k1_moveOutEnd {fIdx} = (x) => {{ // returns html string that will run the function on load {dataIdx} = x; return unescape(`<div id='{pre}_div'></div> %3Cscript%3E setTimeout({'async ' if _async else ''}() => {{ console.log("executed"); document.querySelector('#{pre}_div').innerHTML = {'await ' if _async else ''}{fn}({dataIdx}) }}, 100); %3C/script%3E`); }};""", fIdx # onload