ipynb download badge   Binder badge

Adding new backends

This notebook displays how to integrate a new plotting backend to sisl.viz.

Let’s create a toy graphene band structure to illustrate the conceps throughout this notebook:

[1]:
import sisl

geom = sisl.geom.graphene(orthogonal=True)
H = sisl.Hamiltonian(geom)
H.construct(
    [(0.1, 1.44), (0, -2.7)],
)

band_struct = sisl.BandStructure(H, [[0, 0, 0], [0.5, 0, 0]], 10, ["Gamma", "X"])
info:0: SislInfo: Please install tqdm (pip install tqdm) for better looking progress bars

The final display in the visualization module is controlled by the Figure class.

[2]:
from sisl.viz import Figure

And backends are stored in sisl.viz.figure.BACKENDS. It is just a dictionary containing extensions of the Figure class for particular plotting frameworks.

[3]:
from sisl.viz.figure import BACKENDS

BACKENDS
[3]:
{'plotly': sisl.viz.figure.plotly.PlotlyFigure,
 'matplotlib': sisl.viz.figure.matplotlib.MatplotlibFigure,
 'py3dmol': sisl.viz.figure.Py3DmolFigure,
 'blender': sisl.viz.figure.BlenderFigure}

Therefore, to add a new backend we must follow two steps: 1. Subclass ``Figure``, adding backend specific functionality. 2. Register the backend.

The documentation of the Figure class explains what you should do to extend it:

[4]:
help(Figure)
Help on class Figure in module sisl.viz.figure.figure:

class Figure(builtins.object)
 |  Figure(plot_actions, *args, **kwargs)
 |
 |  Base figure class that all backends should inherit from.
 |
 |  It contains all the plotting actions that should be supported by a figure.
 |
 |  A subclass for a specific backend should implement as many methods as possible
 |  from the ones where Figure returns NotImplementedError.
 |  Other methods are optional because Figure contains a default implementation
 |  using other methods, which should work for most backends.
 |
 |  To create a new backend, one might take the PlotlyFigure as a template.
 |
 |  Methods defined here:
 |
 |  __init__(self, plot_actions, *args, **kwargs)
 |      Initialize self.  See help(type(self)) for accurate signature.
 |
 |  clear(self)
 |      Clears the figure so that we can draw again.
 |
 |  draw_area_line(self, x, y, name=None, line={}, text=None, dependent_axis=None, row=None, col=None, **kwargs)
 |      Same as draw line, but to draw a line with an area. This is for example used to draw fatbands.
 |
 |      Parameters
 |      -----------
 |      x: array-like
 |          the coordinates of the points along the X axis.
 |      y: array-like
 |          the coordinates of the points along the Y axis.
 |      name: str, optional
 |          the name of the scatter
 |      line: dict, optional
 |          specifications for the line style, following plotly standards. The backend
 |          should at least be able to implement `line["color"]` and `line["width"]`, but
 |          it is very advisable that it supports also `line["opacity"]`.
 |      text: str, optional
 |          contains the text asigned to each marker. On plotly this is seen on hover,
 |          other options could be annotating. However, it is not necessary that this
 |          argument is supported.
 |      dependent_axis: str, optional
 |          The axis that contains the dependent variable. This is important because
 |          the area is drawn in parallel to that axis.
 |      row: int, optional
 |          If the figure contains subplots, the row where to draw.
 |      col: int, optional
 |          If the figure contains subplots, the column where to draw.
 |      **kwargs:
 |          should allow other keyword arguments to be passed directly to the creation of
 |          the scatter. This will of course be framework specific
 |
 |  draw_arrows(self, x, y, dxy, arrowhead_scale=0.2, arrowhead_angle=20, scale: 'float' = 1, annotate: 'bool' = False, row=None, col=None, **kwargs)
 |      Draws multiple arrows using the generic draw_line method.
 |
 |      Parameters
 |      -----------
 |      xy: np.ndarray of shape (n_arrows, 2)
 |          the positions where the atoms start.
 |      dxy: np.ndarray of shape (n_arrows, 2)
 |          the arrow vector.
 |      arrow_head_scale: float, optional
 |          how big is the arrow head in comparison to the arrow vector.
 |      arrowhead_angle: angle
 |          the angle that the arrow head forms with the direction of the arrow (in degrees).
 |      scale: float, optional
 |          multiplying factor to display the arrows. It does not affect the underlying data,
 |          therefore if the data is somehow displayed it should be without the scale factor.
 |      annotate:
 |          whether to annotate the arrows with the vector they represent.
 |      row: int, optional
 |          If the figure contains subplots, the row where to draw.
 |      col: int, optional
 |          If the figure contains subplots, the column where to draw.
 |
 |  draw_arrows_3D(self, x, y, z, dxyz, arrowhead_scale=0.3, arrowhead_angle=15, scale: 'float' = 1, row=None, col=None, **kwargs)
 |      Draws multiple 3D arrows using the generic draw_line_3D method.
 |
 |      Parameters
 |      -----------
 |      x: np.ndarray of shape (n_arrows, )
 |          the X coordinates of the arrow's origin.
 |      y: np.ndarray of shape (n_arrows, )
 |          the Y coordinates of the arrow's origin.
 |      z: np.ndarray of shape (n_arrows, )
 |          the Z coordinates of the arrow's origin.
 |      dxyz: np.ndarray of shape (n_arrows, 2)
 |          the arrow vector.
 |      arrow_head_scale: float, optional
 |          how big is the arrow head in comparison to the arrow vector.
 |      arrowhead_angle: angle
 |          the angle that the arrow head forms with the direction of the arrow (in degrees).
 |      scale: float, optional
 |          multiplying factor to display the arrows. It does not affect the underlying data,
 |          therefore if the data is somehow displayed it should be without the scale factor.
 |      row: int, optional
 |          If the figure contains subplots, the row where to draw.
 |      col: int, optional
 |          If the figure contains subplots, the column where to draw.
 |
 |  draw_balls_3D(self, x, y, z, name=None, markers={}, row=None, col=None, **kwargs)
 |      Draws points as 3D spheres.
 |
 |  draw_heatmap(self, values, x=None, y=None, name=None, zsmooth=False, coloraxis=None, opacity=None, textformat=None, textfont={}, row=None, col=None, **kwargs)
 |      Draws a heatmap following the specifications.
 |
 |  draw_line(self, x, y, name=None, line={}, marker={}, text=None, row=None, col=None, **kwargs)
 |      Draws a line satisfying the specifications
 |
 |      Parameters
 |      -----------
 |      x: array-like
 |          the coordinates of the points along the X axis.
 |      y: array-like
 |          the coordinates of the points along the Y axis.
 |      name: str, optional
 |          the name of the line
 |      line: dict, optional
 |          specifications for the line style, following plotly standards. The backend
 |          should at least be able to implement `line["color"]` and `line["width"]`
 |      marker: dict, optional
 |          specifications for the markers style, following plotly standards. The backend
 |          should at least be able to implement `marker["color"]` and `marker["size"]`
 |      text: str, optional
 |          contains the text asigned to each marker. On plotly this is seen on hover,
 |          other options could be annotating. However, it is not necessary that this
 |          argument is supported.
 |      row: int, optional
 |          If the figure contains subplots, the row where to draw.
 |      col: int, optional
 |          If the figure contains subplots, the column where to draw.
 |      **kwargs:
 |          should allow other keyword arguments to be passed directly to the creation of
 |          the line. This will of course be framework specific
 |
 |  draw_line_3D(self, x, y, z, name=None, line={}, marker={}, text=None, row=None, col=None, **kwargs)
 |      Draws a 3D line satisfying the specifications.
 |
 |      Parameters
 |      -----------
 |      x: array-like
 |          the coordinates of the points along the X axis.
 |      y: array-like
 |          the coordinates of the points along the Y axis.
 |      z: array-like
 |          the coordinates of the points along the Z axis.
 |      name: str, optional
 |          the name of the line
 |      line: dict, optional
 |          specifications for the line style, following plotly standards. The backend
 |          should at least be able to implement `line["color"]` and `line["width"]`
 |      marker: dict, optional
 |          specifications for the markers style, following plotly standards. The backend
 |          should at least be able to implement `marker["color"]` and `marker["size"]`
 |      text: str, optional
 |          contains the text asigned to each marker. On plotly this is seen on hover,
 |          other options could be annotating. However, it is not necessary that this
 |          argument is supported.
 |      row: int, optional
 |          If the figure contains subplots, the row where to draw.
 |      col: int, optional
 |          If the figure contains subplots, the column where to draw.
 |      **kwargs:
 |          should allow other keyword arguments to be passed directly to the creation of
 |          the line. This will of course be framework specific
 |
 |  draw_mesh_3D(self, vertices, faces, color=None, opacity=None, name=None, row=None, col=None, **kwargs)
 |      Draws a 3D mesh following the specifications.
 |
 |  draw_multicolor_area_line(self, x, y, name=None, line={}, text=None, dependent_axis=None, row=None, col=None, **kwargs)
 |      Draw a line with an area with multiple colours.
 |
 |      Parameters
 |      -----------
 |      x: array-like
 |          the coordinates of the points along the X axis.
 |      y: array-like
 |          the coordinates of the points along the Y axis.
 |      name: str, optional
 |          the name of the scatter
 |      line: dict, optional
 |          specifications for the line style, following plotly standards. The backend
 |          should at least be able to implement `line["color"]` and `line["width"]`, but
 |          it is very advisable that it supports also `line["opacity"]`.
 |      text: str, optional
 |          contains the text asigned to each marker. On plotly this is seen on hover,
 |          other options could be annotating. However, it is not necessary that this
 |          argument is supported.
 |      dependent_axis: str, optional
 |          The axis that contains the dependent variable. This is important because
 |          the area is drawn in parallel to that axis.
 |      row: int, optional
 |          If the figure contains subplots, the row where to draw.
 |      col: int, optional
 |          If the figure contains subplots, the column where to draw.
 |      **kwargs:
 |          should allow other keyword arguments to be passed directly to the creation of
 |          the scatter. This will of course be framework specific
 |
 |  draw_multicolor_balls_3D(self, x, y, z, name=None, marker={}, row=None, col=None, **kwargs)
 |      Draws points as 3D spheres with different colours.
 |
 |      If marker_color is an array of numbers, a coloraxis is created and values are converted to rgb.
 |
 |  draw_multicolor_line(self, *args, line={}, row=None, col=None, **kwargs)
 |      By default, multicoloured lines are drawn simply by drawing scatter points.
 |
 |  draw_multicolor_line_3D(self, *args, **kwargs)
 |      Draws a multicoloured 3D line.
 |
 |  draw_multicolor_scatter(self, *args, **kwargs)
 |      Draws a multicoloured scatter.
 |
 |      Usually the normal scatter can already support this.
 |
 |  draw_multicolor_scatter_3D(self, *args, **kwargs)
 |      Draws a multicoloured 3D scatter.
 |
 |      Usually the normal 3D scatter can already support this.
 |
 |  draw_multisize_area_line(self, x, y, name=None, line={}, text=None, dependent_axis=None, row=None, col=None, **kwargs)
 |      Draw a line with an area with multiple colours.
 |
 |      This is already usually supported by the normal draw_area_line.
 |
 |      Parameters
 |      -----------
 |      x: array-like
 |          the coordinates of the points along the X axis.
 |      y: array-like
 |          the coordinates of the points along the Y axis.
 |      name: str, optional
 |          the name of the scatter
 |      line: dict, optional
 |          specifications for the line style, following plotly standards. The backend
 |          should at least be able to implement `line["color"]` and `line["width"]`, but
 |          it is very advisable that it supports also `line["opacity"]`.
 |      text: str, optional
 |          contains the text asigned to each marker. On plotly this is seen on hover,
 |          other options could be annotating. However, it is not necessary that this
 |          argument is supported.
 |      dependent_axis: str, optional
 |          The axis that contains the dependent variable. This is important because
 |          the area is drawn in parallel to that axis.
 |      row: int, optional
 |          If the figure contains subplots, the row where to draw.
 |      col: int, optional
 |          If the figure contains subplots, the column where to draw.
 |      **kwargs:
 |          should allow other keyword arguments to be passed directly to the creation of
 |          the scatter. This will of course be framework specific
 |
 |  draw_multisize_balls_3D(self, x, y, z, name=None, marker={}, row=None, col=None, **kwargs)
 |      Draws points as 3D spheres with different sizes.
 |
 |      Usually supported by the normal draw_balls_3D
 |
 |  draw_multisize_line(self, *args, line={}, row=None, col=None, **kwargs)
 |      By default, multisized lines are drawn simple by drawing scatter points.
 |
 |  draw_multisize_line_3D(self, *args, **kwargs)
 |      Draws a multisized 3D line.
 |
 |  draw_multisize_scatter(self, *args, **kwargs)
 |      Draws a multisized scatter.
 |
 |      Usually the normal scatter can already support this.
 |
 |  draw_multisize_scatter_3D(self, *args, **kwargs)
 |      Draws a multisized 3D scatter.
 |
 |      Usually the normal 3D scatter can already support this.
 |
 |  draw_scatter(self, x, y, name=None, marker={}, text=None, row=None, col=None, **kwargs)
 |      Draws a scatter satisfying the specifications
 |
 |      Parameters
 |      -----------
 |      x: array-like
 |          the coordinates of the points along the X axis.
 |      y: array-like
 |          the coordinates of the points along the Y axis.
 |      name: str, optional
 |          the name of the scatter
 |      marker: dict, optional
 |          specifications for the markers style, following plotly standards. The backend
 |          should at least be able to implement `marker["color"]` and `marker["size"]`, but
 |          it is very advisable that it supports also `marker["opacity"]` and `marker["colorscale"]`
 |      text: str, optional
 |          contains the text asigned to each marker. On plotly this is seen on hover,
 |          other options could be annotating. However, it is not necessary that this
 |          argument is supported.
 |      row: int, optional
 |          If the figure contains subplots, the row where to draw.
 |      col: int, optional
 |          If the figure contains subplots, the column where to draw.
 |      **kwargs:
 |          should allow other keyword arguments to be passed directly to the creation of
 |          the scatter. This will of course be framework specific
 |
 |  draw_scatter_3D(self, x, y, z, name=None, marker={}, text=None, row=None, col=None, **kwargs)
 |      Draws a 3D scatter satisfying the specifications
 |
 |      Parameters
 |      -----------
 |      x: array-like
 |          the coordinates of the points along the X axis.
 |      y: array-like
 |          the coordinates of the points along the Y axis.
 |      z: array-like
 |          the coordinates of the points along the Z axis.
 |      name: str, optional
 |          the name of the scatter
 |      marker: dict, optional
 |          specifications for the markers style, following plotly standards. The backend
 |          should at least be able to implement `marker["color"]` and `marker["size"]`
 |      text: str, optional
 |          contains the text asigned to each marker. On plotly this is seen on hover,
 |          other options could be annotating. However, it is not necessary that this
 |          argument is supported.
 |      row: int, optional
 |          If the figure contains subplots, the row where to draw.
 |      col: int, optional
 |          If the figure contains subplots, the column where to draw.
 |      **kwargs:
 |          should allow other keyword arguments to be passed directly to the creation of
 |          the scatter. This will of course be framework specific
 |
 |  init_3D(self)
 |      Called if functions that draw in 3D are going to be called.
 |
 |  init_coloraxis(self, name, cmin=None, cmax=None, cmid=None, colorscale=None, showscale=True, **kwargs)
 |      Initializes a color axis to be used by the drawing functions
 |
 |  init_figure(self, composite_method: "Literal[None, 'same_axes', 'multiple', 'multiple_x', 'multiple_y', 'subplots', 'animation']" = None, plot_actions=(), init_kwargs: 'Dict[str, Any]' = {})
 |
 |  set_axes_equal(self)
 |      Sets the axes equal.
 |
 |  set_axis(self, **kwargs)
 |      Sets the axis parameters.
 |
 |      The specification for the axes is exactly the plotly one. This is to have a good
 |      reference for consistency. Other frameworks should translate the calls to their
 |      functionality.
 |
 |  show(self)
 |
 |  to(self, key: 'str')
 |      Converts the figure to another backend.
 |
 |      Parameters
 |      -----------
 |      key: str
 |          the backend to convert to.
 |
 |  ----------------------------------------------------------------------
 |  Class methods defined here:
 |
 |  fig_has_attr(key: 'str') -> 'bool' from builtins.type
 |      Whether the figure that this class generates has a given attribute.
 |
 |      Parameters
 |      -----------
 |      key
 |          the attribute to check for.
 |
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |
 |  __dict__
 |      dictionary for instance variables (if defined)
 |
 |  __weakref__
 |      list of weak references to the object (if defined)
 |
 |  ----------------------------------------------------------------------
 |  Data and other attributes defined here:
 |
 |  __annotations__ = {'_coloraxes': 'dict', '_cols': 'Optional[int]', '_m...
 |
 |  plot_actions = []

Therefore, we need to implement some of the methods of the Figure class. The more we implement, the more we will support sisl.viz.

Here’s an example of a very simple backend that just writes text:

[5]:
import numpy as np


class TextFigure(Figure):
    def _init_figure(self, *args, **kwargs):
        self.text = ""

    def clear(self):
        self.text = ""

    def draw_line(self, x, y, name, **kwargs):
        self.text += f"\nLINE: {name}\n{np.array(x)}\n{np.array(y)}"

    def draw_scatter(self, x, y, name, **kwargs):
        self.text += f"\nSCATTER: {name}\n{np.array(x)}\n{np.array(y)}"

    def show(self):
        print(self.text)

    def _ipython_display_(self):
        self.show()

And all that is left now is to register the backend by simply adding it to the BACKENDS dictionary.

[6]:
BACKENDS["text"] = TextFigure

Let’s plot the bands to check that it works.

[7]:
plot = band_struct.plot()
plot
_ipython_canary_method_should_not_exist_ False
_ipython_display_ True

The default backend has been used, let’s now change it to our new "text" backend.

[8]:
plot.update_inputs(backend="text")

LINE: 0
[0.         0.08194034 0.16388068 0.24582102 0.32776136 0.4097017
 0.49164204 0.57358238 0.65552272 0.73746306]
[-8.1        -8.07260764 -7.99070941 -7.85514486 -7.66732391 -7.42924537
 -7.14352854 -6.81346515 -6.44310336 -6.03738354]
LINE: 1
[0.         0.08194034 0.16388068 0.24582102 0.32776136 0.4097017
 0.49164204 0.57358238 0.65552272 0.73746306]
[-2.7        -2.78082828 -3.00808297 -3.34614692 -3.75661337 -4.20788704
 -4.67653718 -5.14555076 -5.60235836 -6.03738354]
LINE: 2
[0.         0.08194034 0.16388068 0.24582102 0.32776136 0.4097017
 0.49164204 0.57358238 0.65552272 0.73746306]
[2.7        2.78082828 3.00808297 3.34614692 3.75661337 4.20788704
 4.67653718 5.14555076 5.60235836 6.03738354]
LINE: 3
[0.         0.08194034 0.16388068 0.24582102 0.32776136 0.4097017
 0.49164204 0.57358238 0.65552272 0.73746306]
[8.1        8.07260764 7.99070941 7.85514486 7.66732391 7.42924537
 7.14352854 6.81346515 6.44310336 6.03738354]

Not a very visually appealing backend, but it serves the purpose of demonstrating how it is done. Now it is your turn!

Note

For a complex framework you might take inspiration from the already implemented backends in sisl.viz.figure.*.

[ ]: