# AUTOGENERATED FILE! PLEASE DON'T EDIT
import k1lib as _k1lib, base64 as _base64, io as _io
import matplotlib.pyplot as _plt, random as _random, numpy as _np
from typing import Callable as _Callable, List as _List, Union as _Union
from functools import partial as _partial, update_wrapper as _update_wrapper
class _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."""
    def __init__(self, sliceablePlot:"SliceablePlot", name:str):
        """
:param sliceablePlot: the parent plot
:param name: the decorator's name, like "yscale" """
        self.sliceablePlot = sliceablePlot
        self.name = name; self.args, self.kwargs = None, None
    def __call__(self, *args, **kwargs):
        """Stores all args, then return the parent :class:`SliceablePlot`"""
        self.args = args; self.kwargs = kwargs; return self.sliceablePlot
    def run(self): getattr(_plt, self.name)(*self.args, **self.kwargs)
[docs]class 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])
    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?"""
    def __init__(self, plotF:_Callable[[slice], None], slices:_Union[slice, _List[slice]]=slice(None), plotDecorators:_List[_PlotDecorator]=[], docs=""):
        self.plotF = plotF
        self.slices = [slices] if isinstance(slices, slice) else slices
        self.docs = docs; self.plotDecorators = list(plotDecorators)
[docs]    @staticmethod
    def decorate(f):
        """Decorates a plotting function so that it becomes a
SliceablePlot."""
        answer = _partial(SliceablePlot, plotF=f)
        _update_wrapper(answer, f)
        return answer 
    @property
    def squeezedSlices(self) -> _Union[_List[slice], slice]:
        """If :attr:`slices` only has 1 element, then return that
element, else return the entire list."""
        return _k1lib.squeeze(self.slices)
    def __getattr__(self, attr):
        if attr.startswith("_"): raise AttributeError()
        # automatically assume the attribute is a plt.attr method
        dec = _PlotDecorator(self, attr)
        self.plotDecorators.append(dec); return dec
    def __getitem__(self, idx):
        if type(idx) == slice:
            return SliceablePlot(self.plotF, [idx], self.plotDecorators, self.docs)
        if type(idx) == tuple and all([isinstance(elem, slice) for elem in idx]):
            return SliceablePlot(self.plotF, idx, self.plotDecorators, self.docs)
        raise Exception(f"Don't understand {idx}")
    def __repr__(self):
        self.plotF(self.squeezedSlices)
        for ax in _plt.gcf().get_axes():
            _plt.sca(ax)
            for decorator in self.plotDecorators: decorator.run()
        _plt.show()
        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}""" 
[docs]def plotSegments(x:_List[float], y:_List[float], states:_List[int], colors:_List[str]=None):
    """Plots a line graph, with multiple segments with different colors.
: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 to display for each states
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
"""
    if x is None: x = range(len(y))
    if colors is None: colors = ["tab:blue", "tab:red", "tab:green", "tab:orange", "tab:purple", "tab:brown"][:len(x)]
    _x = []; _y = []; state = -1; count = -1 # stretchs, and bookkeeping nums
    lx = None; ly = None # last x and y from last stretch, for plot autocompletion
    while count + 1 < len(x):
        count += 1
        if state != states[count]:
            if len(_x) > 0 and state >= 0:
                if lx != None: _x = [lx] + _x; _y = [ly] + _y
                _plt.plot(_x, _y, colors[state]); lx = _x[-1]; ly = _y[-1]
            _x = [x[count]]; _y = [y[count]]; state = states[count]
        else: _x.append(x[count]); _y.append(y[count])
    if len(_x) > 0 and state >= 0:
        if lx != None: _x = [lx] + _x; _y = [ly] + _y
        _plt.plot(_x, _y, colors[state]) 
[docs]class Carousel:
    _idx:int = _random.randint(0, 1e6)
[docs]    def __init__(self):
        """Creates a new Carousel. You can then add images and whatnot.
Will even work even when you export the notebook as html. Example::
    import numpy as np, matplotlib.pyplot as plt, k1lib
    c = k1lib.viz.Carousel()
    x = np.linspace(-2, 2); plt.plot(x, x ** 2); c.savePlt()
    x = np.linspace(-1, 3); plt.plot(x, x ** 2); c.savePlt()
    c # displays in notebook cell"""
        self.imgs:_List[str] = [] # all base64 imgs
        self.defaultFormat = "jpeg" 
[docs]    def save(self, f:_Callable[[_io.BytesIO], None]):
        """Generic image save function. Treat :class:`io.BytesIO` as if it's
a file when you're doing this::
    
    with open("file.txt") as f:
        pass # "f" is similar to io.BytesIO
So, you can do stuff like::
    import matplotlib.pyplot as plt, numpy as np
    x = np.linspace(-2, 2)
    plt.plot(x, x**2)
    c = k1lib.viz.Carousel()
    c.save(lambda io: plt.savefig(io, format="png"))
:param f: lambda that provides a :class:`io.BytesIO` for you to write to
"""
        byteArr = _io.BytesIO(); f(byteArr); byteArr.seek(0)
        self.imgs.append(_base64.b64encode(byteArr.read()).decode()) 
[docs]    def savePlt(self):
        """Saves current plot from matplotlib"""
        self.save(lambda byteArr: _plt.savefig(byteArr, format=self.defaultFormat))
        _plt.clf() 
[docs]    def savePIL(self, image):
        """Saves a PIL image"""
        self.save(lambda byteArr: image.save(byteArr, format=self.defaultFormat)) 
    def _repr_html_(self):
        imgs = [f"\"<img src='data:image/{self.defaultFormat};base64, {img}' />\"" for img in self.imgs]
        idx = Carousel._idx; Carousel._idx += 1
        pre = f"k1c_{idx}"
        html = f"""
<style>
    .{pre}_btn {{
        cursor: pointer;
        padding: 10px 15px;
        background: #9e9e9e;
        float: left;
        margin-right: 5px;
        color: #000;
        user-select: none
    }}
    .{pre}_btn:hover {{
        background: #4caf50;
        color: #fff;
    }}
</style>
<div>
    <div id="{pre}_prevBtn" class="{pre}_btn">Prev</div>
    <div id="{pre}_nextBtn" class="{pre}_btn">Next</div>
    <div style="clear:both"/>
    <div id="{pre}_status" style="padding: 10px"></div>
</div>
<div id="{pre}_imgContainer"></div>
<script>
    {pre}_imgs = [{','.join(imgs)}];
    {pre}_imgIdx = 0;
    function {pre}_display() {{
        document.querySelector("#{pre}_imgContainer").innerHTML = {pre}_imgs[{pre}_imgIdx];
        document.querySelector("#{pre}_status").innerHTML = "Image: " + ({pre}_imgIdx + 1) + "/" + {pre}_imgs.length;
    }};
    document.querySelector("#{pre}_prevBtn").onclick = () => {{
        {pre}_imgIdx -= 1;
        {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();
    }};
    {pre}_display();
</script>
        """
        return html