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"])

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'
 |      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
 |
 |  __weakref__
 |      list of weak references to the object
 |
 |  ----------------------------------------------------------------------
 |  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