diff --git a/.github/requirements.txt b/.github/requirements.txt index d410eb7ed..47df11979 100644 --- a/.github/requirements.txt +++ b/.github/requirements.txt @@ -3,7 +3,7 @@ pre-commit coverage[toml]>=6.5 pytest dash[testing] -chromedriver-autoinstaller +chromedriver-autoinstaller-fix toml pyyaml openpyxl diff --git a/vizro-core/changelog.d/20231108_230816_antony.milne_fix_assets_pathname.md b/vizro-core/changelog.d/20231108_230816_antony.milne_fix_assets_pathname.md new file mode 100644 index 000000000..b3de329f2 --- /dev/null +++ b/vizro-core/changelog.d/20231108_230816_antony.milne_fix_assets_pathname.md @@ -0,0 +1,44 @@ + + + + + +### Added + +- `Vizro` takes `**kwargs` that are passed through to `Dash` ([#151](https://github.com/mckinsey/vizro/pull/151)) + +### Changed + +- The path to a custom assets folder is now configurable using the `assets_folder` argument when instantiating `Vizro` ([#151](https://github.com/mckinsey/vizro/pull/151)) + + + +### Fixed + +- Assets are now routed correctly when hosting the dashboard in a subdirectory ([#151](https://github.com/mckinsey/vizro/pull/151)) + + diff --git a/vizro-core/docs/pages/API_reference/vizro.md b/vizro-core/docs/pages/API_reference/vizro.md index 9ce757421..182ba849a 100644 --- a/vizro-core/docs/pages/API_reference/vizro.md +++ b/vizro-core/docs/pages/API_reference/vizro.md @@ -1,3 +1,7 @@ # Vizro ::: vizro + options: + merge_init_into_class: false + docstring_options: + ignore_init_summary: false diff --git a/vizro-core/docs/pages/user_guides/actions.md b/vizro-core/docs/pages/user_guides/actions.md index 855402219..d76b3ea77 100644 --- a/vizro-core/docs/pages/user_guides/actions.md +++ b/vizro-core/docs/pages/user_guides/actions.md @@ -63,8 +63,7 @@ a result, when a dashboard user now clicks the button, all data on the page will dashboard = vm.Dashboard(pages=[page]) - if __name__ == "__main__": - Vizro().build(dashboard).run() + Vizro().build(dashboard).run() ``` === "app.yaml" ```yaml @@ -167,8 +166,7 @@ Here is an example of how to configure a chart interaction when the source is a ] ) - if __name__ == "__main__": - Vizro().build(dashboard).run() + Vizro().build(dashboard).run() ``` === "app.yaml" ```yaml @@ -250,8 +248,7 @@ Here is an example of how to configure a chart interaction when the source is a ] ) - if __name__ == "__main__": - Vizro().build(dashboard).run() + Vizro().build(dashboard).run() ``` === "app.yaml" ```yaml @@ -343,8 +340,7 @@ The order of action execution is guaranteed, and the next action in the list wil dashboard = vm.Dashboard(pages=[page]) - if __name__ == "__main__": - Vizro().build(dashboard).run() + Vizro().build(dashboard).run() ``` === "app.yaml" ```yaml diff --git a/vizro-core/docs/pages/user_guides/assets.md b/vizro-core/docs/pages/user_guides/assets.md index 9672229c6..dced19937 100644 --- a/vizro-core/docs/pages/user_guides/assets.md +++ b/vizro-core/docs/pages/user_guides/assets.md @@ -18,13 +18,6 @@ The user-provided `assets` folder thus always takes precedence. ├── favicon.ico ``` -## Adding static images -We leverage Dash's underlying functionalities to embed images into the app. -For more information, see [here](https://dash.plotly.com/dash-enterprise/static-assets?de-version=5.1#embedding-images-in-your-dash-apps). - -For example, you can leverage the `dash.get_asset_url()` function in your custom components, such that any provided path does not require `assets` as a prefix in the relative path anymore. - - ## Changing the favicon To change the default favicon (website icon appearing in the browser tab), add a file named `favicon.ico` to your `assets` folder. For more information, see [here](https://dash.plotly.com/external-resources#changing-the-favicon). @@ -63,8 +56,6 @@ For reference, all Vizro CSS files can be found [here](https://github.com/mckins dashboard = vm.Dashboard(pages=[page]) - # only required if assets folder is not located at the same directory of app.py - Vizro._user_assets_folder = os.path.abspath("../assets") Vizro().build(dashboard).run() ``` @@ -131,9 +122,6 @@ To achieve this, do the following: dashboard = vm.Dashboard(pages=[page]) - # only required if assets folder is not located at the same directory of app.py - Vizro._user_assets_folder = os.path.abspath("../assets") - Vizro().build(dashboard).run() ``` === "app.yaml" @@ -157,24 +145,17 @@ To achieve this, do the following: [CardCSS]: ../../assets/user_guides/assets/css_change_card.png -???+ note - - CSS properties will be applied with the last served file taking precedence. - Files are served in alphanumerical order. - - **Order of CSS being served to app** +CSS properties will be applied with the last served file taking precedence. The order of serving is: - 1. Dash styling sheets - 2. Vizro external styling sheets - 3. User assets folder - - CSS/JS Files - - Folders - - CSS/JS Files +1. Dash built-in stylesheets +2. Vizro built-in stylesheets +3. User assets folder stylesheets +Within each of these categories, individual files are served in alphanumerical order. ## Changing the `assets` folder path If you do not want to place your `assets` folder in the root directory of your app, you can -also change the reference to your `assets` folder. Note that the path provided needs to be an absolute path. +specify an alternative path through the `assets_folder` argument of the [`Vizro`][vizro.Vizro] class. ```python from vizro import Vizro @@ -183,8 +164,7 @@ import vizro.models as vm page = dashboard = vm.Dashboard(pages=[page]) -Vizro._user_assets_folder = "absolute/path/to/assets" -app = Vizro().build(dashboard).run() +app = Vizro(assets_folder="path/to/assets/folder").build(dashboard).run() ``` diff --git a/vizro-core/docs/pages/user_guides/custom_charts.md b/vizro-core/docs/pages/user_guides/custom_charts.md index 0c8219be6..3dda3f720 100644 --- a/vizro-core/docs/pages/user_guides/custom_charts.md +++ b/vizro-core/docs/pages/user_guides/custom_charts.md @@ -70,8 +70,7 @@ Building on the above, there are several routes one can take. The following exam ) dashboard = vm.Dashboard(pages=[page_0]) - if __name__ == "__main__": - Vizro().build(dashboard).run() + Vizro().build(dashboard).run() ``` === "app.yaml" ```yaml @@ -142,8 +141,7 @@ The below examples shows a more involved use-case. We create and style a waterfa ) dashboard = vm.Dashboard(pages=[page_0]) - if __name__ == "__main__": - Vizro().build(dashboard).run() + Vizro().build(dashboard).run() ``` === "app.yaml" ```yaml diff --git a/vizro-core/docs/pages/user_guides/custom_components.md b/vizro-core/docs/pages/user_guides/custom_components.md index bc61f9e13..f3872edfd 100644 --- a/vizro-core/docs/pages/user_guides/custom_components.md +++ b/vizro-core/docs/pages/user_guides/custom_components.md @@ -207,8 +207,7 @@ vm.Parameter.add_type("selector", TooltipNonCrossRangeSlider) dashboard = vm.Dashboard(pages=[page]) - if __name__ == "__main__": - Vizro().build(dashboard).run() + Vizro().build(dashboard).run() ``` 1. Here we provide a new type for the new component, so it can be distinguished in the discriminated union. @@ -330,8 +329,7 @@ vm.Page.add_type("components", Jumbotron) dashboard = vm.Dashboard(pages=[page]) - if __name__ == "__main__": - Vizro().build(dashboard).run() + Vizro().build(dashboard).run() ``` diff --git a/vizro-core/docs/pages/user_guides/dashboard.md b/vizro-core/docs/pages/user_guides/dashboard.md index c17b3ae51..0088aa2c5 100644 --- a/vizro-core/docs/pages/user_guides/dashboard.md +++ b/vizro-core/docs/pages/user_guides/dashboard.md @@ -168,8 +168,7 @@ To create a dashboard, do the following steps: dashboard = yaml.safe_load(Path("dashboard.yaml").read_text(encoding="utf-8")) dashboard = Dashboard(**dashboard) - if __name__ == "__main__": - Vizro().build(dashboard).run() + Vizro().build(dashboard).run() ``` === "app.py for json" ```py @@ -185,8 +184,7 @@ To create a dashboard, do the following steps: dashboard = json.loads(Path("dashboard.json").read_text(encoding="utf-8")) dashboard = Dashboard(**dashboard) - if __name__ == "__main__": - Vizro().build(dashboard).run() + Vizro().build(dashboard).run() ``` After running the dashboard, you can access the dashboard via `localhost:8050`. diff --git a/vizro-core/docs/pages/user_guides/run.md b/vizro-core/docs/pages/user_guides/run.md index 0bb25c3ba..fd210e79c 100644 --- a/vizro-core/docs/pages/user_guides/run.md +++ b/vizro-core/docs/pages/user_guides/run.md @@ -82,9 +82,13 @@ The dashboard application can be launched in a Jupyter environment in `inline`, dashboard = vm.Dashboard(pages=[page]) app = Vizro().build(dashboard) server = app.dash.server # (1)! + + if __name__ == "__main__": # (2)! + app.run() ``` - 1. Expose the underlying Flask app through `app.dash.server`. + 1. Expose the underlying Flask app through `app.dash.server`; this will be used by Gunicorn. + 2. Enable the same app to still be run using the built-in Flask server with `python app.py` for development purposes. To run using Gunicorn with four worker processes, execute ```bash @@ -100,3 +104,5 @@ A Vizro app wraps a Dash app, which itself wraps a Flask app. Hence to deploy a - [Dash deployment documentation](https://dash.plotly.com/deployment) In particular, `app = Vizro()` exposes the Flask app through `app.dash.server`. As in the [above example with Gunicorn](#gunicorn), this provides the application instance to a WSGI server. + +[`Vizro`][vizro.Vizro] accepts `**kwargs` that are passed through to `Dash`. This allows you to configure the underlying Dash app using the same [argumentst that are available](https://dash.plotly.com/reference#dash.dash) in `Dash`. For example, in a deployment context, you might like to specify a custom `url_base_pathname` to serve your Vizro app at a specific URL rather than at your domain root. diff --git a/vizro-core/examples/default/app.py b/vizro-core/examples/default/app.py index 3f7878595..3bcce552b 100644 --- a/vizro-core/examples/default/app.py +++ b/vizro-core/examples/default/app.py @@ -1,5 +1,4 @@ """Example to show dashboard configuration.""" -import os import pandas as pd @@ -532,5 +531,4 @@ def create_home_page(): ) if __name__ == "__main__": - Vizro._user_assets_folder = os.path.abspath("../assets") - Vizro().build(dashboard).run() + Vizro(assets_folder="../assets").build(dashboard).run() diff --git a/vizro-core/examples/from_dict/app.py b/vizro-core/examples/from_dict/app.py index 804610ecc..f36641ef6 100644 --- a/vizro-core/examples/from_dict/app.py +++ b/vizro-core/examples/from_dict/app.py @@ -1,5 +1,4 @@ """Example to show dashboard configuration specified as a dictionary.""" -import os import pandas as pd @@ -580,8 +579,8 @@ def retrieve_avg_gapminder_year(year: int): } }, } + dashboard = Dashboard(**dashboard) if __name__ == "__main__": - Vizro._user_assets_folder = os.path.abspath("../assets") - Vizro().build(dashboard).run() + Vizro(assets_folder="../assets").build(dashboard).run() diff --git a/vizro-core/examples/from_json/app.py b/vizro-core/examples/from_json/app.py index bedaf58ca..900914bf3 100644 --- a/vizro-core/examples/from_json/app.py +++ b/vizro-core/examples/from_json/app.py @@ -1,6 +1,5 @@ """Example to show dashboard configuration specified as a JSON file.""" import json -import os from pathlib import Path import pandas as pd @@ -63,5 +62,4 @@ def retrieve_avg_gapminder_year(year: int): dashboard = Dashboard(**dashboard) if __name__ == "__main__": - Vizro._user_assets_folder = os.path.abspath("../assets") - Vizro().build(dashboard).run() + Vizro(assets_folder="../assets").build(dashboard).run() diff --git a/vizro-core/examples/from_yaml/app.py b/vizro-core/examples/from_yaml/app.py index 8e53c3f65..4ab29d514 100644 --- a/vizro-core/examples/from_yaml/app.py +++ b/vizro-core/examples/from_yaml/app.py @@ -1,5 +1,4 @@ """Example to show dashboard configuration specified as a YAML file.""" -import os from pathlib import Path import pandas as pd @@ -63,5 +62,4 @@ def retrieve_avg_gapminder_year(year: int): dashboard = Dashboard(**dashboard) if __name__ == "__main__": - Vizro._user_assets_folder = os.path.abspath("../assets") - Vizro().build(dashboard).run() + Vizro(assets_folder="../assets").build(dashboard).run() diff --git a/vizro-core/examples/jupyter/app.ipynb b/vizro-core/examples/jupyter/app.ipynb index d5ab428a6..3879c378b 100644 --- a/vizro-core/examples/jupyter/app.ipynb +++ b/vizro-core/examples/jupyter/app.ipynb @@ -544,8 +544,7 @@ " ]\n", ")\n", "\n", - "Vizro._user_assets_folder = os.path.abspath(\"../assets\")\n", - "Vizro().build(dashboard).run()" + "Vizro(assets_folder=\"../assets\").build(dashboard).run()" ] } ], diff --git a/vizro-core/hatch.toml b/vizro-core/hatch.toml index cec84e1da..615f9c0ab 100644 --- a/vizro-core/hatch.toml +++ b/vizro-core/hatch.toml @@ -20,7 +20,7 @@ dependencies = [ "pytest", "pytest-mock", "dash[testing]", - "chromedriver-autoinstaller", + "chromedriver-autoinstaller-fix", "toml", "pyyaml", "openpyxl" diff --git a/vizro-core/schemas/0.1.6.dev2.json b/vizro-core/schemas/0.1.6.dev2.json new file mode 100644 index 000000000..95b345bcb --- /dev/null +++ b/vizro-core/schemas/0.1.6.dev2.json @@ -0,0 +1,932 @@ +{ + "title": "Dashboard", + "description": "Vizro Dashboard to be used within [`Vizro`][vizro._vizro.Vizro.build].\n\nArgs:\n pages (List[Page]): See [`Page`][vizro.models.Page].\n theme (Literal[\"vizro_dark\", \"vizro_light\"]): Layout theme to be applied across dashboard.\n Defaults to `vizro_dark`.\n navigation (Optional[Navigation]): See [`Navigation`][vizro.models.Navigation]. Defaults to `None`.\n title (Optional[str]): Dashboard title to appear on every page on top left-side. Defaults to `None`.", + "type": "object", + "properties": { + "id": { + "title": "Id", + "description": "ID to identify model. Must be unique throughout the whole dashboard.When no ID is chosen, ID will be automatically generated.", + "type": "string" + }, + "pages": { + "title": "Pages", + "type": "array", + "items": { + "$ref": "#/definitions/Page" + } + }, + "theme": { + "title": "Theme", + "description": "Layout theme to be applied across dashboard. Defaults to `vizro_dark`", + "default": "vizro_dark", + "enum": ["vizro_dark", "vizro_light"], + "type": "string" + }, + "navigation": { + "$ref": "#/definitions/Navigation" + }, + "title": { + "title": "Title", + "description": "Dashboard title to appear on every page on top left-side.", + "type": "string" + } + }, + "required": ["pages"], + "additionalProperties": false, + "definitions": { + "Action": { + "title": "Action", + "description": "Action to be inserted into `actions` of relevant component.\n\nArgs:\n function (CapturedCallable): See [`CapturedCallable`][vizro.models.types.CapturedCallable].\n inputs (List[str]): Inputs in the form `.` passed to the action function.\n Defaults to `[]`.\n outputs (List[str]): Outputs in the form `.` changed by the action function.\n Defaults to `[]`.", + "type": "object", + "properties": { + "id": { + "title": "Id", + "description": "ID to identify model. Must be unique throughout the whole dashboard.When no ID is chosen, ID will be automatically generated.", + "type": "string" + }, + "inputs": { + "title": "Inputs", + "description": "Inputs in the form `.` passed to the action function.", + "default": [], + "pattern": "^[a-zA-Z0-9_]+[.][a-zA-Z_]+$", + "type": "array", + "items": { + "type": "string", + "pattern": "^[a-zA-Z0-9_]+[.][a-zA-Z_]+$" + } + }, + "outputs": { + "title": "Outputs", + "description": "Outputs in the form `.` changed by the action function.", + "default": [], + "pattern": "^[a-zA-Z0-9_]+[.][a-zA-Z_]+$", + "type": "array", + "items": { + "type": "string", + "pattern": "^[a-zA-Z0-9_]+[.][a-zA-Z_]+$" + } + } + }, + "additionalProperties": false + }, + "Button": { + "title": "Button", + "description": "Component provided to `Page` to trigger any defined `action` in `Page`.\n\nArgs:\n type (Literal[\"button\"]): Defaults to `\"button\"`.\n text (str): Text to be displayed on button. Defaults to `\"Click me!\"`.\n actions (List[Action]): See [`Action`][vizro.models.Action]. Defaults to `[]`.", + "type": "object", + "properties": { + "id": { + "title": "Id", + "description": "ID to identify model. Must be unique throughout the whole dashboard.When no ID is chosen, ID will be automatically generated.", + "type": "string" + }, + "type": { + "title": "Type", + "default": "button", + "enum": ["button"], + "type": "string" + }, + "text": { + "title": "Text", + "description": "Text to be displayed on button.", + "default": "Click me!", + "type": "string" + }, + "actions": { + "title": "Actions", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/Action" + } + } + }, + "additionalProperties": false + }, + "Card": { + "title": "Card", + "description": "Creates a card utilizing `dcc.Markdown` as title and text component.\n\nArgs:\n type (Literal[\"card\"]): Defaults to `\"card\"`.\n text (str): Markdown string to create card title/text that should adhere to the CommonMark Spec.\n href (Optional[str]): URL (relative or absolute) to navigate to. If not provided the Card serves as a text card\n only. Defaults to None.", + "type": "object", + "properties": { + "id": { + "title": "Id", + "description": "ID to identify model. Must be unique throughout the whole dashboard.When no ID is chosen, ID will be automatically generated.", + "type": "string" + }, + "type": { + "title": "Type", + "default": "card", + "enum": ["card"], + "type": "string" + }, + "text": { + "title": "Text", + "description": "Markdown string to create card title/text that should adhere to the CommonMark Spec.", + "type": "string" + }, + "href": { + "title": "Href", + "description": "URL (relative or absolute) to navigate to. If not provided the Card serves as a text card only.", + "type": "string" + } + }, + "required": ["text"], + "additionalProperties": false + }, + "Graph": { + "title": "Graph", + "description": "Wrapper for `dcc.Graph` to visualize charts in dashboard.\n\nArgs:\n type (Literal[\"graph\"]): Defaults to `\"graph\"`.\n figure (CapturedCallable): See [`CapturedCallable`][vizro.models.types.CapturedCallable].\n actions (List[Action]): See [`Action`][vizro.models.Action]. Defaults to `[]`.", + "type": "object", + "properties": { + "id": { + "title": "Id", + "description": "ID to identify model. Must be unique throughout the whole dashboard.When no ID is chosen, ID will be automatically generated.", + "type": "string" + }, + "type": { + "title": "Type", + "default": "graph", + "enum": ["graph"], + "type": "string" + }, + "actions": { + "title": "Actions", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/Action" + } + } + }, + "additionalProperties": false + }, + "Table": { + "title": "Table", + "description": "Wrapper for table components to visualize in dashboard.\n\nArgs:\n type (Literal[\"table\"]): Defaults to `\"table\"`.\n figure (CapturedCallable): Table like object to be displayed. Current choices include:\n [`dash_table.DataTable`](https://dash.plotly.com/datatable).\n title (str): Title of the table. Defaults to `None`.\n actions (List[Action]): See [`Action`][vizro.models.Action]. Defaults to `[]`.", + "type": "object", + "properties": { + "id": { + "title": "Id", + "description": "ID to identify model. Must be unique throughout the whole dashboard.When no ID is chosen, ID will be automatically generated.", + "type": "string" + }, + "type": { + "title": "Type", + "default": "table", + "enum": ["table"], + "type": "string" + }, + "title": { + "title": "Title", + "description": "Title of the table", + "type": "string" + }, + "actions": { + "title": "Actions", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/Action" + } + } + }, + "additionalProperties": false + }, + "Layout": { + "title": "Layout", + "description": "Grid specification to place chart/components on the [`Page`][vizro.models.Page].\n\nArgs:\n grid (List[List[int]]): Grid specification to arrange components on screen.\n row_gap (str): Gap between rows in px. Defaults to `\"12px\"`.\n col_gap (str): Gap between columns in px. Defaults to `\"12px\"`.\n row_min_height (str): Minimum row height in px. Defaults to `\"0px\"`.\n col_min_width (str): Minimum column width in px. Defaults to `\"0px\"`.", + "type": "object", + "properties": { + "id": { + "title": "Id", + "description": "ID to identify model. Must be unique throughout the whole dashboard.When no ID is chosen, ID will be automatically generated.", + "type": "string" + }, + "grid": { + "title": "Grid", + "description": "Grid specification to arrange components on screen.", + "type": "array", + "items": { + "type": "array", + "items": { + "type": "integer" + } + } + }, + "row_gap": { + "title": "Row Gap", + "description": "Gap between rows in px. Defaults to 12px.", + "default": "12px", + "pattern": "[0-9]+px", + "type": "string" + }, + "col_gap": { + "title": "Col Gap", + "description": "Gap between columns in px. Defaults to 12px.", + "default": "12px", + "pattern": "[0-9]+px", + "type": "string" + }, + "row_min_height": { + "title": "Row Min Height", + "description": "Minimum row height in px. Defaults to 0px.", + "default": "0px", + "pattern": "[0-9]+px", + "type": "string" + }, + "col_min_width": { + "title": "Col Min Width", + "description": "Minimum column width in px. Defaults to 0px.", + "default": "0px", + "pattern": "[0-9]+px", + "type": "string" + } + }, + "required": ["grid"], + "additionalProperties": false + }, + "OptionsDictType": { + "title": "OptionsDictType", + "type": "object", + "properties": { + "label": { + "title": "Label", + "type": "string" + }, + "value": { + "title": "Value", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "number" + }, + { + "type": "string" + } + ] + } + }, + "required": ["label", "value"], + "additionalProperties": false + }, + "Checklist": { + "title": "Checklist", + "description": "Categorical multi-selector `Checklist` to be provided to [`Filter`][vizro.models.Filter].\n\nArgs:\n type (Literal[\"checklist\"]): Defaults to `\"checklist\"`.\n options (OptionsType): See [`OptionsType`][vizro.models.types.OptionsType]. Defaults to `[]`.\n value (Optional[MultiValueType]): See [`MultiValueType`][vizro.models.types.MultiValueType]. Defaults to `None`.\n title (Optional[str]): Title to be displayed. Defaults to `None`.\n actions (List[Action]): See [`Action`][vizro.models.Action]. Defaults to `[]`.", + "type": "object", + "properties": { + "id": { + "title": "Id", + "description": "ID to identify model. Must be unique throughout the whole dashboard.When no ID is chosen, ID will be automatically generated.", + "type": "string" + }, + "type": { + "title": "Type", + "default": "checklist", + "enum": ["checklist"], + "type": "string" + }, + "options": { + "title": "Options", + "default": [], + "anyOf": [ + { + "type": "array", + "items": { + "type": "boolean" + } + }, + { + "type": "array", + "items": { + "type": "number" + } + }, + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "array", + "items": { + "$ref": "#/definitions/OptionsDictType" + } + } + ] + }, + "value": { + "title": "Value", + "anyOf": [ + { + "type": "array", + "items": { + "type": "boolean" + } + }, + { + "type": "array", + "items": { + "type": "number" + } + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "title": { + "title": "Title", + "description": "Title to be displayed", + "type": "string" + }, + "actions": { + "title": "Actions", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/Action" + } + } + }, + "additionalProperties": false + }, + "Dropdown": { + "title": "Dropdown", + "description": "Categorical multi-selector `Dropdown` to be provided to [`Filter`][vizro.models.Filter].\n\nArgs:\n type (Literal[\"dropdown\"]): Defaults to `\"dropdown\"`.\n options (OptionsType): See [`OptionsType`][vizro.models.types.OptionsType]. Defaults to `[]`.\n value (Optional[Union[SingleValueType, MultiValueType]]): See\n [`SingleValueType`][vizro.models.types.SingleValueType] and\n [`MultiValueType`][vizro.models.types.MultiValueType]. Defaults to `None`.\n multi (bool): Whether to allow selection of multiple values. Defaults to `True`.\n title (Optional[str]): Title to be displayed. Defaults to `None`.\n actions (List[Action]): See [`Action`][vizro.models.Action]. Defaults to `[]`.", + "type": "object", + "properties": { + "id": { + "title": "Id", + "description": "ID to identify model. Must be unique throughout the whole dashboard.When no ID is chosen, ID will be automatically generated.", + "type": "string" + }, + "type": { + "title": "Type", + "default": "dropdown", + "enum": ["dropdown"], + "type": "string" + }, + "options": { + "title": "Options", + "default": [], + "anyOf": [ + { + "type": "array", + "items": { + "type": "boolean" + } + }, + { + "type": "array", + "items": { + "type": "number" + } + }, + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "array", + "items": { + "$ref": "#/definitions/OptionsDictType" + } + } + ] + }, + "value": { + "title": "Value", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "number" + }, + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "boolean" + } + }, + { + "type": "array", + "items": { + "type": "number" + } + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "multi": { + "title": "Multi", + "description": "Whether to allow selection of multiple values", + "default": true, + "type": "boolean" + }, + "title": { + "title": "Title", + "description": "Title to be displayed", + "type": "string" + }, + "actions": { + "title": "Actions", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/Action" + } + } + }, + "additionalProperties": false + }, + "RadioItems": { + "title": "RadioItems", + "description": "Categorical single-selector `RadioItems` to be provided to `Filter`.\n\nArgs:\n type (Literal[\"radio_items\"]): Defaults to `\"radio_items\"`.\n options (OptionsType): See [`OptionsType`][vizro.models.types.OptionsType]. Defaults to `[]`.\n value (Optional[SingleValueType]): See [`SingleValueType`][vizro.models.types.SingleValueType].\n Defaults to `None`.\n title (Optional[str]): Title to be displayed. Defaults to `None`.\n actions (List[Action]): See [`Action`][vizro.models.Action]. Defaults to `[]`.", + "type": "object", + "properties": { + "id": { + "title": "Id", + "description": "ID to identify model. Must be unique throughout the whole dashboard.When no ID is chosen, ID will be automatically generated.", + "type": "string" + }, + "type": { + "title": "Type", + "default": "radio_items", + "enum": ["radio_items"], + "type": "string" + }, + "options": { + "title": "Options", + "default": [], + "anyOf": [ + { + "type": "array", + "items": { + "type": "boolean" + } + }, + { + "type": "array", + "items": { + "type": "number" + } + }, + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "array", + "items": { + "$ref": "#/definitions/OptionsDictType" + } + } + ] + }, + "value": { + "title": "Value", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "number" + }, + { + "type": "string" + } + ] + }, + "title": { + "title": "Title", + "description": "Title to be displayed", + "type": "string" + }, + "actions": { + "title": "Actions", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/Action" + } + } + }, + "additionalProperties": false + }, + "RangeSlider": { + "title": "RangeSlider", + "description": "Numeric multi-selector `RangeSlider`.\n\nCan be provided to [`Filter`][vizro.models.Filter] or\n[`Parameter`][vizro.models.Parameter]. Based on the underlying\n[`dcc.RangeSlider`](https://dash.plotly.com/dash-core-components/rangeslider).\n\nArgs:\n type (Literal[\"range_slider\"]): Defaults to `\"range_slider\"`.\n min (Optional[float]): Start value for slider. Defaults to `None`.\n max (Optional[float]): End value for slider. Defaults to `None`.\n step (Optional[float]): Step-size for marks on slider. Defaults to `None`.\n marks (Optional[Dict[float, str]]): Marks to be displayed on slider. Defaults to `{}`.\n value (Optional[List[float]]): Default start and end value for slider. Must be 2 items. Defaults to `None`.\n title (Optional[str]): Title to be displayed. Defaults to `None`.\n actions (List[Action]): See [`Action`][vizro.models.Action]. Defaults to `[]`.", + "type": "object", + "properties": { + "id": { + "title": "Id", + "description": "ID to identify model. Must be unique throughout the whole dashboard.When no ID is chosen, ID will be automatically generated.", + "type": "string" + }, + "type": { + "title": "Type", + "default": "range_slider", + "enum": ["range_slider"], + "type": "string" + }, + "min": { + "title": "Min", + "description": "Start value for slider.", + "type": "number" + }, + "max": { + "title": "Max", + "description": "End value for slider.", + "type": "number" + }, + "step": { + "title": "Step", + "description": "Step-size for marks on slider.", + "type": "number" + }, + "marks": { + "title": "Marks", + "description": "Marks to be displayed on slider.", + "default": {}, + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "value": { + "title": "Value", + "description": "Default start and end value for slider", + "minItems": 2, + "maxItems": 2, + "type": "array", + "items": { + "type": "number" + } + }, + "title": { + "title": "Title", + "description": "Title to be displayed.", + "type": "string" + }, + "actions": { + "title": "Actions", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/Action" + } + } + }, + "additionalProperties": false + }, + "Slider": { + "title": "Slider", + "description": "Numeric single-selector `Slider`.\n\nCan be provided to [`Filter`][vizro.models.Filter] or\n[`Parameter`][vizro.models.Parameter]. Based on the underlying\n[`dcc.Slider`](https://dash.plotly.com/dash-core-components/slider).\n\nArgs:\n type (Literal[\"range_slider\"]): Defaults to `\"range_slider\"`.\n min (Optional[float]): Start value for slider. Defaults to `None`.\n max (Optional[float]): End value for slider. Defaults to `None`.\n step (Optional[float]): Step-size for marks on slider. Defaults to `None`.\n marks (Optional[Dict[float, str]]): Marks to be displayed on slider. Defaults to `{}`.\n value (Optional[float]): Default value for slider. Defaults to `None`.\n title (Optional[str]): Title to be displayed. Defaults to `None`.\n actions (List[Action]): See [`Action`][vizro.models.Action]. Defaults to `[]`.", + "type": "object", + "properties": { + "id": { + "title": "Id", + "description": "ID to identify model. Must be unique throughout the whole dashboard.When no ID is chosen, ID will be automatically generated.", + "type": "string" + }, + "type": { + "title": "Type", + "default": "slider", + "enum": ["slider"], + "type": "string" + }, + "min": { + "title": "Min", + "description": "Start value for slider.", + "type": "number" + }, + "max": { + "title": "Max", + "description": "End value for slider.", + "type": "number" + }, + "step": { + "title": "Step", + "description": "Step-size for marks on slider.", + "type": "number" + }, + "marks": { + "title": "Marks", + "description": "Marks to be displayed on slider.", + "default": {}, + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "value": { + "title": "Value", + "description": "Default value for slider.", + "type": "number" + }, + "title": { + "title": "Title", + "description": "Title to be displayed.", + "type": "string" + }, + "actions": { + "title": "Actions", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/Action" + } + } + }, + "additionalProperties": false + }, + "Filter": { + "title": "Filter", + "description": "Filter the data supplied to `targets` on the [`Page`][vizro.models.Page].\n\nExamples:\n >>> print(repr(Filter(column=\"species\")))\n\nArgs:\n type (Literal[\"filter\"]): Defaults to `\"filter\"`.\n column (str): Column of `DataFrame` to filter.\n targets (List[ModelID]): Target component to be affected by filter. If none are given then target all components\n on the page that use `column`.\n selector (Optional[SelectorType]): See [SelectorType][vizro.models.types.SelectorType]. Defaults to `None`.", + "type": "object", + "properties": { + "id": { + "title": "Id", + "description": "ID to identify model. Must be unique throughout the whole dashboard.When no ID is chosen, ID will be automatically generated.", + "type": "string" + }, + "type": { + "title": "Type", + "default": "filter", + "enum": ["filter"], + "type": "string" + }, + "column": { + "title": "Column", + "description": "Column of DataFrame to filter.", + "type": "string" + }, + "targets": { + "title": "Targets", + "description": "Target component to be affected by filter. If none are given then target all components on the page that use `column`.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "selector": { + "title": "Selector", + "anyOf": [ + { + "$ref": "#/definitions/Checklist" + }, + { + "$ref": "#/definitions/Dropdown" + }, + { + "$ref": "#/definitions/RadioItems" + }, + { + "$ref": "#/definitions/RangeSlider" + }, + { + "$ref": "#/definitions/Slider" + } + ] + } + }, + "required": ["column"], + "additionalProperties": false + }, + "Parameter": { + "title": "Parameter", + "description": "Alter the arguments supplied to any `targets` on the [`Page`][vizro.models.Page].\n\nExamples:\n >>> print(repr(Parameter(\n >>> targets=[\"scatter.x\"], selector=Slider(min=0, max=1, default=0.8, title=\"Bubble opacity\"))))\n\nArgs:\n type (Literal[\"parameter\"]): Defaults to `\"parameter\"`.\n targets (List[str]): Targets in the form of `.`.\n selector (SelectorType): See [SelectorType][vizro.models.types.SelectorType]. Converts selector value\n `\"NONE\"` into `None` to allow optional parameters.\n\nRaises:\n ValueError: If targets are invalid and not of the form `.`.", + "type": "object", + "properties": { + "id": { + "title": "Id", + "description": "ID to identify model. Must be unique throughout the whole dashboard.When no ID is chosen, ID will be automatically generated.", + "type": "string" + }, + "type": { + "title": "Type", + "default": "parameter", + "enum": ["parameter"], + "type": "string" + }, + "targets": { + "title": "Targets", + "description": "Targets in the form of `.`.", + "type": "array", + "items": { + "type": "string" + } + }, + "selector": { + "title": "Selector", + "description": "Selectors to be used inside a control.", + "discriminator": { + "propertyName": "type", + "mapping": { + "checklist": "#/definitions/Checklist", + "dropdown": "#/definitions/Dropdown", + "radio_items": "#/definitions/RadioItems", + "range_slider": "#/definitions/RangeSlider", + "slider": "#/definitions/Slider" + } + }, + "oneOf": [ + { + "$ref": "#/definitions/Checklist" + }, + { + "$ref": "#/definitions/Dropdown" + }, + { + "$ref": "#/definitions/RadioItems" + }, + { + "$ref": "#/definitions/RangeSlider" + }, + { + "$ref": "#/definitions/Slider" + } + ] + } + }, + "required": ["targets", "selector"], + "additionalProperties": false + }, + "ActionsChain": { + "title": "ActionsChain", + "description": "All models that are registered to the model manager should inherit from this class.\n\nArgs:\n id (Optional[str]): ID to identify model. Must be unique throughout the whole dashboard. Defaults to `None`.\n When no ID is chosen, ID will be automatically generated.", + "type": "object", + "properties": { + "id": { + "title": "Id", + "description": "ID to identify model. Must be unique throughout the whole dashboard.When no ID is chosen, ID will be automatically generated.", + "type": "string" + }, + "trigger": { + "title": "Trigger", + "type": "array", + "items": [ + { + "title": "Component Id", + "type": "string" + }, + { + "title": "Component Property", + "type": "string" + } + ], + "minItems": 2, + "maxItems": 2 + }, + "actions": { + "title": "Actions", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/Action" + } + } + }, + "required": ["trigger"], + "additionalProperties": false + }, + "Page": { + "title": "Page", + "description": "A page in [`Dashboard`][vizro.models.Dashboard] with its own URL path and place in the `Navigation`.\n\nArgs:\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 controls (List[ControlType]): See [ControlType][vizro.models.types.ControlType]. Defaults to `[]`.\n path (Optional[str]): Path to navigate to page. Defaults to `None`.\n\nRaises:\n ValueError: If number of page and grid components is not the same", + "type": "object", + "properties": { + "id": { + "title": "Id", + "description": "ID to identify model. Must be unique throughout the whole dashboard.When no ID is chosen, ID will be automatically generated.", + "type": "string" + }, + "components": { + "title": "Components", + "type": "array", + "items": { + "discriminator": { + "propertyName": "type", + "mapping": { + "button": "#/definitions/Button", + "card": "#/definitions/Card", + "graph": "#/definitions/Graph", + "table": "#/definitions/Table" + } + }, + "oneOf": [ + { + "$ref": "#/definitions/Button" + }, + { + "$ref": "#/definitions/Card" + }, + { + "$ref": "#/definitions/Graph" + }, + { + "$ref": "#/definitions/Table" + } + ] + } + }, + "title": { + "title": "Title", + "description": "Title to be displayed.", + "type": "string" + }, + "layout": { + "$ref": "#/definitions/Layout" + }, + "controls": { + "title": "Controls", + "default": [], + "type": "array", + "items": { + "discriminator": { + "propertyName": "type", + "mapping": { + "filter": "#/definitions/Filter", + "parameter": "#/definitions/Parameter" + } + }, + "oneOf": [ + { + "$ref": "#/definitions/Filter" + }, + { + "$ref": "#/definitions/Parameter" + } + ] + } + }, + "path": { + "title": "Path", + "description": "Path to navigate to page.", + "type": "string" + }, + "actions": { + "title": "Actions", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/ActionsChain" + } + } + }, + "required": ["components", "title"], + "additionalProperties": false + }, + "Navigation": { + "title": "Navigation", + "description": "Navigation in [`Dashboard`][vizro.models.Dashboard] to structure [`Pages`][vizro.models.Page].\n\nArgs:\n pages (Optional[NavigationPagesType]): See [`NavigationPagesType`][vizro.models.types.NavigationPagesType].\n Defaults to `None`.", + "type": "object", + "properties": { + "id": { + "title": "Id", + "description": "ID to identify model. Must be unique throughout the whole dashboard.When no ID is chosen, ID will be automatically generated.", + "type": "string" + }, + "pages": { + "title": "Pages", + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + } + ] + } + }, + "additionalProperties": false + } + } +} diff --git a/vizro-core/src/vizro/__init__.py b/vizro-core/src/vizro/__init__.py index 081b799c9..a6efaeb42 100644 --- a/vizro-core/src/vizro/__init__.py +++ b/vizro-core/src/vizro/__init__.py @@ -5,6 +5,6 @@ __all__ = ["Vizro"] -__version__ = "0.1.6.dev0" +__version__ = "0.1.6.dev2" logging.basicConfig(level=os.getenv("VIZRO_LOG_LEVEL", "WARNING")) diff --git a/vizro-core/src/vizro/_vizro.py b/vizro-core/src/vizro/_vizro.py index 8ed00b326..f0f075f3c 100644 --- a/vizro-core/src/vizro/_vizro.py +++ b/vizro-core/src/vizro/_vizro.py @@ -1,7 +1,6 @@ import logging -import os from pathlib import Path -from typing import Dict, List, Tuple +from typing import List import dash import flask @@ -16,24 +15,39 @@ class Vizro: """The main class of the `vizro` package.""" - _user_assets_folder = Path.cwd() / "assets" - _lib_assets_folder = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static") - - def __init__(self): - """Initializes Dash.""" - _js, _css = _append_styles(self._lib_assets_folder, STATIC_URL_PREFIX) - self.dash = dash.Dash( - use_pages=True, - pages_folder="", - external_scripts=_js, - external_stylesheets=_css, - assets_folder=self._user_assets_folder, - ) + def __init__(self, **kwargs): + """Initializes Dash app, stored in `self.dash`. - @self.dash.server.route("//") - def serve_static(filepath, url_prefix=STATIC_URL_PREFIX): - """Serve vizro static contents.""" - return flask.send_from_directory(self._lib_assets_folder, filepath) + Args: + kwargs: Passed through to `Dash.__init__`, e.g. `assets_folder`, `url_base_pathname`. See + [Dash documentation](https://dash.plotly.com/reference#dash.dash) for possible arguments. + """ + self.dash = dash.Dash(**kwargs, use_pages=True, pages_folder="") + + # Include Vizro assets (in the static folder) as external scripts and stylesheets. We extend self.dash.config + # objects so the user can specify additional external_scripts and external_stylesheets via kwargs. + vizro_assets_folder = Path(__file__).with_name("static") + static_url_path = self.dash.config.requests_pathname_prefix + STATIC_URL_PREFIX + vizro_css = self._get_external_assets(static_url_path, vizro_assets_folder, "css") + vizro_js = [ + {"src": path, "type": "module"} + for path in self._get_external_assets(static_url_path, vizro_assets_folder, "js") + ] + self.dash.config.external_stylesheets.extend(vizro_css) + self.dash.config.external_scripts.extend(vizro_js) + + # Serve all assets (including files other than css and js) that live in vizro_assets_folder at the + # route /vizro. Based on code in Dash.init_app that serves assets_folder. This respects the case that the + # dashboard is not hosted at the root of the server, e.g. http://www.example.com/dashboard/vizro. + blueprint_prefix = self.dash.config.routes_pathname_prefix.replace("/", "_").replace(".", "_") + self.dash.server.register_blueprint( + flask.Blueprint( + f"{blueprint_prefix}vizro_assets", + self.dash.config.name, + static_folder=vizro_assets_folder, + static_url_path=static_url_path, + ) + ) def build(self, dashboard: Dashboard): """Builds the dashboard. @@ -55,8 +69,8 @@ def run(self, *args, **kwargs): # if type annotated, mkdocstring stops seeing t """Runs the dashboard. Args: - args: Any args to `dash.run_server` - kwargs: Any kwargs to `dash.run_server` + args: Passed through to `dash.run`. + kwargs: Passed through to `dash.run`. """ data_manager._frozen_state = True model_manager._frozen_state = True @@ -88,19 +102,11 @@ def _reset(): dash._pages.CONFIG.clear() dash._pages.CONFIG.__dict__.clear() + @staticmethod + def _get_external_assets(new_path: str, folder: Path, extension: str) -> List[str]: + """Returns a list of paths to assets with given extension in folder, prefixed with new_path. -def _append_styles(walk_dir: str, url_prefix: str) -> Tuple[List[Dict[str, str]], List[str]]: - """Append vizro css and js resources.""" - _vizro_css = [] - _vizro_js = [] - - for current_dir, _, files in sorted(os.walk(walk_dir)): - base = "" if current_dir == walk_dir else os.path.relpath(current_dir, walk_dir).replace("\\", "/") - for f in sorted(files): - path = os.path.join("/" + url_prefix, base, f) if base else os.path.join("/" + url_prefix, f) - extension = os.path.splitext(f)[1] - if extension == ".js": - _vizro_js.append({"src": path, "type": "module"}) - elif extension == ".css": - _vizro_css.append(path) - return _vizro_js, _vizro_css + e.g. with new_path="/vizro", extension="css", folder="/path/to/vizro/vizro-core/src/vizro/static", + we will get ["/vizro/css/accordion.css", "/vizro/css/button.css", ...]. + """ + return sorted((new_path / path.relative_to(folder)).as_posix() for path in folder.rglob(f"*.{extension}")) diff --git a/vizro-core/src/vizro/models/_components/card.py b/vizro-core/src/vizro/models/_components/card.py index 01e219854..05600253d 100644 --- a/vizro-core/src/vizro/models/_components/card.py +++ b/vizro-core/src/vizro/models/_components/card.py @@ -43,8 +43,4 @@ def build(self): ) card_container = "nav_card_container" if self.href else "card_container" - return html.Div( - [text, button], - className=card_container, - id=f"{self.id}_outer", - ) + return html.Div([text, button], className=card_container, id=f"{self.id}_outer") diff --git a/vizro-core/src/vizro/models/_dashboard.py b/vizro-core/src/vizro/models/_dashboard.py index cf6fbab63..11d482706 100644 --- a/vizro-core/src/vizro/models/_dashboard.py +++ b/vizro-core/src/vizro/models/_dashboard.py @@ -8,7 +8,7 @@ import dash_bootstrap_components as dbc import dash_daq as daq import plotly.io as pio -from dash import ClientsideFunction, Input, Output, clientside_callback, html +from dash import ClientsideFunction, Input, Output, clientside_callback, get_relative_path, html from pydantic import Field, validator import vizro @@ -139,7 +139,7 @@ def _update_theme(): def _make_page_404_layout(): return html.Div( [ - html.Img(src=STATIC_URL_PREFIX + "/images/errors/error_404.svg"), + html.Img(src=get_relative_path(f"/{STATIC_URL_PREFIX}/images/errors/error_404.svg")), html.Div( [ html.Div( @@ -149,7 +149,7 @@ def _make_page_404_layout(): ], className="error_text_container", ), - dbc.Button("Take me home", href="/", className="button_primary"), + dbc.Button("Take me home", href=get_relative_path("/"), className="button_primary"), ], className="error_content_container", ), diff --git a/vizro-core/tests/integration/test_examples.py b/vizro-core/tests/integration/test_examples.py index 727a9ed55..85885b373 100644 --- a/vizro-core/tests/integration/test_examples.py +++ b/vizro-core/tests/integration/test_examples.py @@ -2,7 +2,7 @@ import os from pathlib import Path -import chromedriver_autoinstaller +import chromedriver_autoinstaller_fix import pytest from vizro import Vizro @@ -22,7 +22,7 @@ def setup_integration_test_environment(monkeypatch_session): monkeypatch_session.setenv("DASH_DEBUG", "false") # We only need to install chromedriver outside CI. if not os.getenv("CI"): - chromedriver_autoinstaller.install() + chromedriver_autoinstaller_fix.install() @pytest.fixture diff --git a/vizro-core/tests/unit/vizro/models/test_dashboard.py b/vizro-core/tests/unit/vizro/models/test_dashboard.py index 49ea53cdf..43353b85d 100644 --- a/vizro-core/tests/unit/vizro/models/test_dashboard.py +++ b/vizro-core/tests/unit/vizro/models/test_dashboard.py @@ -142,7 +142,8 @@ def test_dashboard_page_registry(self, dashboard, mock_page_registry): # Str conversion required as comparison of OrderedDict values result in False otherwise assert str(result.items()) == str(expected.items()) - def test_create_layout_page_404(self, dashboard): + def test_create_layout_page_404(self, dashboard, mocker): + mocker.patch("vizro.models._dashboard.get_relative_path") result = dashboard._make_page_404_layout() result_image = result.children[0] result_div = result.children[1]