Multi-graph visualization based on DynamicArray input

Hi all,

I was wondering if it is possible to ‘mass produce’ graphs based on the input of a dynamic array. To clarify; I created a visualization array in which every row contains information on the graph I want to create; e.g., name, data, time range, etc. I know want to add three functionalities:

  1. Create an action button that creates and stores all graphs.
  2. Create a new step with PlotlyView where you can select one of the graphs added in the step before and view the output
  3. Create a download button that automatically zips all created png figures together.

Unfortunately I cannot find a tutorial on this, so it would be wonderful if one of you could help :slight_smile: .

Cheers!

Hi Jelle,

Thanks for this interesting question. I will answer your question based on the functionalities you requested:

  1. Create an action button that creates and stores all graphs
    For this, you could simply define an ActionButton and store the Plotly graphs as JSON strings using Storage. For more info on how to use Storage, you can refer to this link: Results & visualizations - Storing results | VIKTOR Documentation
    But for this, you do not necessarily have to use Storage to save the graphs. Only in cases where it takes a considerable time to generate the graphs, I would maybe recommend using an ActionButton to achieve this.

  2. Select graph to visualize from DynamicArray
    There are different approaches one could take. The easy way would be to simply have an OptionField with a dynamic list of the dynamic array rows listed. When selecting the option, it takes the index of that row, and visualizes that graph. I made a simple snippet to demonstrate this case:

from viktor import ViktorController, UserError
from viktor.parametrization import ViktorParametrization, NumberField, DynamicArray, Step, TextField, \
    OptionField, OptionListElement
from viktor.views import PlotlyView, PlotlyResult


def graph_options(params, **kwargs):
    return [OptionListElement(label=row.name, value=idx) for idx, row in enumerate(params.step_1.array)]


class Parametrization(ViktorParametrization):
    step_1 = Step('Step 1')
    step_1.array = DynamicArray('Array')
    step_1.array.name = TextField('Name')
    step_1.array.a = NumberField('A', default=1)
    step_1.array.b = NumberField('B', default=2)
    step_1.array.c = NumberField('C', default=3)
    step_1.array.d = NumberField('D', default=4)

    step_2 = Step('Step 2', views=['get_plotly_view'])
    step_2.select_array = OptionField('Select graph to visualize', options=graph_options)


class Controller(ViktorController):
    label = '...'
    parametrization = Parametrization

    @PlotlyView('Results', duration_guess=10)
    def get_plotly_view(self, params, **kwargs):
        if params.step_2.select_array is None:
            raise UserError('Select a graph to visualize')
        row = params.step_1.array[params.step_2.select_array]
        fig = {
            "data": [{"type": "bar", "x": [1, 2, 3, 4], "y": [row.a, row.b, row.c, row.d]}],
            "layout": {"title": {"text": f"Graph that is visualized: {row.name}"}}
        }
        return PlotlyResult(fig)
  1. Create a download button that automatically zips all created png figures together.
    To achieve this, you will 1) need to convert your Plotly plots to PNGs, 2) bundle these PNGs to be zipped, and 3) add the logic to a DownloadButton method. Luckily with VIKTOR, zipping files is a functionality that is included in the DownloadResult. For more info on this, refer to this link.
    For converting a Plotly plot to a PNG, you can follow the steps described here. Make sure to add kaleido to your requirements.
    Here is a snippet that combines all these steps.
    def download_graphs(self, params, **kwargs):
        zipped_files = {}
        for row in params.step_1.array:
            fig_dict = {
                "data": [{"type": "bar", "x": [1, 2, 3, 4], "y": [row.a, row.b, row.c, row.d]}],
                "layout": {"title": {"text": f"Graph that is visualized: {row.name}"}}
            }
            fig = go.Figure(fig_dict)  # make sure to add plotly to requirements.txt
            img_bytes = fig.to_image(format="png")  # make sure to add kaleido to requirements.txt
            zipped_files[f'{row.name}.png'] = File.from_data(img_bytes)
        return DownloadResult(zipped_files=zipped_files, file_name='my_file.zip')

I hope this helps!

PS take note that kaleido gives some issues with some versions. For my local environment, I used kaleido==v0.1.0post1 in my requirements.

Thanks! Is it also possible to save matplotlib graphs directly via the following method:

def graph_function(params, **kwargs):
  [data processing code]
   fig, ax = plt.subplots()
  [... code for graph creation]
  # Adjusting the layout to prevent overlapping
    plt.tight_layout()
  
    # Create graph data
    graph_data = BytesIO()
    data_format = params.step_2.section_1.output_format.replace(".", "")
    fig.savefig(graph_data, format=data_format)
    plt.close()
  
    return graph_data

In the controller:

def create_graphs(self, params, **kwargs):
      for row in params.step_2.section_2.visualization_array:
            graph_data= energy_profile_diagram(params, row)
            graph_name = row.name
            Storage().set(graph_name , data=File.from_data(graph_data), scope='entity')
      return

I believe that should indeed also be possible. Looking at your code, I think the only thing that I could spot that should change is the last line of your code, which should be:

Storage().set(graph_name , data=File.from_data(graph_data.getvalue()), scope='entity')
1 Like

Unfortunately, this line won’t work for me…

Storage().set(graph_name , data=File.from_data(graph_data.getvalue_binary()), scope='entity')

I get the following error:

Storage().set(key_name, data=File.from_data(graph_data.getvalue_binary()), scope=‘entity’)
^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: ‘_io.BytesIO’ object has no attribute ‘getvalue_binary’

Sorry, this should be:

Storage().set(graph_name , data=File.from_data(graph_data.getvalue()), scope='entity')

I confused BytesIO with File.

I’ve edited my post.