diff --git a/.github/actions/failed-artifacts-and-slack-notifications/action.yml b/.github/actions/failed-artifacts-and-slack-notifications/action.yml index 16bfe4dd30..1800456a5b 100644 --- a/.github/actions/failed-artifacts-and-slack-notifications/action.yml +++ b/.github/actions/failed-artifacts-and-slack-notifications/action.yml @@ -15,7 +15,7 @@ runs: - name: Archive production artifacts uses: actions/upload-artifact@v4 with: - name: Failed screenshots + name: Failed screenshots ${{ env.FAILED_SCREENSHOTS_ARCHIVE_POSTFIX }} path: | ${{ env.PROJECT_PATH }}failed_screenshots diff --git a/.github/workflows/test-e2e-component-library-vizro-core.yml b/.github/workflows/test-e2e-component-library-vizro-core.yml index bc162f0f30..a3953f217a 100644 --- a/.github/workflows/test-e2e-component-library-vizro-core.yml +++ b/.github/workflows/test-e2e-component-library-vizro-core.yml @@ -31,6 +31,9 @@ jobs: - name: Install Hatch run: pip install hatch + - name: Install pixelmatch + run: sudo npm install pixelmatch -g + - name: Show dependency tree run: hatch run pip tree diff --git a/.github/workflows/test-e2e-vizro-dom-elements.yml b/.github/workflows/test-e2e-vizro-dom-elements.yml index 4fd83a0b2e..598b7b4037 100644 --- a/.github/workflows/test-e2e-vizro-dom-elements.yml +++ b/.github/workflows/test-e2e-vizro-dom-elements.yml @@ -34,14 +34,17 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up Python ${{ env.PYTHON_VERSION }} + - name: Set up Python ${{ matrix.config.python-version }} uses: actions/setup-python@v5 with: - python-version: ${{ env.PYTHON_VERSION }} + python-version: ${{ matrix.config.python-version }} - name: Install Hatch run: pip install hatch + - name: Install pixelmatch + run: sudo npm install pixelmatch -g + - name: Show dependency tree run: hatch run pip tree diff --git a/.github/workflows/test-e2e-vizro-screenshots.yml b/.github/workflows/test-e2e-vizro-screenshots.yml new file mode 100644 index 0000000000..f52cc77ff4 --- /dev/null +++ b/.github/workflows/test-e2e-vizro-screenshots.yml @@ -0,0 +1,64 @@ +name: e2e vizro screenshots tests + +defaults: + run: + working-directory: vizro-core + +on: + push: + branches: [main] + pull_request: + branches: + - main + +env: + PYTHONUNBUFFERED: 1 + FORCE_COLOR: 1 + PYTHON_VERSION: "3.12" + +jobs: + test-e2e-vizro-screenshots: + name: test-e2e-vizro-screenshots for ${{ matrix.config.browser }} + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + config: + - browser: chrome + command: "-m 'not mobile_screenshots'" + - browser: chrome_mobile + command: "-m mobile_screenshots" + - browser: firefox + command: "--webdriver Firefox -m 'not mobile_screenshots'" + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ env.PYTHON_VERSION }} + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install Hatch + run: pip install hatch + + - name: Install pixelmatch + run: sudo npm install pixelmatch -g + + - name: Show dependency tree + run: hatch run pip tree + + - name: Run e2e vizro-screenshots tests + run: | + export BROWSER=${{ matrix.config.browser }} + hatch run test-e2e-vizro-screenshots ${{ matrix.config.command }} + + - name: Create artifacts and slack notifications + if: failure() + uses: ./.github/actions/failed-artifacts-and-slack-notifications + env: + TESTS_NAME: Vizro e2e vizro screenshots tests for ${{ matrix.config.browser }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + PROJECT_PATH: /home/runner/work/vizro/vizro/vizro-core/ + FAILED_SCREENSHOTS_ARCHIVE_POSTFIX: ${{ matrix.config.browser }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7cc706c8ec..f1c7135b62 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: args: [--autofix] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.4 + rev: v0.9.9 hooks: - id: ruff args: [--fix] @@ -46,14 +46,14 @@ repos: - id: ruff-format - repo: https://github.com/PyCQA/bandit - rev: 1.8.2 + rev: 1.8.3 hooks: - id: bandit args: [-c, pyproject.toml, -ll] additional_dependencies: ["bandit[toml]"] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.14.1 + rev: v1.15.0 hooks: - id: mypy files: ^vizro-core/src/ diff --git a/tools/pycafe/pycafe_utils.py b/tools/pycafe/pycafe_utils.py index 6b09d03269..2b5147b610 100644 --- a/tools/pycafe/pycafe_utils.py +++ b/tools/pycafe/pycafe_utils.py @@ -144,6 +144,7 @@ def get_example_directories() -> dict[str, Optional[list[str]]]: "isort==5.13.2", "plotly==5.24.1", ], + "vizro-core/examples/tutorial/": None, "vizro-ai/examples/dashboard_ui/": [ "vizro-ai>=0.3.0", "black", diff --git a/vizro-core/changelog.d/20250217_105619_huong_li_nguyen_styled_containers.md b/vizro-core/changelog.d/20250217_105619_huong_li_nguyen_styled_containers.md new file mode 100644 index 0000000000..d0ebd565be --- /dev/null +++ b/vizro-core/changelog.d/20250217_105619_huong_li_nguyen_styled_containers.md @@ -0,0 +1,46 @@ + + + + +### Added + +- Enable styling of `vm.Container` with a new argument `theme="plain"/"filled"/"outlined"`. See the user guide on [styled containers](https://vizro.readthedocs.io/en/stable/pages/user-guides/container/#styled-containers) for more details. ([#1002](https://github.com/mckinsey/vizro/pull/1002)) + + + + + diff --git a/vizro-core/changelog.d/20250301_183254_huong_li_nguyen_update_main_vizro_tutorial.md b/vizro-core/changelog.d/20250301_183254_huong_li_nguyen_update_main_vizro_tutorial.md new file mode 100644 index 0000000000..7c0d58d4f8 --- /dev/null +++ b/vizro-core/changelog.d/20250301_183254_huong_li_nguyen_update_main_vizro_tutorial.md @@ -0,0 +1,48 @@ + + + + + + + + + diff --git a/vizro-core/changelog.d/20250302_003210_145135826+vizro-svc_update_static_files.md b/vizro-core/changelog.d/20250302_003210_145135826+vizro-svc_update_static_files.md new file mode 100644 index 0000000000..7c0d58d4f8 --- /dev/null +++ b/vizro-core/changelog.d/20250302_003210_145135826+vizro-svc_update_static_files.md @@ -0,0 +1,48 @@ + + + + + + + + + diff --git a/vizro-core/changelog.d/20250304_164934_maximilian_schulz_enable_tutorial_pycafe.md b/vizro-core/changelog.d/20250304_164934_maximilian_schulz_enable_tutorial_pycafe.md new file mode 100644 index 0000000000..7c0d58d4f8 --- /dev/null +++ b/vizro-core/changelog.d/20250304_164934_maximilian_schulz_enable_tutorial_pycafe.md @@ -0,0 +1,48 @@ + + + + + + + + + diff --git a/vizro-core/docs/assets/tutorials/dashboard/00-dashboard-final.png b/vizro-core/docs/assets/tutorials/dashboard/00-dashboard-final.png new file mode 100644 index 0000000000..375ef59678 Binary files /dev/null and b/vizro-core/docs/assets/tutorials/dashboard/00-dashboard-final.png differ diff --git a/vizro-core/docs/assets/tutorials/dashboard/01-first-page.png b/vizro-core/docs/assets/tutorials/dashboard/01-first-page.png new file mode 100644 index 0000000000..8112e21585 Binary files /dev/null and b/vizro-core/docs/assets/tutorials/dashboard/01-first-page.png differ diff --git a/vizro-core/docs/assets/tutorials/dashboard/02-second-page.png b/vizro-core/docs/assets/tutorials/dashboard/02-second-page.png new file mode 100644 index 0000000000..3aeef80782 Binary files /dev/null and b/vizro-core/docs/assets/tutorials/dashboard/02-second-page.png differ diff --git a/vizro-core/docs/assets/tutorials/dashboard/03-second-page-kpi.png b/vizro-core/docs/assets/tutorials/dashboard/03-second-page-kpi.png new file mode 100644 index 0000000000..afd70644b7 Binary files /dev/null and b/vizro-core/docs/assets/tutorials/dashboard/03-second-page-kpi.png differ diff --git a/vizro-core/docs/assets/tutorials/dashboard/04-second-page-tabs.png b/vizro-core/docs/assets/tutorials/dashboard/04-second-page-tabs.png new file mode 100644 index 0000000000..0c6df611a2 Binary files /dev/null and b/vizro-core/docs/assets/tutorials/dashboard/04-second-page-tabs.png differ diff --git a/vizro-core/docs/assets/tutorials/dashboard/05-second-page-layout.png b/vizro-core/docs/assets/tutorials/dashboard/05-second-page-layout.png new file mode 100644 index 0000000000..8b07e0058b Binary files /dev/null and b/vizro-core/docs/assets/tutorials/dashboard/05-second-page-layout.png differ diff --git a/vizro-core/docs/assets/tutorials/dashboard/06-second-page-controls.png b/vizro-core/docs/assets/tutorials/dashboard/06-second-page-controls.png new file mode 100644 index 0000000000..69da77dbb7 Binary files /dev/null and b/vizro-core/docs/assets/tutorials/dashboard/06-second-page-controls.png differ diff --git a/vizro-core/docs/assets/tutorials/dashboard/07-third-page.png b/vizro-core/docs/assets/tutorials/dashboard/07-third-page.png new file mode 100644 index 0000000000..4ca766206c Binary files /dev/null and b/vizro-core/docs/assets/tutorials/dashboard/07-third-page.png differ diff --git a/vizro-core/docs/assets/tutorials/dashboard/08-third-page-layout.png b/vizro-core/docs/assets/tutorials/dashboard/08-third-page-layout.png new file mode 100644 index 0000000000..6fc1374ebf Binary files /dev/null and b/vizro-core/docs/assets/tutorials/dashboard/08-third-page-layout.png differ diff --git a/vizro-core/docs/assets/tutorials/dashboard/09-third-page-parameter.png b/vizro-core/docs/assets/tutorials/dashboard/09-third-page-parameter.png new file mode 100644 index 0000000000..da7cc0c5c6 Binary files /dev/null and b/vizro-core/docs/assets/tutorials/dashboard/09-third-page-parameter.png differ diff --git a/vizro-core/docs/assets/tutorials/dashboard/10-third-page-custom-chart.png b/vizro-core/docs/assets/tutorials/dashboard/10-third-page-custom-chart.png new file mode 100644 index 0000000000..ab4afdd3e6 Binary files /dev/null and b/vizro-core/docs/assets/tutorials/dashboard/10-third-page-custom-chart.png differ diff --git a/vizro-core/docs/assets/tutorials/dashboard/11-dashboard-title-logo.png b/vizro-core/docs/assets/tutorials/dashboard/11-dashboard-title-logo.png new file mode 100644 index 0000000000..715461487b Binary files /dev/null and b/vizro-core/docs/assets/tutorials/dashboard/11-dashboard-title-logo.png differ diff --git a/vizro-core/docs/assets/tutorials/dashboard/12-dashboard-navigation.png b/vizro-core/docs/assets/tutorials/dashboard/12-dashboard-navigation.png new file mode 100644 index 0000000000..1f0de9bc62 Binary files /dev/null and b/vizro-core/docs/assets/tutorials/dashboard/12-dashboard-navigation.png differ diff --git a/vizro-core/docs/assets/tutorials/dashboard/dashboard-first-page.png b/vizro-core/docs/assets/tutorials/dashboard/dashboard-first-page.png deleted file mode 100644 index ce421fca7b..0000000000 Binary files a/vizro-core/docs/assets/tutorials/dashboard/dashboard-first-page.png and /dev/null differ diff --git a/vizro-core/docs/assets/tutorials/dashboard/dashboard-second-page.png b/vizro-core/docs/assets/tutorials/dashboard/dashboard-second-page.png deleted file mode 100644 index ee48af4d91..0000000000 Binary files a/vizro-core/docs/assets/tutorials/dashboard/dashboard-second-page.png and /dev/null differ diff --git a/vizro-core/docs/assets/tutorials/dashboard/dashboard1.png b/vizro-core/docs/assets/tutorials/dashboard/dashboard1.png deleted file mode 100644 index f768ed8bc1..0000000000 Binary files a/vizro-core/docs/assets/tutorials/dashboard/dashboard1.png and /dev/null differ diff --git a/vizro-core/docs/assets/tutorials/dashboard/dashboard2.png b/vizro-core/docs/assets/tutorials/dashboard/dashboard2.png deleted file mode 100644 index 9790a4a2fa..0000000000 Binary files a/vizro-core/docs/assets/tutorials/dashboard/dashboard2.png and /dev/null differ diff --git a/vizro-core/docs/assets/tutorials/dashboard/dashboard21.png b/vizro-core/docs/assets/tutorials/dashboard/dashboard21.png deleted file mode 100644 index a330d7ecc9..0000000000 Binary files a/vizro-core/docs/assets/tutorials/dashboard/dashboard21.png and /dev/null differ diff --git a/vizro-core/docs/assets/tutorials/dashboard/dashboard22.png b/vizro-core/docs/assets/tutorials/dashboard/dashboard22.png deleted file mode 100644 index 7bc116d592..0000000000 Binary files a/vizro-core/docs/assets/tutorials/dashboard/dashboard22.png and /dev/null differ diff --git a/vizro-core/docs/assets/tutorials/dashboard/dashboard23.png b/vizro-core/docs/assets/tutorials/dashboard/dashboard23.png deleted file mode 100644 index 6a4719bd65..0000000000 Binary files a/vizro-core/docs/assets/tutorials/dashboard/dashboard23.png and /dev/null differ diff --git a/vizro-core/docs/assets/tutorials/dashboard/dashboard231.png b/vizro-core/docs/assets/tutorials/dashboard/dashboard231.png deleted file mode 100644 index 1a604cf5b1..0000000000 Binary files a/vizro-core/docs/assets/tutorials/dashboard/dashboard231.png and /dev/null differ diff --git a/vizro-core/docs/assets/tutorials/dashboard/dashboard232.png b/vizro-core/docs/assets/tutorials/dashboard/dashboard232.png deleted file mode 100644 index fd494c8e5e..0000000000 Binary files a/vizro-core/docs/assets/tutorials/dashboard/dashboard232.png and /dev/null differ diff --git a/vizro-core/docs/assets/tutorials/dashboard/dashboard233.png b/vizro-core/docs/assets/tutorials/dashboard/dashboard233.png deleted file mode 100644 index e25a34208d..0000000000 Binary files a/vizro-core/docs/assets/tutorials/dashboard/dashboard233.png and /dev/null differ diff --git a/vizro-core/docs/assets/tutorials/dashboard/dashboard24.png b/vizro-core/docs/assets/tutorials/dashboard/dashboard24.png deleted file mode 100644 index 2600b4383c..0000000000 Binary files a/vizro-core/docs/assets/tutorials/dashboard/dashboard24.png and /dev/null differ diff --git a/vizro-core/docs/assets/tutorials/dashboard/dashboard3.png b/vizro-core/docs/assets/tutorials/dashboard/dashboard3.png deleted file mode 100644 index c8de95c9c2..0000000000 Binary files a/vizro-core/docs/assets/tutorials/dashboard/dashboard3.png and /dev/null differ diff --git a/vizro-core/docs/assets/tutorials/dashboard/dashboard4.png b/vizro-core/docs/assets/tutorials/dashboard/dashboard4.png deleted file mode 100644 index d1f9a9d87a..0000000000 Binary files a/vizro-core/docs/assets/tutorials/dashboard/dashboard4.png and /dev/null differ diff --git a/vizro-core/docs/assets/tutorials/dashboard/vizro-tutorial.gif b/vizro-core/docs/assets/tutorials/dashboard/vizro-tutorial.gif new file mode 100644 index 0000000000..37848c18e3 Binary files /dev/null and b/vizro-core/docs/assets/tutorials/dashboard/vizro-tutorial.gif differ diff --git a/vizro-core/docs/assets/user_guides/components/container-styled.png b/vizro-core/docs/assets/user_guides/components/container-styled.png new file mode 100644 index 0000000000..3c05441c98 Binary files /dev/null and b/vizro-core/docs/assets/user_guides/components/container-styled.png differ diff --git a/vizro-core/docs/assets/user_guides/custom_css/style-container.png b/vizro-core/docs/assets/user_guides/custom_css/style-container.png deleted file mode 100644 index 1b8c61fa95..0000000000 Binary files a/vizro-core/docs/assets/user_guides/custom_css/style-container.png and /dev/null differ diff --git a/vizro-core/docs/pages/tutorials/explore-components.md b/vizro-core/docs/pages/tutorials/explore-components.md index f59cbf052b..70baaa46df 100644 --- a/vizro-core/docs/pages/tutorials/explore-components.md +++ b/vizro-core/docs/pages/tutorials/explore-components.md @@ -1,49 +1,103 @@ # Explore Vizro -In this tutorial, we walk through the process of creating a sophisticated dashboard. You'll be introduced to some Vizro components and learn how to create a pair of dashboard pages and configure their layout to suit the functionality you need. The example uses the [gapminder data](https://plotly.com/python-api-reference/generated/plotly.express.data.html#plotly.express.data.gapminder). +In this tutorial, you'll learn how to build an interactive dashboard with multiple pages, incorporating a wide range of Vizro's components. This tutorial should take **about an hour to finish**, so grab a coffee or tea and let's dive in! ☕ -If you haven't yet done so, you may want to review the [first dashboard tutorial](../tutorials/first-dashboard.md) before starting on this one. +!!! note + If you're looking for a quick start to get up and running with Vizro, consider reviewing the [first dashboard tutorial](../tutorials/first-dashboard.md) before diving into this one. -## 1. (Optional) Install Vizro and get ready to run your code +**By the end of this tutorial, you have learned how to:** -The code for this tutorial is all available for you to experiment with in [PyCafe](https://py.cafe/) so there is no need to install Vizro and run it locally. For more about how this works, check out the [PyCafe documentation](https://py.cafe/docs/apps/vizro). +- Explore most of [Vizro's components](../user-guides/components.md). +- Use the [Vizro visual vocabulary](https://vizro-demo-visual-vocabulary.hf.space/) to guide your chart creation. +- Design custom charts with [Plotly Express](https://plotly.com/python-api-reference/plotly.express.html). +- Develop multiple pages for the dashboard. +- Customize the layout of the pages. +- Add interactivity using filters and parameters. +- Add a logo and title to the dashboard. +- Customize the dashboard navigation. -However, if you prefer working in a Notebook or Python script, you should [install Vizro](../user-guides/install.md). +This tutorial uses the [tips dataset](https://plotly.com/python-api-reference/generated/plotly.express.data.html#plotly.express.data.tips), which was collected by a waiter who recorded information about each tip he received over several months at a restaurant. -## 2. Create a first dashboard page +![](../../assets/tutorials/dashboard/vizro-tutorial.gif) [Here is a preview of the dashboard you'll build](https://py.cafe/app/vizro-official/vizro-tips-analysis-tutorial) -In this section we create a new [`Page`][vizro.models.Page] called `first_page`. +## 1. Install Vizro or run on PyCafe -The foundation of every Vizro dashboard is a [`Page`][vizro.models.Page] object. A page uses a set of [component types](../user-guides/components.md) to display the content of the page. These components can be objects such as [`Graph`][vizro.models.Graph], [`Table`][vizro.models.Table], [`Card`][vizro.models.Card], [`Button`][vizro.models.Button], [`Container`][vizro.models.Container], or [`Tabs`][vizro.models.Tabs]. +You can experiment with the code for this tutorial directly on [PyCafe](https://py.cafe/vizro-official/vizro-tips-analysis-tutorial), so there's no need to install Vizro locally. We recommend starting with a [blank Vizro project on PyCafe](https://py.cafe/snippet/vizro/v1) and copying the code snippets from this tutorial into it, to see how everything integrates. For more details, check out the [PyCafe documentation](https://py.cafe/docs/apps/vizro). -### 2.1. Add the first figure +??? note "If you prefer working in a Notebook or Python script" + To work in a Notebook or locally using a Python script, you need to [install Vizro](../user-guides/install.md). -Vizro uses [`Graph`][vizro.models.Graph] objects and [Plotly Express functions](https://plotly.com/python-api-reference/plotly.express.html) to build different types of [figures](https://plotly.com/python-api-reference/generated/plotly.graph_objects.Figure.html). + Paste the code from the tutorial into a Notebook cell, run the Notebook, and evaluate it. -The code below shows the steps necessary to add a box plot to the page: + You will need to restart the kernel each time you run the code. Otherwise, you may encounter errors such as *"Components must uniquely map..."* because those components persist from the previous execution. As an alternative to restarting the kernel each time, you can add a cell containing `from vizro import Vizro; Vizro._reset()` to the top of your Notebook and re-run it each time you re-run your code. With this method, there is no need to restart the Jupyter kernel. -1. Add a Vizro [`Graph`][vizro.models.Graph] to the `components` list. -1. Add a [`plotly.express.box`](https://plotly.com/python-api-reference/generated/plotly.express.box.html#plotly.express.box) figure to the list of components. + --- + + If you prefer using Python scripts instead of Notebooks, follow these steps: + + 1. Create a new script called `app.py`. + 1. Copy the code above into the script. + 1. Navigate to the directory where `app.py` file is located using your terminal. + 1. Run the script by executing the command `python app.py`. + + Once the script is running, open your web browser and navigate to `localhost:8050` to view the dashboard. To enable debug mode for hot reloading, add `debug=True` inside the run() method at the end of your `app.py` file: + + `Vizro().build(dashboard).run(debug=True)` + +## 2. Understand the basics + +Before we dive in, let's quickly cover some basics: + +At the top level, you'll be creating a [`Dashboard`][vizro.models.Dashboard]. Here's what you can configure at the dashboard-level: -!!! example "First component" - === "app.py" +- **Pages**: You can add multiple pages; they are the building blocks of your dashboard. +- **Navigation**: You can customize navigation between those different pages. +- **Title/Logo**: You can add your own titles and logos. + +For each [`Page`][vizro.models.Page], you can additionally configure the following: + +- **Components**: Add charts, tables, input/output interfaces, and more. +- **Controls**: Include filters and parameters. +- **Layouts**: Customize the placement of components within a page. +- **Actions/Interactions**: Create interactions between components and use predefined or custom actions. + +## 3. Create a first page + +In this section, you learn how to create a new [`Page`][vizro.models.Page] and store it in a variable called `first_page`. + +A [`Page`][vizro.models.Page] model is the foundation of any Vizro dashboard. It uses a set of components to display content. For a comprehensive list of all Vizro components, refer to the [components overview page](../user-guides/components.md). + +### 3.1. Add a table + +To start, let's get an overview of the data by displaying it in a table using [AgGrid][vizro.models.AgGrid]. Follow these steps to create a page and add a table to it: + +1. Import the necessary packages and load the dataset. +1. Create a [`Page`][vizro.models.Page] and set its `title` to `"Data"`. +1. Add an [`AgGrid`][vizro.models.AgGrid] component to the `components` list. +1. Use the [`dash_ag_grid`][vizro.tables.dash_ag_grid] function inside the `figure` argument of `AgGrid`. +1. Provide details about the data source in the `footer` argument of `AgGrid`. +1. Add the newly created page to the list of `pages` in the [Dashboard][vizro.models.Dashboard]. + +!!! example "First Page" + === "Code - Dashboard" ```{.python pycafe-link} - from vizro import Vizro import vizro.models as vm import vizro.plotly.express as px + from vizro import Vizro + from vizro.tables import dash_ag_grid + from vizro.models.types import capture + from vizro.figures import kpi_card - df = px.data.gapminder() - gapminder_data = ( - df.groupby(by=["continent", "year"]). - agg({"lifeExp": "mean", "pop": "sum", "gdpPercap": "mean"}).reset_index() - ) + tips = px.data.tips() first_page = vm.Page( - title="First Page", + title="Data", components=[ - vm.Graph( - figure=px.box(gapminder_data, x="continent", y="lifeExp", color="continent", - labels={"lifeExp": "Life Expectancy", "continent": "Continent"}), + vm.AgGrid( + figure=dash_ag_grid(tips), + footer="""**Data Source:** Bryant, P. G. and Smith, M. (1995). + Practical Data Analysis: Case Studies in Business Statistics. + Homewood, IL: Richard D. Irwin Publishing.""", ), ], ) @@ -53,528 +107,1355 @@ The code below shows the steps necessary to add a box plot to the page: ``` === "Result" - [![FirstPage1]][firstpage1] - -??? note "To run the dashboard in a Notebook or script" - Paste the above code into a Notebook cell, run the Notebook, and evaluate it. + [![FirstPage]][firstpage] - --- +After running your code (either locally or on PyCafe), you can now view the dashboard (on `localhost:8050` if you ran it locally, or on the right part of the screen if you are using PyCafe). - If you prefer to use Python scripts to Notebooks, here's how to try out the dashboard: +Take a moment to explore the data in the table. You can sort, filter, and search within the `AgGrid` columns to better understand the dataset. - 1. Create a new script called `app.py`. - 1. Copy the code above into the script. - 1. Navigate to the directory where `app.py` file is located using your terminal. - 1. Run the script by executing the command `python app.py`. +You'll notice a toggle in the top-right corner of the dashboard, enabling you to switch between dark and light themes. Try it out! - Once the script is running, open your web browser and go to `localhost:8050`. You should see the dashboard page with the gapminder data displayed, as shown in the `Result` tab above. +**Great job! You've successfully created a first dashboard page!** -As you can see from the code, `first_page` is added to the [`Dashboard`][vizro.models.Dashboard] and the dashboard is displayed by running `Vizro().build(dashboard).run()`. +## 4. Create a second page -### 2.2. Add further components +### 4.1. Add a chart -You can combine and arrange various types of `components` on a dashboard page. The `components` currently available are [`Card`][vizro.models.Card], [`Graph`][vizro.models.Graph], and [`Button`][vizro.models.Button]. For more information, refer to the [components](../user-guides/components.md) overview page to find the guide for each type. +Next, you'll learn how to add a second page to the dashboard, featuring charts and KPI (Key Performance Indicator) cards. -The code below adds two components to the page: +Vizro uses [`Graph`][vizro.models.Graph] models and [Plotly Express functions](https://plotly.com/python/plotly-express/) to create various types of charts. You can explore some of the available chart types and their code examples in the [Vizro visual vocabulary](https://vizro-demo-visual-vocabulary.hf.space). -- A [`Card`][vizro.models.Card] to insert markdown text into the dashboard. -- A [`Graph`][vizro.models.Graph] to illustrate GDP development per continent since 1952 as a line graph. +Follow these steps to add a histogram to the page: -!!! warning "Before you run this code in a Jupyter Notebook" - If you are following this tutorial in a Jupyter Notebook, you need to restart the kernel each time you evaluate the code. If you do not, you will see error messages such as "Components must uniquely map..." because those components already exist from the previous evaluation. +1. Create a second [`Page`][vizro.models.Page] and store it in a variable called `second_page`. Set its `title` to `"Summary"`. +1. Add a [`Graph`][vizro.models.Graph] to the `components` list. +1. Inside the `figure` argument of the `Graph`, use the code for the [px.histogram from the visual vocabulary](https://vizro-demo-visual-vocabulary.hf.space/distribution/histogram). +1. Add the new page to the list of `pages` in the [`Dashboard`][vizro.models.Dashboard] by calling `vm.Dashboard(pages=[first_page, second_page])`. -!!! example "Add components" - === "Code first component" +!!! example "Second Page" + === "Snippet - Second Page" ```py + second_page = vm.Page( + title="Summary", + components=[ + vm.Graph(figure=px.histogram(tips, x="total_bill")), + vm.Graph(figure=px.histogram(tips, x="tip")), + ], + ) + dashboard = vm.Dashboard(pages=[first_page, second_page]) + ``` + + === "Code - Dashboard" + ```{.python pycafe-link hl_lines="22-28 30"} + import vizro.models as vm + import vizro.plotly.express as px + from vizro import Vizro + from vizro.tables import dash_ag_grid + from vizro.models.types import capture + from vizro.figures import kpi_card + + tips = px.data.tips() + + first_page = vm.Page( + title="Data", + components=[ + vm.AgGrid( + figure=dash_ag_grid(tips), + footer="""**Data Source:** Bryant, P. G. and Smith, M. (1995). + Practical Data Analysis: Case Studies in Business Statistics. + Homewood, IL: Richard D. Irwin Publishing.""", + ), + ], + ) - vm.Card( - text=""" - # First dashboard page - This pages shows the inclusion of markdown text in a page and how components - can be structured using Layout. - """, + second_page = vm.Page( + title="Summary", + components=[ + vm.Graph(figure=px.histogram(tips, x="total_bill")), + vm.Graph(figure=px.histogram(tips, x="tip")), + ], ) + dashboard = vm.Dashboard(pages=[first_page, second_page]) + Vizro().build(dashboard).run() ``` - === "Code second component" + === "Result" + [![SecondPage]][secondpage] + +Notice that the charts are automatically stacked vertically in the order specified under `components`, each taking up equal space. This is the default behavior in Vizro, but you'll learn how to customize the layout later! + +Additionally, a page navigation menu has been added to the left side of the dashboard, enabling you to switch between the two pages we’ve created. + +You'll also notice that the left-side menu can be collapsed to provide more space for the dashboard content. **Give it a try!** + +### 4.2. Add KPI cards + +You can combine and arrange various types of `components` on a dashboard page. Refer to the [components overview page](../user-guides/components.md) for a comprehensive list of available components. + +Let's add two KPI cards to the second page. Follow these steps: + +1. Add a [`Figure`][vizro.models.Figure] to the list of `components`. +1. Inside the `figure` argument of the `Figure`, use the [`kpi_card`][vizro.figures.kpi_card] function. +1. Configure your `kpi_card` by setting the `value_column`, `agg_func`, `value_format`, and `title`. To learn more about configuring KPI cards, check out our [guide to KPI cards](../user-guides/figure.md#key-performance-indicator-kpi-cards). +1. Repeat the above steps to add another KPI card to the page. + +!!! example "Add KPI Cards" + === "Snippet - KPI Card I" ```py + vm.Figure( + figure=kpi_card( + data_frame=tips, + value_column="total_bill", + agg_func="mean", + value_format="${value:.2f}", + title="Average Bill", + ) + ) + ``` - vm.Graph( - id="line_gdp", - figure=px.line(gapminder_data, x="year", y="gdpPercap", color="continent", - labels={"year": "Year", "continent": "Continent", - "gdpPercap":"GDP Per Cap"}), + === "Snippet - KPI Card II" + ```py + vm.Figure( + figure=kpi_card( + data_frame=tips, + value_column="tip", + agg_func="mean", + value_format="${value:.2f}", + title="Average Tips" + ) ) ``` - === "app.py" - ```{.python pycafe-link} - from vizro import Vizro + === "Code - dashboard" + ```{.python pycafe-link hl_lines="25-42"} import vizro.models as vm import vizro.plotly.express as px + from vizro import Vizro + from vizro.tables import dash_ag_grid + from vizro.models.types import capture + from vizro.figures import kpi_card - df = px.data.gapminder() - gapminder_data = ( - df.groupby(by=["continent", "year"]). - agg({"lifeExp": "mean", "pop": "sum", "gdpPercap": "mean"}).reset_index() - ) + tips = px.data.tips() first_page = vm.Page( - title="First Page", + title="Data", components=[ - vm.Card( - text=""" - # First dashboard page - This pages shows the inclusion of markdown text in a page and how components - can be structured using Layout. - """, + vm.AgGrid( + figure=dash_ag_grid(tips), + footer="""**Data Source:** Bryant, P. G. and Smith, M. (1995). + Practical Data Analysis: Case Studies in Business Statistics. + Homewood, IL: Richard D. Irwin Publishing.""", ), - vm.Graph( - figure=px.box(gapminder_data, x="continent", y="lifeExp", color="continent", - labels={"lifeExp": "Life Expectancy", "continent": "Continent"}), + ], + ) + + second_page = vm.Page( + title="Summary", + components=[ + vm.Figure( + figure=kpi_card( + data_frame=tips, + value_column="total_bill", + agg_func="mean", + value_format="${value:.2f}", + title="Average Bill", + ) ), - vm.Graph( - figure=px.line(gapminder_data, x="year", y="gdpPercap", color="continent", - labels={"year": "Year", "continent": "Continent", - "gdpPercap":"GDP Per Cap"}), + vm.Figure( + figure=kpi_card( + data_frame=tips, + value_column="tip", + agg_func="mean", + value_format="${value:.2f}", + title="Average Tips" + ) ), + vm.Graph(figure=px.histogram(tips, x="total_bill")), + vm.Graph(figure=px.histogram(tips, x="tip")), + ], + ) + + dashboard = vm.Dashboard(pages=[first_page, second_page]) + Vizro().build(dashboard).run() + ``` + + === "Result" + [![SecondPage2]][secondpage2] + +### 4.3. Add tabs to switch views + +You may not want to display both histograms simultaneously and instead prefer to switch between views. You can achieve this by using the [`Tabs`][vizro.models.Tabs] component. For more details, refer to Vizro's [tabs user guide](../user-guides/tabs.md). + +Let's place the two histograms in separate tabs. Follow these steps: +1. Add each `Graph` to the `components` of a [`Container`][vizro.models.Container]. +1. Set the `title` argument inside each `Container` to the desired tab name. +1. Add the containers to the `tabs` list of the `Tabs` component. +1. Add the `Tabs` component to the `components` of the `Page`. + +!!! example "Add Tabs" + === "Snippet - Tabs" + ```py + vm.Tabs( + tabs=[ + vm.Container( + title="Total Bill ($)", + components=[ + vm.Graph(figure=px.histogram(tips, x="total_bill")), + ], + ), + vm.Container( + title="Total Tips ($)", + components=[ + vm.Graph(figure=px.histogram(tips, x="tip")), + ], + ), ], ) + ``` - dashboard = vm.Dashboard(pages=[first_page]) + === "Code - dashboard" + ```{.python pycafe-link hl_lines="43-59"} + import vizro.models as vm + import vizro.plotly.express as px + from vizro import Vizro + from vizro.tables import dash_ag_grid + from vizro.models.types import capture + from vizro.figures import kpi_card + + tips = px.data.tips() + + first_page = vm.Page( + title="Data", + components=[ + vm.AgGrid( + figure=dash_ag_grid(tips), + footer="""**Data Source:** Bryant, P. G. and Smith, M. (1995). + Practical Data Analysis: Case Studies in Business Statistics. + Homewood, IL: Richard D. Irwin Publishing.""", + ), + ], + ) + + second_page = vm.Page( + title="Summary", + components=[ + vm.Figure( + figure=kpi_card( + data_frame=tips, + value_column="total_bill", + agg_func="mean", + value_format="${value:.2f}", + title="Average Bill", + ) + ), + vm.Figure( + figure=kpi_card( + data_frame=tips, + value_column="tip", + agg_func="mean", + value_format="${value:.2f}", + title="Average Tips" + ) + ), + vm.Tabs( + tabs=[ + vm.Container( + title="Total Bill ($)", + components=[ + vm.Graph(figure=px.histogram(tips, x="total_bill")), + ], + ), + vm.Container( + title="Total Tips ($)", + components=[ + vm.Graph(figure=px.histogram(tips, x="tip")), + ], + ), + ], + ) + ], + ) + + dashboard = vm.Dashboard(pages=[first_page, second_page]) Vizro().build(dashboard).run() ``` === "Result" - [![FirstPage2]][firstpage2] + [![SecondPage3]][secondpage3] -As you explore the dashboard, you may notice that the current layout could be further enhanced. The charts appear cramped, while the text component has ample unused space. The next section explains how to configure the layout and arrange the components. +**Take a moment to switch between the tabs! 🕰️** -!!! note "An introduction to Vizro-AI" - In the example above, the code to create the line graph was generated using [Vizro-AI](https://vizro.readthedocs.io/en/latest/pages/tutorials/first-dashboard/). Vizro-AI enables you to use English, or other languages, to create interactive charts with [Plotly](https://plotly.com/python/) by simplifying the process through use of a large language model. In essence, Vizro-AI generates code from natural language instructions so that you can add it into a Vizro dashboard, such as in the example above. +As you explore the dashboard, you might notice that the current layout could use some adjustments. The histograms appear cramped, while the KPI cards have too much space. In the next section, you'll learn how to configure the layout and better arrange the components. - Find out more in the [Vizro-AI documentation](https://vizro.readthedocs.io/projects/vizro-ai/)! +### 4.4. Configure the layout -### 2.3. Configure the layout +By default, Vizro places each element in the order it was added to `components`, and spaces them equally. You can use the [`Layout`][vizro.models.Layout] to control the placement and size of components on the page. To learn more about how to configure layouts, check out [How to use layouts](../user-guides/layouts.md). -By default, Vizro places each element in the order it was added to `components` list, and spaces them equally. +In the following layout configuration, the layout is divided into **four columns** and **four rows**. The numbers in the grid correspond to the index of the components in the `components` list. -You can use the [`Layout`][vizro.models.Layout] object to specify the placement and size of components on the page. To learn more about how to configure layouts, check out [How to use layouts](../user-guides/layouts.md). +- The first KPI card (0) is positioned at the top, occupying the first cell in the first row. +- The second KPI card (1) is positioned to the right of the first KPI card. +- There are two empty cells to the right of the KPI cards (-1). +- The `Tabs` component (2) is placed below the KPI cards, spanning all cells across the remaining three rows. -The following layout configuration positions the text at the top and the two charts side by side, giving them more space relative to the text component: + -```python -grid = [[0, 0], [1, 2], [1, 2], [1, 2]] ``` +grid = [[0, 1,-1,-1], + [2, 2, 2, 2], + [2, 2, 2, 2], + [2, 2, 2, 2]] +``` + +Run the code below to apply the layout to the dashboard page: + +!!! example "Code - Layout" + === "Snippet - Layout" + ```py + layout = vm.Layout( + grid=[[0, 1, -1, -1], + [2, 2, 2, 2], + [2, 2, 2, 2], + [2, 2, 2, 2]] + ) + ``` + + === "Code - Dashboard" + ```{.python pycafe-link hl_lines="24"} + import vizro.models as vm + import vizro.plotly.express as px + from vizro import Vizro + from vizro.tables import dash_ag_grid + from vizro.models.types import capture + from vizro.figures import kpi_card + + tips = px.data.tips() + + first_page = vm.Page( + title="Data", + components=[ + vm.AgGrid( + figure=dash_ag_grid(tips), + footer="""**Data Source:** Bryant, P. G. and Smith, M (1995) + Practical Data Analysis: Case Studies in Business Statistics. + Homewood, IL: Richard D. Irwin Publishing.""", + ), + ], + ) -Vizro interprets these values as follows. First, the configuration divides the available space into two columns and four rows. Each element in the list (such as `[0,0]`) represents one row of the grid layout: + second_page = vm.Page( + title="Summary", + layout=vm.Layout(grid=[[0, 1, -1, -1], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2]]), + components=[ + vm.Figure( + figure=kpi_card( + data_frame=tips, + value_column="total_bill", + agg_func="mean", + value_format="${value:.2f}", + title="Average Bill", + ) + ), + vm.Figure( + figure=kpi_card( + data_frame=tips, + value_column="tip", + agg_func="mean", + value_format="${value:.2f}", + title="Average Tips" + ) + ), + vm.Tabs( + tabs=[ + vm.Container( + title="Total Bill ($)", + components=[ + vm.Graph(figure=px.histogram(tips, x="total_bill")), + ], + ), + vm.Container( + title="Total Tips ($)", + components=[ + vm.Graph(figure=px.histogram(tips, x="tip")), + ], + ), + ], + ) + ], + ) -![image1](../../assets/tutorials/dashboard/dashboard231.png) + dashboard = vm.Dashboard(pages=[first_page, second_page]) + Vizro().build(dashboard).run() + ``` -Each element in the `components` list is referenced with a unique number, and placed on the grid as visualized with the white frames. The `Card`, is referenced by 0 as it is the first element in the `components` list. It is placed in the first row and spans across both columns (`[0, 0]`). The two `Graph` objects, referenced by 1 and 2, are positioned next to each other and occupy a column each. + === "Result" + [![SecondPage4]][secondpage4] -![image2](../../assets/tutorials/dashboard/dashboard233.png) +**Much better, don't you think? The layout now provides sufficient space for the charts!** -The `Graph` objects occupy three rows, denoted by `[1, 2], [1, 2], [1, 2]`, while the `Card` only occupies one row `[0, 0]`. As a result, the `Graph` objects occupy three-quarters of the vertical space, while the `Card` occupies one-quarter of it. +### 4.5. Add a filter -![image3](../../assets/tutorials/dashboard/dashboard232.png) +[Filters][vizro.models.Filter] enable you to interact with the dashboard by selecting specific data points to display. -Run the code below to apply the layout to the dashboard page: +To add a filter to the dashboard, follow these steps: -!!! example "Configure layout" - === "Code" +1. Add a [`Filter`][vizro.models.Filter] to the `controls` list of the `Page`. +1. Specify the column to be filtered using the `column` argument of the [Filter][vizro.models.Filter]. +1. Change the `selector` in one of the `Filters` to a [`Checklist`][vizro.models.Checklist]. For further customization, refer to the guide on [`How to use selectors`](../user-guides/selectors.md). + +!!! example "Add a filter" + === "Snippet - Filter" ```py - layout=vm.Layout(grid=[[0, 0], [1, 2], [1, 2], [1, 2]]) + controls = [vm.Filter(column="day"), vm.Filter(column="time", selector=vm.Checklist()), vm.Filter(column="size")] ``` - === "app.py" - ```{.python pycafe-link} + === "Code - Dashboard" + ```{.python pycafe-link hl_lines="61"} + import vizro.models as vm + import vizro.plotly.express as px from vizro import Vizro + from vizro.tables import dash_ag_grid + from vizro.models.types import capture + from vizro.figures import kpi_card + + tips = px.data.tips() + + first_page = vm.Page( + title="Data", + components=[ + vm.AgGrid( + figure=dash_ag_grid(tips), + footer="""**Data Source:** Bryant, P. G. and Smith, M (1995) + Practical Data Analysis: Case Studies in Business Statistics. + Homewood, IL: Richard D. Irwin Publishing.""", + ), + ], + ) + + second_page = vm.Page( + title="Summary", + layout=vm.Layout(grid=[[0, 1, -1, -1], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2]]), + components=[ + vm.Figure( + figure=kpi_card( + data_frame=tips, + value_column="total_bill", + agg_func="mean", + value_format="${value:.2f}", + title="Average Bill", + ) + ), + vm.Figure( + figure=kpi_card( + data_frame=tips, + value_column="tip", + agg_func="mean", + value_format="${value:.2f}", + title="Average Tips" + ) + ), + vm.Tabs( + tabs=[ + vm.Container( + title="Total Bill ($)", + components=[ + vm.Graph(figure=px.histogram(tips, x="total_bill")), + ], + ), + vm.Container( + title="Total Tips ($)", + components=[ + vm.Graph(figure=px.histogram(tips, x="tip")), + ], + ), + ], + ) + ], + controls = [vm.Filter(column="day"), vm.Filter(column="time", selector=vm.Checklist()), vm.Filter(column="size")] + ) + + dashboard = vm.Dashboard(pages=[first_page, second_page]) + Vizro().build(dashboard).run() + ``` + + === "Result" + [![SecondPage5]][secondpage5] + +You'll see that a [`Dropdown`][vizro.models.Dropdown] is selected by default for categorical data, while a [`RangeSlider`][vizro.models.RangeSlider] is used for numerical data. Additionally, filters are applied to all components on the page. + +If you want to apply a filter to specific components only, check out [How to use filters](../user-guides/filters.md). + +**Great work! You've just completed a second dashboard page and learned how to:** + +1. [Add a chart to a page using the visual vocabulary](#41-add-a-chart) +1. [Add KPI cards to display summary statistics](#42-add-kpi-cards) +1. [Add tabs to switch views](#43-add-tabs-to-switch-views) +1. [Arrange components by customizing the layout](#44-configure-the-layout) +1. [Add a filter to interact with the dashboard](#45-add-a-filter) + +## 5. Create a third page + +Now that you've learned how to create pages, add components, and configure layouts, you'll create a third page for the dashboard. This will give you the opportunity to practice your skills alongside learning some new concepts! + +This page will feature a bar chart, a violin chart, and a heatmap and take inspiration from the [Vizro visual vocabulary](https://vizro-demo-visual-vocabulary.hf.space/). + +### 5.1. Add multiple charts + +This step should feel familiar. Let's add all three charts to the page. + +1. Create a third [`Page`][vizro.models.Page] and store it in a variable called `third_page`. Set its `title` to "Analysis". +1. Add three [`Graph`][vizro.models.Graph] models to the `components` of the `Page`. +1. For each `Graph`, use the `figure` argument to provide one of the Plotly express functions: + - [px.violin](https://vizro-demo-visual-vocabulary.hf.space/distribution/violin) (copy the code directly) + - [px.bar](https://vizro-demo-visual-vocabulary.hf.space/magnitude/column) (copy the code directly) + - [px.density_heatmap](https://vizro-demo-visual-vocabulary.hf.space/time/heatmap) (update the `data`, `x`, and `y` arguments to match the dataset) +1. Provide a `title` for each `Graph`. +1. Add the new `Page` to the list of `pages` in the [Dashboard][vizro.models.Dashboard]. + +!!! example "Third page" + === "Snippet - third page" + ```py + third_page = vm.Page( + title="Analysis", + components=[ + vm.Graph( + title="Where do we get more tips?", + figure=px.bar(tips, y="tip", x="day"), + ), + vm.Graph( + title="Is the average driven by a few outliers?", + figure=px.violin(tips, y="tip", x="day", color="day", box=True), + ), + vm.Graph( + title="Which group size is more profitable?", + figure=px.density_heatmap(tips, x="day", y="size", z="tip", histfunc="avg", text_auto="$.2f"), + ), + ], + ) + + dashboard = vm.Dashboard(pages=[first_page, second_page, third_page]) + ``` + + === "Code - dashboard" + ```{.python pycafe-link hl_lines="64-80 82"} import vizro.models as vm import vizro.plotly.express as px + from vizro import Vizro + from vizro.tables import dash_ag_grid + from vizro.models.types import capture + from vizro.figures import kpi_card - df = px.data.gapminder() - gapminder_data = ( - df.groupby(by=["continent", "year"]). - agg({"lifeExp": "mean", "pop": "sum", "gdpPercap": "mean"}).reset_index() - ) + tips = px.data.tips() first_page = vm.Page( - title="First Page", - layout=vm.Layout(grid=[[0, 0], [1, 2], [1, 2], [1, 2]]), + title="Data", + components=[ + vm.AgGrid( + figure=dash_ag_grid(tips), + footer="""**Data Source:** Bryant, P. G. and Smith, M. (1995) + Practical Data Analysis: Case Studies in Business Statistics. + Homewood, IL: Richard D. Irwin Publishing.""", + ), + ], + ) + + second_page = vm.Page( + title="Summary", + layout=vm.Layout(grid=[[0, 1, -1, -1], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2]]), + components=[ + vm.Figure( + figure=kpi_card( + data_frame=tips, + value_column="total_bill", + agg_func="mean", + value_format="${value:.2f}", + title="Average Bill", + ) + ), + vm.Figure( + figure=kpi_card( + data_frame=tips, + value_column="tip", + agg_func="mean", + value_format="${value:.2f}", + title="Average Tips", + ) + ), + vm.Tabs( + tabs=[ + vm.Container( + title="Total Bill ($)", + components=[ + vm.Graph(figure=px.histogram(tips, x="total_bill")), + ], + ), + vm.Container( + title="Total Tips ($)", + components=[ + vm.Graph(figure=px.histogram(tips, x="tip")), + ], + ), + ], + ), + ], + controls=[ + vm.Filter(column="day"), + vm.Filter(column="time", selector=vm.Checklist()), + vm.Filter(column="size"), + ], + ) + + third_page = vm.Page( + title="Analysis", components=[ - vm.Card( - text=""" - # First dashboard page - This pages shows the inclusion of markdown text in a page and how components - can be structured using Layout. - """, + vm.Graph( + title="Where do we get more tips?", + figure=px.bar(tips, y="tip", x="day"), ), vm.Graph( - figure=px.box(gapminder_data, x="continent", y="lifeExp", color="continent", - labels={"lifeExp": "Life Expectancy", "continent": "Continent"}), + title="Is the average driven by a few outliers?", + figure=px.violin(tips, y="tip", x="day", color="day", box=True), ), vm.Graph( - figure=px.line(gapminder_data, x="year", y="gdpPercap", color="continent", - labels={"year": "Year", "continent": "Continent", - "gdpPercap":"GDP Per Cap"}), - ), + title="Which group size is more profitable?", + figure=px.density_heatmap(tips, x="day", y="size", z="tip", histfunc="avg", text_auto="$.2f"), + ), ], ) - dashboard = vm.Dashboard(pages=[first_page]) + dashboard = vm.Dashboard(pages=[first_page, second_page, third_page]) Vizro().build(dashboard).run() ``` === "Result" - [![FirstPage3]][firstpage3] + [![ThirdPage]][thirdpage] -### 2.4. Add a control for dashboard interactivity +Depending on your screen size, you may notice that the third chart is not visible. This issue can occur with Plotly charts when there isn't enough space to display them properly. Let's customize the layout again to allocate more space to the heatmap. -Controls add interactivity to the dashboard page and make it more dynamic, enabling users to have greater control and customization over the displayed data and components. +### 5.2. Configure the layout -There are two types of control: +This step should also feel familiar by now. Let's arrange the charts to provide more space for the heatmap. -- [`Filters`][vizro.models.Filter] enable users to filter a column of the underlying data. -- [`Parameters`][vizro.models.Parameter] enable users to change arguments or properties of the components, such as adjusting colors. +In the following layout configuration, the layout is divided into **two columns** and **two rows**: -The guides on [`How to use Filters`](../user-guides/filters.md) and [`How to use Parameters`](../user-guides/parameters.md) offer instructions on their application. For further customization, refer to the guide on [`How to use selectors`](../user-guides/selectors.md). +- The bar chart (0) and violin chart (1) are placed side by side in the first row. +- The heatmap (2) spans the entire second row. -To link a control to a component, use an `id` assigned to the component, which is unique across all dashboard pages and serves as a reference to target it. +Remember, the index corresponds to the order in which the components are added to the `components` of the `Page`. + + + +``` +grid = [[0, 1], + [2, 2]] +``` -To illustrate, let's add a [`Filter`][vizro.models.Filter] on specific continents of the underlying gapminder data. The [`Filter`][vizro.models.Filter] requires the `column` argument, that denotes the target column to be filtered. Each `control` also has a `targets` parameter, to specify the data and components targeted by the `control`. For this dashboard, both charts are listed in the `targets` parameter, meaning that the filter is be applied to both charts. However, you can apply the [`Filter`][vizro.models.Filter] to only one specific chart if required. +Run the code below to apply the layout to the dashboard page: -!!! example "Configure filter" - === "Code" +!!! example "Code - Layout" + === "Snippet - Layout" ```py - controls=[ - vm.Filter(column="continent", targets=["box_cont", "line_gdp"]), - ] + layout=vm.Layout(grid=[[0, 1], [2, 2]]), ``` - === "app.py" - ```{.python pycafe-link} - from vizro import Vizro + === "Code - dashboard" + ```{.python pycafe-link hl_lines="66"} import vizro.models as vm import vizro.plotly.express as px + from vizro import Vizro + from vizro.tables import dash_ag_grid + from vizro.models.types import capture + from vizro.figures import kpi_card - df = px.data.gapminder() - gapminder_data = ( - df.groupby(by=["continent", "year"]). - agg({"lifeExp": "mean", "pop": "sum", "gdpPercap": "mean"}).reset_index() - ) + tips = px.data.tips() first_page = vm.Page( - title="First Page", - layout=vm.Layout(grid=[[0, 0], [1, 2], [1, 2], [1, 2]]), + title="Data", + components=[ + vm.AgGrid( + figure=dash_ag_grid(tips), + footer="""**Data Source:** Bryant, P. G. and Smith, M (1995) + Practical Data Analysis: Case Studies in Business Statistics. + Homewood, IL: Richard D. Irwin Publishing.""", + ), + ], + ) + + second_page = vm.Page( + title="Summary", + layout=vm.Layout(grid=[[0, 1, -1, -1], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2]]), + components=[ + vm.Figure( + figure=kpi_card( + data_frame=tips, + value_column="total_bill", + agg_func="mean", + value_format="${value:.2f}", + title="Average Bill", + ) + ), + vm.Figure( + figure=kpi_card( + data_frame=tips, + value_column="tip", + agg_func="mean", + value_format="${value:.2f}", + title="Average Tips" + ) + ), + vm.Tabs( + tabs=[ + vm.Container( + title="Total Bill ($)", + components=[ + vm.Graph(figure=px.histogram(tips, x="total_bill")), + ], + ), + vm.Container( + title="Total Tips ($)", + components=[ + vm.Graph(figure=px.histogram(tips, x="tip")), + ], + ), + ], + ) + ], + controls=[vm.Filter(column="day"), vm.Filter(column="time", selector=vm.Checklist()), vm.Filter(column="size")] + ) + + third_page = vm.Page( + title="Analysis", + layout=vm.Layout(grid=[[0, 1], [2, 2]]), components=[ - vm.Card( - text=""" - # First dashboard page - This pages shows the inclusion of markdown text in a page and how components - can be structured using Layout. - """, + vm.Graph( + title="Where do we get more tips?", + figure=px.bar(tips, y="tip", x="day"), ), vm.Graph( - id="box_cont", - figure=px.box(gapminder_data, x="continent", y="lifeExp", color="continent", - labels={"lifeExp": "Life Expectancy", "continent": "Continent"}), + title="Is the average driven by a few outliers?", + figure=px.violin(tips, y="tip", x="day", color="day", box=True), ), vm.Graph( - id="line_gdp", - figure=px.line(gapminder_data, x="year", y="gdpPercap", color="continent", - labels={"year": "Year", "continent": "Continent", - "gdpPercap":"GDP Per Cap"}), - ), - ], - controls=[ - vm.Filter(column="continent", targets=["box_cont", "line_gdp"]), + title="Which group size is more profitable?", + figure=px.density_heatmap(tips, x="day", y="size", z="tip", histfunc="avg", text_auto="$.2f"), + ), ], ) - dashboard = vm.Dashboard(pages=[first_page]) + dashboard = vm.Dashboard(pages=[first_page, second_page, third_page]) Vizro().build(dashboard).run() ``` === "Result" - [![FirstPage4]][firstpage4] + [![ThirdPage2]][thirdpage2] + +**Fantastic work! The heatmap looks great, doesn't it?** -Fantastic job! You have completed first dashboard page and gained valuable skills to: +### 5.3. Add a parameter -1. [Create an initial figure on a dashboard page](#2-create-a-first-dashboard-page) -1. [Add extra components](#22-add-further-components) -1. [Arrange them in a layout configuration](#23-configure-the-layout) -1. [Set up an interactive dashboard control](#24-add-a-control-for-dashboard-interactivity). +This section explains how to add a [`Parameter`][vizro.models.Parameter] to your dashboard. A [`Parameter`][vizro.models.Parameter] enables you to dynamically change a component's argument, making the dashboard more interactive. For more information on how to configure [`Parameters`][vizro.models.Parameter], refer to the [guide to parameters](../user-guides/parameters.md). -## 3. Create a second dashboard page +In this section, you learn how to switch the `x` and `color` arguments across all charts, enabling data analysis from different perspectives. -This section adds a second dashboard page and explains how to use controls and selectors. The new page is structured similarly to the page you created, but contains two charts that visualize the [iris data](https://plotly.com/python-api-reference/generated/plotly.express.data.html#plotly.express.data.iris). +To add a parameter to the dashboard: -Every [`Page`][vizro.models.Page] that you want to display needs to be added to the [`Dashboard`][vizro.models.Dashboard] object. The code below illustrates how to add the page, titled `second_page` to the dashboard by calling `vm.Dashboard(pages=[first_page,second_page])`. There are two `Graph` objects added to the list of components. To enable interactivity on those components, we add two [`Parameters`][vizro.models.Parameter] to the list of `controls`. +1. Add a [`Parameter`][vizro.models.Parameter] to the `controls` of the `Page`. +1. Assign an `id` to each `Graph` that the [Parameter][vizro.models.Parameter] should target. +1. Define the parameter's `targets` using the format `component-id.argument`. +1. Set the `selector` of the [Parameter][vizro.models.Parameter] to a [`RadioItems`][vizro.models.RadioItems]. +1. Provide options for the `RadioItems` selector. -In creating a [`Parameter`][vizro.models.Parameter] object, you define the `target` it applies to. In the code below: +!!! example "Add a parameter" + === "Snippet - parameter" + ```py + controls=[ + vm.Parameter( + targets=["violin.x", "violin.color", "heatmap.x", "bar.x"], + selector=vm.RadioItems( + options=["day", "time", "sex", "smoker", "size"], value="day", title="Change x-axis inside charts:" + ), + ), + ] + ``` -- The first parameter enables the user to change the color mapping for the `virginica` category of the iris data, targeting both charts. -- The second parameter adjusts the opacity of the first chart alone, through `scatter_iris.opacity`. + === "Code - dashboard" + ```{.python pycafe-link hl_lines="69 74 84-91"} + import vizro.models as vm + import vizro.plotly.express as px + from vizro import Vizro + from vizro.tables import dash_ag_grid + from vizro.models.types import capture + from vizro.figures import kpi_card -In general, `targets` for [`Parameters`][vizro.models.Parameter] are set following the structure of `component_id.argument`. In certain cases, you may see a nested structure for the `targets`. An example of this is `scatter_iris.color_discrete_map.virginica`. A nested structure targets a specific attribute within a component. In this particular example, it specifies that only the color of the virginica flower type should be changed. More information on how to set `targets` for [`Parameters`][vizro.models.Parameter] can be found in the [how-to guide for parameters](../user-guides/parameters.md). + tips = px.data.tips() + + first_page = vm.Page( + title="Data", + components=[ + vm.AgGrid( + figure=dash_ag_grid(tips), + footer="""**Data Source:** Bryant, P. G. and Smith, M (1995) + Practical Data Analysis: Case Studies in Business Statistics. + Homewood, IL: Richard D. Irwin Publishing.""", + ), + ], + ) -!!! example "Second page" - === "Code" - ```py - iris_data = px.data.iris() second_page = vm.Page( - title="Second Page", + title="Summary", + layout=vm.Layout(grid=[[0, 1, -1, -1], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2]]), + components=[ + vm.Figure( + figure=kpi_card( + data_frame=tips, + value_column="total_bill", + agg_func="mean", + value_format="${value:.2f}", + title="Average Bill", + ) + ), + vm.Figure( + figure=kpi_card( + data_frame=tips, + value_column="tip", + agg_func="mean", + value_format="${value:.2f}", + title="Average Tips" + ) + ), + vm.Tabs( + tabs=[ + vm.Container( + title="Total Bill ($)", + components=[ + vm.Graph(figure=px.histogram(tips, x="total_bill")), + ], + ), + vm.Container( + title="Total Tips ($)", + components=[ + vm.Graph(figure=px.histogram(tips, x="tip")), + ], + ), + ], + ) + ], + controls=[vm.Filter(column="day"), vm.Filter(column="time", selector=vm.Checklist()), vm.Filter(column="size")] + ) + + third_page = vm.Page( + title="Analysis", + layout=vm.Layout(grid=[[0, 1], [2, 2]]), components=[ vm.Graph( - id="scatter_iris", - figure=px.scatter(iris_data, x="sepal_width", y="sepal_length", color="species", - color_discrete_map={"setosa": "#00b4ff", "versicolor": "#ff9222"}, - labels={"sepal_width": "Sepal Width", "sepal_length": "Sepal Length", - "species": "Species"}, - ), + id="bar", + title="Where do we get more tips?", + figure=px.bar(tips, y="tip", x="day"), ), vm.Graph( - id="hist_iris", - figure=px.histogram(iris_data, x="sepal_width", color="species", - color_discrete_map={"setosa": "#00b4ff", "versicolor": "#ff9222"}, - labels={"sepal_width": "Sepal Width", "count": "Count", - "species": "Species"}, - ), + id="violin", + title="Is the average driven by a few outliers?", + figure=px.violin(tips, y="tip", x="day", color="day", box=True), + ), + vm.Graph( + id="heatmap", + title="Which group size is more profitable?", + figure=px.density_heatmap(tips, x="day", y="size", z="tip", histfunc="avg", text_auto="$.2f"), ), ], controls=[ vm.Parameter( - targets=["scatter_iris.color_discrete_map.virginica", - "hist_iris.color_discrete_map.virginica"], - selector=vm.Dropdown( - options=["#ff5267", "#3949ab"], multi=False, value="#3949ab", title="Color Virginica"), + targets=["violin.x", "violin.color", "heatmap.x", "bar.x"], + selector=vm.RadioItems( + options=["day", "time", "sex", "smoker", "size"], value="day", title="Change x-axis inside charts:" ), - vm.Parameter( - targets=["scatter_iris.opacity"], - selector=vm.Slider(min=0, max=1, value=0.8, title="Opacity"), ), ], ) + + dashboard = vm.Dashboard(pages=[first_page, second_page, third_page]) + Vizro().build(dashboard).run() ``` - === "app.py" - ```{.python pycafe-link} - from vizro import Vizro + === "Result" + [![ThirdPage3]][thirdpage3] + +Take a moment to interact with the parameter. Note how the x-axis of all charts updates accordingly. + +**Isn't it amazing how effortlessly it is to shift the data analysis perspective now?** + +### 5.4. Add a custom chart + +You may notice that the `bar` chart has many inner lines. This happens because each line represents a unique data point when an unaggregated dataset is provided to `px.bar`. To avoid this, you can aggregate the data before plotting. However, the aggregation needs to be dynamic, based on the parameter you added in the previous step. + +This requires creating a custom chart with the following steps. For more information on when to create a custom chart, check out [How to create custom charts](../user-guides/custom-charts.md). + +1. Create a function that takes the `data_frame` as input and returns a Plotly figure. +1. Decorate the function with the `@capture(graph)` decorator. +1. Inside the function, aggregate the data, provide a label for the chart, and update the bar width. +1. Use this custom function in the `Graph` component instead of `px.bar`. + +!!! example "Add custom chart" + === "Snippet - custom chart" + ```py + @capture("graph") + def bar_mean(data_frame, x, y): + df_agg = data_frame.groupby(x).agg({y: "mean"}).reset_index() + fig = px.bar(df_agg, x=x, y=y, labels={"tip": "Average Tip ($)"}) + fig.update_traces(width=0.6) + return fig + ``` + + === "Code - dashboard" + ```{.python pycafe-link hl_lines="11-16 80"} import vizro.models as vm import vizro.plotly.express as px + from vizro import Vizro + from vizro.tables import dash_ag_grid + from vizro.models.types import capture + from vizro.figures import kpi_card + + tips = px.data.tips() + + + @capture("graph") + def bar_mean(data_frame, x, y): + df_agg = data_frame.groupby(x).agg({y: "mean"}).reset_index() + fig = px.bar(df_agg, x=x, y=y, labels={"tip": "Average Tip ($)"}) + fig.update_traces(width=0.6) + return fig + - df = px.data.gapminder() - gapminder_data = ( - df.groupby(by=["continent", "year"]). - agg({"lifeExp": "mean", "pop": "sum", "gdpPercap": "mean"}).reset_index() - ) first_page = vm.Page( - title="First Page", - layout=vm.Layout(grid=[[0, 0], [1, 2], [1, 2], [1, 2]]), + title="Data", components=[ - vm.Card( - text=""" - # First dashboard page - This pages shows the inclusion of markdown text in a page and how components - can be structured using Layout. - """, + vm.AgGrid( + figure=dash_ag_grid(tips), + footer="""**Data Source:** Bryant, P. G. and Smith, M (1995) + Practical Data Analysis: Case Studies in Business Statistics. + Homewood, IL: Richard D. Irwin Publishing.""", ), - vm.Graph( - id="box_cont", - figure=px.box(gapminder_data, x="continent", y="lifeExp", color="continent", - labels={"lifeExp": "Life Expectancy", "continent": "Continent"}), - ), - vm.Graph( - id="line_gdp", - figure=px.line(gapminder_data, x="year", y="gdpPercap", color="continent", - labels={"year": "Year", "continent": "Continent", - "gdpPercap":"GDP Per Cap"}), - ), - ], - controls=[ - vm.Filter(column="continent", targets=["box_cont", "line_gdp"]), ], ) - iris_data = px.data.iris() second_page = vm.Page( - title="Second Page", + title="Summary", + layout=vm.Layout(grid=[[0, 1, -1, -1], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2]]), + components=[ + vm.Figure( + figure=kpi_card( + data_frame=tips, + value_column="total_bill", + agg_func="mean", + value_format="${value:.2f}", + title="Average Bill", + ) + ), + vm.Figure( + figure=kpi_card( + data_frame=tips, + value_column="tip", + agg_func="mean", + value_format="${value:.2f}", + title="Average Tips" + ) + ), + vm.Tabs( + tabs=[ + vm.Container( + title="Total Bill ($)", + components=[ + vm.Graph(figure=px.histogram(tips, x="total_bill")), + ], + ), + vm.Container( + title="Total Tips ($)", + components=[ + vm.Graph(figure=px.histogram(tips, x="tip")), + ], + ), + ], + ) + ], + controls=[vm.Filter(column="day"), vm.Filter(column="time", selector=vm.Checklist()), vm.Filter(column="size")] + ) + + third_page = vm.Page( + title="Analysis", + layout=vm.Layout(grid=[[0, 1], [2, 2]]), components=[ vm.Graph( - id="scatter_iris", - figure=px.scatter(iris_data, x="sepal_width", y="sepal_length", color="species", - color_discrete_map={"setosa": "#00b4ff", "versicolor": "#ff9222"}, - labels={"sepal_width": "Sepal Width", "sepal_length": "Sepal Length", - "species": "Species"}, - ), + id="bar", + title="Where do we get more tips?", + figure=bar_mean(tips, y="tip", x="day"), ), vm.Graph( - id="hist_iris", - figure=px.histogram(iris_data, x="sepal_width", color="species", - color_discrete_map={"setosa": "#00b4ff", "versicolor": "#ff9222"}, - labels={"sepal_width": "Sepal Width", "count": "Count", - "species": "Species"}, - ), + id="violin", + title="Is the average driven by a few outliers?", + figure=px.violin(tips, y="tip", x="day", color="day", box=True), + ), + vm.Graph( + id="heatmap", + title="Which group size is more profitable?", + figure=px.density_heatmap(tips, x="day", y="size", z="tip", histfunc="avg", text_auto="$.2f"), ), ], controls=[ vm.Parameter( - targets=["scatter_iris.color_discrete_map.virginica", - "hist_iris.color_discrete_map.virginica"], - selector=vm.Dropdown( - options=["#ff5267", "#3949ab"], multi=False, value="#3949ab", title="Color Virginica"), + targets=["violin.x", "violin.color", "heatmap.x", "bar.x"], + selector=vm.RadioItems( + options=["day", "time", "sex", "smoker", "size"], value="day", title="Change x-axis inside charts:" ), - vm.Parameter( - targets=["scatter_iris.opacity"], - selector=vm.Slider(min=0, max=1, value=0.8, title="Opacity"), ), ], ) - dashboard = vm.Dashboard(pages=[first_page,second_page]) + dashboard = vm.Dashboard(pages=[first_page, second_page, third_page]) Vizro().build(dashboard).run() ``` === "Result" - [![SecondPage]][secondpage] + [![ThirdPage4]][thirdpage4] -### 3.1. Customize with selectors +**Fantastic job reaching this point! You've just completed the final dashboard page and learned how to:** -The code in the example above uses two different types of [`selector`](../user-guides/selectors.md) objects, namely [`Dropdown`][vizro.models.Dropdown] and [`Slider`][vizro.models.Slider] upon the [`Parameters`][vizro.models.Parameter]. The `selectors` enable configuration of the controls to customize their behavior and appearance. +1. [Add multiple charts](#51-add-multiple-charts) +1. [Customize a layout](#52-configure-the-layout) +1. [Add a parameter to interact with the charts](#53-add-a-parameter) +1. [Add a custom chart to the dashboard](#54-add-a-custom-chart) -The first parameter is a [`Dropdown`][vizro.models.Dropdown]. It is configured with two available options, disables multi-selection, and has a default `value` set to blue. Users can choose a single option from the dropdown. +## 6. The final touches -The second parameter is a [`Slider`][vizro.models.Slider] with a default value of 0.8. Users can adjust a value within the specified range of `min=0` and `max=1`. +Now that you've created all the dashboard pages, let's add a personal touch by including a title, logo, and customizing the navigation. -You can apply selectors to configure [`Filters`][vizro.models.Filter] and [`Parameters`][vizro.models.Parameter] to fine-tune the behavior and appearance of the controls. The selectors currently available are as follows: +### 6.1. Add a title and logo -- [`Parameter`][vizro.models.Parameter]: -- [`Checklist`][vizro.models.Checklist] -- [`Dropdown`][vizro.models.Dropdown] -- [`RadioItems`][vizro.models.RadioItems] -- [`RangeSlider`][vizro.models.RangeSlider] -- [`Slider`][vizro.models.Slider] +To add a title and logo to your dashboard, follow these steps: -## 4. The final touches +1. Set the `title` attribute of the [Dashboard][vizro.models.Dashboard] to "Tips Analysis Dashboard". +1. Download the `logo` from [this link](https://raw.githubusercontent.com/mckinsey/vizro/refs/heads/main/vizro-core/examples/dev/assets/logo.svg) and save it in a folder named `assets`. +1. Place the `assets` folder in the same directory as your `app.py/app.ipynb` file. -Each page is added to the dashboard using the following line of code: `vm.Dashboard(pages=[first_page, second_page])`. This ensures that all the pages are accessible. +Your directory structure should look like this: -By default, a navigation panel on the left side enables the user to switch between the two pages. +```text title="Example folder structure" +├── app.py +├── assets +│  ├── logo.svg +``` -!!! example "Final dashboard" - === "Code" - ```python - dashboard = vm.Dashboard(pages=[home_page, first_page, second_page]) - Vizro().build(dashboard).run() +!!! example "Add a dashboard title and logo" + === "Snippet - dashboard title" + ```py + dashboard = vm.Dashboard(pages=[first_page, second_page, third_page], title="Tips Analysis Dashboard") ``` - === "app.py" - ```{.python pycafe-link} - - from vizro import Vizro + === "Code - dashboard" + ```{.python pycafe-link hl_lines="103"} import vizro.models as vm import vizro.plotly.express as px + from vizro import Vizro + from vizro.tables import dash_ag_grid + from vizro.models.types import capture + from vizro.figures import kpi_card + + tips = px.data.tips() + + + @capture("graph") + def bar_mean(data_frame, x, y): + df_agg = data_frame.groupby(x).agg({y: "mean"}).reset_index() + fig = px.bar(df_agg, x=x, y=y, labels={"tip": "Average Tip ($)"}) + fig.update_traces(width=0.6) + return fig + - df = px.data.gapminder() - gapminder_data = ( - df.groupby(by=["continent", "year"]). - agg({"lifeExp": "mean", "pop": "sum", "gdpPercap": "mean"}).reset_index() - ) first_page = vm.Page( - title="First Page", - layout=vm.Layout(grid=[[0, 0], [1, 2], [1, 2], [1, 2]]), + title="Data", + components=[ + vm.AgGrid( + figure=dash_ag_grid(tips), + footer="""**Data Source:** Bryant, P. G. and Smith, M (1995) + Practical Data Analysis: Case Studies in Business Statistics. + Homewood, IL: Richard D. Irwin Publishing.""", + ), + ], + ) + + second_page = vm.Page( + title="Summary", + layout=vm.Layout(grid=[[0, 1, -1, -1], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2]]), components=[ - vm.Card( - text=""" - # First dashboard page - This pages shows the inclusion of markdown text in a page and how components - can be structured using Layout. - """, + vm.Figure( + figure=kpi_card( + data_frame=tips, + value_column="total_bill", + agg_func="mean", + value_format="${value:.2f}", + title="Average Bill", + ) ), + vm.Figure( + figure=kpi_card( + data_frame=tips, + value_column="tip", + agg_func="mean", + value_format="${value:.2f}", + title="Average Tips" + ) + ), + vm.Tabs( + tabs=[ + vm.Container( + title="Total Bill ($)", + components=[ + vm.Graph(figure=px.histogram(tips, x="total_bill")), + ], + ), + vm.Container( + title="Total Tips ($)", + components=[ + vm.Graph(figure=px.histogram(tips, x="tip")), + ], + ), + ], + ) + ], + controls=[vm.Filter(column="day"), vm.Filter(column="time", selector=vm.Checklist()), vm.Filter(column="size")] + ) + + third_page = vm.Page( + title="Analysis", + layout=vm.Layout(grid=[[0, 1], [2, 2]]), + components=[ vm.Graph( - id="box_cont", - figure=px.box(gapminder_data, x="continent", y="lifeExp", color="continent", - labels={"lifeExp": "Life Expectancy", "continent": "Continent"}), + id="bar", + title="Where do we get more tips?", + figure=bar_mean(tips, y="tip", x="day"), ), vm.Graph( - id="line_gdp", - figure=px.line(gapminder_data, x="year", y="gdpPercap", color="continent", - labels={"year": "Year", "continent": "Continent", - "gdpPercap":"GDP Per Cap"}), - ), + id="violin", + title="Is the average driven by a few outliers?", + figure=px.violin(tips, y="tip", x="day", color="day", box=True), + ), + vm.Graph( + id="heatmap", + title="Which group size is more profitable?", + figure=px.density_heatmap(tips, x="day", y="size", z="tip", histfunc="avg", text_auto="$.2f"), + ), ], controls=[ - vm.Filter(column="continent", targets=["box_cont", "line_gdp"]), + vm.Parameter( + targets=["violin.x", "violin.color", "heatmap.x", "bar.x"], + selector=vm.RadioItems( + options=["day", "time", "sex", "smoker", "size"], value="day", title="Change x-axis inside charts:" + ), + ), + ], + ) + + dashboard = vm.Dashboard(pages=[first_page, second_page, third_page], title="Tips Analysis Dashboard") + Vizro().build(dashboard).run() + ``` + + === "Result" + [![Dashboard]][dashboard] + +You should see the logo in the top-left corner of your dashboard header, with the title displayed next to it. If you can't see the logo, make sure the image is called `logo` and is stored in the `assets` folder. For more details on supported image formats, refer to the [How to add a logo](../user-guides/assets.md#add-a-logo-image) guide. + +### 6.2. Customize the navigation + +By default, a navigation panel on the left side enables users to switch between the pages. In this section, you'll learn how to customize it by using a navigation bar with icons instead. + +The navigation bar will have two icons: one for the "Data" page and another for the "Summary" and "Analysis" pages. + +To create a navigation bar, follow these steps: + +1. Set the `navigation` attribute of the [Dashboard][vizro.models.Dashboard] to a [Navigation][vizro.models.Navigation] object. +1. Assign a [NavBar][vizro.models.NavBar] object to the `nav_selector` attribute of the `Navigation`. +1. Populate the `items` of the [NavBar][vizro.models.NavBar] object with a list of [NavLink][vizro.models.NavLink] objects. +1. Assign a [NavBar][vizro.models.NavBar] object to the `nav_selector` attribute of the `Navigation`. +1. Populate the `items` of the [NavBar][vizro.models.NavBar] object with a list of [NavLink][vizro.models.NavLink] objects. +1. Customize each [NavLink][vizro.models.NavLink] object by setting its `label`, `pages`, and `icon` attributes. + - The `label` controls the text displayed in the tooltip when hovering over the navigation icon. + - The `pages` controls the pages included in the accordion navigation for that icon. + - The `icon` sets the icon to display using the [Material Design Icons library](https://fonts.google.com/icons). + +!!! example "Customize navigation" + === "Snippet - navigation" + ```py + navigation=vm.Navigation( + nav_selector=vm.NavBar( + items=[ + vm.NavLink(label="Data", pages=["Data"], icon="database"), + vm.NavLink(label="Charts", pages=["Summary", "Analysis"], icon="bar_chart"), + ] + ) + ) + ``` + + === "Code - dashboard" + ```{.python pycafe-link hl_lines="106-113"} + import vizro.models as vm + import vizro.plotly.express as px + from vizro import Vizro + from vizro.tables import dash_ag_grid + from vizro.models.types import capture + from vizro.figures import kpi_card + + tips = px.data.tips() + + + @capture("graph") + def bar_mean(data_frame, x, y): + df_agg = data_frame.groupby(x).agg({y: "mean"}).reset_index() + fig = px.bar(df_agg, x=x, y=y, labels={"tip": "Average Tip ($)"}) + fig.update_traces(width=0.6) + return fig + + + first_page = vm.Page( + title="Data", + components=[ + vm.AgGrid( + figure=dash_ag_grid(tips), + footer="""**Data Source:** Bryant, P. G. and Smith, M (1995) + Practical Data Analysis: Case Studies in Business Statistics. + Homewood, IL: Richard D. Irwin Publishing.""", + ), ], ) - iris_data = px.data.iris() second_page = vm.Page( - title="Second Page", + title="Summary", + layout=vm.Layout(grid=[[0, 1, -1, -1], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2]]), + components=[ + vm.Figure( + figure=kpi_card( + data_frame=tips, + value_column="total_bill", + agg_func="mean", + value_format="${value:.2f}", + title="Average Bill", + ) + ), + vm.Figure( + figure=kpi_card( + data_frame=tips, + value_column="tip", + agg_func="mean", + value_format="${value:.2f}", + title="Average Tips" + ) + ), + vm.Tabs( + tabs=[ + vm.Container( + title="Total Bill ($)", + components=[ + vm.Graph(figure=px.histogram(tips, x="total_bill")), + ], + ), + vm.Container( + title="Total Tips ($)", + components=[ + vm.Graph(figure=px.histogram(tips, x="tip")), + ], + ), + ], + ) + ], + controls=[vm.Filter(column="day"), vm.Filter(column="time", selector=vm.Checklist()), vm.Filter(column="size")] + ) + + third_page = vm.Page( + title="Analysis", + layout=vm.Layout(grid=[[0, 1], [2, 2]]), components=[ vm.Graph( - id="scatter_iris", - figure=px.scatter(iris_data, x="sepal_width", y="sepal_length", color="species", - color_discrete_map={"setosa": "#00b4ff", "versicolor": "#ff9222"}, - labels={"sepal_width": "Sepal Width", "sepal_length": "Sepal Length", - "species": "Species"}, - ), + id="bar", + title="Where do we get more tips?", + figure=bar_mean(tips, y="tip", x="day"), ), vm.Graph( - id="hist_iris", - figure=px.histogram(iris_data, x="sepal_width", color="species", - color_discrete_map={"setosa": "#00b4ff", "versicolor": "#ff9222"}, - labels={"sepal_width": "Sepal Width", "count": "Count", - "species": "Species"}, - ), + id="violin", + title="Is the average driven by a few outliers?", + figure=px.violin(tips, y="tip", x="day", color="day", box=True), + ), + vm.Graph( + id="heatmap", + title="Which group size is more profitable?", + figure=px.density_heatmap(tips, x="day", y="size", z="tip", histfunc="avg", text_auto="$.2f"), ), ], controls=[ vm.Parameter( - targets=["scatter_iris.color_discrete_map.virginica", - "hist_iris.color_discrete_map.virginica"], - selector=vm.Dropdown( - options=["#ff5267", "#3949ab"], multi=False, value="#3949ab", title="Color Virginica"), + targets=["violin.x", "violin.color", "heatmap.x", "bar.x"], + selector=vm.RadioItems( + options=["day", "time", "sex", "smoker", "size"], value="day", title="Change x-axis inside charts:" ), - vm.Parameter( - targets=["scatter_iris.opacity"], - selector=vm.Slider(min=0, max=1, value=0.8, title="Opacity"), ), ], ) - dashboard = vm.Dashboard(pages=[first_page, second_page]) + dashboard = vm.Dashboard( + pages=[first_page, second_page, third_page], + title="Tips Analysis Dashboard", + navigation=vm.Navigation( + nav_selector=vm.NavBar( + items=[ + vm.NavLink(label="Data", pages=["Data"], icon="database"), + vm.NavLink(label="Charts", pages=["Summary", "Analysis"], icon="bar_chart"), + ] + ) + ), + ) Vizro().build(dashboard).run() ``` - === "Subpage1" - [![FinalPage1]][finalpage1] + === "Result" + [![DashboardFinal]][dashboardfinal] + +Take a moment to explore the navigation bar! Hover over the icons to view the tooltip text, and click on them to navigate between the pages. - === "Subpage2" - [![FinalPage2]][finalpage2] +**Congratulations on completing this tutorial!** -Congratulations on completing this tutorial! You have acquired the knowledge to configure layouts, add components, and implement interactivity in Vizro dashboards, working across two navigable pages. +You now have the skills to configure layouts, and add components and interactivity to Vizro dashboards across multiple navigable pages. -## Find out more +## 7. Find out more -After completing the tutorial you now have a solid understanding of the main elements of Vizro and how to bring them together to create dynamic and interactive data visualizations. +After completing the tutorial, you have a solid understanding of the main elements of Vizro and how to bring them together to create dynamic and interactive data visualizations. -You can find out more about the Vizro by reading the [components overview page](../user-guides/components.md). To gain more in-depth knowledge about the usage and configuration details of individual controls, check out the guides dedicated to [Filters](../user-guides/filters.md), [Parameters](../user-guides/parameters.md), and [Selectors](../user-guides/selectors.md). If you'd like to understand more about different ways to configure the navigation of your dashboard, head to [Navigation](../user-guides/navigation.md). +You can find out more about Vizro's components by reading the [components overview page](../user-guides/components.md). To gain more in-depth knowledge about the usage and configuration details of individual controls, check out the guides dedicated to [Filters](../user-guides/filters.md), [Parameters](../user-guides/parameters.md), and [Selectors](../user-guides/selectors.md). -Vizro doesn't end here, and we only covered the key features, but there is still much more to explore! You can learn: +If you'd like to understand more about different ways to configure the navigation of your dashboard, head to [Navigation](../user-guides/navigation.md). -- How to create you own components under [custom components](../user-guides/custom-components.md). +Vizro doesn't end here; we've only covered the key features, but there's still much more to explore! You can learn: + +- How to use [actions](../user-guides/actions.md) for example, for chart interaction or custom controls. +- How to [extend and customize Vizro dashboards](../user-guides/extensions.md) by creating your own: + - [custom components](../user-guides/custom-components.md). + - [custom actions](../user-guides/custom-actions.md). + - [custom tables](../user-guides/custom-tables.md). + - [custom charts](../user-guides/custom-charts.md). + - [custom figures](../user-guides/custom-figures.md). - How to add custom styling using [static assets](../user-guides/assets.md) such as custom css or JavaScript files. -- How to use [Actions](../user-guides/actions.md) for example, for chart interaction or custom controls. +- How to [customize your data connection](../user-guides/data.md) - How to create dashboards from `yaml`, `dict` or `json` following the [dashboard guide](../user-guides/dashboard.md). - -[finalpage1]: ../../assets/tutorials/dashboard/dashboard-first-page.png -[finalpage2]: ../../assets/tutorials/dashboard/dashboard-second-page.png -[firstpage1]: ../../assets/tutorials/dashboard/dashboard21.png -[firstpage2]: ../../assets/tutorials/dashboard/dashboard22.png -[firstpage3]: ../../assets/tutorials/dashboard/dashboard23.png -[firstpage4]: ../../assets/tutorials/dashboard/dashboard24.png -[secondpage]: ../../assets/tutorials/dashboard/dashboard3.png +- How to [deploy your dashboard](../user-guides/run-deploy.md) +- How to use [Vizro-AI](https://vizro.readthedocs.io/projects/vizro-ai/en/vizro-ai-0.3.6/) to create charts with GenAI + +[dashboard]: ../../assets/tutorials/dashboard/11-dashboard-title-logo.png +[dashboardfinal]: ../../assets/tutorials/dashboard/12-dashboard-navigation.png +[firstpage]: ../../assets/tutorials/dashboard/01-first-page.png +[secondpage]: ../../assets/tutorials/dashboard/02-second-page.png +[secondpage2]: ../../assets/tutorials/dashboard/03-second-page-kpi.png +[secondpage3]: ../../assets/tutorials/dashboard/04-second-page-tabs.png +[secondpage4]: ../../assets/tutorials/dashboard/05-second-page-layout.png +[secondpage5]: ../../assets/tutorials/dashboard/06-second-page-controls.png +[thirdpage]: ../../assets/tutorials/dashboard/07-third-page.png +[thirdpage2]: ../../assets/tutorials/dashboard/08-third-page-layout.png +[thirdpage3]: ../../assets/tutorials/dashboard/09-third-page-parameter.png +[thirdpage4]: ../../assets/tutorials/dashboard/10-third-page-custom-chart.png diff --git a/vizro-core/docs/pages/user-guides/container.md b/vizro-core/docs/pages/user-guides/container.md index 2f379709e3..0169f4a22e 100755 --- a/vizro-core/docs/pages/user-guides/container.md +++ b/vizro-core/docs/pages/user-guides/container.md @@ -2,9 +2,9 @@ This guide shows you how to use containers to group your components into sections and subsections within the page. -A [`Container`][vizro.models.Container] complements the idea of a [`Page`][vizro.models.Page], and the two models have almost identical arguments. [`Page.layout`](layouts.md) offers a way to structure the overall layout of the page, and a `Container` enables more granular control within a specific section of that page. +A [Container][vizro.models.Container] complements a [Page][vizro.models.Page], and both models share nearly identical arguments. While `Page.layout` provides a method for structuring the overall page layout, a `Container` offers more detailed control within a particular section of the page. -While there is currently no clear difference in rendering, extra functionality will be added to the `Container` soon (including controls specific to that container), enhancing the ability to manage related components. +Unlike `Page`, the `Container` includes a `variant` argument. This enables you to choose a style for your container to visually distinguish it from the rest of the page content. Additional functionality will soon be added to the Container, including controls specific to it, which will further enhance the management of related components. !!! note "Displaying multiple containers inside Tabs" An alternative way to display multiple containers on one page is to place them inside [Tabs](tabs.md). @@ -24,6 +24,7 @@ Here are a few cases where you might want to use a `Container` instead of `Page. - If you want to split up your grid into subgrids to organize components together - If you want to add a title to your subgrids - If you want different row and column spacing between subgrids +- If you want to apply a background color or borders to visually distinguish your content - If you want to apply controls to selected subgrids (will be supported soon) ## Basic containers @@ -154,7 +155,88 @@ Containers can be nested, providing a hierarchical structure for organizing comp To create nested containers, add a `Container` to the `components` argument of another `Container`. ```python title="Example" -vm.Container(title="Parent Container", components=[vm.Container(title="Child Container", components=[vm.Button()])]) +vm.Container( + title="Parent Container", + components=[ + vm.Container( + title="Child Container", + components=[vm.Button()], + ) + ], +) ``` +## Styled containers + +To make the `Container` stand out as a distinct section in your dashboard, you can select from the predefined styles available in its `variant` argument. + +!!! example "Container with different styles" + === "app.py" + ```{.python pycafe-link} + import vizro.models as vm + import vizro.plotly.express as px + from vizro import Vizro + + iris = px.data.iris() + + page = vm.Page( + title="Containers with different styles", + layout=vm.Layout(grid=[[0, 1]]), + components=[ + vm.Container( + title="Container with background color", + components=[vm.Graph(figure=px.scatter(iris, x="sepal_width", y="sepal_length", color="species"))], + variant="filled" + ), + vm.Container( + title="Container with borders", + components=[vm.Graph(figure=px.box(iris, x="species", y="sepal_length", color="species"))], + variant="outlined" + ) + ], + ) + + dashboard = vm.Dashboard(pages=[page]) + Vizro().build(dashboard).run() + ``` + + === "app.yaml" + ```yaml + # Still requires a .py to add data to the data manager and parse YAML configuration + # See yaml_version example + pages: + - title: Containers with different styles + layout: + grid: [[0, 1]] + components: + - type: container + title: Container with background color + components: + - type: graph + figure: + _target_: scatter + data_frame: iris + x: sepal_width + y: sepal_length + color: species + variant: filled + - type: container + title: Container with borders + components: + - type: graph + figure: + _target_: box + data_frame: iris + x: species + y: sepal_length + color: species + variant: outlined + ``` + + === "Result" + [![StyleContainer]][stylecontainer] + +If you want to style your `Container` beyond the styling options available inside `variant`, please refer to our user guide on [overwriting CSS for selected components](custom-css.md#overwrite-css-for-selected-components). + [container]: ../../assets/user_guides/components/containers.png +[stylecontainer]: ../../assets/user_guides/components/container-styled.png diff --git a/vizro-core/docs/pages/user-guides/custom-css.md b/vizro-core/docs/pages/user-guides/custom-css.md index edeac92b28..7de0184a65 100755 --- a/vizro-core/docs/pages/user-guides/custom-css.md +++ b/vizro-core/docs/pages/user-guides/custom-css.md @@ -295,7 +295,7 @@ It's essential to understand the relationship between the targeted CSS class or ### Make your CSS responsive to theme switches with variables -To ensure your CSS adapts to theme changes, we recommend using CSS variables (`var`) whenever possible. For a comprehensive list of available variable names, refer to the [Bootstrap documentation](https://getbootstrap.com/docs/5.3/customize/css-variables/). Additionally, you can define your own CSS variables, as demonstrated in the example on [changing the container background color](#change-the-styling-of-a-container). +To ensure your CSS adapts to theme changes, we recommend using CSS variables (`var`) whenever possible. For a comprehensive list of available variable names, refer to the [Bootstrap documentation](https://getbootstrap.com/docs/5.3/customize/css-variables/). ### Turn off page title @@ -351,95 +351,6 @@ By default, the logo appears in the top left corner of the dashboard. You can mo ![Logo positioning](../../assets/user_guides/custom_css/logo-position.png) -### Change the styling of a container - -If you want to make the subsections of your dashboard stand out more, you can do this by placing your components inside a [Container](container.md) and changing the container's styling, for example, background color, borders, padding, etc. - -To do this, you need to change the container's CSS class. Using the DevTool, as explained in the section on [identifying the correct CSS selector](#identify-the-correct-css-selector), you'll find that the CSS class for the `Container` is `page-component-container`. You can then use this class to set a new `background-color` and `padding`. Chart backgrounds are transparent so they automatically match the background color of the container. - -!!! example "Style a container" - === "custom.css" - ```css - /* Assign a variable to the dark and light theme */ - [data-bs-theme="dark"] { - --container-bg-color: #232632; - } - - [data-bs-theme="light"] { - --container-bg-color: #F5F6F6; - } - - /* Use the custom variable var(--container-bg-color) */ - .page-component-container { - background: var(--container-bg-color); - padding: 12px; - } - ``` - - === "app.py" - ```py - import vizro.models as vm - import vizro.plotly.express as px - from vizro import Vizro - - iris = px.data.iris() - - page = vm.Page( - title="Page with subsections", - layout=vm.Layout(grid=[[0, 1]]), - components=[ - vm.Container( - title="Container I", - components=[vm.Graph(figure=px.scatter(iris, x="sepal_width", y="sepal_length", color="species"))] - ), - vm.Container( - title="Container II", - components=[vm.Graph(figure=px.box(iris, x="species", y="sepal_length", color="species"))] - ) - ], - ) - - dashboard = vm.Dashboard(pages=[page]) - Vizro().build(dashboard).run() - ``` - - PyCafe logoRun and edit this code in PyCafe - - === "app.yaml" - ```yaml - # Still requires a .py to add data to the data manager and parse YAML configuration - # See yaml_version example - pages: - - title: Page with subsections - layout: - grid: [[0, 1]] - components: - - type: container - title: Container I - components: - - type: graph - figure: - _target_: scatter - data_frame: iris - x: sepal_width - y: sepal_length - color: species - - type: container - title: Container II - components: - - type: graph - figure: - _target_: box - data_frame: iris - x: species - y: sepal_length - color: species - ``` - - === "Result" - [![StyleContainer]][stylecontainer] - [assetscss]: ../../assets/user_guides/assets/css_change.png [cardcss]: ../../assets/user_guides/assets/css_change_card.png [pagetitle]: ../../assets/user_guides/assets/css_page_title.png -[stylecontainer]: ../../assets/user_guides/custom_css/style-container.png diff --git a/vizro-core/docs/pages/user-guides/layouts.md b/vizro-core/docs/pages/user-guides/layouts.md index af66ec3017..e9d2be9913 100644 --- a/vizro-core/docs/pages/user-guides/layouts.md +++ b/vizro-core/docs/pages/user-guides/layouts.md @@ -128,7 +128,7 @@ This defines a single row that occupies the entire width and height, divided int ## Grid - advanced example -If you want to divide the grid into subgrids with finer control over these, you can use [`Containers`](container.md). See our section on [when to use `Containers` vs. `Page.layout`](container.md#when-to-use-containers) for more information. +If you need to divide the grid into subgrids for finer control or want to visually distinguish your subgrids, you can use [`Containers`](container.md). See our section on [when to use `Containers` vs. `Page.layout`](container.md#when-to-use-containers) for more information. The `Layout` provides full control over the arrangement of top-level components within a page, allowing arbitrarily granular control of the grid by creating larger grids. @@ -412,11 +412,9 @@ For further customization, such as changing the gap between row and column, refe In general, any arbitrarily granular layout can already be achieved using [`Page.layout`](layouts.md) alone and is our recommended approach if you want to arrange components on a page with consistent row and/or column spacing. !!! note "Alternative layout approaches: `Tabs` and `Containers`" - [`Tabs`][vizro.models.Tabs] and [`Containers`][vizro.models.Container] offer an alternative approach to customize your page layout. For example, if you want to have more granular control and break the overall page grid into subgrids, see our [user guide on Containers](container.md). + [`Tabs`][vizro.models.Tabs] and [`Containers`][vizro.models.Container] provide alternative methods for customizing your page layout. For instance, if you need more granular control, want to break the overall page grid into subgrids, or wish to visually distinguish your subgrid, refer to our [user guide on Containers](container.md). - If you want to display multiple containers on one page by putting them into the same screen space, and letting the user switch between them, see our [user guide on Tabs](tabs.md). - - ![tabs](../../assets/user_guides/components/tabs-info.png){ width="500" } +![tabs](../../assets/user_guides/components/tabs-info.png){ width="500" } [grid]: ../../assets/user_guides/layout/one_left_two_right.png [gridadv]: ../../assets/user_guides/layout/grid_advanced.png diff --git a/vizro-core/examples/scratch_dev/assets/css/custom.css b/vizro-core/examples/scratch_dev/assets/css/custom.css index 42c796fc91..f8c8df785d 100644 --- a/vizro-core/examples/scratch_dev/assets/css/custom.css +++ b/vizro-core/examples/scratch_dev/assets/css/custom.css @@ -1,8 +1,3 @@ #page-header { padding-left: 8px; } - -#old-line-height p { - line-height: 1; - margin-bottom: 0.5rem; -} diff --git a/vizro-core/examples/scratch_dev/assets/logo.svg b/vizro-core/examples/scratch_dev/assets/logo.svg new file mode 100644 index 0000000000..0904b87dea --- /dev/null +++ b/vizro-core/examples/scratch_dev/assets/logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/vizro-core/examples/scratch_dev/assets/logo_dark.svg b/vizro-core/examples/scratch_dev/assets/logo_dark.svg deleted file mode 100644 index e55ed46062..0000000000 --- a/vizro-core/examples/scratch_dev/assets/logo_dark.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/vizro-core/examples/scratch_dev/assets/logo_light.svg b/vizro-core/examples/scratch_dev/assets/logo_light.svg deleted file mode 100644 index 1587b08a51..0000000000 --- a/vizro-core/examples/scratch_dev/assets/logo_light.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/vizro-core/examples/tutorial/app.py b/vizro-core/examples/tutorial/app.py new file mode 100644 index 0000000000..9326e8ad98 --- /dev/null +++ b/vizro-core/examples/tutorial/app.py @@ -0,0 +1,120 @@ +"""Example app from the official vizro user tutorial. + +See: https://vizro.readthedocs.io/en/stable/pages/tutorials/explore-components/ +""" + +import vizro.models as vm +import vizro.plotly.express as px +from vizro import Vizro +from vizro.figures import kpi_card +from vizro.models.types import capture +from vizro.tables import dash_ag_grid + +tips = px.data.tips() + + +@capture("graph") +def bar_mean(data_frame, x, y): + """Creates a custom bar chart with aggregated data (mean).""" + df_agg = data_frame.groupby(x).agg({y: "mean"}).reset_index() + fig = px.bar(df_agg, x=x, y=y, labels={"tip": "Average Tip ($)"}) + fig.update_traces(width=0.6) + return fig + + +first_page = vm.Page( + title="Data", + components=[ + vm.AgGrid( + figure=dash_ag_grid(tips), + footer="""**Data Source:** Bryant, P. G. and Smith, M (1995) + Practical Data Analysis: Case Studies in Business Statistics. + Homewood, IL: Richard D. Irwin Publishing.""", + ), + ], +) + +second_page = vm.Page( + title="Summary", + layout=vm.Layout(grid=[[0, 1, -1, -1], [2, 2, 2, 2], [2, 2, 2, 2], [2, 2, 2, 2]]), + components=[ + vm.Figure( + figure=kpi_card( + data_frame=tips, + value_column="total_bill", + agg_func="mean", + value_format="${value:.2f}", + title="Average Bill", + ) + ), + vm.Figure( + figure=kpi_card( + data_frame=tips, value_column="tip", agg_func="mean", value_format="${value:.2f}", title="Average Tips" + ) + ), + vm.Tabs( + tabs=[ + vm.Container( + title="Total Bill ($)", + components=[ + vm.Graph(figure=px.histogram(tips, x="total_bill")), + ], + ), + vm.Container( + title="Total Tips ($)", + components=[ + vm.Graph(figure=px.histogram(tips, x="tip")), + ], + ), + ], + ), + ], + controls=[vm.Filter(column="day"), vm.Filter(column="time", selector=vm.Checklist()), vm.Filter(column="size")], +) + +third_page = vm.Page( + title="Analysis", + layout=vm.Layout(grid=[[0, 1], [2, 2]]), + components=[ + vm.Graph( + id="bar", + title="Where do we get more tips?", + figure=bar_mean(tips, y="tip", x="day"), + ), + vm.Graph( + id="violin", + title="Is the average driven by a few outliers?", + figure=px.violin(tips, y="tip", x="day", color="day", box=True), + ), + vm.Graph( + id="heatmap", + title="Which group size is more profitable?", + figure=px.density_heatmap(tips, x="day", y="size", z="tip", histfunc="avg", text_auto="$.2f"), + ), + ], + controls=[ + vm.Parameter( + targets=["violin.x", "violin.color", "heatmap.x", "bar.x"], + selector=vm.RadioItems( + options=["day", "time", "sex", "smoker", "size"], value="day", title="Change x-axis inside charts:" + ), + ), + ], +) + +dashboard = vm.Dashboard( + pages=[first_page, second_page, third_page], + title="Tips Analysis Dashboard", + navigation=vm.Navigation( + nav_selector=vm.NavBar( + items=[ + vm.NavLink(label="Data", pages=["Data"], icon="database"), + vm.NavLink(label="Charts", pages=["Summary", "Analysis"], icon="bar_chart"), + ] + ) + ), +) + + +if __name__ == "__main__": + Vizro().build(dashboard).run() diff --git a/vizro-core/examples/tutorial/assets/logo.svg b/vizro-core/examples/tutorial/assets/logo.svg new file mode 100644 index 0000000000..0904b87dea --- /dev/null +++ b/vizro-core/examples/tutorial/assets/logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/vizro-core/examples/visual-vocabulary/pages/examples/violin.py b/vizro-core/examples/visual-vocabulary/pages/examples/violin.py index b27ee3ca3a..c1fc176e75 100644 --- a/vizro-core/examples/visual-vocabulary/pages/examples/violin.py +++ b/vizro-core/examples/visual-vocabulary/pages/examples/violin.py @@ -2,4 +2,4 @@ tips = px.data.tips() -fig = px.violin(tips, y="total_bill", x="day", color="day", box=True) +fig = px.violin(tips, y="tip", x="day", color="day", box=True) diff --git a/vizro-core/hatch.toml b/vizro-core/hatch.toml index 2f6ad54040..c524080d8f 100644 --- a/vizro-core/hatch.toml +++ b/vizro-core/hatch.toml @@ -25,16 +25,15 @@ dependencies = [ "jupyter", "pre-commit", "PyGithub", - "imutils", - "opencv-python", "pyhamcrest", - "gunicorn" + "gunicorn", + "pixelmatch" ] features = ["kedro"] installer = "uv" [envs.default.env-vars] -PYTHONPATH = "tests/e2e/vizro/dashboards/default:tests/tests_utils" +PYTHONPATH = "tests/e2e/vizro/dashboards/default:tests/tests_utils:tests/e2e/vizro/custom_components" [envs.default.scripts] download-static-files = "python tools/download_static_files.py" @@ -63,12 +62,25 @@ templates-check = ["python src/vizro/_themes/generate_plotly_templates.py --chec # fix this, but we don't actually use `hatch run test` anywhere right now. # See comments added in https://github.com/mckinsey/vizro/pull/444. test = "pytest tests --headless {args}" -test-e2e-component-library = "pytest tests/e2e/component_library/test_component_library.py --headless {args}" +test-e2e-component-library = "pytest -vs tests/e2e/component_library/test_component_library.py --headless {args}" test-e2e-vizro-dom-elements = [ "gunicorn dashboard:app -b 0.0.0.0:5002 -w 1 --timeout 90 &", "tests/tests_utils/e2e/vizro/dashboards/wait-for-it.sh 127.0.0.1:5002 -t 30", "pytest -vs --reruns 1 tests/e2e/vizro/test_dom_elements --headless {args}" ] +test-e2e-vizro-screenshots = [ + "gunicorn dashboard:app -b 0.0.0.0:5002 -w 1 --timeout 90 &", + "gunicorn dashboard_one_page:app -b 0.0.0.0:5003 -w 1 --timeout 90 &", + "gunicorn dashboard_navbar_accordions:app -b 0.0.0.0:5004 -w 1 --timeout 90 &", + "gunicorn dashboard_navbar_pages:app -b 0.0.0.0:5005 -w 1 --timeout 90 &", + "gunicorn dashboard_navbar_navlink:app -b 0.0.0.0:5006 -w 1 --timeout 90 &", + "tests/tests_utils/e2e/vizro/dashboards/wait-for-it.sh 127.0.0.1:5002 -t 30", + "tests/tests_utils/e2e/vizro/dashboards/wait-for-it.sh 127.0.0.1:5003 -t 30", + "tests/tests_utils/e2e/vizro/dashboards/wait-for-it.sh 127.0.0.1:5004 -t 30", + "tests/tests_utils/e2e/vizro/dashboards/wait-for-it.sh 127.0.0.1:5005 -t 30", + "tests/tests_utils/e2e/vizro/dashboards/wait-for-it.sh 127.0.0.1:5006 -t 30", + "pytest -vs --reruns 1 tests/e2e/vizro/test_screenshots/ --headless {args}" +] test-integration = "pytest tests/integration --headless {args}" test-js = "./tools/run_jest.sh {args}" test-unit = "pytest tests/unit {args}" diff --git a/vizro-core/pyproject.toml b/vizro-core/pyproject.toml index a04cba8b34..fd4f80fc9f 100644 --- a/vizro-core/pyproject.toml +++ b/vizro-core/pyproject.toml @@ -85,7 +85,12 @@ filterwarnings = [ "ignore:HTTPResponse.getheader():DeprecationWarning", # Ignore deprecation warning as it comes from the plotly default template. In our templates `vizro_dark` and # `vizro_light`, we do not use mapbox anymore, see: https://github.com/mckinsey/vizro/pull/974 - "ignore:.*scattermapbox.*is deprecated.*Use.*scattermap.*instead:DeprecationWarning" + "ignore:.*scattermapbox.*is deprecated.*Use.*scattermap.*instead:DeprecationWarning", + # ignore this warning for firefox screenshot tests + "ignore:get_logs always return None with webdrivers other than Chrome" +] +markers = [ + "mobile_screenshots: marks tests for chrome mobile screenshots" ] norecursedirs = ["tests/tests_utils", "tests/js"] pythonpath = ["tests/tests_utils"] diff --git a/vizro-core/schemas/0.1.35.dev0.json b/vizro-core/schemas/0.1.35.dev0.json index cfa0886da5..65347535b8 100644 --- a/vizro-core/schemas/0.1.35.dev0.json +++ b/vizro-core/schemas/0.1.35.dev0.json @@ -316,7 +316,7 @@ }, "Container": { "additionalProperties": false, - "description": "Container to group together a set of components on a page.\n\nArgs:\n type (Literal[\"container\"]): Defaults to `\"container\"`.\n components (list[ComponentType]): See [ComponentType][vizro.models.types.ComponentType]. At least one component\n has to be provided.\n title (str): Title to be displayed.\n layout (Optional[Layout]): Layout to place components in. Defaults to `None`.", + "description": "Container to group together a set of components on a page.\n\nArgs:\n type (Literal[\"container\"]): Defaults to `\"container\"`.\n components (list[ComponentType]): See [ComponentType][vizro.models.types.ComponentType]. At least one component\n has to be provided.\n title (str): Title to be displayed.\n layout (Optional[Layout]): Layout to place components in. Defaults to `None`.\n variant (Literal[\"plain\", \"filled\", \"outlined\"]): Predefined styles to choose from. Options are `plain`,\n `filled` or `outlined`. Defaults to `plain`.", "properties": { "id": { "default": "", @@ -392,6 +392,13 @@ } ], "default": null + }, + "variant": { + "default": "plain", + "description": "Predefined styles to choose from. Options are `plain`, `filled` or `outlined`.Defaults to `plain`.", + "enum": ["plain", "filled", "outlined"], + "title": "Variant", + "type": "string" } }, "required": ["components", "title"], diff --git a/vizro-core/src/vizro/models/_components/container.py b/vizro-core/src/vizro/models/_components/container.py index 627323c82f..45e44b5da3 100644 --- a/vizro-core/src/vizro/models/_components/container.py +++ b/vizro-core/src/vizro/models/_components/container.py @@ -2,6 +2,7 @@ from typing import TYPE_CHECKING, Annotated, Literal, Optional, cast +import dash_bootstrap_components as dbc from dash import html from pydantic import AfterValidator, BeforeValidator, Field, conlist @@ -23,6 +24,8 @@ class Container(VizroBaseModel): has to be provided. title (str): Title to be displayed. layout (Optional[Layout]): Layout to place components in. Defaults to `None`. + variant (Literal["plain", "filled", "outlined"]): Predefined styles to choose from. Options are `plain`, + `filled` or `outlined`. Defaults to `plain`. """ @@ -34,9 +37,32 @@ class Container(VizroBaseModel): ) title: str = Field(description="Title to be displayed.") layout: Annotated[Optional[Layout], AfterValidator(set_layout), Field(default=None, validate_default=True)] + variant: Literal["plain", "filled", "outlined"] = Field( + default="plain", + description="Predefined styles to choose from. Options are `plain`, `filled` or `outlined`." + "Defaults to `plain`.", + ) @_log_call def build(self): + # TODO: TBD on how to encode 'elevated', as box-shadows are not visible on a dark theme + # It needs to be properly designed and tested out (margins have to be added etc.). + # Below corresponds to bootstrap utility classnames, while 'bg-container' is introduced by us. + # See: https://getbootstrap.com/docs/4.0/utilities + variants = {"plain": "", "filled": "bg-container p-3", "outlined": "border p-3"} + + return dbc.Container( + id=self.id, + children=[ + html.H3(children=self.title, className="container-title", id=f"{self.id}_title"), + self._build_inner_layout(), + ], + fluid=True, + className=variants[self.variant], + ) + + def _build_inner_layout(self): + """Builds inner layout and assigns components to grid position.""" # Title is not displayed if Container is inside Tabs using CSS combinators (only applies to outer container) # Other options we might want to consider in the future to hide the title: # 1) Argument inside Container.build that flags if used inside Tabs, then sets hidden attribute for the heading @@ -53,11 +79,5 @@ def build(self): components_container = self.layout.build() for component_idx, component in enumerate(self.components): components_container[f"{self.layout.id}_{component_idx}"].children = component.build() - return html.Div( - id=self.id, - children=[ - html.H3(children=self.title, className="container-title", id=f"{self.id}_title"), - components_container, - ], - className="page-component-container", - ) + + return components_container diff --git a/vizro-core/src/vizro/models/_components/form/date_picker.py b/vizro-core/src/vizro/models/_components/form/date_picker.py index 3126d66d2d..8e8b8bbbcb 100644 --- a/vizro-core/src/vizro/models/_components/form/date_picker.py +++ b/vizro-core/src/vizro/models/_components/form/date_picker.py @@ -66,13 +66,10 @@ def __call__(self, min, max, current_value=None): # - The way how the new Vizro solution is built on top of the Dash persistence bugfix. # - Whether the current value is included in the updated options. # - The way how the validate_options_dict validator and tests are improved. - if not self.value: - self.value = [min, max] if self.range else min - date_picker = dmc.DatePickerInput( id=self.id, minDate=min, - value=self.value, + value=self.value if self.value is not None else [min, max] if self.range else min, maxDate=max, persistence=True, persistence_type="session", @@ -89,6 +86,12 @@ def __call__(self, min, max, current_value=None): ], ) + def _build_dynamic_placeholder(self): + if not self.value: + self.value = [self.min, self.max] if self.range else self.min # type: ignore[list-item] + + return self.__call__(self.min, self.max) + @_log_call def build(self): - return self.__call__(self.min, self.max) + return self._build_dynamic_placeholder() if self._dynamic else self.__call__(self.min, self.max) diff --git a/vizro-core/src/vizro/static/css/bootstrap_overwrites.css b/vizro-core/src/vizro/static/css/bootstrap_overwrites.css index 0f5dfb5eea..7e6de45f2e 100644 --- a/vizro-core/src/vizro/static/css/bootstrap_overwrites.css +++ b/vizro-core/src/vizro/static/css/bootstrap_overwrites.css @@ -5,6 +5,7 @@ for a pure Dash app. All the HEX values starting with --text-code are taken from the Github code highlighting style. */ [data-bs-theme="dark"] { --dropdown-label-bg: var(--primary-800); + --left-side-bg: var(--surfaces-bg02); --right-side-bg: var(--surfaces-bg03); --text-code-string: #95c2e7; --text-code-keyword: #f4766e; @@ -18,6 +19,7 @@ All the HEX values starting with --text-code are taken from the Github code high [data-bs-theme="light"] { --dropdown-label-bg: var(--primary-300); --right-side-bg: var(--surfaces-bg01); + --left-side-bg: var(--surfaces-bg02); --text-code-string: #0a3069; --text-code-keyword: #d12d39; --text-code-meta: #6f42c1; @@ -102,3 +104,20 @@ label[for="theme-selector"] { .card-text > *:last-child { margin-bottom: 0; } + +.container-fluid { + display: flex; + flex-direction: column; + height: 100%; + padding: 0; +} + +/* Introduce vizro utility classes here */ +.bg-container { + background: var(--left-side-bg); +} + +/* Change filled container bg inside filled container */ +.bg-container .bg-container { + background-color: var(--right-side-bg); +} diff --git a/vizro-core/src/vizro/static/css/layout.css b/vizro-core/src/vizro/static/css/layout.css index 8654ccdb0b..c6a8cb0dae 100644 --- a/vizro-core/src/vizro/static/css/layout.css +++ b/vizro-core/src/vizro/static/css/layout.css @@ -23,7 +23,7 @@ } #left-side { - background-color: var(--surfaces-bg02); + background-color: var(--left-side-bg); display: flex; flex-direction: row; height: 100%; @@ -48,7 +48,7 @@ #page-header { align-items: center; - background-color: var(--surfaces-bg02); + background-color: var(--left-side-bg); display: flex; flex-direction: row; height: 60px; @@ -176,12 +176,6 @@ width: 0; } -.page-component-container { - display: flex; - flex-direction: column; - height: 100%; -} - /* Note: This is only meant as a quick-fix to improve some of the mobile layouts. */ /* Long-term wise this should be replaced by refactoring our CSS code and components to be mobile-compatible. */ diff --git a/vizro-core/src/vizro/static/css/mantine_dates.css b/vizro-core/src/vizro/static/css/mantine_dates.css index c71da7d343..3233965f14 100644 --- a/vizro-core/src/vizro/static/css/mantine_dates.css +++ b/vizro-core/src/vizro/static/css/mantine_dates.css @@ -1,4 +1,3 @@ -/* Comment here just to test that it gets removed when update static files GHA runs. */ .m_468e7eda { padding-top: 0; padding-bottom: 0; diff --git a/vizro-core/tests/e2e/screenshots/chrome/main_ag_grid_page.png b/vizro-core/tests/e2e/screenshots/chrome/main_ag_grid_page.png new file mode 100644 index 0000000000..dc2b2b6ff3 Binary files /dev/null and b/vizro-core/tests/e2e/screenshots/chrome/main_ag_grid_page.png differ diff --git a/vizro-core/tests/e2e/screenshots/chrome/main_export_action_page[one_page].png b/vizro-core/tests/e2e/screenshots/chrome/main_export_action_page[one_page].png new file mode 100644 index 0000000000..5b8a7a869c Binary files /dev/null and b/vizro-core/tests/e2e/screenshots/chrome/main_export_action_page[one_page].png differ diff --git a/vizro-core/tests/e2e/screenshots/chrome/main_homepage.png b/vizro-core/tests/e2e/screenshots/chrome/main_homepage.png new file mode 100644 index 0000000000..e88860a7f1 Binary files /dev/null and b/vizro-core/tests/e2e/screenshots/chrome/main_homepage.png differ diff --git a/vizro-core/tests/e2e/screenshots/main_kpi_card_component_library.png b/vizro-core/tests/e2e/screenshots/chrome/main_kpi_card_component_library.png similarity index 100% rename from vizro-core/tests/e2e/screenshots/main_kpi_card_component_library.png rename to vizro-core/tests/e2e/screenshots/chrome/main_kpi_card_component_library.png diff --git a/vizro-core/tests/e2e/screenshots/chrome/main_kpi_indicators_page.png b/vizro-core/tests/e2e/screenshots/chrome/main_kpi_indicators_page.png new file mode 100644 index 0000000000..ac4fa8d6bf Binary files /dev/null and b/vizro-core/tests/e2e/screenshots/chrome/main_kpi_indicators_page.png differ diff --git a/vizro-core/tests/e2e/screenshots/chrome/main_navbar_filters_page[navbar_pages].png b/vizro-core/tests/e2e/screenshots/chrome/main_navbar_filters_page[navbar_pages].png new file mode 100644 index 0000000000..ad5b7c96a0 Binary files /dev/null and b/vizro-core/tests/e2e/screenshots/chrome/main_navbar_filters_page[navbar_pages].png differ diff --git a/vizro-core/tests/e2e/screenshots/chrome/main_navbar_kpi_indicators_page[navbar_accordions].png b/vizro-core/tests/e2e/screenshots/chrome/main_navbar_kpi_indicators_page[navbar_accordions].png new file mode 100644 index 0000000000..bbdf9159cc Binary files /dev/null and b/vizro-core/tests/e2e/screenshots/chrome/main_navbar_kpi_indicators_page[navbar_accordions].png differ diff --git a/vizro-core/tests/e2e/screenshots/chrome/main_navbar_kpi_indicators_page[navbar_navlink].png b/vizro-core/tests/e2e/screenshots/chrome/main_navbar_kpi_indicators_page[navbar_navlink].png new file mode 100644 index 0000000000..e3a9078825 Binary files /dev/null and b/vizro-core/tests/e2e/screenshots/chrome/main_navbar_kpi_indicators_page[navbar_navlink].png differ diff --git a/vizro-core/tests/e2e/screenshots/chrome/main_nested_tabs_filters_page.png b/vizro-core/tests/e2e/screenshots/chrome/main_nested_tabs_filters_page.png new file mode 100644 index 0000000000..ce8977e339 Binary files /dev/null and b/vizro-core/tests/e2e/screenshots/chrome/main_nested_tabs_filters_page.png differ diff --git a/vizro-core/tests/e2e/screenshots/chrome/main_table_interactions_page.png b/vizro-core/tests/e2e/screenshots/chrome/main_table_interactions_page.png new file mode 100644 index 0000000000..10f9904497 Binary files /dev/null and b/vizro-core/tests/e2e/screenshots/chrome/main_table_interactions_page.png differ diff --git a/vizro-core/tests/e2e/screenshots/chrome/main_table_page.png b/vizro-core/tests/e2e/screenshots/chrome/main_table_page.png new file mode 100644 index 0000000000..83cf8ea373 Binary files /dev/null and b/vizro-core/tests/e2e/screenshots/chrome/main_table_page.png differ diff --git a/vizro-core/tests/e2e/screenshots/chrome/main_tabs_parameters_page.png b/vizro-core/tests/e2e/screenshots/chrome/main_tabs_parameters_page.png new file mode 100644 index 0000000000..95e23c4edb Binary files /dev/null and b/vizro-core/tests/e2e/screenshots/chrome/main_tabs_parameters_page.png differ diff --git a/vizro-core/tests/e2e/screenshots/chrome_mobile/main_filter_interactions_dark_theme_page[mobile].png b/vizro-core/tests/e2e/screenshots/chrome_mobile/main_filter_interactions_dark_theme_page[mobile].png new file mode 100644 index 0000000000..47670ed086 Binary files /dev/null and b/vizro-core/tests/e2e/screenshots/chrome_mobile/main_filter_interactions_dark_theme_page[mobile].png differ diff --git a/vizro-core/tests/e2e/screenshots/chrome_mobile/main_filter_interactions_page[mobile].png b/vizro-core/tests/e2e/screenshots/chrome_mobile/main_filter_interactions_page[mobile].png new file mode 100644 index 0000000000..4b8730d8f7 Binary files /dev/null and b/vizro-core/tests/e2e/screenshots/chrome_mobile/main_filter_interactions_page[mobile].png differ diff --git a/vizro-core/tests/e2e/screenshots/chrome_mobile/main_homepage_mobile.png b/vizro-core/tests/e2e/screenshots/chrome_mobile/main_homepage_mobile.png new file mode 100644 index 0000000000..839ae6edf0 Binary files /dev/null and b/vizro-core/tests/e2e/screenshots/chrome_mobile/main_homepage_mobile.png differ diff --git a/vizro-core/tests/e2e/screenshots/firefox/main_ag_grid_page.png b/vizro-core/tests/e2e/screenshots/firefox/main_ag_grid_page.png new file mode 100644 index 0000000000..0659d3e9f2 Binary files /dev/null and b/vizro-core/tests/e2e/screenshots/firefox/main_ag_grid_page.png differ diff --git a/vizro-core/tests/e2e/screenshots/firefox/main_export_action_page[one_page].png b/vizro-core/tests/e2e/screenshots/firefox/main_export_action_page[one_page].png new file mode 100644 index 0000000000..f5d20c5408 Binary files /dev/null and b/vizro-core/tests/e2e/screenshots/firefox/main_export_action_page[one_page].png differ diff --git a/vizro-core/tests/e2e/screenshots/firefox/main_homepage.png b/vizro-core/tests/e2e/screenshots/firefox/main_homepage.png new file mode 100644 index 0000000000..ce6e7b828a Binary files /dev/null and b/vizro-core/tests/e2e/screenshots/firefox/main_homepage.png differ diff --git a/vizro-core/tests/e2e/screenshots/firefox/main_kpi_indicators_page.png b/vizro-core/tests/e2e/screenshots/firefox/main_kpi_indicators_page.png new file mode 100644 index 0000000000..7bcee81f94 Binary files /dev/null and b/vizro-core/tests/e2e/screenshots/firefox/main_kpi_indicators_page.png differ diff --git a/vizro-core/tests/e2e/screenshots/firefox/main_navbar_filters_page[navbar_pages].png b/vizro-core/tests/e2e/screenshots/firefox/main_navbar_filters_page[navbar_pages].png new file mode 100644 index 0000000000..4eb117131e Binary files /dev/null and b/vizro-core/tests/e2e/screenshots/firefox/main_navbar_filters_page[navbar_pages].png differ diff --git a/vizro-core/tests/e2e/screenshots/firefox/main_navbar_kpi_indicators_page[navbar_accordions].png b/vizro-core/tests/e2e/screenshots/firefox/main_navbar_kpi_indicators_page[navbar_accordions].png new file mode 100644 index 0000000000..a55f86fb3f Binary files /dev/null and b/vizro-core/tests/e2e/screenshots/firefox/main_navbar_kpi_indicators_page[navbar_accordions].png differ diff --git a/vizro-core/tests/e2e/screenshots/firefox/main_navbar_kpi_indicators_page[navbar_navlink].png b/vizro-core/tests/e2e/screenshots/firefox/main_navbar_kpi_indicators_page[navbar_navlink].png new file mode 100644 index 0000000000..3d2ff2c6c3 Binary files /dev/null and b/vizro-core/tests/e2e/screenshots/firefox/main_navbar_kpi_indicators_page[navbar_navlink].png differ diff --git a/vizro-core/tests/e2e/screenshots/firefox/main_nested_tabs_filters_page.png b/vizro-core/tests/e2e/screenshots/firefox/main_nested_tabs_filters_page.png new file mode 100644 index 0000000000..a07c50bf8b Binary files /dev/null and b/vizro-core/tests/e2e/screenshots/firefox/main_nested_tabs_filters_page.png differ diff --git a/vizro-core/tests/e2e/screenshots/firefox/main_table_interactions_page.png b/vizro-core/tests/e2e/screenshots/firefox/main_table_interactions_page.png new file mode 100644 index 0000000000..e07b5234d6 Binary files /dev/null and b/vizro-core/tests/e2e/screenshots/firefox/main_table_interactions_page.png differ diff --git a/vizro-core/tests/e2e/screenshots/firefox/main_table_page.png b/vizro-core/tests/e2e/screenshots/firefox/main_table_page.png new file mode 100644 index 0000000000..15bf06f11a Binary files /dev/null and b/vizro-core/tests/e2e/screenshots/firefox/main_table_page.png differ diff --git a/vizro-core/tests/e2e/screenshots/firefox/main_tabs_parameters_page.png b/vizro-core/tests/e2e/screenshots/firefox/main_tabs_parameters_page.png new file mode 100644 index 0000000000..fc12889c18 Binary files /dev/null and b/vizro-core/tests/e2e/screenshots/firefox/main_tabs_parameters_page.png differ diff --git a/vizro-core/tests/e2e/vizro/test_dom_elements/conftest.py b/vizro-core/tests/e2e/vizro/conftest.py similarity index 50% rename from vizro-core/tests/e2e/vizro/test_dom_elements/conftest.py rename to vizro-core/tests/e2e/vizro/conftest.py index 42588e888a..e6d10643a2 100644 --- a/vizro-core/tests/e2e/vizro/test_dom_elements/conftest.py +++ b/vizro-core/tests/e2e/vizro/conftest.py @@ -1,27 +1,42 @@ +import os + import e2e.vizro.constants as cnst import pytest from e2e.vizro.checkers import browser_console_warnings_checker +from e2e.vizro.waiters import callbacks_finish_waiter from selenium.common import WebDriverException from selenium.webdriver.chrome.options import Options as ChromeOptions # dash_br_driver options hook def pytest_setup_options(): - options = ChromeOptions() - options.add_argument("--headless") - options.add_argument("--window-size=1920,1080") - return options + if os.getenv("BROWSER") == "chrome_mobile": + options = ChromeOptions() + options.add_experimental_option("mobileEmulation", {"deviceName": "iPhone 14 Pro Max"}) + return options def make_teardown(dash_br): + # TODO: move it after the specific test execution (after moving test to vizro repo) + # rewriting dynamic_filters_data.yml after each test + # data = { + # "max": 7, + # "min": 6, + # "setosa": 5, + # "versicolor": 10, + # "virginica": 15, + # } + # with open(cnst.DYNAMIC_FILTERS_DATA_CONFIG, "w") as file: + # yaml.dump(data, file) + # checking for browser console errors - try: - log_levels = [level for level in dash_br.get_logs() if level["level"] == "SEVERE" or "WARNING"] - if log_levels: - for log_level in log_levels: - browser_console_warnings_checker(log_level, log_levels) - except WebDriverException: - pass + if os.getenv("BROWSER") in ["chrome", "chrome_mobile"]: + try: + error_logs = [log for log in dash_br.get_logs() if log["level"] == "SEVERE" or "WARNING"] + for log in error_logs: + browser_console_warnings_checker(log, error_logs) + except WebDriverException: + pass @pytest.fixture(autouse=True) @@ -29,7 +44,14 @@ def dash_br_driver(dash_br, request): """Built-in driver from the dash library.""" port = request.param.get("port", cnst.DEFAULT_PORT) if hasattr(request, "param") else cnst.DEFAULT_PORT path = request.param.get("path", "") if hasattr(request, "param") else "" + dash_br.driver.set_window_size(1920, 1080) dash_br.server_url = f"http://127.0.0.1:{port}/{path}" + return dash_br + + +@pytest.fixture(autouse=True) +def wait_for_callbacks(dash_br): + callbacks_finish_waiter(dash_br) @pytest.fixture(autouse=True) diff --git a/vizro-core/tests/e2e/vizro/dashboards/assets/banner.svg b/vizro-core/tests/e2e/vizro/dashboards/assets/banner.svg new file mode 100644 index 0000000000..0904b87dea --- /dev/null +++ b/vizro-core/tests/e2e/vizro/dashboards/assets/banner.svg @@ -0,0 +1,3 @@ + + + diff --git a/vizro-core/tests/e2e/vizro/dashboards/assets/css/custom.css b/vizro-core/tests/e2e/vizro/dashboards/assets/css/custom.css new file mode 100644 index 0000000000..79fb766470 --- /dev/null +++ b/vizro-core/tests/e2e/vizro/dashboards/assets/css/custom.css @@ -0,0 +1,24 @@ +.anchor-container { + align-items: center; + background: var(--text-primary); + border-top-left-radius: 8px; + bottom: 0; + color: var(--text-primary-inverted) !important; + display: flex; + font-size: 0.8rem; + font-weight: 500; + height: 24px; + padding: 0 12px; + position: fixed; + right: 0; +} + +.anchor-container:focus, +.anchor-container:hover { + background: var(--text-secondary); + color: var(--text-primary-inverted); +} + +img#banner { + height: 16px; +} diff --git a/vizro-core/tests/e2e/vizro/dashboards/default/dashboard.py b/vizro-core/tests/e2e/vizro/dashboards/default/dashboard.py index 00f76b2f09..bc28aa282a 100644 --- a/vizro-core/tests/e2e/vizro/dashboards/default/dashboard.py +++ b/vizro-core/tests/e2e/vizro/dashboards/default/dashboard.py @@ -1,27 +1,51 @@ import e2e.vizro.constants as cnst from pages.ag_grid_page import ag_grid_page from pages.datepicker_page import datepicker_page +from pages.dynamic_data_page import dynamic_data_page +from pages.dynamic_filters_pages import dynamic_filters_categorical_page, dynamic_filters_numerical_page +from pages.filter_interactions_page import filter_interactions_page from pages.filters_page import filters_page from pages.homepage import homepage from pages.kpi_indicators_page import kpi_indicators_page from pages.parameters_page import parameters_page +from pages.table_interactions_page import table_interactions_page +from pages.table_page import table_page import vizro.models as vm from vizro import Vizro dashboard = vm.Dashboard( title="Vizro dashboard for integration testing", - pages=[homepage, filters_page, parameters_page, kpi_indicators_page, datepicker_page, ag_grid_page], + pages=[ + homepage, + filters_page, + parameters_page, + filter_interactions_page, + kpi_indicators_page, + datepicker_page, + ag_grid_page, + table_page, + table_interactions_page, + dynamic_data_page, + dynamic_filters_categorical_page, + dynamic_filters_numerical_page, + ], navigation=vm.Navigation( pages={ cnst.GENERAL_ACCORDION: [ cnst.HOME_PAGE_ID, cnst.FILTERS_PAGE, cnst.PARAMETERS_PAGE, + cnst.FILTER_INTERACTIONS_PAGE, cnst.KPI_INDICATORS_PAGE, ], cnst.DATEPICKER_ACCORDION: [cnst.DATEPICKER_PAGE], - cnst.AG_GRID_ACCORDION: [cnst.TABLE_AG_GRID_PAGE], + cnst.AG_GRID_ACCORDION: [cnst.TABLE_PAGE, cnst.TABLE_INTERACTIONS_PAGE, cnst.TABLE_AG_GRID_PAGE], + cnst.DYNAMIC_DATA_ACCORDION: [ + cnst.DYNAMIC_DATA_PAGE, + cnst.DYNAMIC_FILTERS_NUMERICAL_PAGE, + cnst.DYNAMIC_FILTERS_CATEGORICAL_PAGE, + ], } ), theme="vizro_light", diff --git a/vizro-core/tests/e2e/vizro/dashboards/default/dashboard_navbar_accordions.py b/vizro-core/tests/e2e/vizro/dashboards/default/dashboard_navbar_accordions.py new file mode 100644 index 0000000000..d4e2afba73 --- /dev/null +++ b/vizro-core/tests/e2e/vizro/dashboards/default/dashboard_navbar_accordions.py @@ -0,0 +1,40 @@ +import dash_bootstrap_components as dbc +import e2e.vizro.constants as cnst +from dash import get_asset_url, html +from pages.homepage import homepage +from pages.kpi_indicators_page import kpi_indicators_page + +import vizro.models as vm +from vizro import Vizro + +dashboard = vm.Dashboard( + title="Vizro dashboard for integration testing", + pages=[ + homepage, + kpi_indicators_page, + ], + navigation=vm.Navigation( + pages={ + cnst.GENERAL_ACCORDION: [cnst.HOME_PAGE_ID], + cnst.KPI_ACCORDION: [cnst.KPI_INDICATORS_PAGE], + }, + nav_selector=vm.NavBar(), + ), +) + +app = Vizro(assets_folder="../assets").build(dashboard) +app.dash.layout.children.append( + dbc.NavLink( + [ + "Made with ", + html.Img(src=get_asset_url("banner.svg"), id="banner", alt="Vizro logo"), + "vizro", + ], + href="https://github.com/mckinsey/vizro", + target="_blank", + className="anchor-container", + ) +) + +if __name__ == "__main__": + app.run(debug=True) diff --git a/vizro-core/tests/e2e/vizro/dashboards/default/dashboard_navbar_navlink.py b/vizro-core/tests/e2e/vizro/dashboards/default/dashboard_navbar_navlink.py new file mode 100644 index 0000000000..d55d5833c0 --- /dev/null +++ b/vizro-core/tests/e2e/vizro/dashboards/default/dashboard_navbar_navlink.py @@ -0,0 +1,48 @@ +import e2e.vizro.constants as cnst +from pages.datepicker_page import datepicker_page +from pages.kpi_indicators_page import kpi_indicators_page +from pages.table_page import table_page + +import vizro.models as vm +from vizro import Vizro + +dashboard = vm.Dashboard( + title="Vizro dashboard for integration testing", + pages=[ + table_page, + kpi_indicators_page, + datepicker_page, + ], + navigation=vm.Navigation( + nav_selector=vm.NavBar( + items=[ + vm.NavLink( + pages={ + # configured accordion here to see that it is not shown with one page inside + cnst.AG_GRID_ACCORDION: [ + cnst.TABLE_PAGE, + ], + }, + icon="Arrow Back IOS", + label="Text to be used for Arrow Back IOS icon description", + ), + vm.NavLink( + pages={ + cnst.GENERAL_ACCORDION: [ + cnst.KPI_INDICATORS_PAGE, + cnst.DATEPICKER_PAGE, + ], + }, + icon="Bolt", + label="Text to be used for Bolt icon description", + ), + ] + ) + ), + theme="vizro_light", +) + +app = Vizro(assets_folder="../assets").build(dashboard) + +if __name__ == "__main__": + app.run(debug=True) diff --git a/vizro-core/tests/e2e/vizro/dashboards/default/dashboard_navbar_pages.py b/vizro-core/tests/e2e/vizro/dashboards/default/dashboard_navbar_pages.py new file mode 100644 index 0000000000..13f8ee6211 --- /dev/null +++ b/vizro-core/tests/e2e/vizro/dashboards/default/dashboard_navbar_pages.py @@ -0,0 +1,47 @@ +import dash_bootstrap_components as dbc +import e2e.vizro.constants as cnst +from dash import get_asset_url, html +from pages.filter_interactions_page import filter_interactions_page +from pages.filters_page import filters_page +from pages.homepage import homepage +from pages.parameters_page import parameters_page + +import vizro.models as vm +from vizro import Vizro + +dashboard = vm.Dashboard( + title="Vizro dashboard for integration testing", + pages=[ + homepage, + filters_page, + parameters_page, + filter_interactions_page, + ], + navigation=vm.Navigation( + pages=[ + cnst.HOME_PAGE_ID, + cnst.FILTERS_PAGE, + cnst.PARAMETERS_PAGE, + cnst.FILTER_INTERACTIONS_PAGE, + ], + nav_selector=vm.NavBar(), + ), + theme="vizro_light", +) + +app = Vizro(assets_folder="../assets").build(dashboard) +app.dash.layout.children.append( + dbc.NavLink( + [ + "Made with ", + html.Img(src=get_asset_url("banner.svg"), id="banner", alt="Vizro logo"), + "vizro", + ], + href="https://github.com/mckinsey/vizro", + target="_blank", + className="anchor-container", + ) +) + +if __name__ == "__main__": + app.run(debug=True) diff --git a/vizro-core/tests/e2e/vizro/dashboards/default/dashboard_one_page.py b/vizro-core/tests/e2e/vizro/dashboards/default/dashboard_one_page.py new file mode 100644 index 0000000000..88768bbdef --- /dev/null +++ b/vizro-core/tests/e2e/vizro/dashboards/default/dashboard_one_page.py @@ -0,0 +1,15 @@ +from pages.export_action_page import export_action_page + +import vizro.models as vm +from vizro import Vizro + +dashboard = vm.Dashboard( + title="Vizro dashboard for integration testing", + pages=[export_action_page], + theme="vizro_light", +) + +app = Vizro(assets_folder="../assets").build(dashboard) + +if __name__ == "__main__": + app.run(debug=True) diff --git a/vizro-core/tests/e2e/vizro/dashboards/default/dynamic_filters_data.yaml b/vizro-core/tests/e2e/vizro/dashboards/default/dynamic_filters_data.yaml new file mode 100644 index 0000000000..b602c156d8 --- /dev/null +++ b/vizro-core/tests/e2e/vizro/dashboards/default/dynamic_filters_data.yaml @@ -0,0 +1,5 @@ +max: 7 +min: 6 +setosa: 5 +versicolor: 10 +virginica: 15 diff --git a/vizro-core/tests/e2e/vizro/dashboards/default/pages/dynamic_data_page.py b/vizro-core/tests/e2e/vizro/dashboards/default/pages/dynamic_data_page.py new file mode 100644 index 0000000000..4ec98a6bd1 --- /dev/null +++ b/vizro-core/tests/e2e/vizro/dashboards/default/pages/dynamic_data_page.py @@ -0,0 +1,42 @@ +import e2e.vizro.constants as cnst +from flask_caching import Cache + +import vizro.models as vm +import vizro.plotly.express as px +from vizro.managers import data_manager + + +def load_iris_data_sample(number_of_points=10): + iris = px.data.iris().sample(number_of_points) + return iris + + +data_manager.cache = Cache(config={"CACHE_TYPE": "FileSystemCache", "CACHE_DIR": "cache"}) +data_manager["iris_cached"] = load_iris_data_sample +data_manager["iris_cached"].timeout = 300 +data_manager["iris_not_cached"] = load_iris_data_sample +data_manager["iris_not_cached"].timeout = -1 # NOT cached + +dynamic_data_page = vm.Page( + title=cnst.DYNAMIC_DATA_PAGE, + components=[ + vm.Graph( + id=cnst.SCATTER_DYNAMIC_CACHED_ID, + figure=px.scatter("iris_cached", x="species", y="petal_width", color="species"), + ), + vm.Graph( + id=cnst.SCATTER_DYNAMIC_ID, + figure=px.scatter("iris_not_cached", x="species", y="petal_width", color="species"), + ), + ], + controls=[ + vm.Parameter( + targets=[f"{cnst.SCATTER_DYNAMIC_CACHED_ID}.data_frame.number_of_points"], + selector=vm.Slider(id=cnst.SLIDER_DYNAMIC_DATA_CACHED_ID, min=10, max=100, step=10, value=10), + ), + vm.Parameter( + targets=[f"{cnst.SCATTER_DYNAMIC_ID}.data_frame.number_of_points"], + selector=vm.Slider(id=cnst.SLIDER_DYNAMIC_DATA_ID, min=10, max=100, step=10, value=10), + ), + ], +) diff --git a/vizro-core/tests/e2e/vizro/dashboards/default/pages/dynamic_filters_pages.py b/vizro-core/tests/e2e/vizro/dashboards/default/pages/dynamic_filters_pages.py new file mode 100644 index 0000000000..19d173e3b4 --- /dev/null +++ b/vizro-core/tests/e2e/vizro/dashboards/default/pages/dynamic_filters_pages.py @@ -0,0 +1,103 @@ +from functools import partial + +import e2e.vizro.constants as cnst +import pandas as pd +import yaml +from flask_caching import Cache + +import vizro.models as vm +import vizro.plotly.express as px +from vizro.managers import data_manager + +SPECIES_COLORS = {"setosa": "#00b4ff", "versicolor": "#ff9222", "virginica": "#3949ab"} +BAR_CHART_CONF = { + "x": "species", + "color": "species", + "color_discrete_map": SPECIES_COLORS, +} + + +def load_from_file(filter_column=None, parametrized_species=None): + df = px.data.iris() + + if parametrized_species: + return df[df["species"].isin(parametrized_species)] + + with open(cnst.DYNAMIC_FILTERS_DATA_CONFIG) as file: + data = yaml.safe_load(file) + data = data or {} + + if filter_column == "species": + final_df = pd.concat( + objs=[ + df[df[filter_column] == "setosa"].head(data.get("setosa", 0)), + df[df[filter_column] == "versicolor"].head(data.get("versicolor", 0)), + df[df[filter_column] == "virginica"].head(data.get("virginica", 0)), + ], + ignore_index=True, + ) + elif filter_column == "sepal_length": + final_df = df[df[filter_column].between(data.get("min"), data.get("max"), inclusive="both")] + else: + raise ValueError("Invalid FILTER_COLUMN") + + return final_df + + +data_manager.cache = Cache(config={"CACHE_TYPE": "FileSystemCache", "CACHE_DIR": "cache"}) + +data_manager["load_from_file_species"] = partial(load_from_file, filter_column="species") +data_manager["load_from_file_species"].timeout = -1 +data_manager["load_from_file_sepal_length"] = partial(load_from_file, filter_column="sepal_length") +data_manager["load_from_file_sepal_length"].timeout = -1 + + +dynamic_filters_categorical_page = vm.Page( + title=cnst.DYNAMIC_FILTERS_CATEGORICAL_PAGE, + components=[ + vm.Graph( + id=cnst.BOX_DYNAMIC_FILTERS_ID, + figure=px.bar(data_frame="load_from_file_species", **BAR_CHART_CONF), + ), + ], + controls=[ + vm.Filter( + id=cnst.DROPDOWN_MULTI_DYNAMIC_FILTER_ID, + column="species", + selector=vm.Dropdown(), + ), + vm.Filter( + id=cnst.DROPDOWN_DYNAMIC_FILTER_ID, + column="species", + selector=vm.Dropdown(multi=False), + ), + vm.Filter(id=cnst.CHECKLIST_DYNAMIC_FILTER_ID, column="species", selector=vm.Checklist()), + vm.Filter( + id=cnst.RADIOITEMS_DYNAMIC_FILTER_ID, + column="species", + selector=vm.RadioItems(), + ), + ], +) + +dynamic_filters_numerical_page = vm.Page( + title=cnst.DYNAMIC_FILTERS_NUMERICAL_PAGE, + components=[ + vm.Graph( + id=cnst.BAR_DYNAMIC_FILTER_ID, + figure=px.bar(data_frame="load_from_file_sepal_length", **BAR_CHART_CONF), + ), + ], + controls=[ + vm.Filter( + id=cnst.SLIDER_DYNAMIC_FILTER_ID, + column="sepal_length", + selector=vm.Slider(step=0.5), + ), + vm.Filter( + id=cnst.RANGE_SLIDER_DYNAMIC_FILTER_ID, + column="sepal_length", + selector=vm.RangeSlider(step=0.5), + ), + ], +) diff --git a/vizro-core/tests/e2e/vizro/dashboards/default/pages/export_action_page.py b/vizro-core/tests/e2e/vizro/dashboards/default/pages/export_action_page.py new file mode 100644 index 0000000000..ab224bcbf5 --- /dev/null +++ b/vizro-core/tests/e2e/vizro/dashboards/default/pages/export_action_page.py @@ -0,0 +1,39 @@ +import e2e.vizro.constants as cnst + +import vizro.models as vm +import vizro.plotly.express as px +from vizro.actions import export_data + +iris = px.data.iris() + +export_action_page = vm.Page( + title=cnst.EXPORT_PAGE, + path=cnst.EXPORT_PAGE_PATH, + layout=vm.Layout(grid=[[0], [1]]), + components=[ + vm.Graph( + id=cnst.LINE_EXPORT_ID, + figure=px.line( + iris, + x="sepal_length", + y="petal_width", + color="sepal_width", + ), + ), + vm.Button( + text="Export data", + actions=[ + vm.Action( + function=export_data( + file_format="csv", + ) + ), + vm.Action( + function=export_data( + file_format="xlsx", + ) + ), + ], + ), + ], +) diff --git a/vizro-core/tests/e2e/vizro/dashboards/default/pages/filter_interactions_page.py b/vizro-core/tests/e2e/vizro/dashboards/default/pages/filter_interactions_page.py new file mode 100644 index 0000000000..37afd22d21 --- /dev/null +++ b/vizro-core/tests/e2e/vizro/dashboards/default/pages/filter_interactions_page.py @@ -0,0 +1,51 @@ +import e2e.vizro.constants as cnst +from custom_actions.custom_actions import my_custom_action + +import vizro.models as vm +import vizro.plotly.express as px +from vizro.actions import filter_interaction + +iris = px.data.iris() + + +filter_interactions_page = vm.Page( + title=cnst.FILTER_INTERACTIONS_PAGE, + layout=vm.Layout(grid=[[0], [2], [1]]), + components=[ + vm.Graph( + id=cnst.SCATTER_INTERACTIONS_ID, + figure=px.scatter( + iris, + x="sepal_length", + y="petal_width", + color="species", + custom_data=["species"], + ), + actions=[ + vm.Action(function=filter_interaction(targets=[cnst.BOX_INTERACTIONS_ID])), + vm.Action( + function=my_custom_action(), + inputs=[f"{cnst.SCATTER_INTERACTIONS_ID}.clickData"], + outputs=[f"{cnst.CARD_INTERACTIONS_ID}.children"], + ), + ], + ), + vm.Card(id=cnst.CARD_INTERACTIONS_ID, text="### No data clicked."), + vm.Graph( + id=cnst.BOX_INTERACTIONS_ID, + figure=px.box( + iris, + x="sepal_length", + y="petal_width", + color="species", + ), + ), + ], + controls=[ + vm.Filter(column="species", targets=[cnst.BOX_INTERACTIONS_ID]), + vm.Parameter( + targets=[f"{cnst.BOX_INTERACTIONS_ID}.title"], + selector=vm.RadioItems(options=["red", "blue"], value="blue"), + ), + ], +) diff --git a/vizro-core/tests/e2e/vizro/dashboards/default/pages/homepage.py b/vizro-core/tests/e2e/vizro/dashboards/default/pages/homepage.py index dadbcebad8..2bf0078efd 100644 --- a/vizro-core/tests/e2e/vizro/dashboards/default/pages/homepage.py +++ b/vizro-core/tests/e2e/vizro/dashboards/default/pages/homepage.py @@ -64,6 +64,6 @@ ), ], controls=[ - vm.Filter(column="species", targets=[cnst.AREA_GRAPH_ID]), + vm.Filter(column="species"), ], ) diff --git a/vizro-core/tests/e2e/vizro/dashboards/default/pages/table_interactions_page.py b/vizro-core/tests/e2e/vizro/dashboards/default/pages/table_interactions_page.py new file mode 100644 index 0000000000..c3d8e119b8 --- /dev/null +++ b/vizro-core/tests/e2e/vizro/dashboards/default/pages/table_interactions_page.py @@ -0,0 +1,53 @@ +import e2e.vizro.constants as cnst + +import vizro.models as vm +import vizro.plotly.express as px +from vizro.actions import filter_interaction +from vizro.tables import dash_data_table + +gapminder = px.data.gapminder() + +table_interactions_page = vm.Page( + title=cnst.TABLE_INTERACTIONS_PAGE, + components=[ + vm.Table( + id=cnst.TABLE_INTERACTIONS_ID, + title="Table Country", + figure=dash_data_table( + id="dash_data_table_country", + data_frame=gapminder, + ), + actions=[ + vm.Action( + function=filter_interaction( + targets=[ + cnst.LINE_INTERACTIONS_ID, + ] + ) + ) + ], + ), + vm.Graph( + id=cnst.LINE_INTERACTIONS_ID, + figure=px.line( + gapminder, + title="Line Country", + x="year", + y="gdpPercap", + markers=True, + ), + ), + ], + controls=[ + vm.Filter( + column="year", + targets=[cnst.TABLE_INTERACTIONS_ID], + selector=vm.Dropdown(value=2007), + ), + vm.Filter( + column="continent", + targets=[cnst.TABLE_INTERACTIONS_ID], + selector=vm.RadioItems(options=["Europe", "Africa", "Americas"]), + ), + ], +) diff --git a/vizro-core/tests/e2e/vizro/dashboards/default/pages/table_page.py b/vizro-core/tests/e2e/vizro/dashboards/default/pages/table_page.py new file mode 100644 index 0000000000..20b7d73afb --- /dev/null +++ b/vizro-core/tests/e2e/vizro/dashboards/default/pages/table_page.py @@ -0,0 +1,40 @@ +import e2e.vizro.constants as cnst + +import vizro.models as vm +import vizro.plotly.express as px +from vizro.tables import dash_data_table + +gapminder = px.data.gapminder() + +table_page = vm.Page( + title=cnst.TABLE_PAGE, + components=[ + vm.Container( + title=cnst.TABLE_CONTAINER, + components=[ + vm.Table( + id=cnst.TABLE_ID, + title="Table Country", + figure=dash_data_table( + data_frame=gapminder, + ), + ) + ], + ) + ], + controls=[ + vm.Filter(column="year", selector=vm.Dropdown(value=2007)), + vm.Filter( + column="continent", + selector=vm.RadioItems(options=["Europe", "Africa", "Americas"]), + ), + vm.Filter( + column="continent", + selector=vm.Checklist(options=["Asia", "Oceania"]), + ), + vm.Filter( + column="pop", + selector=vm.RangeSlider(step=1000000.0, min=1000000, max=10000000), + ), + ], +) diff --git a/vizro-core/tests/e2e/vizro/test_dom_elements/test_dynamic_data.py b/vizro-core/tests/e2e/vizro/test_dom_elements/test_dynamic_data.py new file mode 100644 index 0000000000..6ca03e116f --- /dev/null +++ b/vizro-core/tests/e2e/vizro/test_dom_elements/test_dynamic_data.py @@ -0,0 +1,56 @@ +from pathlib import Path + +import pytest +from e2e.asserts import assert_image_not_equal, assert_pixelmatch +from e2e.vizro import constants as cnst +from e2e.vizro.navigation import accordion_select, page_select, select_slider_handler +from e2e.vizro.waiters import callbacks_finish_waiter + + +@pytest.mark.parametrize( + "cache, slider_id", + [ + ("cached", cnst.SLIDER_DYNAMIC_DATA_CACHED_ID), + ("cached_not", cnst.SLIDER_DYNAMIC_DATA_ID), + ], +) +def test_data_dynamic_parametrization(dash_br, cache, slider_id): + """This test checks parametrized data loading and how it is working with and without cache.""" + first_screen = f"{cache}_screen_first_test_data_dynamic_parametrization.png" + second_screen = f"{cache}_screen_second_test_data_dynamic_parametrization.png" + third_screen = f"{cache}_screen_third_test_data_dynamic_parametrization.png" + accordion_select( + dash_br, accordion_name=cnst.DYNAMIC_DATA_ACCORDION.upper(), accordion_number=cnst.DYNAMIC_DATA_ACCORDION_NUMBER + ) + page_select( + dash_br, + page_path=cnst.DYNAMIC_DATA_PAGE_PATH, + page_name=cnst.DYNAMIC_DATA_PAGE, + graph_id=cnst.SCATTER_DYNAMIC_ID, + ) + + # move slider to value '20' + select_slider_handler(dash_br, elem_id=slider_id, value=2) + callbacks_finish_waiter(dash_br) + dash_br.driver.save_screenshot(first_screen) + + # move slider to value '60' + select_slider_handler(dash_br, elem_id=slider_id, value=6) + callbacks_finish_waiter(dash_br) + dash_br.driver.save_screenshot(second_screen) + + # move slider to value '20' + select_slider_handler(dash_br, elem_id=slider_id, value=2) + callbacks_finish_waiter(dash_br) + dash_br.driver.save_screenshot(third_screen) + + # first and second screens should be different + assert_image_not_equal(first_screen, second_screen) + if cache == "cached": + # first and third screens should be the same + assert_pixelmatch(first_screen, third_screen) + if cache == "not_cached": + # first and third screens should be different + assert_image_not_equal(first_screen, third_screen) + for file in Path(".").glob("*test_data_dynamic_parametrization*"): + file.unlink() diff --git a/vizro-core/tests/e2e/vizro/test_dom_elements/test_filters.py b/vizro-core/tests/e2e/vizro/test_dom_elements/test_filters.py index b7169429a0..206ba8b9d6 100644 --- a/vizro-core/tests/e2e/vizro/test_dom_elements/test_filters.py +++ b/vizro-core/tests/e2e/vizro/test_dom_elements/test_filters.py @@ -2,7 +2,7 @@ import pytest from e2e.vizro.checkers import check_graph_is_loading, check_slider_value from e2e.vizro.navigation import page_select, select_dropdown_value -from e2e.vizro.paths import categorical_components_value_path, slider_value_path +from e2e.vizro.paths import categorical_components_value_path, kpi_card_path, slider_value_path from e2e.vizro.waiters import graph_load_waiter from hamcrest import assert_that, equal_to @@ -51,8 +51,8 @@ def test_dropdown_homepage(dash_br): def test_dropdown_kpi_indicators_page(dash_br): page_select(dash_br, page_path=cnst.KPI_INDICATORS_PAGE_PATH, page_name=cnst.KPI_INDICATORS_PAGE) - dash_br.wait_for_text_to_equal(".card-body", "73902") - values = dash_br.find_elements(".card-body") + dash_br.wait_for_text_to_equal(kpi_card_path(), "73902") + values = dash_br.find_elements(kpi_card_path()) values_text = [value.text for value in values] assert_that( actual_or_assertion=values_text, @@ -73,8 +73,8 @@ def test_dropdown_kpi_indicators_page(dash_br): ), ) select_dropdown_value(dash_br, value=2) - dash_br.wait_for_text_to_equal(".card-body", "67434") - values = dash_br.find_elements(".card-body") + dash_br.wait_for_text_to_equal(kpi_card_path(), "67434") + values = dash_br.find_elements(kpi_card_path()) values_text = [value.text for value in values] assert_that( actual_or_assertion=values_text, diff --git a/vizro-core/tests/e2e/vizro/test_dom_elements/test_themes.py b/vizro-core/tests/e2e/vizro/test_dom_elements/test_themes.py index c9a1d2a70c..20eeff1e5b 100644 --- a/vizro-core/tests/e2e/vizro/test_dom_elements/test_themes.py +++ b/vizro-core/tests/e2e/vizro/test_dom_elements/test_themes.py @@ -45,7 +45,9 @@ def test_themes(dash_br, dashboard_id): indirect=["dash_br"], ) def test_ag_grid_themes(dash_br, dashboard_id): - accordion_select(dash_br, accordion_name=cnst.AG_GRID_ACCORDION.upper(), accordion_number=3) + accordion_select( + dash_br, accordion_name=cnst.AG_GRID_ACCORDION.upper(), accordion_number=cnst.AG_GRID_ACCORDION_NUMBER + ) page_select( dash_br, page_path=cnst.TABLE_AG_GRID_PAGE_PATH, diff --git a/vizro-core/tests/e2e/vizro/test_screenshots/test_screenshots.py b/vizro-core/tests/e2e/vizro/test_screenshots/test_screenshots.py new file mode 100644 index 0000000000..0c47bc14d6 --- /dev/null +++ b/vizro-core/tests/e2e/vizro/test_screenshots/test_screenshots.py @@ -0,0 +1,154 @@ +import pytest +from e2e.asserts import assert_image_equal, make_screenshot_and_paths +from e2e.vizro import constants as cnst +from e2e.vizro.checkers import check_graph_color, check_theme_color +from e2e.vizro.navigation import accordion_select, page_select +from e2e.vizro.paths import kpi_card_path, nav_card_link_path, theme_toggle_path +from e2e.vizro.waiters import callbacks_finish_waiter, graph_load_waiter + + +def image_assertion(func): + """Wait until all callbacks are finished and compare screenshots.""" + + def wrapper(dash_br, request): + result = func(dash_br) + callbacks_finish_waiter(dash_br) + result_image_path, expected_image_path = make_screenshot_and_paths(dash_br.driver, request.node.name) + assert_image_equal(result_image_path, expected_image_path) + return result + + return wrapper + + +@image_assertion +def test_kpi_indicators_page(dash_br): + page_select(dash_br, page_path=cnst.KPI_INDICATORS_PAGE_PATH, page_name=cnst.KPI_INDICATORS_PAGE) + # check if first kpi card have correct value + dash_br.wait_for_text_to_equal(kpi_card_path(), "73902") + + +@image_assertion +def test_homepage(dash_br): + graph_load_waiter(dash_br, graph_id=cnst.AREA_GRAPH_ID) + + +@image_assertion +def test_ag_grid_page(dash_br): + accordion_select( + dash_br, accordion_name=cnst.AG_GRID_ACCORDION.upper(), accordion_number=cnst.AG_GRID_ACCORDION_NUMBER + ) + page_select( + dash_br, + page_path=cnst.TABLE_AG_GRID_PAGE_PATH, + page_name=cnst.TABLE_AG_GRID_PAGE, + graph_id=cnst.BOX_AG_GRID_PAGE_ID, + ) + # check if column 'country' is available + dash_br.wait_for_element(f"div[id='{cnst.TABLE_AG_GRID_ID}'] div:nth-of-type(1) div[col-id='country']") + + +@image_assertion +def test_table_page(dash_br): + accordion_select( + dash_br, accordion_name=cnst.AG_GRID_ACCORDION.upper(), accordion_number=cnst.AG_GRID_ACCORDION_NUMBER + ) + page_select( + dash_br, + page_path=cnst.TABLE_PAGE_PATH, + page_name=cnst.TABLE_PAGE, + ) + # check if country Albania is available + dash_br.wait_for_text_to_equal( + f"div[id='{cnst.TABLE_ID}'] tr:nth-of-type(2) div[class='unfocused selectable dash-cell-value']", "Albania" + ) + + +@image_assertion +def test_table_interactions_page(dash_br): + accordion_select( + dash_br, accordion_name=cnst.AG_GRID_ACCORDION.upper(), accordion_number=cnst.AG_GRID_ACCORDION_NUMBER + ) + page_select( + dash_br, + page_path=cnst.TABLE_INTERACTIONS_PAGE_PATH, + page_name=cnst.TABLE_INTERACTIONS_PAGE, + graph_id=cnst.LINE_INTERACTIONS_ID, + ) + # click on Bosnia and Herzegovina country + dash_br.multiple_click( + f"div[id='{cnst.TABLE_INTERACTIONS_ID}'] tr:nth-of-type(5) div[class='unfocused selectable dash-cell-value']", 1 + ) + + +@image_assertion +def test_tabs_parameters_page(dash_br): + page_select( + dash_br, + page_path=cnst.PARAMETERS_PAGE_PATH, + page_name=cnst.PARAMETERS_PAGE, + graph_id=cnst.BAR_GRAPH_ID, + ) + + +@image_assertion +def test_nested_tabs_filters_page(dash_br): + page_select(dash_br, page_path=cnst.FILTERS_PAGE_PATH, page_name=cnst.FILTERS_PAGE, graph_id=cnst.SCATTER_GRAPH_ID) + + +@pytest.mark.parametrize( + "dash_br_driver", [({"port": cnst.ONE_PAGE_PORT})], ids=["one_page"], indirect=["dash_br_driver"] +) +@image_assertion +def test_export_action_page(dash_br_driver): + graph_load_waiter(dash_br_driver, graph_id=cnst.LINE_EXPORT_ID) + + +@pytest.mark.parametrize( + "dash_br_driver", + [ + ({"port": cnst.NAVBAR_ACCORDIONS_PORT}), + ({"port": cnst.NAVBAR_NAVLINK_PORT}), + ], + ids=["navbar_accordions", "navbar_navlink"], + indirect=["dash_br_driver"], +) +@image_assertion +def test_navbar_kpi_indicators_page(dash_br_driver): + dash_br_driver.multiple_click(nav_card_link_path(cnst.KPI_INDICATORS_PAGE_PATH), 1) + # check if first kpi card have correct value + dash_br_driver.wait_for_text_to_equal(kpi_card_path(), "73902") + + +@pytest.mark.parametrize( + "dash_br_driver", [({"port": cnst.NAVBAR_PAGES_PORT})], ids=["navbar_pages"], indirect=["dash_br_driver"] +) +@image_assertion +def test_navbar_filters_page(dash_br_driver): + dash_br_driver.multiple_click(nav_card_link_path(cnst.FILTERS_PAGE_PATH), 1) + + +@pytest.mark.mobile_screenshots +@image_assertion +def test_homepage_mobile(dash_br): + graph_load_waiter(dash_br, graph_id=cnst.AREA_GRAPH_ID) + + +@pytest.mark.mobile_screenshots +@pytest.mark.parametrize( + "dash_br_driver", [({"path": "filter-interactions-page"})], ids=["mobile"], indirect=["dash_br_driver"] +) +@image_assertion +def test_filter_interactions_page(dash_br_driver): + graph_load_waiter(dash_br_driver, graph_id=cnst.BOX_INTERACTIONS_ID) + + +@pytest.mark.mobile_screenshots +@pytest.mark.parametrize( + "dash_br_driver", [({"path": "filter-interactions-page"})], ids=["mobile"], indirect=["dash_br_driver"] +) +@image_assertion +def test_filter_interactions_dark_theme_page(dash_br_driver): + graph_load_waiter(dash_br_driver, graph_id=cnst.BOX_INTERACTIONS_ID) + dash_br_driver.multiple_click(theme_toggle_path(), 1) + check_graph_color(dash_br_driver, style_background=cnst.STYLE_TRANSPARENT, color=cnst.RGBA_TRANSPARENT) + check_theme_color(dash_br_driver, color=cnst.THEME_DARK) diff --git a/vizro-core/tests/integration/test_examples.py b/vizro-core/tests/integration/test_examples.py index 5a9707445c..d0b57800c8 100644 --- a/vizro-core/tests/integration/test_examples.py +++ b/vizro-core/tests/integration/test_examples.py @@ -54,6 +54,7 @@ def dashboard(request, monkeypatch): (examples_path / "scratch_dev", "yaml_version"), (examples_path / "dev", ""), (examples_path / "dev", "yaml_version"), + (examples_path / "tutorial", ""), ], ids=str, ) diff --git a/vizro-core/tests/tests_utils/e2e/asserts.py b/vizro-core/tests/tests_utils/e2e/asserts.py index acdf9b94ab..3b5c9fe129 100644 --- a/vizro-core/tests/tests_utils/e2e/asserts.py +++ b/vizro-core/tests/tests_utils/e2e/asserts.py @@ -1,69 +1,56 @@ +import os import shutil +import subprocess from pathlib import Path -import cv2 -import imutils -from hamcrest import assert_that, equal_to - - -def _compare_images(expected_image, result_image): - """Comparison process.""" - # Subtract two images - difference = cv2.subtract(expected_image, result_image) - # Splitting image into separate channels - blue, green, red = cv2.split(difference) - # Counting non-zero pixels and comparing it to zero - assert_that(cv2.countNonZero(blue), equal_to(0), reason="Blue channel is different") - assert_that(cv2.countNonZero(green), equal_to(0), reason="Green channel is different") - assert_that(cv2.countNonZero(red), equal_to(0), reason="Red channel is different") - - -def _create_image_difference(expected_image, result_image): - """Creates new image with diff of images comparison.""" - # Calculate the difference between the two images - diff = cv2.absdiff(expected_image, result_image) - # Convert image to grayscale - gray = cv2.cvtColor(diff, cv2.COLOR_BGR2GRAY) - for i in range(0, 3): - # Dilation of the image - dilated = cv2.dilate(gray.copy(), None, iterations=i + 1) - # Apply threshold to the dilated image - (t_var, thresh) = cv2.threshold(dilated, 3, 255, cv2.THRESH_BINARY) - # Calculate difference contours for the image - cnts = cv2.findContours(thresh, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) - cnts = imutils.grab_contours(cnts) - for contour in cnts: - # Calculate bounding rectangles around detected contour - (x, y, width, height) = cv2.boundingRect(contour) - # Draw red rectangle around difference area - cv2.rectangle(expected_image, (x, y), (x + width, y + height), (0, 0, 255), 2) - return expected_image +import pytest +from e2e.vizro import constants as cnst def make_screenshot_and_paths(driver, request_node_name): """Creates image paths and makes screenshot during the test run.""" result_image_path = f"{request_node_name}_branch.png" - expected_image_path = f"tests/e2e/screenshots/{request_node_name.replace('test', 'main')}.png" + expected_image_path = ( + f"tests/e2e/screenshots/{os.getenv('BROWSER', 'chrome')}/{request_node_name.replace('test', 'main')}.png" + ) driver.save_screenshot(result_image_path) return result_image_path, expected_image_path +def assert_pixelmatch(result_image_path, expected_image_path): + expected_image_name = Path(expected_image_path).name + # pixelmatch docs: https://github.com/mapbox/pixelmatch + subprocess.run( + [ + "pixelmatch", + expected_image_path, + result_image_path, + f"{expected_image_name.replace('.', '_difference_from_main.')}", + cnst.PIXELMATCH_THRESHOLD, + ], + capture_output=True, + text=True, + check=True, + ) + Path(f"{expected_image_name.replace('.', '_difference_from_main.')}").unlink() + + def assert_image_equal(result_image_path, expected_image_path): """Comparison logic and diff files creation.""" - expected_image = cv2.imread(expected_image_path) expected_image_name = Path(expected_image_path).name - result_image = cv2.imread(result_image_path) try: - _compare_images(expected_image=expected_image, result_image=result_image) - # Deleting created branch image to leave only failed for github artifacts + assert_pixelmatch(result_image_path, expected_image_path) Path(result_image_path).unlink() - except AssertionError as exc: - # Copy created branch image to the one with the name from main for easier replacement in the repo - shutil.copy(result_image_path, expected_image_name) - diff = _create_image_difference(expected_image=expected_image, result_image=result_image) - # Writing image with differences to a new file - cv2.imwrite(f"{result_image_path}_difference_from_main.png", diff) - raise AssertionError("pictures are not the same") from exc - except cv2.error as exc: + except subprocess.CalledProcessError as err: shutil.copy(result_image_path, expected_image_name) - raise cv2.error("pictures has different sizes") from exc + shutil.copy(expected_image_path, f"{expected_image_name.replace('.', '_old.')}") + Path(result_image_path).unlink() + raise Exception(err.stdout) + + +def assert_image_not_equal(image_one, image_two): + try: + assert_pixelmatch(image_one, image_two) + pytest.fail("Images should be different") + except subprocess.CalledProcessError: + pass diff --git a/vizro-core/tests/tests_utils/e2e/vizro/constants.py b/vizro-core/tests/tests_utils/e2e/vizro/constants.py index 73ead126d5..1986f22c65 100644 --- a/vizro-core/tests/tests_utils/e2e/vizro/constants.py +++ b/vizro-core/tests/tests_utils/e2e/vizro/constants.py @@ -39,11 +39,18 @@ BAR_GRAPH_ID_CONTAINER = "b@R-graph__container" HISTOGRAM_GRAPH_ID = "-histogram-graph--" +FILTER_INTERACTIONS_PAGE = "filter interactions page" +FILTER_INTERACTIONS_PAGE_PATH = "/filter-interactions-page" +SCATTER_INTERACTIONS_ID = "scatter_inter" +BOX_INTERACTIONS_ID = "box_inter" +CARD_INTERACTIONS_ID = "card_inter" + KPI_INDICATORS_PAGE = "kpi indicators page" KPI_INDICATORS_PAGE_PATH = "/kpi-indicators-page" EXPORT_PAGE = "export page" EXPORT_PAGE_PATH = "/exportp" +LINE_EXPORT_ID = "line--export--id" DATEPICKER_PAGE = "DATEpicker page" DATEPICKER_PAGE_PATH = "/datepicker-page" @@ -58,6 +65,37 @@ BOX_AG_GRID_PAGE_ID = "B@x on ag grid page" TABLE_AG_GRID_CONTAINER = "table_ag_grid_container" +TABLE_PAGE = "table page" +TABLE_PAGE_PATH = "/table-page" +TABLE_ID = "123_table" +TABLE_CONTAINER = "table container" + +TABLE_INTERACTIONS_PAGE = "table inter page" +TABLE_INTERACTIONS_PAGE_PATH = "/table-inter-page" +TABLE_INTERACTIONS_ID = "interactions-123_table" +LINE_INTERACTIONS_ID = "line inter id" + +DYNAMIC_DATA_PAGE = "dynamic data page" +DYNAMIC_DATA_PAGE_PATH = "/dynamic-data-page" +SCATTER_DYNAMIC_CACHED_ID = "scatter_dynamic_cached" +SCATTER_DYNAMIC_ID = "scatter_dynamic" +SLIDER_DYNAMIC_DATA_CACHED_ID = "slider_dynamic_data_cached" +SLIDER_DYNAMIC_DATA_ID = "slider_dynamic_data" + +DYNAMIC_FILTERS_CATEGORICAL_PAGE = "dynamic filters categorical" +DYNAMIC_FILTERS_CATEGORICAL_PAGE_PATH = "/dynamic-filters-categorical" +BOX_DYNAMIC_FILTERS_ID = "box dynamic" +DROPDOWN_MULTI_DYNAMIC_FILTER_ID = "dropdown_multi_dynamic" +DROPDOWN_DYNAMIC_FILTER_ID = "dropdown_dynamic" +CHECKLIST_DYNAMIC_FILTER_ID = "checklist_dynamic" +RADIOITEMS_DYNAMIC_FILTER_ID = "radio_dynamic" + +DYNAMIC_FILTERS_NUMERICAL_PAGE = "dynamic filters numerical" +DYNAMIC_FILTERS_NUMERICAL_PAGE_PATH = "/dynamic-filters-numerical" +BAR_DYNAMIC_FILTER_ID = "bar_dynamic" +SLIDER_DYNAMIC_FILTER_ID = "slider_dynamic" +RANGE_SLIDER_DYNAMIC_FILTER_ID = "range_slider_dynamic" + PAGE_404_PATH = "/404-page" # Accordion names @@ -65,10 +103,18 @@ GENERAL_ACCORDION = "generAl" DATEPICKER_ACCORDION = "DATEpicker" AG_GRID_ACCORDION = "AGgrid" +AG_GRID_ACCORDION_NUMBER = 3 +DYNAMIC_DATA_ACCORDION = "DYNAMIC data" +DYNAMIC_DATA_ACCORDION_NUMBER = 4 +KPI_ACCORDION = "KPI" # Ports DEFAULT_PORT = 5002 +ONE_PAGE_PORT = 5003 +NAVBAR_ACCORDIONS_PORT = 5004 +NAVBAR_PAGES_PORT = 5005 +NAVBAR_NAVLINK_PORT = 5006 # Dashboards @@ -82,3 +128,8 @@ STYLE_TRANSPARENT = "background: rgba(0, 0, 0, 0);" AG_GRID_DARK = "ag-theme-quartz-dark ag-theme-vizro" AG_GRID_LIGHT = "ag-theme-quartz ag-theme-vizro" + +# Configs + +DYNAMIC_FILTERS_DATA_CONFIG = "tests/e2e/vizro/dashboards/default/dynamic_filters_data.yaml" +PIXELMATCH_THRESHOLD = "0.18" diff --git a/vizro-core/tests/tests_utils/e2e/vizro/navigation.py b/vizro-core/tests/tests_utils/e2e/vizro/navigation.py index a47541e32b..1669ed25b8 100644 --- a/vizro-core/tests/tests_utils/e2e/vizro/navigation.py +++ b/vizro-core/tests/tests_utils/e2e/vizro/navigation.py @@ -1,7 +1,7 @@ import time from e2e.vizro.checkers import check_accordion_active -from e2e.vizro.paths import page_title_path +from e2e.vizro.paths import page_title_path, slider_handler_path, slider_value_path from e2e.vizro.waiters import graph_load_waiter @@ -27,3 +27,8 @@ def select_dropdown_value(driver, value): driver.multiple_click(".Select-clear", 1) driver.multiple_click(".Select-arrow", 1) driver.multiple_click(f".ReactVirtualized__Grid__innerScrollContainer div:nth-of-type({value})", 1) + + +def select_slider_handler(driver, elem_id, value, handler_class="rc-slider-handle"): + driver.multiple_click(slider_value_path(elem_id=elem_id, value=value), 1) + driver.multiple_click(slider_handler_path(elem_id=elem_id, handler_class=handler_class), 1) diff --git a/vizro-core/tests/tests_utils/e2e/vizro/paths.py b/vizro-core/tests/tests_utils/e2e/vizro/paths.py index 5f9f8a7249..449c3e365d 100644 --- a/vizro-core/tests/tests_utils/e2e/vizro/paths.py +++ b/vizro-core/tests/tests_utils/e2e/vizro/paths.py @@ -24,5 +24,13 @@ def slider_value_path(elem_id, value): return f"div[id='{elem_id}'] div div span:nth-of-type({value})" +def slider_handler_path(elem_id, handler_class="rc-slider-handle"): + return f"div[id='{elem_id}'] div[class^='{handler_class}']" + + def categorical_components_value_path(elem_id, value): return f"div[id='{elem_id}'] div:nth-of-type({value}) input" + + +def kpi_card_path(): + return ".card-body" diff --git a/vizro-core/tests/tests_utils/e2e/vizro/waiters.py b/vizro-core/tests/tests_utils/e2e/vizro/waiters.py index 8ac64d64a6..819ea0c031 100644 --- a/vizro-core/tests/tests_utils/e2e/vizro/waiters.py +++ b/vizro-core/tests/tests_utils/e2e/vizro/waiters.py @@ -1,3 +1,10 @@ +from dash.testing.wait import until + + def graph_load_waiter(driver, graph_id): """Waiting for graph's x-axis to appear.""" driver.wait_for_element(f"div[id='{graph_id}'] path[class='xtick ticks crisp']") + + +def callbacks_finish_waiter(driver): + until(driver._wait_for_callbacks, timeout=40, poll=0.3) diff --git a/vizro-core/tests/unit/vizro/models/_components/test_container.py b/vizro-core/tests/unit/vizro/models/_components/test_container.py index 8c8a6d1852..c83a4b24fd 100644 --- a/vizro-core/tests/unit/vizro/models/_components/test_container.py +++ b/vizro-core/tests/unit/vizro/models/_components/test_container.py @@ -17,15 +17,22 @@ def test_create_container_mandatory_only(self): assert isinstance(container.components[0], vm.Button) and isinstance(container.components[1], vm.Button) assert container.layout.grid == [[0], [1]] assert container.title == "Title" + assert container.variant == "plain" - def test_create_container_mandatory_and_optional(self): + @pytest.mark.parametrize("variant", ["plain", "filled", "outlined"]) + def test_create_container_mandatory_and_optional(self, variant): container = vm.Container( - id="my-id", title="Title", components=[vm.Button(), vm.Button()], layout=vm.Layout(grid=[[0, 1]]) + id="my-id", + title="Title", + components=[vm.Button(), vm.Button()], + layout=vm.Layout(grid=[[0, 1]]), + variant=variant, ) assert container.id == "my-id" assert isinstance(container.components[0], vm.Button) and isinstance(container.components[1], vm.Button) assert container.layout.grid == [[0, 1]] assert container.title == "Title" + assert container.variant == variant def test_mandatory_title_missing(self): with pytest.raises(ValidationError, match="Field required"): @@ -35,6 +42,10 @@ def test_mandatory_components_missing(self): with pytest.raises(ValidationError, match="Field required"): vm.Container(title="Title") + def test_invalid_variant(self): + with pytest.raises(ValidationError, match="Input should be 'plain', 'filled' or 'outlined'."): + vm.Container(title="Title", components=[vm.Button()], variant="test") + class TestContainerBuildMethod: def test_container_build(self): @@ -42,10 +53,19 @@ def test_container_build(self): id="container", title="Title", components=[vm.Button()], layout=vm.Layout(id="layout_id", grid=[[0]]) ).build() assert_component_equal( - result, html.Div(id="container", className="page-component-container"), keys_to_strip={"children"} + result, dbc.Container(id="container", className="", fluid=True), keys_to_strip={"children"} ) assert_component_equal(result.children, [html.H3(), html.Div()], keys_to_strip=STRIP_ALL) # We still want to test the exact H3 produced in Container.build: assert_component_equal(result.children[0], html.H3("Title", className="container-title", id="container_title")) # And also that a button has been inserted in the right place: assert_component_equal(result["layout_id_0"].children, dbc.Button(), keys_to_strip=STRIP_ALL) + + @pytest.mark.parametrize( + "variant, expected_classname", [("plain", ""), ("filled", "bg-container p-3"), ("outlined", "border p-3")] + ) + def test_container_with_variant(self, variant, expected_classname): + result = vm.Container(title="Title", components=[vm.Button()], variant=variant).build() + assert_component_equal( + result, dbc.Container(className=expected_classname, fluid=True), keys_to_strip={"children", "id"} + ) diff --git a/vizro-core/tests/unit/vizro/models/_components/test_tabs.py b/vizro-core/tests/unit/vizro/models/_components/test_tabs.py index a64a1b9941..f537345261 100644 --- a/vizro-core/tests/unit/vizro/models/_components/test_tabs.py +++ b/vizro-core/tests/unit/vizro/models/_components/test_tabs.py @@ -3,7 +3,6 @@ import dash_bootstrap_components as dbc import pytest from asserts import assert_component_equal -from dash import html from pydantic import ValidationError import vizro.models as vm @@ -49,8 +48,8 @@ def test_tabs_build(self, containers): assert_component_equal( [tab.children for tab in result.children], [ - html.Div(id="container-1", className="page-component-container"), - html.Div(id="container-2", className="page-component-container"), + dbc.Container(id="container-1", className="", fluid=True), + dbc.Container(id="container-2", className="", fluid=True), ], keys_to_strip={"children"}, )