mckinsey / vizro

Vizro is a toolkit for creating modular data visualization applications.
https://vizro.readthedocs.io/en/stable/
Apache License 2.0
2.72k stars 142 forks source link

Multi-step wizard #888

Open mohammadaffaneh opened 2 days ago

mohammadaffaneh commented 2 days ago

Which package?

vizro

What's the problem this feature will solve?

Vizro excels at creating modular dashboards, but as users tackle more sophisticated applications, the need arises for reusable and extensible complex UI components. These include multi-step wizards with dynamic behavior, CRUD operations, and seamless integration with external systems. Currently, building such components requires significant effort, often resulting in custom, non-reusable code. This limits the scalability and maintainability of applications developed with Vizro.

I’m working on applications that require complex workflows, such as multi-step wizards with real-time input validation and CRUD operations. While I’ve managed to achieve this using Dash callbacks and custom Python code, the lack of modularity and reusability makes the process cumbersome. Every new project requires re-implementing these components, which is time-consuming and error-prone.

Describe the solution you'd like

I envision Vizro evolving to support the creation of highly reusable and extensible complex components, which could transform how users approach sophisticated Dash applications. Here’s what this could look like:


How This Could Enhance Vizro
By introducing such capabilities, Vizro would empower users to go beyond dashboards and build complex, enterprise-level applications more efficiently. These features could help attract a broader audience, including those who require not only dashboards but also robust, interactive data workflows in their data applications.

Similar Solutions for Inspiration


My imagination:

Below is an high-level and simple implementation of the multistep wizard, where all wizard components and functionalities are isolated into a class (Wizard) following the Facade Design Pattern, complemented by elements of the Factory Pattern and the State Pattern. This class dynamically creates the logic based on the parameters and integration with the steps. The Step class represents individual steps.

wizard_module.py

from dash import html, dcc, Input, Output, State, MATCH, ALL, ctx

class Step:
    def __init__(self, id, label, components, validation_rules):
        self.id = id
        self.label = label
        self.components = components
        self.validation_rules = validation_rules

class Wizard:
    def __init__(
        self,
        steps,
        title=None,
        previous_button_text='Previous',
        next_button_text='Next',
        current_step_store_id='current_step',
        form_data_store_id='form_data',
        wizard_content_id='wizard_content',
        wizard_message_id='wizard_message',
        prev_button_id='prev_button',
        next_button_id='next_button',
        message_style=None,
        navigation_style=None,
        validate_on_next=True,
        custom_callbacks=None,
    ):
        # Instance attributes

    def render_layout(self):
        # Returns the UI Components of the form, tabs, buttons ..etc

    def render_step(self, step):
        # Returns the UI Components of a step

    def register_callbacks(self, app):
        # Dynamic callbacks for the multistep logic such as navigation, and feedback.

app.py

from dash import Dash
from wizard_module import Wizard, Step

# Define the wizard steps
steps = [
    Step(
        id=1,
        label="Step 1: User Info",
        components=[
            {"id": "name_input", "placeholder": "Enter your name"},
            {"id": "email_input", "placeholder": "Enter your email", "input_type": "email"},
            {"id": "password_input", "placeholder": "Enter your password", "input_type": "password"},
        ],
        validation_rules=[
            {"id": "name_input", "property": "value"},
            {"id": "email_input", "property": "value"},
            {"id": "password_input", "property": "value"},
        ],
    ),
    Step(
        id=2,
        label="Step 2: Address Info",
        components=[
            {"id": "address_input", "placeholder": "Enter your address"},
            {"id": "city_input", "placeholder": "Enter your city"},
            {"id": "state_input", "placeholder": "Enter your state"},
        ],
        validation_rules=[
            {"id": "address_input", "property": "value"},
            {"id": "city_input", "property": "value"},
            {"id": "state_input", "property": "value"},
        ],
    )
]

# Initialize the wizard
wizard = Wizard(
    steps=steps,
    title="User Registration Wizard",
    previous_button_text='Back',
    next_button_text='Continue',
    message_style={'color': 'blue', 'marginTop': '10px'},
    navigation_style={'marginTop': '30px'},
    validate_on_next=True,
    custom_callbacks={'on_complete': some_completion_function}
)

# Create the Dash app
app = Dash(__name__)
app.layout = wizard.render_layout()

# Register wizard callbacks
wizard.register_callbacks(app)

if __name__ == '__main__':
    app.run_server(debug=True)

Explanation:

Code of Conduct

antonymilne commented 2 days ago

Wow, this is an amazing issue, thank you for taking the time to write everything up so carefully and thoroughly @mohammadaffaneh! 🌟

You are absolutely right about the current strengths and weaknesses in Vizro in providing this functionality. I've actually made very similar Vizro apps to what you describe, with Previous/Next buttons stepping through a form. They're internal dashboards but if you're interested I can try to dig them out and upload a sanitised version.

Something similar is already on our roadmap, although it might take a while before we get to something that closely resembles your Wizard functionality, which has a lot of features!

Here's very roughly where I hope things will go:

  1. we implement an MVP Form component or at least show how Container can be used similarly, together with any remaining form input components. As you can from the files with prefix _ these have existed for a while but are not officially supported/documented yet although some people have used them already and they generally work fine
  2. we make it possible to step through a form like a wizard. Not sure whether each step would be a separate page on the dashboard or it would be a single page. Multi-page forms are a bit more challenging because it means persisting user input between pages, which is possible but adds a bit of complexity.
  3. we add some more bells and whistles like conditional form fields (e.g. depending on radio item selected, a different form input may or may not display), validation, etc.

Currently the main thing stopping this from being developed is that we're busy reworking the actions system which I think will need to be more robust and flexible to handle this sort of thing. After that it's towards the top of our list of priorities, so no promises but hopefully at least some of this will be possible within a few months 🤞

One challenge here is that we need to develop something that is general enough to be useful across a variety of use cases while specific and powerful enough that it's easy to use for each person's individual use case. e.g. we would like to make it easier for a user to configure machine learning model parameters and then run a pipeline from Vizro. This is quite different from what you're trying to do but would need to use similar wizard/form components.

I have a few questions for you, just to understand your requirements a bit better:

Configurable steps that can be added or modified dynamically.

What do you mean by this? The sort of thing like form fields appearing/disappearing depending on the values of previously input fields?

Seamless integration with external databases or APIs.

Is this achieved just by connecting your custom_callbacks={'on_complete': some_completion_function} to something that does request.post or inserts a row into a database or similar? What else are you imagining here? e.g. maybe something that loads up a row from a database, allows the user to edit the values and then save back into the database?

The validation logic is applied dynamically using the validation_rules defined in each Step instance.

How would you expect this validation logic to be applied? On the server side or clientside? e.g. one good option here might be to use pydantic to do validation.

It's not exactly what you describe, but this may be of interest to you: https://community.plotly.com/t/dash-pydantic-form/84435/31

mohammadaffaneh commented 2 days ago

@antonymilne Thank you for your prompt and detailed response! I appreciate the insights you've shared and the effort you're putting into enhancing Vizro's capabilities.

Regarding your questions:


  1. Configurable steps that can be added or modified dynamically.

    What do you mean by this? The sort of thing like form fields appearing/disappearing depending on the values of previously input fields?

    Answer:

    By "configurable steps that can be added or modified dynamically," I mean the ability to adjust the wizard's flow based on user inputs or external data during runtime. This includes:

    • Adding or Removing Steps: Introducing new steps or skipping existing ones based on conditions met in earlier steps.
    • Modifying Step Content: Dynamically changing the components or questions within a step depending on prior responses.
    • Branching Logic: Allowing the wizard to follow different paths (e.g., if a user selects "Yes," proceed to Step 3; if "No," skip to Step 5).

    This functionality enables the creation of more personalized and efficient user experiences, similar to conditional routing in forms. And yes this can be achieved by changing the 'style' property of a component within a callback.

  2. Seamless integration with external databases or APIs.

    Is this achieved just by connecting your custom_callbacks={'on_complete': some_completion_function} to something that does request.post or inserts a row into a database or similar? What else are you imagining here? e.g., maybe something that loads up a row from a database, allows the user to edit the values, and then save back into the database?

    Answer:

    Yes, integrating with external databases or APIs would involve using callbacks to perform operations like requests.post or database transactions. Additionally, I envision:

    • Real-Time Validation: Validating inputs against external systems (e.g., checking if a username is already taken).
    • CRUD Operations: Allowing users to create, read, update, or delete records within the wizard interface.
    • Two-Way Data Binding: Ensuring changes in the wizard are reflected in external systems and vice versa, possibly using websockets or integrating a semantic layer similar to what Apache Superset offers.
  3. The validation logic is applied dynamically using the validation_rules defined in each Step instance.

    How would you expect this validation logic to be applied? On the server side or client-side? e.g., one good option here might be to use Pydantic to do validation.

    Answer:

    I anticipate a hybrid approach for validation:

    • Client-Side Validation: For immediate feedback on input formats, required fields, and basic checks to enhance user experience and live validation.
    • Server-Side Validation: Utilizing libraries like Pydantic for more complex validation rules, data consistency checks, and security validations that require server resources or access to protected data.

    This ensures robust validation while maintaining responsiveness and security.


Thank you again for your efforts and quick reply. I'm excited about the direction Vizro is heading and am happy to assist further or provide feedback on early implementations.

antonymilne commented 22 hours ago

Thank you for all the answers @mohammadaffaneh, that's very helpful! 🙏 I think it will be a while before we can achieve many of the advanced features you're looking for here, but we'll definitely try and make some progress and refer back to these ideas when we develop the Form component. FYI @maxschulz-COL.