Source code for spb.backends.bokeh

import os
from spb.defaults import cfg
from spb.backends.base_backend import Plot
from sympy.external import import_module
import warnings

def compute_streamlines(x, y, u, v, density=1.0):
    """Return streamlines of a vector flow.

    * x and y are 1d arrays defining an *evenly spaced* grid.
    * u and v are 2d arrays (shape [y,x]) giving velocities.
    * density controls the closeness of the streamlines.

    Credit: https://docs.bokeh.org/en/latest/docs/gallery/quiver.html
    """
    np = import_module('numpy')

    ## Set up some constants - size of the grid used.
    NGX = len(x)
    NGY = len(y)

    ## Constants used to convert between grid index coords and user coords.
    DX = x[1] - x[0]
    DY = y[1] - y[0]
    XOFF = x[0]
    YOFF = y[0]

    ## Now rescale velocity onto axes-coordinates
    u = u / (x[-1] - x[0])
    v = v / (y[-1] - y[0])
    speed = np.sqrt(u * u + v * v)
    ## s (path length) will now be in axes-coordinates, but we must
    ## rescale u for integrations.
    u *= NGX
    v *= NGY
    ## Now u and v in grid-coordinates.

    NBX = int(30 * density)
    NBY = int(30 * density)

    blank = np.zeros((NBY, NBX))

    bx_spacing = NGX / float(NBX - 1)
    by_spacing = NGY / float(NBY - 1)

    def blank_pos(xi, yi):
        return int((xi / bx_spacing) + 0.5), int((yi / by_spacing) + 0.5)

    def value_at(a, xi, yi):
        if type(xi) == np.ndarray:
            x = xi.astype(int)
            y = yi.astype(int)
        else:
            x = int(xi)
            y = int(yi)
        a00 = a[y, x]
        a01 = a[y, x + 1]
        a10 = a[y + 1, x]
        a11 = a[y + 1, x + 1]
        xt = xi - x
        yt = yi - y
        a0 = a00 * (1 - xt) + a01 * xt
        a1 = a10 * (1 - xt) + a11 * xt
        return a0 * (1 - yt) + a1 * yt

    def rk4_integrate(x0, y0):
        ## This function does RK4 forward and back trajectories from
        ## the initial conditions, with the odd 'blank array'
        ## termination conditions. TODO tidy the integration loops.

        def f(xi, yi):
            dt_ds = 1.0 / value_at(speed, xi, yi)
            ui = value_at(u, xi, yi)
            vi = value_at(v, xi, yi)
            return ui * dt_ds, vi * dt_ds

        def g(xi, yi):
            dt_ds = 1.0 / value_at(speed, xi, yi)
            ui = value_at(u, xi, yi)
            vi = value_at(v, xi, yi)
            return -ui * dt_ds, -vi * dt_ds

        check = lambda xi, yi: xi >= 0 and xi < NGX - 1 and yi >= 0 and yi < NGY - 1

        bx_changes = []
        by_changes = []

        ## Integrator function
        def rk4(x0, y0, f):
            ds = 0.01  # min(1./NGX, 1./NGY, 0.01)
            stotal = 0
            xi = x0
            yi = y0
            xb, yb = blank_pos(xi, yi)
            xf_traj = []
            yf_traj = []
            while check(xi, yi):
                # Time step. First save the point.
                xf_traj.append(xi)
                yf_traj.append(yi)
                # Next, advance one using RK4
                try:
                    k1x, k1y = f(xi, yi)
                    k2x, k2y = f(xi + 0.5 * ds * k1x, yi + 0.5 * ds * k1y)
                    k3x, k3y = f(xi + 0.5 * ds * k2x, yi + 0.5 * ds * k2y)
                    k4x, k4y = f(xi + ds * k3x, yi + ds * k3y)
                except IndexError:
                    # Out of the domain on one of the intermediate steps
                    break
                xi += ds * (k1x + 2 * k2x + 2 * k3x + k4x) / 6.0
                yi += ds * (k1y + 2 * k2y + 2 * k3y + k4y) / 6.0
                # Final position might be out of the domain
                if not check(xi, yi):
                    break
                stotal += ds
                # Next, if s gets to thres, check blank.
                new_xb, new_yb = blank_pos(xi, yi)
                if new_xb != xb or new_yb != yb:
                    # New square, so check and colour. Quit if required.
                    if blank[new_yb, new_xb] == 0:
                        blank[new_yb, new_xb] = 1
                        bx_changes.append(new_xb)
                        by_changes.append(new_yb)
                        xb = new_xb
                        yb = new_yb
                    else:
                        break
                if stotal > 2:
                    break
            return stotal, xf_traj, yf_traj

        integrator = rk4

        sf, xf_traj, yf_traj = integrator(x0, y0, f)
        sb, xb_traj, yb_traj = integrator(x0, y0, g)
        stotal = sf + sb
        x_traj = xb_traj[::-1] + xf_traj[1:]
        y_traj = yb_traj[::-1] + yf_traj[1:]

        ## Tests to check length of traj. Remember, s in units of axes.
        if len(x_traj) < 1:
            return None
        if stotal > 0.2:
            initxb, inityb = blank_pos(x0, y0)
            blank[inityb, initxb] = 1
            return x_traj, y_traj
        else:
            for xb, yb in zip(bx_changes, by_changes):
                blank[yb, xb] = 0
            return None

    ## A quick function for integrating trajectories if blank==0.
    trajectories = []

    def traj(xb, yb):
        if xb < 0 or xb >= NBX or yb < 0 or yb >= NBY:
            return
        if blank[yb, xb] == 0:
            t = rk4_integrate(xb * bx_spacing, yb * by_spacing)
            if t is not None:
                trajectories.append(t)

    ## Now we build up the trajectory set. I've found it best to look
    ## for blank==0 along the edges first, and work inwards.
    for indent in range((max(NBX, NBY)) // 2):
        for xi in range(max(NBX, NBY) - 2 * indent):
            traj(xi + indent, indent)
            traj(xi + indent, NBY - 1 - indent)
            traj(indent, xi + indent)
            traj(NBX - 1 - indent, xi + indent)

    xs = [np.array(t[0]) * DX + XOFF for t in trajectories]
    ys = [np.array(t[1]) * DY + YOFF for t in trajectories]

    return xs, ys


[docs]class BokehBackend(Plot): """ A backend for plotting SymPy's symbolic expressions using Bokeh. This implementation only supports 2D plots. Parameters ========== aspect : str Set the aspect ratio of a 2D plot. Default to ``None``. Set it to ``"equal"`` to sets equal spacing on the axis. rendering_kw : dict, optional A dictionary of keywords/values which is passed to Matplotlib's plot functions to customize the appearance of lines, surfaces, images, contours, quivers, streamlines... To learn more about customization: * Refer to: - [#fn1]_ to customize lines plots. Default to: ``dict(line_width = 2)``. - [#fn6]_ to customize scatter plots. Default to: ``dict(marker = "circle")``. * Default options for quiver plots: .. code-block:: python dict( scale = 1, pivot = "mid", # "mid", "tip" or "tail" arrow_heads = True, # show/hide arrow line_width = 1 ) * Default options for streamline plots: ``dict(line_width=2, line_alpha=0.8)`` theme : str, optional Set the theme. Find more Bokeh themes at [#fn2]_ . annotations : list, optional A list of dictionaries specifying the type of annotation required. The keys in the dictionary should be equivalent to the arguments of the `bokeh.models.LabelSet` class. This feature is experimental. It might get removed in the future. markers : list, optional A list of dictionaries specifying the type the markers required. The keys in the dictionary should be equivalent to the arguments of the `bokeh.models.Scatter` class. This feature is experimental. It might get removed in the future. rectangles : list, optional A list of dictionaries specifying the dimensions of the rectangles to be plotted. The ``"args"`` key must contain the `bokeh.models.ColumnDataSource` object containing the data. All other keyword arguments will be passed to the `bokeh.models.Rect` class. This feature is experimental. It might get removed in the future. fill : dict, optional A dictionary specifying the type of color filling required in the plot. The keys in the dictionary should be equivalent to the arguments of the `bokeh.models.VArea` class. This feature is experimental. It might get removed in the future. References ========== .. [#fn1] https://docs.bokeh.org/en/latest/docs/reference/plotting.html#bokeh.plotting.Figure.line .. [#fn2] https://docs.bokeh.org/en/latest/docs/reference/themes.html .. [#fn6] https://docs.bokeh.org/en/latest/docs/reference/plotting/figure.html#bokeh.plotting.Figure.scatter See also ======== Plot, MatplotlibBackend, PlotlyBackend, K3DBackend """ _library = "bokeh" _allowed_keys = Plot._allowed_keys + [ "markers", "annotations", "fill", "rectangles"] colorloop = [] colormaps = [] cyclic_colormaps = [] def __new__(cls, *args, **kwargs): return object.__new__(cls) def __init__(self, *args, **kwargs): self.bokeh = import_module( 'bokeh', import_kwargs={'fromlist': ['models', 'events', 'plotting', 'io', 'palettes', 'embed', 'resources']}, warn_not_installed=True, min_module_version='2.3.0') bp = self.bokeh.palettes cc = import_module( 'colorcet', min_module_version='3.0.0') matplotlib = import_module( 'matplotlib', import_kwargs={'fromlist': ['pyplot', 'cm']}, min_module_version='1.1.0', catch=(RuntimeError,)) cm = matplotlib.cm self.colorloop = bp.Category10[10] self.colormaps = [cc.bmy, "aggrnyl", cc.kbc, cc.bjy, "plotly3"] self.cyclic_colormaps = [cm.hsv, cm.twilight, cc.cyclic_mygbm_30_95_c78_s25] self._init_cyclers() super().__init__(*args, **kwargs) if self.polar_axis: raise ValueError("BokehBackend doesn't support polar axis.") # set labels self._use_latex = kwargs.get("use_latex", cfg["bokeh"]["use_latex"]) self._set_labels() self._theme = kwargs.get("theme", cfg["bokeh"]["theme"]) self._run_in_notebook = False if self._get_mode() == 0: self._run_in_notebook = True self.bokeh.io.output_notebook(hide_banner=True) if ((len([s for s in self._series if s.is_2Dline]) > 10) and (not type(self).colorloop) and not ("process_piecewise" in kwargs.keys())): # add colors if needed self.colorloop = bp.Category20[20] self._handles = dict() # empty plots (len(series)==0) should only have x, y tooltips TOOLTIPS = [("x", "$x"), ("y", "$y")] if len(self.series) > 0: if all([s.is_parametric for s in self.series]): # with parametric plots, also visualize the parameter TOOLTIPS += [("u", "@us")] if any([s.is_complex and s.is_domain_coloring for s in self.series]): # with complex domain coloring, shows the magnitude and phase # in the tooltip TOOLTIPS += [("Abs", "@abs"), ("Arg", "@arg")] sizing_mode = cfg["bokeh"]["sizing_mode"] if any(s.is_complex and s.is_domain_coloring for s in self.series): # for complex domain coloring sizing_mode = None kw = dict( title=self.title, x_axis_label=self.xlabel if self.xlabel else "x", y_axis_label=self.ylabel if self.ylabel else "y", sizing_mode="fixed" if self.size else sizing_mode, width=int(self.size[0]) if self.size else 600, height=int(self.size[1]) if self.size else 400, x_axis_type=self.xscale, y_axis_type=self.yscale, tools="pan,wheel_zoom,box_zoom,reset,hover,save", tooltips=TOOLTIPS, match_aspect=True if self.aspect == "equal" else False, ) if self.xlim: kw["x_range"] = self.xlim if self.ylim: kw["y_range"] = self.ylim self._fig = self.bokeh.plotting.figure(**kw) self.grid = kwargs.get("grid", cfg["bokeh"]["grid"]) self._fig.grid.visible = self.grid if cfg["bokeh"]["show_minor_grid"]: self._fig.grid.minor_grid_line_alpha = cfg["bokeh"]["minor_grid_line_alpha"] self._fig.grid.minor_grid_line_color = self._fig.grid.grid_line_color[0] self._fig.grid.minor_grid_line_dash = cfg["bokeh"]["minor_grid_line_dash"] @property def fig(self): """Returns the figure.""" if len(self.series) != len(self._fig.renderers): # if the backend was created without showing it self.process_series() return self._fig def process_series(self): """ Loop over data series, generates numerical data and add it to the figure. """ self._process_series(self._series) def _set_piecewise_color(self, s, color): """Set the color to the given series""" if "color" not in s.rendering_kw: # only set the color if the user didn't do that already s.rendering_kw["color"] = color if s.is_point and (not s.is_filled): s.rendering_kw["fill_color"] = "white" @staticmethod def _do_sum_kwargs(p1, p2): kw = p1._copy_kwargs() kw["theme"] = p1._theme return kw def _process_series(self, series): np = import_module('numpy') merge = self.merge self._init_cyclers() # clear figure. Must clear both the renderers as well as the # colorbars which are added to the right side. self._fig.renderers = [] self._fig.right = [] for i, s in enumerate(series): kw = None if s.is_2Dline: if s.is_parametric and s.use_cm: x, y, param = s.get_data() colormap = ( next(self._cyccm) if self._use_cyclic_cm(param, s.is_complex) else next(self._cm) ) ds, line, cb, kw = self._create_gradient_line( x, y, param, colormap, s.get_label(self._use_latex), s.rendering_kw, s.is_point) self._fig.add_glyph(ds, line) if self.legend: self._handles[i] = cb self._fig.add_layout(cb, "right") else: if s.is_parametric: x, y, param = s.get_data() source = {"xs": x, "ys": y, "us": param} else: x, y = s.get_data() source = { "xs": x if not s.is_polar else y * np.cos(x), "ys": y if not s.is_polar else y * np.sin(x) } color = next(self._cl) if s.line_color is None else s.line_color lkw = dict(line_width=2, legend_label=s.get_label(self._use_latex), color=color) if not s.is_point: kw = merge({}, lkw, s.rendering_kw) self._fig.line("xs", "ys", source=source, **kw) else: lkw["size"] = 8 lkw["marker"] = "circle" if not s.is_filled: lkw["fill_color"] = "white" kw = merge({}, lkw, s.rendering_kw) self._fig.scatter("xs", "ys", source=source, **kw) elif s.is_contour and (not s.is_complex): if s.is_polar: raise NotImplementedError() x, y, z = s.get_data() x, y, zz = [t.flatten() for t in [x, y, z]] minx, miny, minz = min(x), min(y), min(zz) maxx, maxy, maxz = max(x), max(y), max(zz) cm = next(self._cm) ckw = dict(palette=cm) kw = merge({}, ckw, s.rendering_kw) if not s.is_filled: warnings.warn("Bokeh does not support line contours.") self._fig.image( image=[z], x=minx, y=miny, dw=abs(maxx - minx), dh=abs(maxy - miny), **kw ) colormapper = self.bokeh.models.LinearColorMapper( palette=cm, low=minz, high=maxz) cbkw = dict(width=8, title=s.get_label(self._use_latex)) colorbar = self.bokeh.models.ColorBar( color_mapper=colormapper, **cbkw) self._fig.add_layout(colorbar, "right") self._handles[i] = colorbar elif s.is_2Dvector: if s.is_streamlines: x, y, u, v = s.get_data() sqk = dict(color=next(self._cl), line_width=2, line_alpha=0.8) stream_kw = s.rendering_kw.copy() density = stream_kw.pop("density", 2) kw = merge({}, sqk, stream_kw) xs, ys = compute_streamlines( x[0, :], y[:, 0], u, v, density=density) self._fig.multi_line(xs, ys, **kw) else: x, y, u, v = s.get_data() mag = np.sqrt(u**2 + v**2) u0, v0 = [t.copy() for t in [u, v]] if s.normalize: u, v = [t / mag for t in [u, v]] data, quiver_kw = self._get_quivers_data(x, y, u, v, **s.rendering_kw.copy()) color_val = mag if s.color_func is not None: color_val = s.eval_color_func(x, y, u0, v0) color_val = np.tile(color_val.flatten(), 3) data["color_val"] = color_val color_mapper = self.bokeh.models.LinearColorMapper( palette=next(self._cm), low=min(color_val), high=max(color_val)) # don't use color map if a scalar field is visible or if # use_cm=False line_color = ( {"field": "color_val", "transform": color_mapper} if ((not s.use_quiver_solid_color) and s.use_cm) else next(self._cl) ) source = self.bokeh.models.ColumnDataSource(data=data) qkw = dict(line_color=line_color, line_width=1, name=s.get_label(self._use_latex)) kw = merge({}, qkw, quiver_kw) glyph = self.bokeh.models.Segment( x0="x0", y0="y0", x1="x1", y1="y1", **kw) self._fig.add_glyph(source, glyph) if isinstance(line_color, dict): colorbar = self.bokeh.models.ColorBar( color_mapper=color_mapper, width=8, title=s.get_label(self._use_latex)) self._fig.add_layout(colorbar, "right") self._handles[i] = colorbar elif s.is_complex and s.is_domain_coloring and not s.is_3Dsurface: x, y, mag, angle, img, colors = s.get_data() img = self._get_img(img) source = self.bokeh.models.ColumnDataSource( { "image": [img], "abs": [mag], "arg": [angle], } ) self._fig.image_rgba( source=source, x=x.min(), y=y.min(), dw=x.max() - x.min(), dh=y.max() - y.min(), ) if colors is not None: # chroma/phase-colorbar cm1 = self.bokeh.models.LinearColorMapper( palette=[tuple(c) for c in colors], low=-np.pi, high=np.pi) ticks = [-np.pi, -np.pi / 2, 0, np.pi / 2, np.pi] labels = ["-Ï€", "-Ï€ / 2", "0", "Ï€ / 2", "Ï€"] colorbar1 = self.bokeh.models.ColorBar( color_mapper=cm1, title="Argument", ticker=self.bokeh.models.tickers.FixedTicker(ticks=ticks), major_label_overrides={k: v for k, v in zip(ticks, labels)}) self._fig.add_layout(colorbar1, "right") elif s.is_geometry: x, y = s.get_data() color = next(self._cl) pkw = dict(alpha=0.5, line_width=2, line_color=color, fill_color=color) kw = merge({}, pkw, s.rendering_kw) self._fig.patch(x, y, **kw) elif s.is_generic: if s.type == "markers": kw = merge({}, {"color": next(self._cl)}, s.rendering_kw) self._fig.scatter(*s.args, **kw) elif s.type == "annotations": self._fig.add_layout( self.bokeh.models.LabelSet(*s.args, **s.rendering_kw)) elif s.type == "fill": kw = merge({}, {"fill_color": next(self._cl)}, s.rendering_kw) self._fig.varea(*s.args, **kw) elif s.type == "rectangles": kw = merge({}, {"fill_color": next(self._cl)}, s.rendering_kw) glyph = self.bokeh.models.Rect(**kw) self._fig.add_glyph(*s.args, glyph) else: raise NotImplementedError( "{} is not supported by {}\n".format(type(s), type(self).__name__) + "Bokeh only supports 2D plots." ) if len(self._fig.legend) > 0: self._fig.legend.visible = self.legend # interactive legend self._fig.legend.click_policy = "hide" self._fig.add_layout(self._fig.legend[0], "right") def _get_img(self, img): np = import_module('numpy') new_img = np.zeros(img.shape[:2], dtype=np.uint32) pixel = new_img.view(dtype=np.uint8).reshape((*img.shape[:2], 4)) for i in range(img.shape[1]): for j in range(img.shape[0]): pixel[j, i] = [*img[j, i], 255] return new_img def _get_segments(self, x, y, u): # MultiLine works with line segments, not with line points! :| xs = [x[i - 1 : i + 1] for i in range(1, len(x))] ys = [y[i - 1 : i + 1] for i in range(1, len(y))] # let n be the number of points. Then, the number of segments # will be (n - 1). Therefore, we remove one parameter. If n is # sufficiently high, there shouldn't be any noticeable problem in # the visualization. us = u[:-1] return xs, ys, us def _create_gradient_line(self, x, y, u, colormap, name, line_kw, is_point=False): merge = self.merge if not is_point: xs, ys, us = self._get_segments(x, y, u) else: xs, ys, us = x, y, u color_mapper = self.bokeh.models.LinearColorMapper( palette=colormap, low=min(us), high=max(us)) data_source = self.bokeh.models.ColumnDataSource( dict(xs=xs, ys=ys, us=us)) lkw = dict( line_width=2, name=name, line_color={"field": "us", "transform": color_mapper}, ) kw = merge({}, lkw, line_kw) if not is_point: glyph = self.bokeh.models.MultiLine(xs="xs", ys="ys", **kw) else: glyph = self.bokeh.models.Scatter(x="xs", y="ys", **kw) colorbar = self.bokeh.models.ColorBar( color_mapper=color_mapper, title=name, width=8) return data_source, glyph, colorbar, kw def update_interactive(self, params): """Implement the logic to update the data generated by interactive-widget plots. Parameters ========== params : dict Map parameter-symbols to numeric values. """ np = import_module('numpy') rend = self.fig.renderers if len(rend) != len(self.series): self._process_series(self.series) for i, s in enumerate(self.series): if s.is_interactive: self.series[i].params = params if s.is_2Dline and s.is_parametric and s.use_cm: x, y, param = self.series[i].get_data() if not s.is_point: xs, ys, us = self._get_segments(x, y, param) else: xs, ys, us = x, y, param rend[i].data_source.data.update({"xs": xs, "ys": ys, "us": us}) if i in self._handles.keys(): cb = self._handles[i] cb.color_mapper.update(low=min(us), high=max(us)) elif s.is_2Dline: if s.is_parametric: x, y, param = self.series[i].get_data() source = {"xs": x, "ys": y, "us": param} else: x, y = self.series[i].get_data() source = { "xs": x if not s.is_polar else y * np.cos(x), "ys": y if not s.is_polar else y * np.sin(x) } rend[i].data_source.data.update(source) elif s.is_contour and (not s.is_complex): x, y, z = s.get_data() minx, miny, minz = x.min(), y.min(), z.min() maxx, maxy, maxz = x.max(), y.max(), z.max() cb = self._handles[i] rend[i].data_source.data.update({"image": [z]}) rend[i].glyph.x = minx rend[i].glyph.y = miny rend[i].glyph.dw = abs(maxx - minx) rend[i].glyph.dh = abs(maxy - miny) cb.color_mapper.update(low=minz, high=maxz) elif s.is_2Dvector: x, y, u, v = s.get_data() if s.is_streamlines: density = s.rendering_kw.copy().pop("density", 2) xs, ys = compute_streamlines( x[0, :], y[:, 0], u, v, density=density ) rend[i].data_source.data.update({"xs": xs, "ys": ys}) else: quiver_kw = s.rendering_kw.copy() mag = np.sqrt(u**2 + v**2) u0, v0 = [t.copy() for t in [u, v]] if s.normalize: u, v = [t / mag for t in [u, v]] data, quiver_kw = self._get_quivers_data( x, y, u, v, **quiver_kw ) color_val = mag if s.color_func is not None: color_val = s.eval_color_func(x, y, u0, v0) color_val = np.tile(color_val.flatten(), 3) data["color_val"] = color_val rend[i].data_source.data.update(data) line_color = rend[i].glyph.line_color if (not s.use_quiver_solid_color) and s.use_cm: # update the colorbar cmap = line_color["transform"].palette color_val = data["color_val"] color_mapper = self.bokeh.models.LinearColorMapper( palette=cmap, low=min(color_val), high=max(color_val)) line_color = quiver_kw.get( "line_color", { "field": "color_val", "transform": color_mapper }, ) rend[i].glyph.line_color = line_color cb = self._handles[i] cb.color_mapper.update( low=min(color_val), high=max(color_val)) elif s.is_complex and s.is_domain_coloring and not s.is_3Dsurface: x, y, mag, angle, img, _ = s.get_data() minx, miny = x.min(), y.min() maxx, maxy = x.max(), y.max() img = self._get_img(img) source = { "image": [img], "abs": [mag], "arg": [angle], } rend[i].data_source.data.update(source) rend[i].glyph.x = minx rend[i].glyph.y = miny rend[i].glyph.dw = abs(maxx - minx) rend[i].glyph.dh = abs(maxy - miny) elif s.is_geometry and (not s.is_2Dline): x, y = s.get_data() source = {"x": x, "y": y} rend[i].data_source.data.update(source) def save(self, path, **kwargs): """ Export the plot to a static picture or to an interactive html file. Refer to [#fn3]_ and [#fn4]_ to visualize all the available keyword arguments. Notes ===== 1. In order to export static pictures, the user also need to install the packages listed in [#fn5]_. 2. When exporting a fully portable html file, by default the necessary Javascript libraries will be loaded with a CDN. This creates the smallest file size possible, but it requires an internet connection in order to view/load the file and its dependencies. References ========== .. [#fn3] https://docs.bokeh.org/en/latest/docs/user_guide/export.html .. [#fn4] https://docs.bokeh.org/en/latest/docs/user_guide/embed.html .. [#fn5] https://docs.bokeh.org/en/latest/docs/reference/io.html#module-bokeh.io.export """ merge = self.merge ext = os.path.splitext(path)[1] if ext.lower() in [".htm", ".html"]: CDN = self.bokeh.resources.CDN file_html = self.bokeh.embed.file_html skw = dict(resources=CDN, title="Bokeh Plot") html = file_html(self.fig, **merge(skw, kwargs)) with open(path, 'w') as f: f.write(html) elif ext == ".svg": self._fig.output_backend = "svg" self.bokeh.io.export_svg(self.fig, filename=path) else: if ext == "": path += ".png" self._fig.output_backend = "canvas" self.bokeh.io.export_png(self._fig, filename=path) def show(self): """Visualize the plot on the screen.""" if len(self._fig.renderers) != len(self.series): self._process_series(self._series) # if the backend it running from a python interpreter, the server # wont' work. Hence, launch a static figure, which doesn't listen # to events (no pan-auto-update). curdoc = self.bokeh.io.curdoc curdoc().theme = self._theme self.bokeh.plotting.show(self._fig) def _get_quivers_data(self, xs, ys, u, v, **quiver_kw): """Compute the segments coordinates to plot quivers. Parameters ========== xs : np.ndarray A 2D numpy array representing the discretization in the x-coordinate ys : np.ndarray A 2D numpy array representing the discretization in the y-coordinate u : np.ndarray A 2D numpy array representing the x-component of the vector v : np.ndarray A 2D numpy array representing the x-component of the vector kwargs : dict, optional An optional Returns ======= data: dict A dictionary suitable to create a data source to be used with Bokeh's Segment. quiver_kw : dict A dictionary containing keywords to customize the appearance of Bokeh's Segment glyph """ np = import_module('numpy') scale = quiver_kw.pop("scale", 1.0) pivot = quiver_kw.pop("pivot", "mid") arrow_heads = quiver_kw.pop("arrow_heads", True) xs, ys, u, v = [t.flatten() for t in [xs, ys, u, v]] magnitude = np.sqrt(u ** 2 + v ** 2) rads = np.arctan2(v, u) lens = magnitude / max(magnitude) * scale # Compute segments and arrowheads # Compute offset depending on pivot option xoffsets = np.cos(rads) * lens / 2.0 yoffsets = np.sin(rads) * lens / 2.0 if pivot == "mid": nxoff, pxoff = xoffsets, xoffsets nyoff, pyoff = yoffsets, yoffsets elif pivot == "tip": nxoff, pxoff = 0, xoffsets * 2 nyoff, pyoff = 0, yoffsets * 2 elif pivot == "tail": nxoff, pxoff = xoffsets * 2, 0 nyoff, pyoff = yoffsets * 2, 0 x0s, x1s = (xs + nxoff, xs - pxoff) y0s, y1s = (ys + nyoff, ys - pyoff) if arrow_heads: arrow_len = lens / 4.0 xa1s = x0s - np.cos(rads + np.pi / 4) * arrow_len ya1s = y0s - np.sin(rads + np.pi / 4) * arrow_len xa2s = x0s - np.cos(rads - np.pi / 4) * arrow_len ya2s = y0s - np.sin(rads - np.pi / 4) * arrow_len x0s = np.tile(x0s, 3) x1s = np.concatenate([x1s, xa1s, xa2s]) y0s = np.tile(y0s, 3) y1s = np.concatenate([y1s, ya1s, ya2s]) data = { "x0": x0s, "x1": x1s, "y0": y0s, "y1": y1s, } return data, quiver_kw
BB = BokehBackend