diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8c8d615a2..21bc464cf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -45,7 +45,7 @@ repos: args: [--autofix] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.7.2 + rev: v0.8.1 hooks: - id: ruff args: [--fix] @@ -53,7 +53,7 @@ repos: - id: ruff-format - repo: https://github.com/PyCQA/bandit - rev: 1.7.10 + rev: 1.8.0 hooks: - id: bandit args: [-c, pyproject.toml, -ll] @@ -112,3 +112,4 @@ ci: - codespell - bandit - mypy + diff --git a/vizro-ai/docs/pages/user-guides/install.md b/vizro-ai/docs/pages/user-guides/install.md index 76ed1108c..8feda60d0 100644 --- a/vizro-ai/docs/pages/user-guides/install.md +++ b/vizro-ai/docs/pages/user-guides/install.md @@ -66,7 +66,7 @@ Vizro-AI supports **any** model that is available via [Langchain's `BaseChatMode To use OpenAI with Vizro-AI you need an API key, which you can get by [creating an OpenAI account if you don't already have one](https://platform.openai.com/account/api-keys). -We recommend that you consult the [third-party API key section of the disclaimer documentation](../explanation/disclaimer.md) documentation. +We recommend that you consult the [third-party API key section of the disclaimer documentation](../explanation/disclaimer.md). There are two common ways to set up the API key in a development environment. diff --git a/vizro-core/changelog.d/20241202_150655_huong_li_nguyen_refactor_bs_example.md b/vizro-core/changelog.d/20241202_150655_huong_li_nguyen_refactor_bs_example.md new file mode 100644 index 000000000..7c0d58d4f --- /dev/null +++ b/vizro-core/changelog.d/20241202_150655_huong_li_nguyen_refactor_bs_example.md @@ -0,0 +1,48 @@ + + + + + + + + + diff --git a/vizro-core/docs/pages/user-guides/data.md b/vizro-core/docs/pages/user-guides/data.md index bd59096c9..51bf84b97 100644 --- a/vizro-core/docs/pages/user-guides/data.md +++ b/vizro-core/docs/pages/user-guides/data.md @@ -2,12 +2,11 @@ Vizro supports two different types of data: -- [Static data](#static-data): pandas DataFrame. This is the simplest method and best to use if you do not need the more advanced functionality of dynamic data. -- [Dynamic data](#dynamic-data): function that returns a pandas DataFrame. This is a bit more complex to understand but has more advanced functionality such as the ability to refresh data while the dashboard is running. +* [Static data](#static-data): pandas DataFrame. This is the simplest method and best to use if you do not need the more advanced functionality of dynamic data. +* [Dynamic data](#dynamic-data): function that returns a pandas DataFrame. This is a bit more complex to understand but has more advanced functionality such as the ability to refresh data while the dashboard is running. The following flowchart shows what you need to consider when choosing how to set up your data. - -```mermaid +``` mermaid graph TD refresh["`Do you need your data to refresh while the dashboard is running?`"] specification["`Do you need to specify your dashboard through a configuration language like YAML?`"] @@ -28,10 +27,11 @@ graph TD ``` ??? note "Static vs. dynamic data comparison" + This table gives a full comparison between static and dynamic data. Do not worry if you do not yet understand everything in it; it will become clearer after reading more about [static data](#static-data) and [dynamic data](#dynamic-data)! | | Static | Dynamic | - | ------------------------------------------------------------- | ---------------- | ---------------------------------------- | + |---------------------------------------------------------------|------------------|------------------------------------------| | Required Python type | pandas DataFrame | Function that returns a pandas DataFrame | | Can be supplied directly in `data_frame` argument of `figure` | Yes | No | | Can be referenced by name after adding to data manager | Yes | Yes | @@ -73,13 +73,14 @@ The below example uses the Iris data saved to a file `iris.csv` in the same dire ``` 1. `iris` is a pandas DataFrame created by reading from the CSV file `iris.csv`. - === "Result" - [![DataBasic]][databasic] + [![DataBasic]][DataBasic] -The [`Graph`][vizro.models.Graph], [`AgGrid`][vizro.models.AgGrid] and [`Table`][vizro.models.Table] models all have an argument called `figure`. This accepts a function (in the above example, `px.scatter`) that takes a pandas DataFrame as its first argument. The name of this argument is always `data_frame`. When configuring the dashboard using Python, it is optional to give the name of the argument: if you like, you could write `data_frame=iris` instead of `iris`. + [DataBasic]: ../../assets/user_guides/data/data_pandas_dataframe.png +The [`Graph`][vizro.models.Graph], [`AgGrid`][vizro.models.AgGrid] and [`Table`][vizro.models.Table] models all have an argument called `figure`. This accepts a function (in the above example, `px.scatter`) that takes a pandas DataFrame as its first argument. The name of this argument is always `data_frame`. When configuring the dashboard using Python, it is optional to give the name of the argument: if you like, you could write `data_frame=iris` instead of `iris`. !!! note + With static data, once the dashboard is running, the data shown in the dashboard cannot change even if the source data in `iris.csv` changes. The code `iris = pd.read_csv("iris.csv")` is only executed once when the dashboard is first started. If you would like changes to source data to flow through to the dashboard then you must use [dynamic data](#dynamic-data). ### Reference by name @@ -106,27 +107,27 @@ If you would like to specify your dashboard configuration through YAML then you ``` 1. `"iris"` is the name of a data source added to the data manager. This data is a pandas DataFrame created by reading from the CSV file `iris.csv`. - === "dashboard.yaml" ```yaml pages: - - components: - - figure: - _target_: box - data_frame: iris # (1)! - x: species - y: petal_width - color: species - type: graph + - components: + - figure: + _target_: box + data_frame: iris # (1)! + x: species + y: petal_width + color: species + type: graph title: Static data example ``` 1. Refer to the `"iris"` data source in the data manager. - === "Result" - [![DataBasic]][databasic] + [![DataBasic]][DataBasic] + + [DataBasic]: ../../assets/user_guides/data/data_pandas_dataframe.png -It is also possible to refer to a named data source using the Python API: `px.scatter("iris", ...)` or `px.scatter(data_frame="iris", ...)` would work if the `"iris"` data source has been registered in the data manager. +It is also possible to refer to a named data source using the Python API: `px.scatter("iris", ...)` or `px.scatter(data_frame="iris", ...)` would work if the `"iris"` data source has been registered in the data manager. ## Dynamic data @@ -165,26 +166,28 @@ The example below shows how data is fetched dynamically every time the page is r ``` 1. `iris` is a pandas DataFrame created by reading from the CSV file `iris.csv`. - 1. To demonstrate that dynamic data can change when the page is refreshed, select 50 points at random. This simulates what would happen if your file `iris.csv` were constantly changing. - 1. To use `load_iris_data` as dynamic data it must be added to the data manager. You should **not** actually call the function as `load_iris_data()`; doing so would result in static data that cannot be reloaded. - 1. Dynamic data is referenced by the name of the data source `"iris"`. + 2. To demonstrate that dynamic data can change when the page is refreshed, select 50 points at random. This simulates what would happen if your file `iris.csv` were constantly changing. + 3. To use `load_iris_data` as dynamic data it must be added to the data manager. You should **not** actually call the function as `load_iris_data()`; doing so would result in static data that cannot be reloaded. + 4. Dynamic data is referenced by the name of the data source `"iris"`. === "Result" - [![DynamicData]][dynamicdata] + [![DynamicData]][DynamicData] + + [DynamicData]: ../../assets/user_guides/data/dynamic_data.gif Since dynamic data sources must always be added to the data manager and referenced by name, they may be used in YAML configuration [exactly the same way as for static data sources](#reference-by-name). ### Configure cache -By default, each time the dashboard is refreshed a dynamic data function executes again. In fact, if there are multiple graphs on the same page using the same dynamic data source then the loading function executes _multiple_ times, once for each graph on the page. Hence, if loading your data is a slow operation, your dashboard performance may suffer. +By default, a dynamic data function executes every time the dashboard is refreshed. Data loading is batched so that a dynamic data function that supplies multiple graphs on the same page only executes _once_ per page refresh. Even with this batching, if loading your data is a slow operation, your dashboard performance may suffer. The Vizro data manager has a server-side caching mechanism to help solve this. Vizro's cache uses [Flask-Caching](https://flask-caching.readthedocs.io/en/latest/), which supports a number of possible cache backends and [configuration options](https://flask-caching.readthedocs.io/en/latest/#configuring-flask-caching). By default, the cache is turned off. - In a development environment the easiest way to enable caching is to use a [simple memory cache](https://cachelib.readthedocs.io/en/stable/simple/) with the default configuration options. This is achieved by adding one line to the above example to set `data_manager.cache`: !!! example "Simple cache with default timeout of 5 minutes" + ```py hl_lines="13" from flask_caching import Cache from vizro import Vizro @@ -217,11 +220,12 @@ By default, when caching is turned on, dynamic data is cached in the data manage If you would like to alter some options, such as the default cache timeout, then you can specify a different cache configuration: -```py title="Simple cache with timeout set to 10 minutes" +```python title="Simple cache with timeout set to 10 minutes" data_manager.cache = Cache(config={"CACHE_TYPE": "SimpleCache", "CACHE_DEFAULT_TIMEOUT": 600}) ``` !!! warning + Simple cache exists purely for single-process development purposes and is not intended to be used in production. If you deploy with multiple workers, [for example with Gunicorn](run.md/#gunicorn), then you should use a production-ready cache backend. All of Flask-Caching's [built-in backends](https://flask-caching.readthedocs.io/en/latest/#built-in-cache-backends) other than `SimpleCache` are suitable for production. In particular, you might like to use [`FileSystemCache`](https://cachelib.readthedocs.io/en/stable/file/) or [`RedisCache`](https://cachelib.readthedocs.io/en/stable/redis/): ```py title="Production-ready caches" @@ -235,9 +239,7 @@ data_manager.cache = Cache(config={"CACHE_TYPE": "SimpleCache", "CACHE_DEFAULT_T Since Flask-Caching relies on [`pickle`](https://docs.python.org/3/library/pickle.html), which can execute arbitrary code during unpickling, you should not cache data from untrusted sources. Doing so [could be unsafe](https://github.com/pallets-eco/flask-caching/pull/209). Note that when a production-ready cache backend is used, the cache is persisted beyond the Vizro process and is not cleared by restarting your server. To clear the cache then you must do so manually, for example, if you use `FileSystemCache` then you would delete your `cache` directory. Persisting the cache can also be useful for development purposes when handling data that takes a long time to load: even if you do not need the data to refresh while your dashboard is running, it can speed up your development loop to use dynamic data with a cache that is persisted between repeated runs of Vizro. - - #### Set timeouts You can change the timeout of the cache independently for each dynamic data source in the data manager using the `timeout` setting (measured in seconds). A `timeout` of 0 indicates that the cache does not expire. This is effectively the same as using [static data](#static-data). @@ -266,15 +268,20 @@ data_manager["no_expire_data"].timeout = 0 ### Parametrize data loading -You can supply arguments to your dynamic data loading function that can be modified from the dashboard. For example, if you are handling big data then you can use an argument to specify the number of entries or size of chunk of data. +You can give arguments to your dynamic data loading function that can be modified from the dashboard. For example: + +- To load different versions of the same data. +- To handle large datasets you can use an argument that controls the amount of data that is loaded. This effectively pre-filters data before it reaches the Vizro dashboard. + +In general, a parametrized dynamic data source should always return a pandas DataFrame with a fixed schema (column names and types). This ensures that page components and controls continue to work as expected when the parameter is changed on screen. To add a parameter to control a dynamic data source, do the following: 1. add the appropriate argument to your dynamic data function and specify a default value for the argument. -1. give an `id` to all components that have the data source you wish to alter through a parameter. -1. [add a parameter](parameters.md) with `targets` of the form `.data_frame.` and a suitable [selector](selectors.md). +2. give an `id` to all components that have the data source you wish to alter through a parameter. +3. [add a parameter](parameters.md) with `targets` of the form `.data_frame.` and a suitable [selector](selectors.md). -For example, let us extend the [dynamic data example](#dynamic-data) above to show how the `load_iris_data` can take an argument `number_of_points` controlled from the dashboard with a [`Slider`][vizro.models.Slider]. +For example, let us extend the [dynamic data example](#dynamic-data) above into an example of how parametrized dynamic data works. The `load_iris_data` can take an argument `number_of_points` controlled from the dashboard with a [`Slider`][vizro.models.Slider]. !!! example "Parametrized dynamic data" === "app.py" @@ -311,34 +318,101 @@ For example, let us extend the [dynamic data example](#dynamic-data) above to sh ``` 1. `load_iris_data` takes a single argument, `number_of_points`, with a default value of 10. - 1. `iris` is a pandas DataFrame created by reading from the CSV file `iris.csv`. - 1. Sample points at random, where `number_of_points` gives the number of points selected. - 1. To use `load_iris_data` as dynamic data it must be added to the data manager. You should **not** actually call the function as `load_iris_data()` or `load_iris_data(number_of_points=...)`; doing so would result in static data that cannot be reloaded. - 1. Give the `vm.Graph` component `id="graph"` so that the `vm.Parameter` can target it. Dynamic data is referenced by the name of the data source `"iris"`. - 1. Create a `vm.Parameter` to target the `number_of_points` argument for the `data_frame` used in `graph`. + 2. `iris` is a pandas DataFrame created by reading from the CSV file `iris.csv`. + 3. Sample points at random, where `number_of_points` gives the number of points selected. + 4. To use `load_iris_data` as dynamic data it must be added to the data manager. You should **not** actually call the function as `load_iris_data()` or `load_iris_data(number_of_points=...)`; doing so would result in static data that cannot be reloaded. + 5. Give the `vm.Graph` component `id="graph"` so that the `vm.Parameter` can target it. Dynamic data is referenced by the name of the data source `"iris"`. + 6. Create a `vm.Parameter` to target the `number_of_points` argument for the `data_frame` used in `graph`. === "Result" - [![ParametrizedDynamicData]][parametrizeddynamicdata] + [![ParametrizedDynamicData]][ParametrizedDynamicData] + + [ParametrizedDynamicData]: ../../assets/user_guides/data/parametrized_dynamic_data.gif Parametrized data loading is compatible with [caching](#configure-cache). The cache uses [memoization](https://flask-caching.readthedocs.io/en/latest/#memoization), so that the dynamic data function's arguments are included in the cache key. This means that `load_iris_data(number_of_points=10)` is cached independently of `load_iris_data(number_of_points=20)`. !!! warning - You should always [treat the content of user input as untrusted](https://community.plotly.com/t/writing-secure-dash-apps-community-thread/54619). For example, you should not expose a file path to load without passing it through a function like [`werkzeug.utils.secure_filename`](https://werkzeug.palletsprojects.com/en/3.0.x/utils/#werkzeug.utils.secure_filename), or you might enable arbitrary access to files on your server. + + You should always [treat the content of user input as untrusted](https://community.plotly.com/t/writing-secure-dash-apps-community-thread/54619). For example, you should not expose a filepath to load without passing it through a function like [`werkzeug.utils.secure_filename`](https://werkzeug.palletsprojects.com/en/3.0.x/utils/#werkzeug.utils.secure_filename), or you might enable arbitrary access to files on your server. You cannot pass [nested parameters](parameters.md#nested-parameters) to dynamic data. You can only target the top-level arguments of the data loading function, not the nested keys in a dictionary. -### Filter update limitation +### Filters + +When a [filter](filters.md) depends on dynamic data and no `selector` is explicitly defined in the `vm.Filter` model, the available selector values update on page refresh to reflect the latest dynamic data. This is called a _dynamic filter_. + +The mechanism behind updating dynamic filters works exactly like other non-control components such as `vm.Graph`. However, unlike such components, a filter can depend on multiple data sources. If at least one data source of the components in the filter's `targets` is dynamic then the filter is dynamic. Remember that when `targets` is not explicitly specified, a filter applies to all the components on a page that use a DataFrame including `column`. + +When the page is refreshed, the behavior of a dynamic filter is as follows: + +- The filter's selector updates its available values: + - For [categorical selectors](selectors.md#categorical-selectors), `options` updates to give all unique values found in `column` across all the data sources of components in `targets`. + - For [numerical selectors](selectors.md#numerical-selectors), `min` and `max` update to give the overall minimum and maximum values found in `column` across all the data sources of components in `targets`. +- The value selected on screen by a dashboard user _does not_ change. If the selected value is not already present in the new set of available values then the `options` or `min` and `max` are modified to include it. In this case, the filtering operation might result in an empty DataFrame. +- Even though the values present in a data source can change, the schema should not: `column` should remain present and of the same type in the data sources. The `targets` of the filter and selector type cannot change while the dashboard is running. For example, a `vm.Dropdown` selector cannot turn into `vm.RadioItems`. + +For example, let us add two filters to the [dynamic data example](#dynamic-data) above: + +!!! example "Dynamic filters" + + ```py hl_lines="10 20 21" + from vizro import Vizro + import pandas as pd + import vizro.plotly.express as px + import vizro.models as vm + + from vizro.managers import data_manager + + def load_iris_data(): + iris = pd.read_csv("iris.csv") + return iris.sample(5) # (1)! + + data_manager["iris"] = load_iris_data + + page = vm.Page( + title="Update the chart and filters on page refresh", + components=[ + vm.Graph(figure=px.box("iris", x="species", y="petal_width", color="species")) + ], + controls=[ + vm.Filter(column="species"), # (2)! + vm.Filter(column="sepal_length"), # (3)! + ], + ) + + dashboard = vm.Dashboard(pages=[page]) + + Vizro().build(dashboard).run() + ``` + + 1. We sample only 5 rather than 50 points so that changes to the available values in the filtered columns are more apparent when the page is refreshed. + 2. This filter implicitly controls the dynamic data source `"iris"`, which supplies the `data_frame` to the targeted `vm.Graph`. On page refresh, Vizro reloads this data, finds all the unique values in the `"species"` column and sets the categorical selector's `options` accordingly. + 3. Similarly, on page refresh, Vizro finds the minimum and maximum values of the `"sepal_length"` column in the reloaded data and sets new `min` and `max` values for the numerical selector accordingly. -If your dashboard includes a [filter](filters.md) then the values shown on a filter's [selector](selectors.md) _do not_ update while the dashboard is running. This is a known limitation that will be lifted in future releases, but if is problematic for you already then [raise an issue on our GitHub repo](https://github.com/mckinsey/vizro/issues/). +Consider a filter that depends on dynamic data, where you do **not** want the available values to change when the dynamic data changes. You should manually specify the `selector`'s `options` field (categorical selector) or `min` and `max` fields (numerical selector). In the above example, this could be achieved as follows: -This limitation is why all arguments of your dynamic data loading function must have a default value. Regardless of the value of the `vm.Parameter` selected in the dashboard, these default parameter values are used when the `vm.Filter` is built. This determines the type of selector used in a filter and the options shown, which cannot currently be changed while the dashboard is running. +```python title="Override selector options to make a dynamic filter static" +controls = [ + vm.Filter(column="species", selector=vm.Dropdown(options=["setosa", "versicolor", "virginica"])), + vm.Filter(column="sepal_length", selector=vm.RangeSlider(min=4.3, max=7.9)), +] +``` -Although a selector is automatically chosen for you in a filter when your dashboard is built, remember that [you can change this choice](filters.md#changing-selectors). For example, we could ensure that a dropdown always contains the options "setosa", "versicolor" and "virginica" by explicitly specifying your filter as follows. +If you [use a specific selector](filters.md#change-selector) for a dynamic filter without manually specifying `options` (categorical selector) or `min` and `max` (numerical selector) then the selector remains dynamic. For example: -```py -vm.Filter(column="species", selector=vm.Dropdown(options=["setosa", "versicolor", "virginica"]) +```python title="Dynamic filter with specific selector is still dynamic" +controls = [ + vm.Filter(column="species", selector=vm.Checklist()), + vm.Filter(column="sepal_length", selector=vm.Slider()), +] ``` -[databasic]: ../../assets/user_guides/data/data_pandas_dataframe.png -[dynamicdata]: ../../assets/user_guides/data/dynamic_data.gif -[parametrizeddynamicdata]: ../../assets/user_guides/data/parametrized_dynamic_data.gif +When Vizro initially builds a filter that depends on parametrized dynamic data loading, data is loaded using the default argument values. This data is used to: + +* perform initial validation +* check which data sources contain the specified `column` (unless `targets` is explicitly specified) and +* find the type of selector to use (unless `selector` is explicitly specified). + +!!! note + + When the value of a dynamic data parameter is changed by a dashboard user, the data underlying a dynamic filter can change. Currently this change affects page components such as `vm.Graph` but does not affect the available values shown in a dynamic filter, which only update on page refresh. This functionality will be coming soon! diff --git a/vizro-core/docs/pages/user-guides/filters.md b/vizro-core/docs/pages/user-guides/filters.md index 90179625c..bc3649178 100644 --- a/vizro-core/docs/pages/user-guides/filters.md +++ b/vizro-core/docs/pages/user-guides/filters.md @@ -2,16 +2,19 @@ This guide shows you how to add filters to your dashboard. One main way to interact with the charts/components on your page is by filtering the underlying data. A filter selects a subset of rows of a component's underlying DataFrame which alters the appearance of that component on the page. -The [`Page`][vizro.models.Page] model accepts the `controls` argument, where you can enter a [`Filter`][vizro.models.Filter] model. This model enables the automatic creation of [selectors](../user-guides/selectors.md) (such as Dropdown, RadioItems, Slider, ...) that operate upon the charts/components on the screen. +The [`Page`][vizro.models.Page] model accepts the `controls` argument, where you can enter a [`Filter`][vizro.models.Filter] model. +This model enables the automatic creation of [selectors](selectors.md) (for example, `Dropdown` or `RangeSlider`) that operate on the charts/components on the screen. + +By default, filters that control components with [dynamic data](data.md#dynamic-data) are [dynamically updated](data.md#filters) when the underlying data changes while the dashboard is running. ## Basic filters To add a filter to your page, do the following: 1. add the [`Filter`][vizro.models.Filter] model into the `controls` argument of the [`Page`][vizro.models.Page] model -1. configure the `column` argument, which denotes the target column to be filtered +2. configure the `column` argument, which denotes the target column to be filtered -By default, all components on a page with such a `column` present will be filtered. The selector type will be chosen automatically based on the target column, for example, a dropdown for categorical data, a range slider for numerical data, or a date picker for temporal data. +You can also set `targets` to specify which components on the page the filter should apply to. If this is not explicitly set then `targets` defaults to all components on the page whose data source includes `column`. !!! example "Basic Filter" === "app.py" @@ -36,7 +39,6 @@ By default, all components on a page with such a `column` present will be filter Vizro().build(dashboard).run() ``` - === "app.yaml" ```yaml # Still requires a .py to add data to the data manager and parse YAML configuration @@ -55,15 +57,89 @@ By default, all components on a page with such a `column` present will be filter type: filter title: My first page ``` + === "Result" + + [![Filter]][Filter] + + [Filter]: ../../assets/user_guides/control/control1.png + +The selector is configured automatically based on the target column type data as follows: + + - Categorical data uses [`vm.Dropdown(multi=True)`][vizro.models.Dropdown] where `options` is the set of unique values found in `column` across all the data sources of components in `targets`. + - [Numerical data](https://pandas.pydata.org/docs/reference/api/pandas.api.types.is_numeric_dtype.html) uses [`vm.RangeSlider`][vizro.models.RangeSlider] where `min` and `max` are the overall minimum and maximum values found in `column` across all the data sources of components in `targets`. + - [Temporal data](https://pandas.pydata.org/docs/reference/api/pandas.api.types.is_datetime64_any_dtype.html) uses [`vm.DatePicker(range=True)`][vizro.models.DatePicker] where `min` and `max` are the overall minimum and maximum values found in `column` across all the data sources of components in `targets`. A column can be converted to this type with [pandas.to_datetime](https://pandas.pydata.org/docs/reference/api/pandas.to_datetime.html). + +The following example demonstrates these default selector types. + +!!! example "Default Filter selectors" + === "app.py" + ```{.python pycafe-link} + import pandas as pd + from vizro import Vizro + import vizro.plotly.express as px + import vizro.models as vm + df_stocks = px.data.stocks(datetimes=True) + + df_stocks_long = pd.melt( + df_stocks, + id_vars='date', + value_vars=['GOOG', 'AAPL', 'AMZN', 'FB', 'NFLX', 'MSFT'], + var_name='stocks', + value_name='value' + ) + + df_stocks_long['value'] = df_stocks_long['value'].round(3) + + page = vm.Page( + title="My first page", + components=[ + vm.Graph(figure=px.line(df_stocks_long, x="date", y="value", color="stocks")), + ], + controls=[ + vm.Filter(column="stocks"), + vm.Filter(column="value"), + vm.Filter(column="date"), + ], + ) + + 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: + - components: + - figure: + _target_: line + data_frame: df_stocks_long + x: date + y: value + color: stocks + type: graph + controls: + - column: stocks + type: filter + - column: value + type: filter + - column: date + type: filter + title: My first page + ``` === "Result" - [![Filter]][filter] + [![Filter]][Filter] -## Changing selectors + [Filter]: ../../assets/user_guides/selectors/default_filter_selectors.png -If you want to have a different selector for your filter, you can give the `selector` argument of the [`Filter`][vizro.models.Filter] a different selector model. Currently available selectors are [`Checklist`][vizro.models.Checklist], [`Dropdown`][vizro.models.Dropdown], [`RadioItems`][vizro.models.RadioItems], [`RangeSlider`][vizro.models.RangeSlider], [`Slider`][vizro.models.Slider], and [`DatePicker`][vizro.models.DatePicker]. +## Change selector -!!! example "Filter with custom Selector" +If you want to have a different selector for your filter, you can give the `selector` argument of the [`Filter`][vizro.models.Filter] a different selector model. +Currently available selectors are [`Checklist`][vizro.models.Checklist], [`Dropdown`][vizro.models.Dropdown], [`RadioItems`][vizro.models.RadioItems], [`RangeSlider`][vizro.models.RangeSlider], [`Slider`][vizro.models.Slider], and [`DatePicker`][vizro.models.DatePicker]. + +!!! example "Filter with different selector" === "app.py" ```{.python pycafe-link} from vizro import Vizro @@ -86,7 +162,6 @@ If you want to have a different selector for your filter, you can give the `sele Vizro().build(dashboard).run() ``` - === "app.yaml" ```yaml # Still requires a .py to add data to the data manager and parse YAML configuration @@ -106,17 +181,18 @@ If you want to have a different selector for your filter, you can give the `sele type: filter title: My first page ``` - === "Result" - [![Selector]][selector] + + [![Selector]][Selector] + + [Selector]: ../../assets/user_guides/control/control2.png ## Further customization -For further customizations, you can always refer to the [`Filter`][vizro.models.Filter] reference. Some popular choices are: +For further customizations, you can always refer to the [`Filter` model][vizro.models.Filter] reference and the [guide to selectors](selectors.md). Some popular choices are: - select which component the filter will apply to by using `targets` -- select what the target column type is, hence choosing the default selector by using `column_type` -- choose options of lower level components, such as the `selector` models +- specify configuration of the `selector`, for example `multi` to switch between a multi-option and single-option selector, `options` for a categorical filter or `min` and `max` for a numerical filter Below is an advanced example where we only target one page component, and where we further customize the chosen `selector`. @@ -136,7 +212,7 @@ Below is an advanced example where we only target one page component, and where vm.Graph(figure=px.scatter(iris, x="petal_length", y="sepal_width", color="species")), ], controls=[ - vm.Filter(column="petal_length",targets=["scatter_chart"],selector=vm.RangeSlider(step=1)), + vm.Filter(column="petal_length",targets=["scatter_chart"], selector=vm.RangeSlider(step=1)), ], ) @@ -144,7 +220,6 @@ Below is an advanced example where we only target one page component, and where Vizro().build(dashboard).run() ``` - === "app.yaml" ```yaml # Still requires a .py to add data to the data manager and parse YAML configuration @@ -169,17 +244,17 @@ Below is an advanced example where we only target one page component, and where controls: - column: petal_length targets: - - scatter_chart + - scatter_chart selector: step: 1 type: range_slider type: filter title: My first page ``` - === "Result" - [![Advanced]][advanced] -[advanced]: ../../assets/user_guides/control/control3.png -[filter]: ../../assets/user_guides/control/control1.png -[selector]: ../../assets/user_guides/control/control2.png + [![Advanced]][Advanced] + + [Advanced]: ../../assets/user_guides/control/control3.png + +To further customize selectors, see our [how-to-guide on creating custom components](custom-components.md). diff --git a/vizro-core/src/vizro/models/_dashboard.py b/vizro-core/src/vizro/models/_dashboard.py index 2fd7c7b00..b635b8e95 100644 --- a/vizro-core/src/vizro/models/_dashboard.py +++ b/vizro-core/src/vizro/models/_dashboard.py @@ -308,31 +308,18 @@ def _make_page_404_layout(self): return html.Div( [ # Theme switch is added such that the 404 page has the same theme as the user-selected one. - html.Div( - children=dbc.Switch( - id="theme-selector", - value=self.theme == "vizro_light", - persistence=True, - persistence_type="session", - ), - id="settings", + dbc.Switch( + id="theme-selector", + value=self.theme == "vizro_light", + persistence=True, + persistence_type="session", ), html.Img(src=f"data:image/svg+xml;base64,{error_404_svg}"), - html.Div( - [ - html.Div( - children=[ - html.H3("This page could not be found.", className="heading-3-600"), - html.P("Make sure the URL you entered is correct."), - ], - className="error-text-container", - ), - dbc.Button(children="Take me home", href=get_relative_path("/")), - ], - className="error-content-container", - ), + html.H3("This page could not be found."), + html.P("Make sure the URL you entered is correct."), + dbc.Button(children="Take me home", href=get_relative_path("/"), className="mt-4"), ], - className="page-error-container", + className="d-flex flex-column align-items-center justify-content-center min-vh-100", ) @staticmethod diff --git a/vizro-core/src/vizro/static/css/layout.css b/vizro-core/src/vizro/static/css/layout.css index 996c1108c..a7c66f864 100644 --- a/vizro-core/src/vizro/static/css/layout.css +++ b/vizro-core/src/vizro/static/css/layout.css @@ -85,31 +85,6 @@ border-bottom: 1px solid var(--border-subtleAlpha01); } -.page-error-container { - align-items: center; - display: flex; - flex-direction: column; - height: 100vh; - justify-content: center; - width: 100vw; -} - -.error-content-container { - align-items: center; - display: inline-flex; - flex-direction: column; - gap: 24px; - margin-top: -32px; -} - -.error-text-container { - display: flex; - flex-direction: column; - gap: 8px; - text-align: center; - width: 336px; -} - .dashboard_title { display: flex; flex-direction: column; diff --git a/vizro-core/tests/unit/vizro/models/test_dashboard.py b/vizro-core/tests/unit/vizro/models/test_dashboard.py index 670550349..7d9ca5f08 100644 --- a/vizro-core/tests/unit/vizro/models/test_dashboard.py +++ b/vizro-core/tests/unit/vizro/models/test_dashboard.py @@ -243,35 +243,20 @@ def test_make_page_404_layout(self, page_1, vizro_app): # vizro_app fixture is needed to avoid mocking out get_relative_path. expected = html.Div( [ - html.Div( - children=dbc.Switch( - id="theme-selector", - value=False, - persistence=True, - persistence_type="session", - ), - id="settings", + dbc.Switch( + id="theme-selector", + value=False, + persistence=True, + persistence_type="session", ), html.Img(), - html.Div( - [ - html.Div( - [ - html.H3("This page could not be found.", className="heading-3-600"), - html.P("Make sure the URL you entered is correct."), - ], - className="error-text-container", - ), - dbc.Button("Take me home", href="/"), - ], - className="error-content-container", - ), + html.H3("This page could not be found."), + html.P("Make sure the URL you entered is correct."), + dbc.Button(children="Take me home", href="/", className="mt-4"), ], - className="page-error-container", + className="d-flex flex-column align-items-center justify-content-center min-vh-100", ) - - # Strip out src since it's too long to be worth comparing and just comes directly - # from reading a file. + # Strip out src since it's too long to be worth comparing and just comes directly from reading a file. assert_component_equal(vm.Dashboard(pages=[page_1])._make_page_404_layout(), expected, keys_to_strip={"src"})