Download IPython notebook here. Binder badge.

Building a plot class

Following this guide, you will not only build a very flexible plot class that you will be able to use in a wide range of cases, but also your class will be automatically recognized by the graphical interface. Therefore, you will get visual interactivity for free.

Warning

Please make sure to read this brief introduction to sisl’s visualization framework before you go on with this notebook. It will only take a few minutes and you will understand the concepts much easier! :)

Let’s begin!

Class definition

Things that don’t start in the right way are not likely to end well.

Therefore, make sure that all plot classes that you develop inherit from the parent class ``Plot``.

That is, if you were to define a new class to plot, let’s say, the happiness you feel for having found this notebook, you would define it as class HappinessPlot(Plot):.

In this way, your plots will profit from all the generic methods and processes that are implemented there. The Plot class is meant for you to write as little code as possible while still getting a powerful and dynamic representation.

More info on class inheritance: written explanation, Youtube video.

Let’s do it!

[1]:
from sisl.viz.plotly import Plot

class HappinessPlot(Plot):
    pass

And just like that, you have your first plot class. Let’s play with it:

[2]:
plt = HappinessPlot()
info:0: SislInfo:

The plot has been initialized correctly, but the current settings were not enough to generate the figure.
Error: The attribute 'source' was not found either in the plot, its figure, or in shared attributes.

Well, that seems reasonable. Our plot has no data because our class does not know how to get it yet.

However, we can already do all the general things a plot is expected to do:

[3]:
print(plt)
Plot class: HappinessPlot    Plot type: HappinessPlot

Settings:
        - root_fdf: None
        - results_path:
[4]:
plt
[5]:
plt.update_layout(xaxis_title = "Meaningless axis (eV)",
                  xaxis_showgrid = True, xaxis_gridcolor = "red")

If you are done generating and playing with useless plot classes, let’s continue our way to usefulness…

Parameters

It is only when you define something that it begins to exist.

Before starting to write methods for our new class, we will write the parameters that define it. We will store them in a class variable called _parameters. Here is the definition of the _parameters variable that your class should contain:

_parameters (tuple of InputFields): it contains all the parameters that the user can tweak in your analysis. Each parameter or setting should use an input field object (see the cell below to see types of input fields that you can use). Why do we need to do it like this? Well, this has three main purposes:

  • If you use an input field, the graphical interface already knows how to display it.

  • It will make documentation very consistent in the long term.

  • You will be able to access their values very easily at any point in the plot’s methods.

  • Helpful methods can be implemented to input fields to facilitate some recurrent work on the inputs.

Let’s begin populating our HappinessPlot class:

[6]:
# These are some input fields that are available to you.
# The names are quite self-explanatory
from sisl.viz.plotly.input_fields import TextInput, SwitchInput, \
    ColorPicker, DropdownInput, IntegerInput, FloatInput, \
    RangeSlider, QueriesInput, ProgramaticInput

class HappinessPlot(Plot):

    # The _plot_type variable is the name that will be displayed for the plot
    # If not present, it will be the class name (HappinessPlot).
    _plot_type = "Happiness Plot"

    _parameters = (

        # This is our first parameter
        FloatInput(
            # "key" will allow you to identify the parameter during your data processing
            # (be patient, we are getting there)
            key="init_happiness",
            # "name" is the name that will be displayed (because, you know,
            # init_happiness is not a beautiful name to show to non-programmers)
            name="Initial happiness level",
            # "default" is the default value for the parameter
            default=0,
            # "help" is a helper message that will be displayed to the user when
            # they don't know what the parameter means. It will also be used in
            # the automated docs of the plot class.
            help="This is your level of happiness before reading this notebook.",
        ),

        # This is our second parameter
        SwitchInput(
            key="read_notebook",
            name="Notebook has been read?",
            default=False,
            help="Whether you have read the DIY notebook yet.",
        )

    )

Now we have something! Let’s check if it works:

[7]:
plt = HappinessPlot( init_happiness = 3 )
info:0: SislInfo:

The plot has been initialized correctly, but the current settings were not enough to generate the figure.
Error: The attribute 'source' was not found either in the plot, its figure, or in shared attributes.

[8]:
print(plt)
Plot class: HappinessPlot    Plot type: Happiness Plot

Settings:
        - init_happiness: 3
        - read_notebook: False
        - root_fdf: None
        - results_path:

You can see that our settings have appeared, but they are still meaningless, let’s continue.

Flow methods

Is this class just a poser or does it actually do something?

After defining the parameters that our analysis will depend on and that the user will be able to tweak, we can proceed to actually using them to read, process and show data.

As mentioned in the introductory page, the Plot class will control the flow of our plot and will be in charge of managing how it needs to behave at each situation. Because Plot is an experienced class that has seen many child classes fail, it knows all the things that can go wrong and what is the best way to do things. Therefore, all the methods called by the user will actually be methods of Plot, not our class.

Note

Don’t worry, this is just true for the main plotting flow! Besides that, you can add as much public methods as you wish to make the usage of your class much more convenient.

However, Plot is of course not omniscient, so it needs the help of your class to do the particular analysis that you need. During the workflow, there are many points where Plot will try to use methods of your class, and that is where you can do the processing required for your plots. At first, this might seem annoying and limiting, but the flexibility provided is very high and in this way you can be 100% sure that your code is ran in the right moments without having to think much about it.

The flow of the Plot class is quite simple. There are three main steps represented by three different methods: read_data, set_data and get_figure. The names can already give you a first idea of what each step does, but let’s get to the details of each method and show you where you will be able to do your magic:

Note

Following, you will find advice of what to do at each point of the workflow. But really, do whatever you need to do, don’t feel limited by our advice!

  • .__init__(), the party starter:

    Of course, before ever thinking of doing things with your plot, we need to initialize it. On initialization, your plot will inherit everything from the parent classes, and all the parameters under the _parameters variable (both in your class and in Plot) will be transferred to self.settings, a dictionary that will contain all the current values for each parameter. You will also get a full copy of _parameters under self.params, in case you need to check something at any point.

    Warning

    Please don’t ever use ``_parameters`` directly, as you would have the risk of changing the parameters for the whole class, not only your plot.

    You should let Plot.__init__() do its thing, but after it is done, you have the first place where you can act. If your class has an _after_init method, it will be ran at this point. This is a good place to intialize your plot attributes if you are a clean coder and don’t initialize attributes all over the place. But hey, we don’t judge!

  • .read_data(), the heavy lifter:

    This method will probably be the most time and resource consuming of your class, therefore we need to make sure that we store all the important things inside our object so that we don’t have to use it frequently, only if there is a change in the reading method or the files that must be read.

    Our advice is that, at the end of this method, you end up with a pandas dataframe, xarray Dataarray or Dataset or whatever other ordered way to store the data, so that later operations that need to be run more frequently and will query bits of this data can be performed in a quick and efficient manner.

    read_data is a polite method, so it will let you do something first if you need to by using the _before_read method. We have not thinked of something that would be good to do here yet, but you may need it, so there you have it…

    After that, it will attempt to initialize the plot from the different entry points until it finds one that succeeds. Entry points are signalled with the entry_point wrapper, as follows:

[9]:
from sisl.viz.plotly.plot import entry_point

class HappinessPlot(Plot):

    @entry_point("my first entry point") # This is the name of the entry point
    def _just_continue():
        """Some docs for the entry point"""
        pass

Note

The order in which read_data goes through entry points is the same in which you have defined them.

When an entry point succeeds (that is, ends without raising an exception), you will get the source of the data under self.source for if you need to know it further down in your processing. Then Plot will let you have one last word with the _after_read method, before moving on to the next step. This is a good point to update self.params or self.settings according to the data you have read. For instance, in a PDOS plot the orbitals, atomic species and atoms available are only known after you have read the data, so you will use _after_read to set the options of the corresponding input fields.

  • .set_data(), the picky one:

    Great! You have all the data in the world now stored in your plot object, but you sure enough don’t want to plot it all. And even if you did, you probably don’t want to display just all the numbers on the screen. In this step you should pick the data that you need from self.df (or whatever place you have stored your data), and organize it in plot elements (i.e. lines, scatter points, bars, pies…).

    In this method, you should end up populating the plot with all traces that are related to the data. Keep in mind that our plot is an extension of plotly’s Figure, so you can use any of the methods they provide to add data. The most common ones are self.add_trace() and self.add_traces(), but they have plenty of them, so you can check their documentation.

    You are kind of alone in this step, as Plot will only ensure that the basics are there and execute your _set_data() method. By the way, you don’t need to worry about cleaning previous data. Each time _set_data is called all traces are removed.

  • .get_figure(), the beautifier:

    You can rest now, all the work is done. Plot will not need to do anything here, but other subclasses like Animation might need to set up some things in the figure.

    But hey, you still get the chance to give a final touch to your work with ._after_get_figure, which is executed after the figure is built and before showing it to the world. You may want to add annotations, lines that highlight facts about your plot or whatever other thing here. By keeping it separate from the actual processing of your data, setting updates that only concern ._after_get_figure will get executed much faster.

Accessing settings

When you need to access the value of a setting inside a method, just add it as an argument.

[10]:
class HappinessPlot(Plot):

    _parameters = (
        IntegerInput(key="n", name="Just a test setting", default=3)
    ,)

    def _method_that_uses_n(self, n):
        pass

    def _method_that_uses_n_and_provides_default(self, n=5):
        pass

Then the values will be directly provided to you and their use will be registered so that Plot knows what to run when a given setting is updated.

After some thought, this turned up to be the best way of managing settings because it allows you to use the methods even if you are not inside the plot class.

Note

The defaults specified in the method are ignored if the method is called within the plot instance. I.e: in _method_that_uses_n_and_provides_default, n will default to:

  • 3 if it’s called from the plot instance.

  • 5 if the method is used externally.

Wow, that was long…

It might seem intimidating, but rest assured that your life will be extremely easy after this. Let’s see an example of how to apply the knowledge that we acquired to our class:

[11]:
class HappinessPlot(Plot):

    _plot_type = "Happiness Plot"

    _parameters = (

        FloatInput(
            key="init_happiness",
            name="Initial happiness level",
            default=0,
            help="This is your level of happiness before reading this notebook.",
        ),

        SwitchInput(
            key="read_notebook",
            name="Notebook has been read?",
            default=False,
            help="Whether you have read the DIY notebook yet.",
        )

    )

    # The _layout_defaults allow you to provide some default values
    # for your plot's layout (See https://plotly.com/python/creating-and-updating-figures/#the-layout-key
    # and https://plotly.com/python/reference/#layout)
    # Let's help the users understand what they are seeing with axes titles
    _layout_defaults = {
        "yaxis_title": "Happiness level",
        "xaxis_title": "Time"
    }

    @entry_point("Previously happy")
    def _init_with_happiness(self, init_happiness):
        """Given that you were happy enough, sets the background color pink"""
        if init_happiness <= 0:
            raise ValueError(f"Your level of happiness ({init_happiness}) is not enough to use this entry point.")
        self.update_layout(paper_bgcolor="pink")


    @entry_point("Being sad")
    def _init_with_sadness(self, init_happiness):
        """Lets you in if you're sad, that's all."""
        if init_happiness > 0:
            raise ValueError(f"You are too intrinsically happy to use this entry point")
        pass

    def _set_data(self, init_happiness, read_notebook):
        # The _set_data method needs to generate the plot elements
        # (in this case, a line)

        #Calculate the final happiness based on the settings values
        if read_notebook:
            final_happiness = (init_happiness + 1) * 100
        else:
            final_happiness = init_happiness

        # Define a line that goes from the initial happiness to the final happiness
        self.add_trace({
                # The type of element
                'type': 'scatter',
                # Draw a line
                'mode': 'lines+markers',
                # The values for Y (X will be automatic, we don't care now)
                'y': [init_happiness, final_happiness],
                # Name that shows in the legend
                'name': 'Happiness evolution',
                # Other plotly fancy stuff that we don't really need
                'hovertemplate': 'Happiness level: %{y}',
                'line': {"color": "red" if final_happiness <= init_happiness else "green"}

        })

And just like this, we have our first “meaningful” plot!

[12]:
plt = HappinessPlot()
plt
[13]:
plt.update_settings(init_happiness=100 ,read_notebook=True)

Simplicity is great, but that is too simple… let’s add more things to our plot!

Additional public methods

You might feel like you are always at the mercy of the Plot class, but that’s not completely true.Plot expects your class to have certain methods and automatically provides your class with useful plot manipulation methods, but you can always add methods that you think will be helpful for users that will use your particular plot.

Note

If you believe that a method can be useful for plots other than yours, consider contributing it to the Plot class :)

Let’s see how this could work with our happiness plot. We will add a method read_notebook, which simulates that we just read the notebook.

[14]:
class HappinessPlot(Plot):

    _plot_type = "Happiness Plot"

    _parameters = (

        FloatInput(
            key="init_happiness",
            name="Initial happiness level",
            default=0,
            help="This is your level of happiness before reading this notebook.",
        ),

        SwitchInput(
            key="read_notebook",
            name="Notebook has been read?",
            default=False,
            help="Whether you have read the DIY notebook yet.",
        )

    )

    _layout_defaults = {
        "yaxis_title": "Happiness level",
        "xaxis_title": "Time"
    }

    @entry_point("Previously happy")
    def _init_with_happiness(self, init_happiness):
        """Given that you were happy enough, sets the background color pink"""
        if init_happiness <= 0:
            raise ValueError(f"Your level of happiness ({init_happiness}) is not enough to use this entry point.")
        self.update_layout(paper_bgcolor="pink")


    @entry_point("Being sad")
    def _init_with_sadness(self, init_happiness):
        """Lets you in if you're sad, that's all."""
        if init_happiness > 0:
            raise ValueError(f"You are too intrinsically happy to use this entry point")
        pass

    def _set_data(self, init_happiness, read_notebook):

        #Calculate the final happiness based on the settings values
        if read_notebook:
            final_happiness = (init_happiness + 1) * 100
        else:
            final_happiness = init_happiness

        # Define a line that goes from the initial happiness to the final happiness
        self.add_trace({
                'type': 'scatter',
                'mode': 'lines+markers',
                'y': [init_happiness, final_happiness],
                'name': 'Happiness evolution',
                'hovertemplate': 'Happiness level: %{y}',
                'line': {"color": "red" if final_happiness <= init_happiness else "green"}

        })

    def read_notebook(self, location="your computer"):
        """Method that 'reads the notebook'."""
        import time

        # Let's do a little show
        print(f"Reading the notebook in {location}...")
        time.sleep(3)
        self.update_settings(read_notebook=True)
        print("Read")

        return self
[15]:
plt = HappinessPlot()
plt.show("png")
plt.read_notebook()
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
/tmp/ipykernel_3569/2167855664.py in <module>
      1 plt = HappinessPlot()
----> 2 plt.show("png")
      3 plt.read_notebook()

~/checkouts/readthedocs.org/user_builds/sisl/checkouts/v0.11.0/sisl/viz/plotly/plot.py in show(self, listen, return_figWidget, *args, **kwargs)
   1187                 warn(e)
   1188
-> 1189         return self.figure.show(*args, **kwargs)
   1190
   1191     def _ipython_display_(self, return_figWidget=False, **kwargs):

~/checkouts/readthedocs.org/user_builds/sisl/conda/v0.11.0/lib/python3.8/site-packages/plotly/basedatatypes.py in show(self, *args, **kwargs)
   3396         import plotly.io as pio
   3397
-> 3398         return pio.show(self, *args, **kwargs)
   3399
   3400     def to_json(self, *args, **kwargs):

~/checkouts/readthedocs.org/user_builds/sisl/conda/v0.11.0/lib/python3.8/site-packages/plotly/io/_renderers.py in show(fig, renderer, validate, **kwargs)
    387
    388     # Mimetype renderers
--> 389     bundle = renderers._build_mime_bundle(fig_dict, renderers_string=renderer, **kwargs)
    390     if bundle:
    391         if not ipython_display:

~/checkouts/readthedocs.org/user_builds/sisl/conda/v0.11.0/lib/python3.8/site-packages/plotly/io/_renderers.py in _build_mime_bundle(self, fig_dict, renderers_string, **kwargs)
    295                         setattr(renderer, k, v)
    296
--> 297                 bundle.update(renderer.to_mimebundle(fig_dict))
    298
    299         return bundle

~/checkouts/readthedocs.org/user_builds/sisl/conda/v0.11.0/lib/python3.8/site-packages/plotly/io/_base_renderers.py in to_mimebundle(self, fig_dict)
    126
    127     def to_mimebundle(self, fig_dict):
--> 128         image_bytes = to_image(
    129             fig_dict,
    130             format=self.format,

~/checkouts/readthedocs.org/user_builds/sisl/conda/v0.11.0/lib/python3.8/site-packages/plotly/io/_kaleido.py in to_image(fig, format, width, height, scale, validate, engine)
    132     # Raise informative error message if Kaleido is not installed
    133     if scope is None:
--> 134         raise ValueError(
    135             """
    136 Image export using the "kaleido" engine requires the kaleido package,

ValueError:
Image export using the "kaleido" engine requires the kaleido package,
which can be installed using pip:
    $ pip install -U kaleido

Congratulations, you know everything now!

Well, not really, because there are still some things missing like adding keyboard shortcuts or default animations. But yeah, you know some things…

Just kidding, this is more than enough to get you started! Try to build your own plots and come back for more tutorials when you feel like it. We’ll be waiting for you.

Note

Note that this plot class that we built here is directly usable by the graphical user interface. So its use does not end in a python script.

Cheers, checkin’ out!

[ ]: