Vizro: a high-level framework built on top of Dash

Hello everyone! I’m very excited to introduce Vizro, a high-level framework built on top of Dash for quick and easy creation of beautiful multi-page dashboards.
vizro_demonstration

What does “high-level framework built on top of Dash for quick and easy creation of beautiful multi-page dashboards” mean?

  • High-level: Vizro abstracts away common elements you see in dashboards, e.g. filter a pandas DataFrame and update a graph is encapsulated by a Filter. Vizro uses pydantic to parse and validate configuration according to a declarative “grammar of dashboards”.
  • Quick and easy: no need to write any callbacks or lay things out on the screen yourself. You don’t actually need to know any Dash to make a Vizro dashboard, since the relevant code is generated for you by simple configuration.
  • Multi-page: a page is a first-class citizen in Vizro. Thanks to the underlying Dash Pages functionality, creation of complex multi-page apps is easy.
  • Beautiful: Vizro has built in dark and light themes that follow design best practices. You don’t need to know any HTML or CSS to make a nice looking dashboard.
  • Built on top of Dash: users benefit from all the underlying power of the Dash framework: scalability, flexibility, customisability and so on. While Vizro aims to simplify the creation of dashboards for beginner users, it also allows advanced users to extend functionality by writing pure Dash code.

Vizro is currently at version 0.1.7 and under very active development, so look out for more releases and updates! Fellow Vizro maintainer @li.nguyen and I would love to hear what you think - any feedback, questions, comments are gratefully received. Feel free to post in this topic or open issues on our repo!

Learn more about Vizro

15 Likes

Vizro is awesome. It makes creating Dash Apps even easier and faster.

Thanks for sharing @antony.milne

Where is the best place to follow new releases and updates?

3 Likes

This is an amazing project! :star_struck:

I encourage everyone to check out the docs and try out the live demo app shown in the gif above. When you do, keep in mind that the entire app is written with about 500 lines of code. And actually, there is very little code – it’s mostly app content.

Thanks McKinsey for open sourcing this powerful app framework and making it available to everyone!

8 Likes

Thank you @adamschroeder and @AnnMarieW for your kind comments! The best place to follow updates would be our Github releases page :slight_smile:

3 Likes

Hi @antony.milne, I am eager to try out Vizro too. Since callback is now abastract away, are there any way to do periodic update using dcc.Interval?

Hi @chainster_mike, thanks for the question! It is possible to do periodic updates using dcc.Interval, although you might not be able to achieve exactly what you want yet. This isn’t natively possible using Actions, so you’ll need to use a Dash callback to do it. In the near future we’re adding a feature for data refresh so that you will be able to easily update data sources and graphs, but that also doesn’t exist yet. So depending on exactly what you want to do, it may or may not already be possible, and hopefully this will be easier in future when we’ve added some more features.

Here’s a very simple example where I update the text inside a card according to dcc.Interval. One of advantages of Vizro is that it builds directly on top of Dash, so it’s still possible to use Dash callbacks and inject arbitrary Dash components into the layout.

ezgif-3-61ebf8e1af

from dash import dcc, Output, Input, callback

import vizro.models as vm
from vizro import Vizro

import datetime


dashboard = vm.Dashboard(
    pages=[
        vm.Page(
            title="Page with updates",
            components=[vm.Card(id="card_to_update", text="Placeholder")],
        )
    ]
)

# Just a normal Dash callback, like on https://dash.plotly.com/live-updates
@callback(Output("card_to_update", "children"), Input("interval", "n_intervals"))
def update_card(n_interval):
    return f"Page has updated {n_interval} times. The time is now {datetime.datetime.now()}"


app = Vizro().build(dashboard)
# Inject the dcc.Interval into the page layout - you could also do this just on one page of your app
app.dash.layout.children.append(dcc.Interval(id="interval"))
app.run()

Hi @antony.milne, sorry for the late reply. Thank you very much! This is exactly what I am looking for. Thanks!

2 Likes

I love the look of this. @antony.milne, could this be used for applications that are not focused on data visualisation, but for example could hold forms that a user can submit, and then see the results of the underlying process?

so amazing strong motivation

2 Likes

Hi @Wabiloo - thanks for your comment, and that’s a great question. The answer is that this is not possible right now, but it’s already on the roadmap, so watch this space! Here’s a rough prototype of what it might look like when it’s done:

3 Likes

Hi @antony.milne

As a long-time dash user, I’m just getting started with vizro for a quick prototype. I have to say that after a period of hesitation, I’m starting to appreciate the high-level approach of vizro!

I have a curious question - is it possible to disable the “ALL” option that is automatically generated in a dropdown selector when adding

vm.Filter(
    column="sepal_length",
    selector=vm.Dropdown(),
)

to my page?

1 Like

Hello @sislvacl ,

@antony.milne is currently on vacation, so hope you don’t mind if I jump in :slight_smile:
I am also curious, why did you hesitate in the beginning? Great to hear that you enjoy using Vizro now though!

That’s a great question! :bulb:

The ALL option is automatically added to the vm.Dropdown as soon as multi=True, so one way turning off the ALL option is setting multi=False:

1. Choosing a single-selector by setting multi=False

vm.Filter(
    column="sepal_length",
    selector=vm.Dropdown(multi=False),
)

However, if you actually want to keep multi=True while simultaneously turning off the ALL option, you can always extend our existing models, see this guide on custom components for reference. In general, custom components allow you to extend or add any dash-component that you might not find in our repository :+1:

Applying to your example, it would look like this:

2. Extend existing component using custom components

2.1. First define your custom component and change the build method as explained in the user guide above :slight_smile: You then need to register it as a type for the Filter.

class CustomDropdown(vm.Dropdown):
    """Custom Dropdown where `ALL` is turned off."""

    type: Literal["custom_dropdown"] = "custom_dropdown"

    def build(self):
        dropdown_build = super().build()
        dropdown_build[self.id].options = self.options
        dropdown_build[self.id].value = self.options[0]
        return dropdown_build

vm.Filter.add_type("selector", CustomDropdown)
vm.Parameter.add_type("selector", CustomDropdown)

2.2. Use your custom component

vm.Filter(column="sepal_length", selector=CustomDropdown())

Hope that helps! :crossed_fingers:

Best,
Li

3 Likes

Hi @li.nguyen

Thank you for comprehensive instructions, it worked perfectly!

I am also curious, why did you hesitate in the beginning? Great to hear that you enjoy using Vizro now though!

I think it was typical unwillingness to accept a new dogma. I was used to write callback functions to define logic in my apps and the idea of writing an app without any callbacks and expect the the app will magically work felt odd. But now after a few days I can see that there’s a segment of apps for which vizro is great. Typically data exploratory apps that sit on top of a single tidy dataframe.

I much appreciate that a vizro app is immediately pretty and I almost don’t need to focus on visuals at all.

One concern that I have is that I might run into a situation where I first build a prototype using vizro, then develop the app further and realise I need to do something more low-level to achieve the desired functionality. Then I wouldn’t know how I would migrate from vizro back to dash while keeping the nice visuals built into vizro.

3 Likes

Hey @sislvacl,

Amazing feedback btw! @li.nguyen is in turn on vacation today :joy: , so I thought I’d quickly reply.

One concern that I have is that I might run into a situation where I first build a prototype using vizro, then develop the app further and realise I need to do something more low-level to achieve the desired functionality. Then I wouldn’t know how I would migrate from vizro back to dash while keeping the nice visuals built into vizro.

You are definitely correct in that this may seem daunting to new adopters, and we are taking this feedback to improve our docs and tutorials to make the connection / translation / bridge between Vizro <> Dash even clearer!

That being said, one things to note about Vizro is that it is of course only a framework for Dash, and that Dash can be continued to be used almost as is! E.g. if you are in principle happy with Vizro, its design and layout, but you then want to weave in some “low level” things, you can just do it!

Something like

@callback(
    Output(component_id='my-output', component_property='children'),
    Input(component_id='my-input', component_property='value')
)

continues to work, you can insert almost any component via custom components as Li showed, you could go as far as hacking the build method of the vm.Dashboard and of course changing the visual appearance with CSS is also possible.

So really starting with Vizro, and then adjusting it once you get to more tricky bits is often a great archetype, because it speeds up a lot at the start, defines a clear and structured way that is always the same, but still allows you to customize if the need ever arises (and often it may not!)

5 Likes

Hi @antony.milne ,

It is very good example.

I have a question. Suppose if we have multiple callbacks with different Inputs and different outputs. How we do it on vizro.

For example:

from dash import Dash, dash_table, dcc, html, Input, Output, callback
import pandas as pd

import vizro.models as vm
from typing import List
from vizro import Vizro
from vizro.tables import dash_data_table
from vizro.models.types import capture


df = pd.read_csv('https://raw.githubusercontent.com/plotly/datasets/master/gapminder2007.csv')


@capture("table")
def my_custom_table(data_frame=None):
    """Custom table."""
    my_dash_table = dash_table.DataTable(
        id='datatable-interactivity',
        columns=[
            {"name": i, "id": i, "deletable": True, "selectable": True} for i in df.columns
        ],
        data=df.to_dict('records'),
        editable=True,
        filter_action="native",
        sort_action="native",
        sort_mode="multi",
        column_selectable="single",
        row_selectable="multi",
        row_deletable=True,
        selected_columns=[],
        selected_rows=[],
        page_action="native",
        page_current= 0,
        page_size= 10,
    ),
    return my_dash_table



page = vm.Page(
    title="Callback example with Dash DataTable",
    components=[
            vm.Table(
                title="Custom Dash DataTable",
                figure=my_custom_table(
                    data_frame=df
                ),
            ),

            # Here display the selected row countries with Radio Items
            # Once Radio Item selected needs to display the selected row data.
            # vm.Card()

            # Here display the Graph with selected Callback.
            # vm.Card()
        ],
)

@callback(
    Output('datatable-interactivity', 'style_data_conditional'),
    Input('datatable-interactivity', 'selected_columns')
)
def update_styles(selected_columns):
    return [{
        'if': { 'column_id': i },
        'background_color': '#D2F3FF'
    } for i in selected_columns]

@callback(
    Output('datatable-interactivity-container', "children"),
    Input('datatable-interactivity', "derived_virtual_data"),
    Input('datatable-interactivity', "derived_virtual_selected_rows"))
def update_graphs(rows, derived_virtual_selected_rows):
    # When the table is first rendered, `derived_virtual_data` and
    # `derived_virtual_selected_rows` will be `None`. This is due to an
    # idiosyncrasy in Dash (unsupplied properties are always None and Dash
    # calls the dependent callbacks when the component is first rendered).
    # So, if `rows` is `None`, then the component was just rendered
    # and its value will be the same as the component's dataframe.
    # Instead of setting `None` in here, you could also set
    # `derived_virtual_data=df.to_rows('dict')` when you initialize
    # the component.
    if derived_virtual_selected_rows is None:
        derived_virtual_selected_rows = []

    dff = df if rows is None else pd.DataFrame(rows)

    colors = ['#7FDBFF' if i in derived_virtual_selected_rows else '#0074D9'
              for i in range(len(dff))]

    return [
        dcc.Graph(
            id=column,
            figure={
                "data": [
                    {
                        "x": dff["country"],
                        "y": dff[column],
                        "type": "bar",
                        "marker": {"color": colors},
                    }
                ],
                "layout": {
                    "xaxis": {"automargin": True},
                    "yaxis": {
                        "automargin": True,
                        "title": {"text": column}
                    },
                    "height": 250,
                    "margin": {"t": 10, "l": 10, "r": 10},
                },
            },
        )
        # check if column exists - user may have deleted it
        # If `column.deletable=False`, then you don't
        # need to do this check.
        for column in ["pop", "lifeExp", "gdpPercap"] if column in dff
    ]


dashboard = vm.Dashboard(pages=[page])

Vizro().build(dashboard).run()


  1. How we display the user selected rows output in first card?
  2. How we can change the graph with user selected rows(through callback)?
  3. Is it possible to transfer data from one page to other?

Example Link: Sorting, Filtering, Selecting, and Paging Natively | Dash for Python Documentation | Plotly

Hi @Nagendra and welcome to the community! Thanks for your comments and question!

Because Vizro is built on top of Dash, the whole Dash callback mechanism is still available inside Vizro and works exactly the same way as in Dash.

The example in the docs actually has a lot going on, and some of it is done in a slightly unusual way (e.g. writing the graph out using data and layout arguments explicitly). So I’ve simplified it a bit here just to make it easier to follow. All the additional column selection callbacks etc. that the full example has are possible though.

Note:

  • supplying keyword arguments to dash_data_table doesn’t need to you to define a custom table - just using the built
    in vizro.tables.dash_data_table with those arguments is enough - see docs. You only really need to define a custom table if you’re doing something quite advanced/unusual
  • it is possible to transfer data from one page to another. The easiest way to do this is a dcc.Store (see @AnnMarieW’s example here - Vizro uses Dash pages under the hood). If you explain a bit more what you’d like to do then I can show you how to do this in Vizro. In the future we will probably have built in actions to simplify this process (e.g. you might just do send_to_store(data) and fetch_from_store(data)) but for now the solution is basically identical to Dash
import pandas as pd

import vizro.models as vm
from vizro import Vizro
from vizro.tables import dash_data_table

df = pd.read_csv("https://raw.githubusercontent.com/plotly/datasets/master/gapminder2007.csv")

table = dash_data_table(
    id="datatable-interactivity",
    data_frame=df,
    sort_action="native",
    row_selectable="multi",
    page_action="native",
    page_current=0,
    page_size=10,
)

# RadioItems don't typically live in the components part of the screen (they're normally in controls)
# so you need to enable them manually.
vm.Page.add_type("components", vm.RadioItems)

page = vm.Page(
    title="Callback example with Dash DataTable",
    components=[
        vm.Table(
            title="Custom Dash DataTable",
            figure=table,
        ),
        vm.RadioItems(id="country_select", options=[""]),
        # I made this a Card rather than Graph to keep it simple but the principle is the same
        vm.Card(id="card", text="Select countries and then a radio item"),
    ],
)


@callback(
    Output("country_select", "options"),
    Input("datatable-interactivity", "derived_virtual_data"),
    Input("datatable-interactivity", "derived_virtual_selected_rows"),
)
def update_radio_options(data, selected_row_ids):
    # Don't supply any values to radio items if no rows are selected
    if selected_row_ids is None:
        return None

    data = pd.DataFrame(data)
    countries = data.loc[selected_row_ids, "country"]

    return countries


@callback(Output("card", "children"), Input("country_select", "value"), prevent_initial_call=True)
def update_card(value):
    return f"You chose **{value}**"


dashboard = vm.Dashboard(pages=[page])
Vizro().build(dashboard).run()
2 Likes

Hi @antony.milne,

Thank you so much for your prompt reply and detailed explanation of my doubts. It worked well.

May be the below line is missed in above code.

from dash import callback, Output, Input

I am facing issues in plotting graph using vizro. The below is the code.

import pandas as pd
import vizro.models as vm
from vizro import Vizro
from vizro.tables import dash_data_table
from dash import callback, Output, Input, dcc

df = pd.read_csv("https://raw.githubusercontent.com/plotly/datasets/master/gapminder2007.csv")

table = dash_data_table(
    id="datatable-interactivity",
    data_frame=df,
    columns=[
            {"name": i, "id": i, "deletable": True, "selectable": True} for i in df.columns
        ],
    editable=True,
    filter_action="native",
    sort_action="native",
    sort_mode="multi",
    column_selectable="single",
    row_selectable="multi",
    row_deletable=True,
    selected_columns=[],
    selected_rows=[],
    page_action="native",
    page_current= 0,
)

page = vm.Page(
    title="Callback example with Dash DataTable",
    components=[
        vm.Table(
            title="Custom Dash DataTable",
            figure=table,
        ),
    
        vm.Card(id ='datatable-interactivity-container'),
    ],
)


@callback(
    Output('datatable-interactivity', 'style_data_conditional'),
    Input('datatable-interactivity', 'selected_columns'),
    prevent_initial_call=True
)
def update_styles(selected_columns):
    return [{
        'if': { 'column_id': i },
        'background_color': 'Green'
    } for i in selected_columns]


@callback(
    Output('datatable-interactivity-container', "children"),
    Input('datatable-interactivity', "derived_virtual_data"),
    Input('datatable-interactivity', "derived_virtual_selected_rows"))
def update_graphs(rows, derived_virtual_selected_rows):
    if derived_virtual_selected_rows is None:
        derived_virtual_selected_rows = []

    dff = df if rows is None else pd.DataFrame(rows)

    colors = ['#7FDBFF' if i in derived_virtual_selected_rows else '#0074D9'
              for i in range(len(dff))]
    return vm.Graph(
        dcc.Graph(
            id=column,
            figure={
                "data": [
                    {
                        "x": dff["country"],
                        "y": dff[column],
                        "type": "bar",
                        "marker": {"color": colors},
                    }
                ],
                "layout": {
                    "xaxis": {"automargin": True},
                    "yaxis": {
                        "automargin": True,
                        "title": {"text": column}
                    },
                    "height": 250,
                    "margin": {"t": 10, "l": 10, "r": 10},
                },
            },
        )
        # check if column exists - user may have deleted it
        # If `column.deletable=False`, then you don't
        # need to do this check.
        for column in ["pop", "lifeExp", "gdpPercap"] if column in dff
    )


dashboard = vm.Dashboard(pages=[page])
Vizro().build(dashboard).run()

In the above code, first callback is working, Iam facing issue in second callback.

 raise validation_error
pydantic.v1.error_wrappers.ValidationError: 1 validation error for Card
text
  field required (type=value_error.missing)

It would be the great help, if you help me in this case.

Thanks in advance.