diff --git a/.github/actions/failed-artifacts-and-slack-notifications/action.yml b/.github/actions/failed-artifacts-and-slack-notifications/action.yml new file mode 100644 index 000000000..a0b1ce6de --- /dev/null +++ b/.github/actions/failed-artifacts-and-slack-notifications/action.yml @@ -0,0 +1,31 @@ +name: "Create artifacts and slack notifications" +description: "Creates failed artifacts with screenshots and sends slack notifications if build failed" + +runs: + using: "composite" + steps: + - name: Copy failed screenshots + shell: bash + run: | + mkdir /home/runner/work/vizro/vizro/vizro-core/failed_screenshots/ + cd /home/runner/work/vizro/vizro/vizro-core/ + cp *.png failed_screenshots + + - name: Archive production artifacts + uses: actions/upload-artifact@v4 + with: + name: Failed screenshots + path: | + /home/runner/work/vizro/vizro/vizro-core/failed_screenshots/*.png + + - name: Send custom JSON data to Slack + id: slack + uses: slackapi/slack-github-action@v1.26.0 + with: + payload: | + { + "text": "${{ env.TESTS_NAME }} build result: ${{ job.status }}\nBranch: ${{ github.head_ref }}\n${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + } + env: + SLACK_WEBHOOK_URL: ${{ env.SLACK_WEBHOOK_URL }} + SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK diff --git a/.github/images/infographic.svg b/.github/images/infographic.svg new file mode 100644 index 000000000..95b4b1c88 --- /dev/null +++ b/.github/images/infographic.svg @@ -0,0 +1 @@ +production scalingbuild prototypes and deploy to production at scale in minutesin-built best practicesapply beautiful visual design and engineering forpowerfulfeaturesopen-source toolsenjoy the freedom of open source with the flexibility of Plotly, Dash and Pydanticlow-codeuse a few lines of simple configuration to build apps quickly and easilyefficient communicationshare insights without needing advanced engineering or visual design expertisecode extensionsenable customization for advanced users using Python, JavaScript, HTML and CSS diff --git a/.github/images/logo_watermarks.svg b/.github/images/logo_watermarks.svg new file mode 100644 index 000000000..6b4baba31 --- /dev/null +++ b/.github/images/logo_watermarks.svg @@ -0,0 +1 @@ +enabled by:built and maintained by:plotly& dashpythonpydantic diff --git a/.github/images/toolkit_dashboard_examples.png b/.github/images/toolkit_dashboard_examples.png new file mode 100644 index 000000000..6e0c37885 Binary files /dev/null and b/.github/images/toolkit_dashboard_examples.png differ diff --git a/.github/images/toolkit_framework.svg b/.github/images/toolkit_framework.svg new file mode 100644 index 000000000..1751d5c74 --- /dev/null +++ b/.github/images/toolkit_framework.svg @@ -0,0 +1 @@ + diff --git a/.github/images/toolkit_visual_vocabulary.png b/.github/images/toolkit_visual_vocabulary.png new file mode 100644 index 000000000..cb6045ca1 Binary files /dev/null and b/.github/images/toolkit_visual_vocabulary.png differ diff --git a/.github/images/toolkit_vizro_ai.gif b/.github/images/toolkit_vizro_ai.gif new file mode 100644 index 000000000..b7f8c7c56 Binary files /dev/null and b/.github/images/toolkit_vizro_ai.gif differ diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 44e958f1c..ea40a09f2 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -6,8 +6,8 @@ - [ ] I acknowledge and agree that, by checking this box and clicking "Submit Pull Request": - - I submit this contribution under the [Apache 2.0 license](https://www.apache.org/licenses/LICENSE-2.0.txt) and represent that I am entitled to do so on behalf of myself, my employer, or relevant third parties, as applicable. - - I certify that (a) this contribution is my original creation and / or (b) to the extent it is not my original creation, I am authorized to submit this contribution on behalf of the original creator(s) or their licensees. - - I certify that the use of this contribution as authorized by the Apache 2.0 license does not violate the intellectual property rights of anyone else. - - I have not referenced individuals, products or companies in any commits, directly or indirectly. - - I have not added data or restricted code in any commits, directly or indirectly. + - I submit this contribution under the [Apache 2.0 license](https://www.apache.org/licenses/LICENSE-2.0.txt) and represent that I am entitled to do so on behalf of myself, my employer, or relevant third parties, as applicable. + - I certify that (a) this contribution is my original creation and / or (b) to the extent it is not my original creation, I am authorized to submit this contribution on behalf of the original creator(s) or their licensees. + - I certify that the use of this contribution as authorized by the Apache 2.0 license does not violate the intellectual property rights of anyone else. + - I have not referenced individuals, products or companies in any commits, directly or indirectly. + - I have not added data or restricted code in any commits, directly or indirectly. diff --git a/.github/workflows/test-e2e-component-library-vizro-core.yml b/.github/workflows/test-e2e-component-library-vizro-core.yml new file mode 100644 index 000000000..85c08a0e6 --- /dev/null +++ b/.github/workflows/test-e2e-component-library-vizro-core.yml @@ -0,0 +1,45 @@ +name: e2e tests of component library for Vizro + +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-component-library-vizro-core: + runs-on: ubuntu-latest + + 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: Show dependency tree + run: hatch run pip tree + + - name: Run e2e component library tests + run: hatch run test-e2e-component-library + + - name: Create artifacts and slack notifications + if: failure() + uses: ./.github/actions/failed-artifacts-and-slack-notifications + env: + TESTS_NAME: Vizro e2e component library tests + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} diff --git a/.github/workflows/test-integration-vizro-ai.yml b/.github/workflows/test-integration-vizro-ai.yml index f2a6ed5ab..95eb14531 100644 --- a/.github/workflows/test-integration-vizro-ai.yml +++ b/.github/workflows/test-integration-vizro-ai.yml @@ -144,7 +144,7 @@ jobs: - name: Send custom JSON data to Slack id: slack - uses: slackapi/slack-github-action@v1.27.0 + uses: slackapi/slack-github-action@v2.0.0 if: failure() with: payload: | diff --git a/.github/workflows/test-score-vizro-ai.yml b/.github/workflows/test-score-vizro-ai.yml index b97918453..813ecba8c 100644 --- a/.github/workflows/test-score-vizro-ai.yml +++ b/.github/workflows/test-score-vizro-ai.yml @@ -94,7 +94,7 @@ jobs: - name: Send custom JSON data to Slack id: slack - uses: slackapi/slack-github-action@v1.27.0 + uses: slackapi/slack-github-action@v2.0.0 if: failure() with: payload: | diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 386ccc6a3..d0596372c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,8 +34,9 @@ repos: description: Linter for json, yaml, md, css and more entry: prettier --write --ignore-unknown language: node - "types": [text] + types: [text] additional_dependencies: ["prettier@3.3.3"] + exclude_types: [markdown] - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks rev: v2.14.0 @@ -44,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] @@ -52,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] @@ -83,16 +84,19 @@ repos: - stylelint-order@4.1.0 args: ["--fix"] - - repo: https://github.com/errata-ai/vale - rev: v3.8.0 + - repo: https://github.com/executablebooks/mdformat + rev: 0.7.18 hooks: - - id: vale - args: [--config=.vale/.vale.ini] - # There's no way to automatically convert vale suggestions/warnings to errors, and so they won't appear at all unless - # there's an error raised. - # pre-commit's verbose mode means that suggestions and warnings are always shown even if there's no error raised. - # See https://github.com/errata-ai/vale/issues/575. - verbose: true + - id: mdformat + args: + [ + --ignore-missing-references, + --wrap=no, + --align-semantic-breaks-in-lists, + ] + exclude: ^vizro-core/docs/pages/API-reference|^vizro-ai/docs/pages/API-reference|vizro-core/docs/pages/user-guides/custom-components.md|^vizro-core/changelog.d|^vizro-ai/changelog.d + additional_dependencies: + - mdformat-mkdocs[recommended]==3.1.1 # Configuration for https://pre-commit.ci/. ci: @@ -108,4 +112,3 @@ ci: - codespell - bandit - mypy - - vale diff --git a/.stylelintrc b/.stylelintrc index 7075ac01f..2949d83ae 100644 --- a/.stylelintrc +++ b/.stylelintrc @@ -8,6 +8,7 @@ "color-function-notation": "legacy", "alpha-value-notation": "number", "color-hex-length": "long", - "selector-not-notation": null + "selector-not-notation": null, + "declaration-property-value-no-unknown": true } } diff --git a/.vale/styles/Microsoft/Acronyms.yml b/.vale/styles/Microsoft/Acronyms.yml index 07daefeeb..3ba7d818f 100644 --- a/.vale/styles/Microsoft/Acronyms.yml +++ b/.vale/styles/Microsoft/Acronyms.yml @@ -39,6 +39,7 @@ exceptions: - LESS - LLDB - LLM + - MIT - NET - NOTE - NVDA diff --git a/.vale/styles/Microsoft/Headings.yml b/.vale/styles/Microsoft/Headings.yml index b212336e7..f5c7d461e 100644 --- a/.vale/styles/Microsoft/Headings.yml +++ b/.vale/styles/Microsoft/Headings.yml @@ -71,6 +71,8 @@ exceptions: - Kubernetes Service - Lambda - LangChain + - LangFuse + - LangSmith - Linux - MySQL - Notebook diff --git a/.vale/styles/Microsoft/ignore.txt b/.vale/styles/Microsoft/ignore.txt index 7bd23f73b..5926042ac 100644 --- a/.vale/styles/Microsoft/ignore.txt +++ b/.vale/styles/Microsoft/ignore.txt @@ -19,6 +19,7 @@ formatters fsspec globals Globals +interpretability Kaggle namespace namespaces diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 421f29147..6173d9378 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -2,17 +2,11 @@ ## Our Pledge -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, sex characteristics, gender identity and expression, -level of experience, education, socioeconomic status, nationality, personal -appearance, race, religion, or sexual identity and orientation. +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socioeconomic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards -Examples of behavior that contributes to creating a positive environment -include: +Examples of behavior that contributes to creating a positive environment include: - Using welcoming and inclusive language - Being respectful of differing viewpoints and experiences @@ -22,57 +16,34 @@ include: Examples of unacceptable behavior by participants include: -- The use of sexualised language or imagery and unwelcome sexual attention or - advances +- The use of sexualised language or imagery and unwelcome sexual attention or advances - Trolling, insulting/derogatory comments, and personal or political attacks - Public or private harassment -- Publishing others' private information, such as a physical or electronic - address, without explicit permission -- Other conduct which could reasonably be considered inappropriate in a - professional setting +- Publishing others' private information, such as a physical or electronic address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team. All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. **Investigation Timeline:** The project team will make all reasonable efforts to initiate and conclude the investigation in a timely fashion. Depending on the type of investigation the steps and timeline for each investigation will vary. ## Attribution -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html -[homepage]: https://www.contributor-covenant.org +For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq -For answers to common questions about this code of conduct, see -https://www.contributor-covenant.org/faq +[homepage]: https://www.contributor-covenant.org diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 66bcc019a..31a65c178 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,3 @@ # Contributing -Contributions of all experience levels are welcome! If you're interested in making a contribution, -please refer to our [contributing guide](https://vizro.readthedocs.io/en/stable/pages/explanation/contributing/) for more information. +Contributions of all experience levels are welcome! If you're interested in making a contribution, please refer to our [contributing guide](https://vizro.readthedocs.io/en/stable/pages/explanation/contributing/) for more information. diff --git a/README.md b/README.md index be083bde8..d30fa8e29 100644 --- a/README.md +++ b/README.md @@ -11,19 +11,13 @@
-[![Python version](https://img.shields.io/badge/python-3.9%20%7C%203.10%20%7C%203.11%20%7C%203.12%20%7C%203.13-blue.svg)](https://pypi.org/project/vizro/) -[![PyPI version](https://badge.fury.io/py/vizro.svg)](https://badge.fury.io/py/vizro) -[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/mckinsey/vizro/blob/main/LICENSE.md) -[![Documentation](https://readthedocs.org/projects/vizro/badge/?version=stable)](https://vizro.readthedocs.io/) -[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/7858/badge)](https://www.bestpractices.dev/projects/7858) +[![Python version](https://img.shields.io/badge/python-3.9%20%7C%203.10%20%7C%203.11%20%7C%203.12%20%7C%203.13-blue.svg)](https://pypi.org/project/vizro/) [![PyPI version](https://badge.fury.io/py/vizro.svg)](https://badge.fury.io/py/vizro) [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/mckinsey/vizro/blob/main/LICENSE.md) [![Documentation](https://readthedocs.org/projects/vizro/badge/?version=stable)](https://vizro.readthedocs.io/) [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/7858/badge)](https://www.bestpractices.dev/projects/7858)
-Documentation | -Get Started | -Vizro examples gallery +Documentation | Get Started | Vizro examples gallery
@@ -108,9 +102,7 @@ You can see Vizro in action by clicking on the following image or by visiting [t ## Visual vocabulary -Our visual vocabulary dashboard helps you to select and create various types of charts. It helps you decide when to use -each chart type, and offers sample Python code to create these charts with [Plotly](https://plotly.com/python/) and -embed them into a Vizro dashboard. +Our visual vocabulary dashboard helps you to select and create various types of charts. It helps you decide when to use each chart type, and offers sample Python code to create these charts with [Plotly](https://plotly.com/python/) and embed them into a Vizro dashboard. diff --git a/pyproject.toml b/pyproject.toml index 2a1469171..3c3b4570e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ exclude_dirs = ["tests"] [tool.codespell] builtin = "clear,rare,en-GB_to_en-US" ignore-words-list = "grey,ned,sav,Thur" -skip = "*.min.css.map,*.min.css,.vale/*, *assets/*" +skip = "*.min.css.map,*.min.css,.vale/*, *assets/*,.github/*" [tool.mypy] # strict checks : strict = true diff --git a/vizro-ai/CHANGELOG.md b/vizro-ai/CHANGELOG.md index 72186c8b1..98a162904 100644 --- a/vizro-ai/CHANGELOG.md +++ b/vizro-ai/CHANGELOG.md @@ -141,5 +141,4 @@ See the fragment files in the [changelog.d directory](https://github.com/mckinse ## Highlights ✨ -- Initial release of Vizro-AI package. Vizro-AI is a tool for generating data - visualizations. ([#138](https://github.com/mckinsey/vizro/pull/138)) +- Initial release of Vizro-AI package. Vizro-AI is a tool for generating data visualizations. ([#138](https://github.com/mckinsey/vizro/pull/138)) diff --git a/vizro-ai/README.md b/vizro-ai/README.md index 888b79af8..faed6c79a 100644 --- a/vizro-ai/README.md +++ b/vizro-ai/README.md @@ -2,11 +2,7 @@
-[![Python version](https://img.shields.io/badge/python-3.9%20%7C%203.10%20%7C%203.11%20%7C%203.12-blue.svg)](https://pypi.org/project/vizro/) -[![PyPI version](https://badge.fury.io/py/vizro_ai.svg)](https://badge.fury.io/py/vizro_ai) -[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/mckinsey/vizro/blob/main/LICENSE.md) -[![Documentation](https://readthedocs.org/projects/vizro-ai/badge/?version=latest)](https://vizro-ai.readthedocs.io/) -[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/7858/badge)](https://www.bestpractices.dev/projects/7858) +[![Python version](https://img.shields.io/badge/python-3.9%20%7C%203.10%20%7C%203.11%20%7C%203.12-blue.svg)](https://pypi.org/project/vizro/) [![PyPI version](https://badge.fury.io/py/vizro_ai.svg)](https://badge.fury.io/py/vizro_ai) [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/mckinsey/vizro/blob/main/LICENSE.md) [![Documentation](https://readthedocs.org/projects/vizro-ai/badge/?version=latest)](https://vizro-ai.readthedocs.io/) [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/7858/badge)](https://www.bestpractices.dev/projects/7858) Gif to demonstrate vizro-ai diff --git a/vizro-ai/changelog.d/20240917_174515_alexey_snigir_automatic_vizro_ai_score_tests.md b/vizro-ai/changelog.d/20240917_174515_alexey_snigir_automatic_vizro_ai_score_tests.md index f1f65e73c..aa45b72f1 100644 --- a/vizro-ai/changelog.d/20240917_174515_alexey_snigir_automatic_vizro_ai_score_tests.md +++ b/vizro-ai/changelog.d/20240917_174515_alexey_snigir_automatic_vizro_ai_score_tests.md @@ -10,36 +10,42 @@ Uncomment the section that is right (remove the HTML comment wrapper). - A bullet item for the Highlights ✨ category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX ([#1](https://github.com/mckinsey/vizro/pull/1)) --> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vizro-core/changelog.d/20241030_104644_antony.milne_0_1_26.md b/vizro-ai/changelog.d/20241205_104116_nadija_ratkusic_graca_update_safeguard_whitelist.md similarity index 100% rename from vizro-core/changelog.d/20241030_104644_antony.milne_0_1_26.md rename to vizro-ai/changelog.d/20241205_104116_nadija_ratkusic_graca_update_safeguard_whitelist.md diff --git a/vizro-ai/changelog.d/new_fragment.md.j2 b/vizro-ai/changelog.d/new_fragment.md.j2 index 6ca28d646..a09907bc6 100644 --- a/vizro-ai/changelog.d/new_fragment.md.j2 +++ b/vizro-ai/changelog.d/new_fragment.md.j2 @@ -8,7 +8,7 @@ Uncomment the section that is right (remove the HTML comment wrapper). {% endfor -%} diff --git a/vizro-ai/docs/index.md b/vizro-ai/docs/index.md index 9d629e9bc..9d6b190fb 100644 --- a/vizro-ai/docs/index.md +++ b/vizro-ai/docs/index.md @@ -8,52 +8,40 @@ Even if you are an experienced data practitioner, Vizro-AI optimizes how you cre Gif to demonstrate vizro-ai -Below is a table of the Vizro components currently supported by Vizro-AI. This list is not exhaustive, and we are actively working on adding more features to Vizro-AI. - -| Feature type | Feature | Availability | -|----------------------|------------------------------------------------------------------------------------------------------------------------|--------------| -| **Components** | [Graph](https://vizro.readthedocs.io/en/stable/pages/user-guides/graph/) | ✔ | -| | [AG Grid](https://vizro.readthedocs.io/en/stable/pages/user-guides/table/#ag-grid) | ✔ | -| | [Card](https://vizro.readthedocs.io/en/stable/pages/user-guides/card-button/) | ✔ | -| | [Button](https://vizro.readthedocs.io/en/stable/pages/user-guides/card-button/) | ✖ | -| | [Tabs](https://vizro.readthedocs.io/en/stable/pages/user-guides/tabs/) | ✖ | -| | [Containers](https://vizro.readthedocs.io/en/stable/pages/user-guides/container/) | ✖ | -| **Controls** | [Filter](https://vizro.readthedocs.io/en/stable/pages/user-guides/filters/) | ✔ | -| | [Parameter](https://vizro.readthedocs.io/en/stable/pages/user-guides/parameters/) | ✖ | -| **Navigation** | [Default navigation](https://vizro.readthedocs.io/en/stable/pages/user-guides/navigation/#use-the-default-navigation) | ✔ | -| | [Custom navigation](https://vizro.readthedocs.io/en/stable/pages/user-guides/navigation/#customize-the-navigation-bar) | ✖ | -| **Layout** | [Layout](https://vizro.readthedocs.io/en/stable/pages/user-guides/layouts/) | ✔ | -| **Dashboard header** | [Dashboard title](https://vizro.readthedocs.io/en/stable/pages/user-guides/dashboard/) | ✔ | -| | [Logo](https://vizro.readthedocs.io/en/stable/pages/user-guides/dashboard/) | ✖ | - -If a feature you need for your dashboard isn't currently supported by Vizro-AI you can [retrieve the dashboard code](https://vizro.readthedocs.io/projects/vizro-ai/en/vizro-ai-0.2.3/pages/user-guides/retrieve-dashboard-code/) and add the missing components before running the dashboard. -
-- :fontawesome-solid-forward-fast:{ .lg .middle } __New to Vizro-AI?__ +- :fontawesome-solid-forward-fast:{ .lg .middle } __New to Vizro-AI?__ --- - [:octicons-arrow-right-24: Install Vizro-AI](pages/user-guides/install.md)
- [:octicons-arrow-right-24: Quickstart chart generation](pages/tutorials/quickstart.md)
- [:octicons-arrow-right-24: Quickstart dashboard generation](pages/tutorials/quickstart-dashboard.md)
+ [:octicons-arrow-right-24: Install Vizro-AI](pages/user-guides/install.md) + + [:octicons-arrow-right-24: Quickstart chart generation](pages/tutorials/quickstart.md) + + [:octicons-arrow-right-24: Quickstart dashboard generation](pages/tutorials/quickstart-dashboard.md) - :fontawesome-solid-keyboard:{ .lg .middle } __Get hands-on__ --- - [:octicons-arrow-right-24: How to run Vizro-AI](pages/user-guides/run-vizro-ai.md)
- [:octicons-arrow-right-24: Model usage](pages/user-guides/customize-vizro-ai.md)
- [:octicons-arrow-right-24: Create advanced charts](pages/user-guides/create-advanced-charts.md)
- [:octicons-arrow-right-24: Add charts to a dashboard](pages/user-guides/add-generated-chart-usecase.md)
- [:octicons-arrow-right-24: Retrieve code for a generated dashboard](pages/user-guides/retrieve-dashboard-code.md) + [:octicons-arrow-right-24: How to run Vizro-AI](pages/user-guides/run-vizro-ai.md) + + [:octicons-arrow-right-24: Model usage](pages/user-guides/customize-vizro-ai.md) + + [:octicons-arrow-right-24: Create advanced charts](pages/user-guides/create-advanced-charts.md) + + [:octicons-arrow-right-24: Add charts to a dashboard](pages/user-guides/add-generated-chart-usecase.md) + + [:octicons-arrow-right-24: Retrieve code for a generated dashboard](pages/user-guides/run-vizro-ai-dashboard.md) - :material-format-font:{ .lg .middle } __Find out more__ --- - [:octicons-arrow-right-24: FAQs](pages/explanation/faq.md)
- [:octicons-arrow-right-24: Safeguard dynamic code execution](pages/explanation/safeguard.md)
+ [:octicons-arrow-right-24: FAQs](pages/explanation/faq.md) + + [:octicons-arrow-right-24: Safeguard dynamic code execution](pages/explanation/safeguard.md) + [:octicons-arrow-right-24: Guidelines for use of LLMs](pages/explanation/safety-in-vizro-ai.md) - :fontawesome-solid-chart-column:{ .lg .middle } __Vizro__ @@ -62,14 +50,9 @@ If a feature you need for your dashboard isn't currently supported by Vizro-AI y [:octicons-arrow-right-24: Vizro documentation](https://vizro.readthedocs.io/) -
!!! notice "Notice" + Review the [disclaimer](pages/explanation/disclaimer.md) before using the `vizro-ai` package. - Review the [disclaimer](pages/explanation/disclaimer.md) - before using the `vizro-ai` package. - - Users must connect to large language models (LLMs) to use Vizro-AI. - Please review our [guidelines on the use of LLMs](pages/explanation/safety-in-vizro-ai.md) - and the required [safeguarding for dynamic code evaluation](pages/explanation/safeguard.md). + Users must connect to large language models (LLMs) to use Vizro-AI. Please review our [guidelines on the use of LLMs](pages/explanation/safety-in-vizro-ai.md) and the required [safeguarding for dynamic code evaluation](pages/explanation/safeguard.md). diff --git a/vizro-ai/docs/pages/explanation/disclaimer.md b/vizro-ai/docs/pages/explanation/disclaimer.md index 05cd4d691..4af4d8215 100644 --- a/vizro-ai/docs/pages/explanation/disclaimer.md +++ b/vizro-ai/docs/pages/explanation/disclaimer.md @@ -1,7 +1,6 @@ # Disclaimer -Users must select one of the [supported large language models (LLMs)](../user-guides/customize-vizro-ai.md#supported-models) to use the `vizro_ai` package, -and are responsible for obtaining their own suitable API key for the relevant model. +Users must select one of the [supported large language models (LLMs)](../user-guides/customize-vizro-ai.md#supported-models) to use the `vizro_ai` package, and are responsible for obtaining their own suitable API key for the relevant model. @@ -17,14 +16,11 @@ and are responsible for obtaining their own suitable API key for the relevant mo Users acknowledge and agree that: -Any results, options, data, recommendations, analyses, code, -or other information (“Outputs”) generated by any third-party generative AI tools (“GenAI Tools”) may contain some inaccuracies, biases, illegitimate, potentially infringing, -or otherwise inappropriate content that may be mistaken, discriminatory, or misleading. +Any results, options, data, recommendations, analyses, code, or other information (“Outputs”) generated by any third-party generative AI tools (“GenAI Tools”) may contain some inaccuracies, biases, illegitimate, potentially infringing, or otherwise inappropriate content that may be mistaken, discriminatory, or misleading. McKinsey & Company: -(i) expressly disclaims the accuracy, adequacy, timeliness, reliability, merchantability, fitness for a particular purpose, non-infringement, -safety or completeness of any Outputs, +(i) expressly disclaims the accuracy, adequacy, timeliness, reliability, merchantability, fitness for a particular purpose, non-infringement, safety or completeness of any Outputs, (ii) shall not be liable for any errors, omissions, or other defects in, delays or interruptions in such Outputs, or for any actions taken in reliance thereon, and @@ -32,7 +28,6 @@ safety or completeness of any Outputs, The Outputs shall be verified and validated by the users and shall not be used without human oversight and as a sole basis for making decisions impacting individuals. -Users remain solely responsible for the use of the Output, in particular, the users will need to determine the level of human oversight needed to be given the context and use case, -as well as for informing the users’ personnel and other affected users about the nature of the GenAI Output. -Users are also fully responsible for their decisions, actions, use of Vizro and Vizro-AI and compliance with applicable laws, rules, and regulations, including but not limited to confirming that the Outputs do not infringe any third-party rights. +Users remain solely responsible for the use of the Output, in particular, the users will need to determine the level of human oversight needed to be given the context and use case, as well as for informing the users’ personnel and other affected users about the nature of the GenAI Output. Users are also fully responsible for their decisions, actions, use of Vizro and Vizro-AI and compliance with applicable laws, rules, and regulations, including but not limited to confirming that the Outputs do not infringe any third-party rights. + diff --git a/vizro-ai/docs/pages/explanation/faq.md b/vizro-ai/docs/pages/explanation/faq.md index 46dfcd2ce..9eb0fad6f 100644 --- a/vizro-ai/docs/pages/explanation/faq.md +++ b/vizro-ai/docs/pages/explanation/faq.md @@ -3,20 +3,11 @@ ## Who works on Vizro-AI? ### Current team members -[Alexey Snigir](https://github.com/l0uden), -[Anna Xiong](https://github.com/Anna-Xiong), -[Antony Milne](https://github.com/antonymilne), -[Dan Dumitriu](https://github.com/dandumitriu1), -[Huong Li Nguyen](https://github.com/huong-li-nguyen), -[Jo Stichbury](https://github.com/stichbury), -[Joseph Perkins](https://github.com/Joseph-Perkins), -[Lingyi Zhang](https://github.com/lingyielia), -[Maximilian Schulz](https://github.com/maxschulz-COL), -[Nadija Graca](https://github.com/nadijagraca), -[Petar Pejovic](https://github.com/petar-qb). -With thanks to Sam Bourton and Stephen Xu for sponsorship, inspiration and guidance, plus everyone else who helped to test, guide, support and encourage development. +[Alexey Snigir](https://github.com/l0uden), [Anna Xiong](https://github.com/Anna-Xiong), [Antony Milne](https://github.com/antonymilne), [Dan Dumitriu](https://github.com/dandumitriu1), [Huong Li Nguyen](https://github.com/huong-li-nguyen), [Jo Stichbury](https://github.com/stichbury), [Joseph Perkins](https://github.com/Joseph-Perkins), [Lingyi Zhang](https://github.com/lingyielia), [Maximilian Schulz](https://github.com/maxschulz-COL), [Nadija Graca](https://github.com/nadijagraca), [Petar Pejovic](https://github.com/petar-qb). +With thanks to Sam Bourton and Stephen Xu for sponsorship, inspiration and guidance, plus everyone else who helped to test, guide, support and encourage development. ## Which large language models are supported by vizro-ai? + Refer to [supported models](../user-guides/customize-vizro-ai.md#supported-models) in `vizro-ai` docs. diff --git a/vizro-ai/docs/pages/explanation/safeguard.md b/vizro-ai/docs/pages/explanation/safeguard.md index f4311fcd6..aacd2c503 100644 --- a/vizro-ai/docs/pages/explanation/safeguard.md +++ b/vizro-ai/docs/pages/explanation/safeguard.md @@ -1,88 +1,74 @@ # Safeguard dynamic code execution in Vizro-AI -Vizro-AI uses the `exec()` statement in Python to run generated code from large language models (LLMs) for -self-debugging and automatic visual rendering in methods such as `.get_fig_object()` and `VizroAI.plot()`. -One of the primary concerns is the potential for malicious code to access or change critical system resources or data. +Vizro-AI uses the `exec()` statement in Python to run generated code from large language models (LLMs) for self-debugging and automatic visual rendering in methods such as `.get_fig_object()` and `VizroAI.plot()`. One of the primary concerns is the potential for malicious code to access or change critical system resources or data. ## Understand `exec()` -The `exec()` function enables the dynamic execution of Python programs which can either be a string or object code. -While it offers great flexibility, it also poses a significant security risk, especially when executing untrusted code. +The `exec()` function enables the dynamic execution of Python programs which can either be a string or object code. While it offers great flexibility, it also poses a significant security risk, especially when executing untrusted code. ## Safeguarding code execution -While we have made considerable efforts to safeguard its usage by limiting the usage to specific modules and functions and by restricting certain built-in operations, -these measures cannot guarantee absolute security. It is imperative for users to take extra precautions. +While we have made considerable efforts to safeguard its usage by limiting the usage to specific modules and functions and by restricting certain built-in operations, these measures cannot guarantee absolute security. It is imperative for users to take extra precautions. ### Our effort on safeguarding code execution in Vizro-AI -To help to mitigate these risks, we limit the execution of certain modules and functions. -One approach is to use Python's built-in `sys` module to restrict access to unsafe modules or functions. -By defining a whitelist of safe modules and packages and restricting certain built-in functions. +To help to mitigate these risks, we limit the execution of certain modules and functions. One approach is to use Python's built-in `sys` module to restrict access to unsafe modules or functions. By defining a whitelist of safe modules and packages and restricting certain built-in functions. !!! Warning - - While some measures have been put in place to help safeguard against known vulnerabilities, - it is important to run such systems in an isolated environment and avoid providing malicious inputs, - since such **safeguards can never be 100% effective**. Always ensure the security infrastructure when implementing and using such systems. + While some measures have been put in place to help safeguard against known vulnerabilities, it is important to run such systems in an isolated environment and avoid providing malicious inputs, since such **safeguards can never be 100% effective**. Always ensure the security infrastructure when implementing and using such systems. The white lists indicate allowed packages and built-in functions. - The red lists represent potentially - unsafe methods or operations that are restricted. + The red lists represent potentially unsafe methods or operations that are restricted. The lists below are a reflection of the security and functionality we have implemented with Vizro-AI: -??? success "Whitelisted Packages" + - - `pandas` - - `numpy` - - `vizro` - - `plotly` - - `datetime` - - `matplotlib` - - `dash` - - `scipy` - - `sklearn` +??? success "Whitelisted Packages" + - `pandas` + - `numpy` + - `vizro` + - `plotly` + - `datetime` + - `matplotlib` + - `dash` + - `scipy` + - `sklearn` ??? success "Whitelisted Builtins" - - - abs - - len - - max - - min - - print - - sum - - None - - False - - True - - dict - - enumerate - - float - - int - - list - - map - - str - - tuple + - abs + - len + - max + - min + - print + - sum + - None + - False + - True + - dict + - enumerate + - float + - int + - list + - map + - str + - tuple ??? failure "Redlisted Class Methods" - - subclasses - builtins ??? failure "Redlisted Data Handling Methods and Formats" - - Various data file formats (such as .csv, .tsv, .xlsx, .json, and so on) - Specific methods related to data input/output operations (such as .to_csv, .read_excel, .loadtxt) + + ### Safeguard for user environment and input -- **Isolated environment**: Always run code in an isolated or contained environment, such as a virtual environment, - virtual machine or container, to minimize potential harm to the primary system. +- **Isolated environment**: Always run code in an isolated or contained environment, such as a virtual environment, virtual machine or container, to minimize potential harm to the primary system. -- **Avoid malicious input**: Never feed untrusted or malicious input. Regardless of safeguards, - there's always a risk associated with executing dynamic code. - It remains the user's responsibility to ensure the safety and appropriateness of executing any generated code, - particularly in sensitive or critical contexts. +- **Avoid malicious input**: Never feed untrusted or malicious input. Regardless of safeguards, there's always a risk associated with executing dynamic code. It remains the user's responsibility to ensure the safety and appropriateness of executing any generated code, particularly in sensitive or critical contexts. - **Accessible to trusted users**: Only trusted users should be given access to the system to run Vizro-AI. diff --git a/vizro-ai/docs/pages/explanation/safety-in-vizro-ai.md b/vizro-ai/docs/pages/explanation/safety-in-vizro-ai.md index 669f01d30..fc54596bb 100644 --- a/vizro-ai/docs/pages/explanation/safety-in-vizro-ai.md +++ b/vizro-ai/docs/pages/explanation/safety-in-vizro-ai.md @@ -4,60 +4,63 @@ Vizro-AI uses generative AI models because large language models (LLMs) represen We recommend users research and understand the selected model before using `vizro_ai` package. -Users are encouraged to treat AI-generated content as supplementary, **always apply human judgment**, -approach with caution, review the relevant [disclaimer](disclaimer.md) page, and consider the following: +Users are encouraged to treat AI-generated content as supplementary, **always apply human judgment**, approach with caution, review the relevant [disclaimer](disclaimer.md) page, and consider the following: + + ### 1. Hallucination and misrepresentation Generative models can potentially generate information while appearing factual, being entirely fictitious or misleading. + -The vendor models might lack real-time knowledge or events beyond its last updates. -`vizro_ai` output may vary and you should always verify critical information. -It is the user's responsibility to discern the accuracy, consistent, and reliability of the generated content. + +The vendor models might lack real-time knowledge or events beyond its last updates. `vizro_ai` output may vary and you should always verify critical information. It is the user's responsibility to discern the accuracy, consistent, and reliability of the generated content. + ### 2. Unintended and sensitive output + -The outputs from these models can be unexpected, inappropriate, or even harmful. -Users as human in the loop is an essential part. Users must check and interpret the final output. -It is necessary to approach the generated content with caution, especially when shared or applied in various contexts. + +The outputs from these models can be unexpected, inappropriate, or even harmful. Users as human in the loop is an essential part. Users must check and interpret the final output. It is necessary to approach the generated content with caution, especially when shared or applied in various contexts. + ### 3. Data privacy + -Your data is sent to model vendors if you connect to LLMs via their APIs. -For example, if you connect to the model from OpenAI, your data will be sent to OpenAI via their API. -Users should be cautious about sharing or inputting any personal or sensitive information. + +Your data is sent to model vendors if you connect to LLMs via their APIs. For example, if you connect to the model from OpenAI, your data will be sent to OpenAI via their API. Users should be cautious about sharing or inputting any personal or sensitive information. + ### 4. Bias and fairness + -Generative AI can exhibit biases present in their training data. -Users need to be aware of and navigate potential biases in generated outputs and be cautious when interpreting the generated content. + +Generative AI can exhibit biases present in their training data. Users need to be aware of and navigate potential biases in generated outputs and be cautious when interpreting the generated content. + ### 5. Malicious use + + These models can be exploited for various malicious activities. Users should be cautious about how and where they deploy and access such models. It's crucial for users to remain informed, cautious, and ethical in their applications. - ## Dependencies, code scanners, and information security -It may occur that dependencies of `vizro_ai` get flagged by code scanners and other information security tools. As a consequence it may happen that -`vizro_ai` also get flagged. +It may occur that dependencies of `vizro_ai` get flagged by code scanners and other information security tools. As a consequence it may happen that `vizro_ai` also get flagged. -While we aim to resolve any flagged issues as soon as possible, there may not always be an immediate available fix, especially in a dynamic environment such as generative AI. We encourage users to investigate if any flagged information security issues are actually related -to any functionality used in our code base or if they only concern functionality outside the scope of `vizro_ai`. +While we aim to resolve any flagged issues as soon as possible, there may not always be an immediate available fix, especially in a dynamic environment such as generative AI. We encourage users to investigate if any flagged information security issues are actually related to any functionality used in our code base or if they only concern functionality outside the scope of `vizro_ai`. In case those issues are related to code execution, note that `vizro_ai` has its own process of executing dynamic code (see [Safeguard Execution of Dynamic Code](safeguard.md)), and does not rely on its dependencies to do so. - ## Execution of dynamic code in Vizro-AI -The `exec()` statement is used in `vizro_ai`. It enables dynamic execution of Python programs which can be powerful, but can also pose security risk -if used without caution. When paired with outputs from generative models, there is potential for unintended or malicious code execution. +The `exec()` statement is used in `vizro_ai`. It enables dynamic execution of Python programs which can be powerful, but can also pose security risk if used without caution. When paired with outputs from generative models, there is potential for unintended or malicious code execution. Users must exercise caution when executing code generated by or influenced by AI models. It's essential to: @@ -68,3 +71,11 @@ Users must exercise caution when executing code generated by or influenced by AI - Always review and understand the selected model before connecting with `vizro_ai` To learn more, refer to the section that describes how to [safeguard execution of dynamic code](safeguard.md). + +## Debugging in Vizro-AI with LangSmith and LangFuse + +[LangSmith](https://docs.smith.langchain.com/) and [LangFuse](https://langfuse.com/docs) are tools designed to enhance transparency and interpretability in AI workflows. To improve debugging and traceability, you can use the advanced observability and evaluation features of these tools with Vizro-AI. + +To ensure responsible use, review their data privacy and security policies. See [LangFuse Data Security & Privacy ](https://langfuse.com/docs/data-security-privacy) and [LangSmith Privacy Policy](https://www.langchain.com/privacy-policy) site for more details. + +Additionally, both support self-hosting options, which may be critical for security oriented users.See [LangSmith](https://docs.smith.langchain.com/self_hosting) and [LangFuse](https://langfuse.com/docs/deployment/self-host) self-hosting guides for more details. LangFuse offers a self-hosting service under the MIT License. diff --git a/vizro-ai/docs/pages/explanation/why-use-vizro-ai.md b/vizro-ai/docs/pages/explanation/why-use-vizro-ai.md new file mode 100644 index 000000000..162a9515d --- /dev/null +++ b/vizro-ai/docs/pages/explanation/why-use-vizro-ai.md @@ -0,0 +1,12 @@ +# Why use Vizro-AI over ChatGPT? + +Vizro-AI offers an easy way to turn natural language instructions into professional visualizations, like interactive charts and dashboards, without needing advanced coding skills. + +Like ChatGPT, Vizro-AI uses generative AI to understand and execute your instructions in English, or any other language. However, there are few advantages of using Vizro-AI over ChatGPT. + +- **Tailored for data visualization**: Vizro-AI is specifically designed to create charts and dashboards with in-built visual design best practices. +- **Data visualization best practices applied**: Vizro’s chart theme ensures your charts follow many best practices, such as using color-blind friendly colors and maintaining a decluttered design. +- **Data Insights**: Vizro-AI uses advanced prompt engineering to integrate additional context and data-specific insights, enriching your visualizations with more comprehensive information and allowing for more effective data storytelling. +- **Automatic code debugging**: Vizro-AI debugs the generated code, making sure that it is error-free and ready to run. +- **Instant chart execution**: Vizro-AI generates and executes chart code automatically, allowing you to see the chart output immediately. +- **Export options**: Unlike ChatGPT, Vizro-AI enables you to download charts as interactive HTML or JSON files directly through its chart UI hosted on PyCafe. diff --git a/vizro-ai/docs/pages/tutorials/quickstart-dashboard.md b/vizro-ai/docs/pages/tutorials/quickstart-dashboard.md index c29607baa..2118112d7 100644 --- a/vizro-ai/docs/pages/tutorials/quickstart-dashboard.md +++ b/vizro-ai/docs/pages/tutorials/quickstart-dashboard.md @@ -8,12 +8,11 @@ You may also want to review the [Vizro dashboard tutorial](https://vizro.readthe If you haven't already installed Vizro-AI and set up the API key for OpenAI, follow the [installation guide](../user-guides/install.md). - ## 2. Open a Notebook + A good way to initially explore Vizro-AI is from inside a Jupyter Notebook. ??? "If you haven't used Jupyter before..." - You may need to install the Jupyter package if you . From the terminal window: ```bash @@ -39,6 +38,7 @@ print(vizro_ai.__version__) You should see a return output of the form `x.y.z`. ## 3. Instantiate VizroAI + ```py from vizro_ai import VizroAI @@ -46,6 +46,7 @@ vizro_ai = VizroAI() ``` ## 4. Prepare the data + Next, prepare the data to pass to Vizro-AI. In this example, we use the [gapminder data](https://plotly.com/python-api-reference/generated/plotly.express.data.html#plotly.express.data.gapminder). ```py @@ -87,6 +88,7 @@ dashboard = vizro_ai.dashboard([df], user_question) The call to `dashboard()` initiates dashboard generation. By default, it generates the Vizro `Dashboard` Object. ## 7. Build dashboard + Once dashboard generation is complete, launch the dashboard with `build()`. ```py @@ -95,7 +97,6 @@ Vizro().build(dashboard).run() ``` !!! example "Generated dashboard" - === "Code for the cell" ```py from vizro import Vizro @@ -119,6 +120,6 @@ Vizro().build(dashboard).run() ``` === "Result" - [![VizroAIDashboardPage1]][VizroAIDashboardPage1] + [![VizroAIDashboardPage1]][vizroaidashboardpage1] - [VizroAIDashboardPage1]: ../../assets/tutorials/dashboard/dashboard0_page1.png +[vizroaidashboardpage1]: ../../assets/tutorials/dashboard/dashboard0_page1.png diff --git a/vizro-ai/docs/pages/tutorials/quickstart.md b/vizro-ai/docs/pages/tutorials/quickstart.md index 17d673927..64574cd77 100644 --- a/vizro-ai/docs/pages/tutorials/quickstart.md +++ b/vizro-ai/docs/pages/tutorials/quickstart.md @@ -1,20 +1,24 @@ # Chart generation + This tutorial introduces you to chart generation using Vizro-AI. It explains the basics of creating a plotly chart that can be added to a Vizro dashboard. When you have followed it, you are set up to explore the Vizro and Vizro-AI packages further. + ### 1. Install Vizro-AI and its dependencies + If you haven't already installed Vizro-AI and set up the API key for OpenAI, follow the [installation guide](../user-guides/install.md). + ### 2. Open a Jupyter Notebook + A good way to initially explore Vizro-AI is from inside a Jupyter Notebook. ??? "If you haven't used Jupyter before..." - You may need to install the Jupyter package if you . From the terminal window: ```bash @@ -40,7 +44,9 @@ print(vizro_ai.__version__) You should see a return output of the form `x.y.z`. + ### 3. Create your first plotly chart using Vizro-AI + Let's create a chart to illustrate the GDP of various continents while including a reference line for the average. We give Vizro-AI the English language instruction "*describe the composition of GDP in continent and color by continent, and add a horizontal line for avg GDP*". @@ -54,12 +60,12 @@ import vizro.plotly.express as px df = px.data.gapminder() ``` - Next, we instantiate `VizroAI`: ```python vizro_ai = VizroAI() ``` + To learn how to customize the `VizroAI` class, check out the guide on [how to customize models](../user-guides/customize-vizro-ai.md). Finally, we call the `plot()` method with our English language instruction, to generate the visualization: @@ -74,7 +80,6 @@ vizro_ai.plot( ``` !!! warning "Help! The LLM request was unauthorized" - If you see an error similar to this, your LLM API key is not valid: `INFO:httpx:HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 401 Unauthorized"` @@ -83,12 +88,11 @@ vizro_ai.plot( `export OPENAI_API_KEY="sk-YOURKEY"`. - The call above makes the API key available from that terminal instance. If you want to access Vizro-AI from a Notebook, you should then run `jupyter notebook` (or just work within that terminal to run your Python script in `app.py`. When you restart the terminal, you'll need to call `export` again. + The call above makes the API key available from that terminal instance. If you want to access Vizro-AI from a Notebook, you should then run `jupyter notebook` (or just work within that terminal to run your Python script in `app.py`. When you restart the terminal, you'll need to call `export` again. And that's it! By passing the prepared data and written visualization request, Vizro-AI takes care of the processing. It generates the necessary code for data manipulation and chart creation, and returns the chart by executing the generated code. !!! example "Vizro-AI Syntax" - === "Code for the cell" ```py import vizro.plotly.express as px @@ -104,38 +108,40 @@ And that's it! By passing the prepared data and written visualization request, V Make sure to take average over continent.""", ) ``` - === "Result" - [![LineGraph]][LineGraph] - [LineGraph]: ../../assets/tutorials/chart/GDP_Composition_Graph.png + === "Result" + [![LineGraph]][linegraph] The chart created is interactive: you can hover over the data for more information. Passing `return_elements=True` to the `plot()` method returns an object, which includes the code along with a set of insights to explain the rendered chart in detail. You can then use the code within a Vizro dashboard as illustrated in the [Vizro documentation](https://vizro.readthedocs.io/en/stable/pages/tutorials/explore-components/#22-add-further-components). For the line graph above, the code returned may be as follows: !!! example "Returned by Vizro-AI" - ```python from vizro.models.types import capture import vizro.plotly.express as px import pandas as pd - @capture('graph') + + + @capture("graph") def custom_chart(data_frame): - df = data_frame.groupby(['year', 'continent'])['gdpPercap'].mean().unstack().reset_index() - fig = px.line(df, x='year', y=['Africa', 'Americas', 'Asia', 'Europe', 'Oceania']) + df = data_frame.groupby(["year", "continent"])["gdpPercap"].mean().unstack().reset_index() + fig = px.line(df, x="year", y=["Africa", "Americas", "Asia", "Europe", "Oceania"]) return fig + fig = custom_chart(data_frame=df) ``` + ### 4. Get an explanation with your chart + Let's create another example to illustrate the code and insights returned when passing `return_elements=True` as a parameter to `plot()`: !!! example "Specify `return_elements=True`" - === "Code for the cell" ```py res = vizro_ai.plot(df, "show me the geo distribution of life expectancy", return_elements=True) @@ -143,8 +149,10 @@ Let's create another example to illustrate the code and insights returned when p print(res.chart_insights) print(res.code_explanation) ``` + === "Result" Code + ```py import plotly.express as px @@ -164,13 +172,17 @@ Let's create another example to illustrate the code and insights returned when p ) return fig ``` + Chart insights + ``` This choropleth map visualizes the global distribution of life expectancy across different countries. It highlights variations and trends in life expectancy, providing a clear visual representation of geographical disparities. ``` + Code explanation + ``` - Import Plotly Express. - Create a choropleth map using the `px.choropleth` function. @@ -180,13 +192,14 @@ Let's create another example to illustrate the code and insights returned when p - Update layout to enhance map readability and aesthetics. ``` - [GeoDistribution]: ../../assets/tutorials/chart/GeoDistribution.png - + ### 5. Explore further - + Congratulations! You have created your first charts with Vizro-AI and you are ready to explore further. A good place to start would be to review the different how-to guides to learn [the different ways to run Vizro-AI](../user-guides/run-vizro-ai.md), [how to create advanced charts](../user-guides/create-advanced-charts.md) and [how to add your Vizro-AI charts to a Vizro dashboard](../user-guides/add-generated-chart-usecase.md). You may also want to review the tutorial on [how to generate a Vizro dashboard with Vizro-AI](quickstart-dashboard.md) + +[linegraph]: ../../assets/tutorials/chart/GDP_Composition_Graph.png diff --git a/vizro-ai/docs/pages/user-guides/add-generated-chart-usecase.md b/vizro-ai/docs/pages/user-guides/add-generated-chart-usecase.md index fa87f16e1..60eb24100 100644 --- a/vizro-ai/docs/pages/user-guides/add-generated-chart-usecase.md +++ b/vizro-ai/docs/pages/user-guides/add-generated-chart-usecase.md @@ -2,17 +2,15 @@ This guide explains the different ways in which you can add a chart generated by Vizro-AI to an existing [Vizro dashboard](https://github.com/mckinsey/vizro/tree/main/vizro-core). -## Use Vizro-AI's generated code +## Use Vizro-AI's generated code 1. Create a chart with Vizro-AI that you would like to visualize in a dashboard. In this example, we aim to create a chart that illustrates the population development of each continent over time. To gain deeper insights and access the underlying code responsible for generating the chart, include `return_elements=True` in the `plot()` method. Let's execute the provided code and examine the outcome. !!! example "Vizro-AI chart" - === "Code for the cell" - - ```py + ```python import vizro_ai from vizro_ai import VizroAI import vizro.plotly.express as px @@ -23,23 +21,25 @@ This guide explains the different ways in which you can add a chart generated by df = px.data.gapminder() vizro_ai = VizroAI(model="gpt-4o") - result = vizro_ai.plot(df, - """Plot a bubble chart to show the changes in life expectancy - and GDP per capita for each country over time. - Color the bubbles by continent. - Add animation on yearly basis, and do not use facets. - Put the legend on top""", return_elements=True) + result = vizro_ai.plot( + df, + """Plot a bubble chart to show the changes in life expectancy + and GDP per capita for each country over time. + Color the bubbles by continent. + Add animation on yearly basis, and do not use facets. + Put the legend on top""", + return_elements=True, + ) - print(f"Insight:\n{result.chart_insights}\n" ) - print(f"Code explanation:\n{result.code_explanation}\n\nCode:\n{result.code_vizro}\n" ) + print(f"Insight:\n{result.chart_insights}\n") + print(f"Code explanation:\n{result.code_explanation}\n\nCode:\n{result.code_vizro}\n") result.get_fig_object(df).show() ``` - === "Result" - [![VizroAIChart]][VizroAIChart] - [VizroAIChart]: ../../assets/user_guides/vizro-ai-chart.png + === "Result" + [![VizroAIChart]][vizroaichart] -2. Insert the resulting chart into a dashboard. +1. Insert the resulting chart into a dashboard. Once you are satisfied with the chart, you can add it to a [Vizro](https://github.com/mckinsey/vizro/tree/main/vizro-core) dashboard. @@ -62,11 +62,9 @@ This guide explains the different ways in which you can add a chart generated by return fig ``` -The `custom_chart` function is an example of the [custom chart](https://vizro.readthedocs.io/en/stable/pages/user-guides/custom-charts). It returns a `go.Figure()` object. -This object must then be passed to the `figure` argument of the Vizro [Graph](https://vizro.readthedocs.io/en/stable/pages/user-guides/graph) model. +The `custom_chart` function is an example of the [custom chart](https://vizro.readthedocs.io/en/stable/pages/user-guides/custom-charts). It returns a `go.Figure()` object. This object must then be passed to the `figure` argument of the Vizro [Graph](https://vizro.readthedocs.io/en/stable/pages/user-guides/graph) model. !!! example "Vizro-AI chart within vizro dashboard" - === "Code for the cell" ```py hl_lines="8-23 31" from vizro import Vizro @@ -99,36 +97,31 @@ This object must then be passed to the `figure` argument of the Vizro [Graph](ht page = vm.Page( title = 'Demographics', components = [ - vm.Graph(id='bubble chart', figure=custom_chart(df)), - vm.Graph(id='histogram', figure = px.box(df, - x='continent', - y='lifeExp', - color='continent', - title='Life Expectancy per Continent'))], + vm.Graph(figure=custom_chart(df)), + vm.Graph( + figure=px.box( + df, x='continent', y='lifeExp', color='continent', title='Life Expectancy per Continent' + ) + ) + ], controls = [ vm.Filter(column='country'), vm.Filter(column='continent')]) Vizro().build(vm.Dashboard(pages=[page])).run(port=8090) ``` - === "Result" - [![VizroDashboard]][VizroDashboard] - - [VizroDashboard]: ../../assets/user_guides/chart_into_dashboard_large.png + === "Result" + [![VizroDashboard]][vizrodashboard] ## Use Vizro-AI dynamically to return a `fig` object -We can also use Vizro-AI dynamically and assign the output of `plot()` directly to the fig variable, enabling its reuse in the `vm.Graph.figure` argument. -This method offers streamlined efficiency, eliminating the need for code copying. -Note that each dashboard run triggers an API call to the LLM, possibly escalating costs. This can be avoided if the `fig` object is stored and retrieved as needed, instead of making repeated calls to `plot()`. +We can also use Vizro-AI dynamically and assign the output of `plot()` directly to the fig variable, enabling its reuse in the `vm.Graph.figure` argument. This method offers streamlined efficiency, eliminating the need for code copying. Note that each dashboard run triggers an API call to the LLM, possibly escalating costs. This can be avoided if the `fig` object is stored and retrieved as needed, instead of making repeated calls to `plot()`. Executing the code below yields the identical dashboard as the example above. - !!! example "Use Vizro-AI fig directly in vizro dashboard" === "Code for the cell" - ```py hl_lines="13-18 23" from vizro import Vizro import vizro.models as vm @@ -143,28 +136,33 @@ Executing the code below yields the identical dashboard as the example above. vizro_ai = VizroAI(model="gpt-4o") fig = vizro_ai.plot(df, - """Plot a bubble chart to show the changes in life expectancy - and GDP per capita for each country over time. - Color the bubbles by continent. - Add animation on yearly basis, and do not use facets. - Put the legend on top""") + """Plot a bubble chart to show the changes in life expectancy + and GDP per capita for each country over time. + Color the bubbles by continent. + Add animation on yearly basis, and do not use facets. + Put the legend on top""" + ) page = vm.Page( title = 'Demographics', components = [ - vm.Graph(id='bubble chart', figure=fig), - vm.Graph(id='histogram', figure = px.box(df, - x='continent', - y='lifeExp', - color='continent', - title='Life Expectancy per Continent'))], + vm.Graph(figure=fig), + vm.Graph( + figure=px.box( + df, x='continent', y='lifeExp', color='continent', title='Life Expectancy per Continent' + ) + ) + ], controls = [ vm.Filter(column='country'), vm.Filter(column='continent')]) Vizro().build(vm.Dashboard(pages=[page])).run(port=8090) ``` + === "Result" - [![VizroDashboard2]][VizroDashboard2] + [![VizroDashboard2]][vizrodashboard2] - [VizroDashboard2]: ../../assets/user_guides/chart_into_dashboard_large.png +[vizroaichart]: ../../assets/user_guides/vizro-ai-chart.png +[vizrodashboard]: ../../assets/user_guides/chart_into_dashboard_large.png +[vizrodashboard2]: ../../assets/user_guides/chart_into_dashboard_large.png diff --git a/vizro-ai/docs/pages/user-guides/advanced-options.md b/vizro-ai/docs/pages/user-guides/advanced-options.md index 528a74912..4d63eaa35 100644 --- a/vizro-ai/docs/pages/user-guides/advanced-options.md +++ b/vizro-ai/docs/pages/user-guides/advanced-options.md @@ -4,19 +4,20 @@ This guide shows you how to use the advanced options of `VizroAI.plot`. First we show how to change the input parameters of the function, as follows: -* control over whether code gets executed, -* the number of retries of `.plot` when it fails validation, -* how to request a comprehensive output (when `return_elements=True`). +- control over whether code gets executed, +- the number of retries of `.plot` when it fails validation, +- how to request a comprehensive output (when `return_elements=True`). Second we show how to use this more comprehensive output, enabling control of code generation and `fig` object production. ## Inputs of `VizroAI.plot` ### `user_input` -This is the natural language query from which, together with a data sample, the LLM creates a plotly chart. For the query, you can [use English or a different language](use-different-languages.md). The complexity of the resulting chart [depends on the vendor model capabilities](customize-vizro-ai.md#what-model-to-choose). +This is the natural language query from which, together with a data sample, the LLM creates a plotly chart. For the query, you can [use English or a different language](use-different-languages.md). The complexity of the resulting chart [depends on the vendor model capabilities](customize-vizro-ai.md#what-model-to-choose). ### `df` + Supply any `pandas` data frame to base your query on. The LLM will receive a sample of this data frame to form an appropriate graph. If the option `validate_code` is set to `True` (which it is by default), the LLM created chart code will be evaluated on a sample of this data frame. @@ -24,10 +25,13 @@ If the option `validate_code` is set to `True` (which it is by default), the LLM If `return_elements` is set to `False`, then the returned `fig` object will be created based on this (entire) data frame. + ### `max_debug_retry` + This number determines how often the tool will try to correct an incorrect response (that fails various validation criteria). Under the hood this is [implemented via pydantic validators](https://docs.pydantic.dev/1.10/usage/validators/). The last response will be re-sent to the LLM together with the validation error(s) in order to receive an improved response. This concept is [inspired by the amazing instructor library](https://github.com/jxnl/instructor). ### `return_elements` + This boolean (by default `False`) determines the return type of `VizroAI.plot`. If set to `False`, then dynamically generated Python code is executed to produce a `plotly.graph_objects.Figure` object from the LLM response and the user supplied data frame. Strictly speaking, it produces a `vizro.charts._charts_utils._DashboardReadyFigure`, which behaves essentially like the former, but is ready to be [inserted](add-generated-chart-usecase.md) into a [Vizro](https://vizro.readthedocs.io/en/stable/) dashboard. It also comes with the default Vizro dark theme. @@ -35,8 +39,11 @@ If set to `False`, then dynamically generated Python code is executed to produce If set to `True`, a class (pydantic model) is returned from which the `fig` object, but also various other outputs can be generated. (see below) ### `validate_code` + This boolean (by default `True`) determines whether the LLM generated Python code executes with a sample of the data in order to verify that it runs and produces a plotly figure. Be sure [to read and understand what it means when dynamically generated code is executed](../explanation/safety-in-vizro-ai.md#execution-of-dynamic-code-in-vizro-ai). + + If `return_elements=True` **and** `validate_code=False`, then no code is executed to obtain the return of `VizroAI.plot`. This means that the code string obtained is not validated, but also that no code was executed. ## Output if `return_elements=True` @@ -44,10 +51,10 @@ If `return_elements=True` **and** `validate_code=False`, then no code is execute If `return_elements=True`, then instead of a `fig` object, a class is returned, which enables the following options: ### Obtain `vizro` code string + You can obtain the code string that would produce the answer to the user query as a Vizro dashboard ready figure as follows. The name for the function will be `custom_chart`: !!! example "Vizro code" - === "Code" ```py from vizro_ai import VizroAI @@ -76,10 +83,10 @@ You can obtain the code string that would produce the answer to the user query a ``` ### Obtain `plotly` code string + You can obtain the code string that would produce the answer to the user query as a pure `plotly.graph_objects.Figure` as follows. The name for the function will be `custom_chart`: !!! example "Plotly code" - === "Code" ```py from vizro_ai import VizroAI @@ -110,10 +117,10 @@ You can obtain the code string that would produce the answer to the user query a You can create the `fig` object using either of the above produced code strings (vizro or plotly), changing the chart name, and using different data. Note that when executing this function, the produced code string will be dynamically executed. Be sure [to read and understand what it means when dynamically generated code is executed](../explanation/safety-in-vizro-ai.md#execution-of-dynamic-code-in-vizro-ai). #### Vizro ready + This `fig` object is in the standard `vizro_dark` theme, and can [be inserted into a Vizro dashboard](add-generated-chart-usecase.md). !!! example "Vizro `fig` object" - === "Code" ```py from vizro_ai import VizroAI @@ -128,15 +135,13 @@ This `fig` object is in the standard `vizro_dark` theme, and can [be inserted in ``` === "Result" - [![VizroAIChartVizro]][VizroAIChartVizro] - ``` - [VizroAIChartVizro]: ../../assets/user_guides/VizroAIVizro.png + [![VizroAIChartVizro]][vizroaichartvizro] #### Pure Plotly/Dash + This `fig` object is a basic plotly figure. !!! example "Plotly `fig` object" - === "Code" ```py from vizro_ai import VizroAI @@ -151,19 +156,18 @@ This `fig` object is a basic plotly figure. ``` === "Result" - [![VizroAIChartPlotly]][VizroAIChartPlotly] - ``` - [VizroAIChartPlotly]: ../../assets/user_guides/VizroAIPlotly.png + [![VizroAIChartPlotly]][vizroaichartplotly] #### Using different data + + You can create the `fig` object with different data while ensuring the overall schema remains consistent. You can re-evaluate this function to generate various `fig` objects for different data. For example, the code could be generated using fake or sample data fed into Vizro-AI. When moving to production, you can switch the data source to the complete dataset, as long as the data schema is consistent. + !!! example "Different data" - === "Code" - ```py from vizro_ai import VizroAI import plotly.express as px @@ -180,19 +184,14 @@ You can create the `fig` object with different data while ensuring the overall s ``` === "Result" - [![VizroAINewData]][VizroAINewData] - ``` - [VizroAINewData]: ../../assets/user_guides/VizroAINewData.png - ``` + [![VizroAINewData]][vizroainewdata] #### Changing the chart name -This option executes the chart code with the name given under `chart_name`. This can be important when you want to avoid overwriting variables in the namespace. +This option executes the chart code with the name given under `chart_name`. This can be important when you want to avoid overwriting variables in the namespace. !!! example "Changing the `chart_name`" - === "Code" - ```py from vizro_ai import VizroAI import plotly.express as px @@ -209,3 +208,7 @@ This option executes the chart code with the name given under `chart_name`. This ```py ``` + +[vizroaichartplotly]: ../../assets/user_guides/VizroAIPlotly.png +[vizroaichartvizro]: ../../assets/user_guides/VizroAIVizro.png +[vizroainewdata]: ../../assets/user_guides/VizroAINewData.png diff --git a/vizro-ai/docs/pages/user-guides/chart-examples.md b/vizro-ai/docs/pages/user-guides/chart-examples.md deleted file mode 100644 index 4721980c4..000000000 --- a/vizro-ai/docs/pages/user-guides/chart-examples.md +++ /dev/null @@ -1,95 +0,0 @@ -# Gallery of examples - -Take a look at some more advanced charts that can be created with Vizro-AI using data from [Plotly Express](https://plotly.com/python-api-reference/generated/plotly.express.data.html). The examples below use the OpenAI `"gpt-4o"` model as we are going to request specific updates to the layout of the charts. - -### Polar bar chart - -A polar bar chart is a circular graph where each axis represents a different variable, typically used for displaying cyclical or directional data. -It's suitable for comparing multiple variables across different categories or directions. Let's make one using Vizro-AI. - - -!!! example "Polar Bar Chart" - - === "Resulting chart" - [![VizroAIChart1]][VizroAIChart1] - - === "Code for the cell" - ```py - import vizro_ai - from vizro_ai import VizroAI - import plotly.express as px - - from dotenv import load_dotenv - load_dotenv() - - df = px.data.wind() - - vizro_ai = VizroAI(model="gpt-4o") - fig = vizro_ai.plot(df, - """Describe wind frequency and direction using bar_polar chart. - Increase the width and height of the figure. - Improve layout by placing title to the left. Show legend""") - fig.show() - ``` - - [VizroAIChart1]: ../../assets/user_guides/polar_bar_chart.png - - -### Geographical map chart - -The next chart we'll look at is a geographical map chart to visualize spatial patterns in data, which often reveals insights not seen in other charts. - -!!! example "Map chart" - - === "Resulting chart" - [![VizroAIChart2]][VizroAIChart2] - - === "Code for the cell" - ```py - import vizro_ai - from vizro_ai import VizroAI - import plotly.express as px - - from dotenv import load_dotenv - load_dotenv() - - df = px.data.gapminder() - - vizro_ai = VizroAI(model="gpt-4o") - fig = vizro_ai.plot(df, - """Visualize life expectancy over the years using map chart. Use life expectancy as the color dimension. - Improve layout by using Arial font. Increase the width and height of the map area. Outline continents on the map. - Show countries on the map. - Increase the width and height of the figure.""") - fig.show() - ``` - - [VizroAIChart2]: ../../assets/user_guides/map_chart.gif - - -### 3D surface plot - -Let's explore how to generate a 3-dimensional surface plot with VizroAI. - -!!! example "Surface plot" - - === "Resulting chart" - [![VizroAIChart3]][VizroAIChart3] - - === "Code for the cell" - ```py - import vizro_ai - from vizro_ai import VizroAI - import plotly.express as px - - from dotenv import load_dotenv - load_dotenv() - - df = px.data.gapminder() - - vizro_ai = VizroAI(model="gpt-4o") - fig = vizro_ai.plot(df, "create a surface plot") - fig.show() - ``` - - [VizroAIChart3]: ../../assets/user_guides/surface_plot.gif diff --git a/vizro-ai/docs/pages/user-guides/create-advanced-charts.md b/vizro-ai/docs/pages/user-guides/create-advanced-charts.md index c616f88bd..4a8604511 100644 --- a/vizro-ai/docs/pages/user-guides/create-advanced-charts.md +++ b/vizro-ai/docs/pages/user-guides/create-advanced-charts.md @@ -1,12 +1,12 @@ # Advanced charts + This page explains how to use Vizro-AI to create charts with advanced visualizations and enhanced formatting. -## Animated bar chart +## Animated map chart -We'll create an animated bar chart illustrating the GDP per capita of each continent over time. Run the code below and look at the result. +We'll create an animated map chart illustrating the GDP per capita of each continent over time. Run the code below and look at the result. !!! example "Vizro-AI animated chart" - === "Code" ```py from vizro_ai import VizroAI @@ -18,16 +18,13 @@ We'll create an animated bar chart illustrating the GDP per capita of each conti fig = vizro_ai.plot(df, "Visualize GDP per capita over the years for each country using map chart.") fig.show() ``` - === "Result" - [![AnimatedChart1]][AnimatedChart1] - [AnimatedChart1]: ../../assets/tutorials/chart/advanced_chart_1.png + === "Result" + [![AnimatedChart1]][animatedchart1] -Having unveiled our animated map chart showcasing GDP per capita development per country, it's clear that the map area is small, and it is difficult to differentiate countries. -Next, we will try to tweak our prompt to improve the overall layout. +Having unveiled our animated map chart showcasing GDP per capita development per country, it's clear that the map area is small, and it is difficult to differentiate countries. Next, we will try to tweak our prompt to improve the overall layout. !!! example "Vizro-AI animated chart" - === "Code" ```py from vizro_ai import VizroAI @@ -41,18 +38,15 @@ Next, we will try to tweak our prompt to improve the overall layout. Show countries on the map. Increase the width and height of the figure.""") fig.show() ``` - === "Result" - [![AnimatedChart2]][AnimatedChart2] - - [AnimatedChart2]: ../../assets/tutorials/chart/advanced_chart_2.png + === "Result" + [![AnimatedChart2]][animatedchart2] By incorporating the directive `Increase the width and height of the figure.` and `Show countries on the map.` we've successfully refined our animation. Upon closer inspection, the title is too long and the color palette used does not match our needs. We can fix those issues with better and more specific prompting. Let's run the code below to visually improve the chart. !!! example "Vizro-AI animated chart" - === "Code" ```py from vizro_ai import VizroAI @@ -67,12 +61,14 @@ Upon closer inspection, the title is too long and the color palette used does no Set title to be: `GDP per Capita over the years`. Use `Blues` as color sequence. """) fig.show() ``` - === "Result" - [![AnimatedChart3]][AnimatedChart3] - [AnimatedChart3]: ../../assets/tutorials/chart/animated_advanced_chart.gif + === "Result" + [![AnimatedChart3]][animatedchart3] Congratulations! You've now gained insights into harnessing the power of a LLM and Vizro-AI for crafting advanced charts and improving formatting. Don't forget, enabling `return_elements=True` in `.plot()` and check `chart_insights` and `code_explanation` is a good way of learning more about how a chart can be further improved and formatted. - Advanced charts are well-suited for [Vizro](https://github.com/mckinsey/vizro/tree/main/vizro-core) dashboard applications. You can create a chart using `vizro-ai` to plug into your `vizro` dashboard in seconds! + +[animatedchart1]: ../../assets/tutorials/chart/advanced_chart_1.png +[animatedchart2]: ../../assets/tutorials/chart/advanced_chart_2.png +[animatedchart3]: ../../assets/tutorials/chart/animated_advanced_chart.gif diff --git a/vizro-ai/docs/pages/user-guides/create-complex-dashboard.md b/vizro-ai/docs/pages/user-guides/create-complex-dashboard.md index 5ea3721ca..fbec99025 100644 --- a/vizro-ai/docs/pages/user-guides/create-complex-dashboard.md +++ b/vizro-ai/docs/pages/user-guides/create-complex-dashboard.md @@ -9,6 +9,7 @@ The following example shows how to use Vizro-AI to generate a complex Vizro dash If you haven't already installed Vizro-AI and set up the API key for OpenAI, follow the [installation guide](../user-guides/install.md). ## 1. Prepare the data + Next, prepare the data to pass to Vizro-AI. In this example, we use the [election data](https://plotly.com/python-api-reference/generated/plotly.express.data.html#plotly.express.data.election) and the [stocks data](https://plotly.com/python-api-reference/generated/plotly.express.data.html#plotly.express.data.stocks). ```py @@ -18,7 +19,6 @@ df1 = px.data.election() df2 = px.data.stocks(datetimes=True) ``` - ## 2. Prepare the user prompt Devise a string of text to form the prompt that requests Vizro-AI to generate the Vizro dashboard. @@ -104,17 +104,81 @@ dashboard = vizro_ai.dashboard([df1, df2], user_question) The call to `dashboard()` triggers the dashboard building process. Once Vizro-AI finishes this process, you can launch the dashboard with `build()`. !!! example "Generated dashboard" - === "Code" ```py + import vizro_ai + from vizro_ai import VizroAI + import plotly.express as px + + from dotenv import load_dotenv + load_dotenv() + + df1 = px.data.election() + df2 = px.data.stocks(datetimes=True) + + user_question = """ + Create a 2-page dashabord. + + + Visualize the election result. + + NOTE: + 1. use consistent and default color scheme. + 1. make axis label and chart title simple and readable. + + I need 3 pie charts, 3 bar charts, 1 table, and 1 radio button as filter. + + pie chart 1: shows number of votes Coderre received, compared to total votes. + pie chart 2: shows number of votes Bergeron received, compared to total votes. + pie chart 3: shows number of votes Joly received, compared to total votes. + + bar chart 1: shows number of districts Coderre won. Put `result` on y-axis, put "count of districts" on x-axis. + bar chart 2: shows number of districts Bergeron won. Put `result` on y-axis, put "count of districts" on x-axis. + bar chart 3: shows number of districts Joly won. Put `result` on y-axis, put "count of districts" on x-axis. + + use table to show the election data. + + Layout of page 1: + Imagine the whole page is divided by a (3 by 3) grid, with 3 rows and 3 columns. + Row 1 - pie chart 1 takes column 1; pie chart 2 takes column 2; pie chart 3 takes column 3. + Row 2 - bar chart 1 takes column 1; bar chart 2 takes column 2; bar chart 3 takes column 3. + Row 3 - the table span all three columns. + + Add a filter to filter all pie charts by district, using radio button as selector. + + + + Visualize the tech company stock data. + I need 1 line chart, 6 cards. + + line chart: shows the stock price history of all companies. Put data on x-axis, company names as facet_row. make the y-axis label simple and readable. + + For cards, render the exact text as requested. + Card 1 has text `> Dow Jones \n\n ## **39,737.26**\n` + Card 2 has text `> S&P 500 \n\n ## **4,509.61**\n` + Card 3 has text `> NASDAQ Composite \n\n ## **14,141.48**\n` + Card 4 has text `> FTSE 100 \n\n ## **7,592.66**\n` + Card 5 has text `> DAX \n\n ## **15,948.85**\n` + Card 6 has text `> Nikkei 225 \n\n ## **32,210.78**\n` + + Page Layout: + In a grid of 7 rows and 6 columns: + column 1 to column 5 - the line chart spans 5 columns (all 7 rows) from the left. + column 6 - card 1 takes row 1; card 2 takes row 2; card 3 takes row 3; ... card 6 takes row 6; row 7 is empty. + """ + + vizro_ai = VizroAI(model="gpt-4o") + dashboard = vizro_ai.dashboard([df1, df2], user_question) + Vizro().build(dashboard).run() + ``` === "Page1" - [![VizroAIDashboardPage1]][VizroAIDashboardPage1] + [![VizroAIDashboardPage1]][vizroaidashboardpage1] === "Page2" - [![VizroAIDashboardPage2]][VizroAIDashboardPage2] + [![VizroAIDashboardPage2]][vizroaidashboardpage2] - [VizroAIDashboardPage1]: ../../assets/user_guides/dashboard/dashboard1_page1.png - [VizroAIDashboardPage2]: ../../assets/user_guides/dashboard/dashboard1_page2.png +[vizroaidashboardpage1]: ../../assets/user_guides/dashboard/dashboard1_page1.png +[vizroaidashboardpage2]: ../../assets/user_guides/dashboard/dashboard1_page2.png diff --git a/vizro-ai/docs/pages/user-guides/customize-vizro-ai.md b/vizro-ai/docs/pages/user-guides/customize-vizro-ai.md index 3087d7f1a..03f35cbe5 100644 --- a/vizro-ai/docs/pages/user-guides/customize-vizro-ai.md +++ b/vizro-ai/docs/pages/user-guides/customize-vizro-ai.md @@ -3,8 +3,8 @@ This guide shows how to set up a large language model (LLM) for use with Vizro-AI. Setting up a LLM is required for the package to generate charts and dashboards based on natural language queries. To ensure responsible use, review the vendor’s guidelines on risk mitigation before using the model to understand potential model limitations and best practices. ## Supported models -Vizro-AI supports **any** model that is available via [Langchain's `BaseChatModel` class](https://api.python.langchain.com/en/latest/language_models/langchain_core.language_models.chat_models.BaseChatModel.html#langchain_core.language_models.chat_models.BaseChatModel), and that has the [`with_structured_output` method](https://python.langchain.com/v0.2/docs/how_to/structured_output/#the-with_structured_output-method) implemented. An overview of the [most common vendor models supporting this functionality](https://python.langchain.com/v0.2/docs/integrations/chat/) can be found in Langchain's documentation. For ease of use one can also choose some models via a string parameter. +Vizro-AI supports **any** model that is available via [Langchain's `BaseChatModel` class](https://api.python.langchain.com/en/latest/language_models/langchain_core.language_models.chat_models.BaseChatModel.html#langchain_core.language_models.chat_models.BaseChatModel), and that has the [`with_structured_output` method](https://python.langchain.com/v0.2/docs/how_to/structured_output/#the-with_structured_output-method) implemented. An overview of the [most common vendor models supporting this functionality](https://python.langchain.com/v0.2/docs/integrations/chat/) can be found in Langchain's documentation. For ease of use one can also choose some models via a string parameter. ### Setting model via string for ease of use @@ -14,34 +14,30 @@ We have created shortcuts with sensible defaults (mainly setting `temperature=0` vizro_ai = VizroAI(model="") ``` -!!!note +!!! note For the string settings to work, you must supply your API key via environment variables. The relevant variable names to be set are noted in each vendor tab. === "OpenAI" + | Environment variable | Name(s) | + | -------------------- | ----------------- | + | API key | `OPENAI_API_KEY` | + | Base API URL | `OPENAI_API_BASE` | - | Env variable | Name(s) | - | ----------- | ------------------------------------ | - | API key | `OPENAI_API_KEY` | - | Base API URL | `OPENAI_API_BASE` | - - To use OpenAI with Vizro-AI, you must have an account with paid-for credits available. None of the free accounts will suffice. [Check the OpenAI models and pricing on their website](https://platform.openai.com/docs/models). Before using a model, please review OpenAI's guidelines on risk mitigation to understand potential model limitations and best practices. - [See the OpenAI site for more details on responsible usage](https://platform.openai.com/docs/guides/safety-best-practices). + To use OpenAI with Vizro-AI, you must have an account with paid-for credits available. None of the free accounts will suffice. [Check the OpenAI models and pricing on their website](https://platform.openai.com/docs/models). Before using a model, please review OpenAI's guidelines on risk mitigation to understand potential model limitations and best practices. [See the OpenAI site for more details on responsible usage](https://platform.openai.com/docs/guides/safety-best-practices). - `gpt-4o-mini` **default** - `gpt-4-turbo` - `gpt-4o` === "Anthropic" - _Currently works only for `VizroAI.plot` - we are working on making it available for `VizroAI.dashboard`_ - | Env variable | Name(s) | - | ----------- | ------------------------------------ | - | API key | `ANTHROPIC_API_KEY` | - | Base API URL | `ANTHROPIC_API_URL`,`ANTHROPIC_BASE_URL` | + | Environment variable | Name(s) | + | -------------------- | ---------------------------------------- | + | API key | `ANTHROPIC_API_KEY` | + | Base API URL | `ANTHROPIC_API_URL`,`ANTHROPIC_BASE_URL` | - To use Anthropic with Vizro-AI, you must have an account with paid-for credits available. None of the free accounts will suffice. [Check the Anthropic models and pricing on their website](https://docs.anthropic.com/en/docs/about-claude/models). Before using a model, please review Anthropic guidelines on risk mitigation to understand potential model limitations and best practices. - [See the Anthropic site for more details on responsible usage](https://support.anthropic.com/en/collections/4078535-trust-safety/). + To use Anthropic with Vizro-AI, you must have an account with paid-for credits available. None of the free accounts will suffice. [Check the Anthropic models and pricing on their website](https://docs.anthropic.com/en/docs/about-claude/models). Before using a model, please review Anthropic guidelines on risk mitigation to understand potential model limitations and best practices. [See the Anthropic site for more details on responsible usage](https://support.anthropic.com/en/collections/4078535-trust-safety/). - `claude-3-5-sonnet-latest` - `claude-3-opus-latest` @@ -55,17 +51,14 @@ vizro_ai = VizroAI(model="") ``` === "MistralAI" + _Currently works only for `VizroAI.plot` - we are working on making it available for `VizroAI.dashboard`_ - _Currently works only for `VizroAI.plot` - we are working on making it available for `VizroAI.dashboard`_ - - | Env variable | Name(s) | - | ----------- | ------------------------------------ | - | API key | `MISTRAL_API_KEY` | - | Base API URL | `MISTRAL_BASE_URL` | + | Environment variable | Name(s) | + | -------------------- | ------------------ | + | API key | `MISTRAL_API_KEY` | + | Base API URL | `MISTRAL_BASE_URL` | - To use Mistral with Vizro-AI, you can either use their API, which comes with [an associated cost](https://mistral.ai/technology/#pricing), or you could use their models for free under the Apache 2.0 license. In that case you need to setup the model API yourself. You can check [all available Mistral models including pricing on their website](https://docs.mistral.ai/getting-started/models/models_overview). This will also explain which version the below string acronyms currently point to. - Before usage, please review Mistral guidelines on risk mitigation to understand potential model limitations and best practices. - [See the Mistral site for more details on responsible usage](https://help.mistral.ai/en/collections/272960-le-chat/). + To use Mistral with Vizro-AI, you can either use their API, which comes with [an associated cost](https://mistral.ai/technology/#pricing), or you could use their models for free under the Apache 2.0 license. In that case you need to setup the model API yourself. You can check [all available Mistral models including pricing on their website](https://docs.mistral.ai/getting-started/models/models_overview). This will also explain which version the below string acronyms currently point to. Before usage, please review Mistral guidelines on risk mitigation to understand potential model limitations and best practices. [See the Mistral site for more details on responsible usage](https://help.mistral.ai/en/collections/272960-le-chat/). - `mistral-large-latest` - `open-mistral-nemo` @@ -79,18 +72,23 @@ vizro_ai = VizroAI(model="") At the time of writing, we found that even the best Mistral models struggled to produce more than the simplest charts, but these outcomes can change drastically overtime. -!!!note +!!! note When choosing the string representation, it sometimes can be tricky to have the correct environment variable set for the API key (and potential base URL). In case you cannot get this to work, we recommend instantiating the model directly (see below) and providing the API key via the models parameters. + ### Setting model via class for additional configuration + + Beyond passing a string, you can pass **any** model derived from [Langchain's `BaseChatModel` class](https://api.python.langchain.com/en/latest/language_models/langchain_core.language_models.chat_models.BaseChatModel.html#langchain_core.language_models.chat_models.BaseChatModel) that has the [`with_structured_output` method](https://python.langchain.com/v0.2/docs/how_to/structured_output/#the-with_structured_output-method) implemented. An overview of the [most common vendor models supporting this functionality](https://python.langchain.com/v0.2/docs/integrations/chat/) can be found in Langchain's documentation. When choosing this approach, you can customize your model beyond the chosen default from the string instantiation. The choice of available arguments depends on the specific vendor implementation, but usually the main parameter to tweak is the temperature. + To ensure a deterministic answer to our queries, we've set the temperature to 0 in the string instantiation. If you prefer more creative (but potentially more unstable) responses, you can raise the temperature to a maximum of 1. + Below you can find an example where a custom model is instantiated with various custom parameters. Note that we manually set the API key and base URL, which is the easiest way to get it set up. diff --git a/vizro-ai/docs/pages/user-guides/install.md b/vizro-ai/docs/pages/user-guides/install.md index 9ecee589f..8feda60d0 100644 --- a/vizro-ai/docs/pages/user-guides/install.md +++ b/vizro-ai/docs/pages/user-guides/install.md @@ -4,12 +4,11 @@ In this guide you'll learn how to set up the prerequisites needed for Vizro-AI, Vizro-AI supports macOS, Linux, and Windows. It works with Python 3.9 and later. You can specify the version of Python to use with Vizro-AI when you set up a virtual environment. - ## Set up a virtual environment + You should create a virtual environment for each Vizro-AI project you work on to isolate its Python dependencies from those of other projects. See the following references to learn more about [Python virtual environments](https://realpython.com/python-virtual-environments-a-primer/), [Conda virtual environments](https://docs.conda.io/projects/conda/en/latest/user-guide/getting-started.html#starting-conda) or [watch an explainer video about them](https://youtu.be/YKfAwIItO7M). ??? information "How to create a virtual environment for your Vizro-AI project" - The simplest way to create a virtual environment in Python is `venv`, which is included in the Python standard library. Create a directory for your project and navigate to it. For example: ```bash @@ -18,6 +17,7 @@ You should create a virtual environment for each Vizro-AI project you work on to ``` Next, create and activate a new virtual environment in this directory with `venv`: + ```bash python3 -m venv .venv source .venv/bin/activate @@ -62,18 +62,17 @@ You should see a return output of the form `x.y.z`. Vizro-AI supports **any** model that is available via [Langchain's `BaseChatModel` class](https://api.python.langchain.com/en/latest/language_models/langchain_core.language_models.chat_models.BaseChatModel.html#langchain_core.language_models.chat_models.BaseChatModel), and that has the [`with_structured_output` method](https://python.langchain.com/v0.2/docs/how_to/structured_output/#the-with_structured_output-method) implemented. An overview of the [most common vendor models supporting this functionality](https://python.langchain.com/v0.2/docs/integrations/chat/) can be found in Langchain's documentation. - ### Set up access to OpenAI (as an example for any vendor) + 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. __Method 1: Set an environment variable for a single project__ -To make the API key available for a single project, you can create a local `.env` -file to store it. Then, you can load the API key from that `.env` file in your development environment. +To make the API key available for a single project, you can create a local `.env` file to store it. Then, you can load the API key from that `.env` file in your development environment. The `.env` file should look as follows (containing your key rather than `abc123`): @@ -86,8 +85,7 @@ By default, `vizro-ai` automatically loads the `.env` file, by searching the cur If you would like to customize the `.env` file location and name, you can manually customize the search to override the default and specify the path and name of a custom `.env` file. ??? example "How to override the default location of the .`env` file:" - - ```py + ```python from dotenv import load_dotenv, find_dotenv from pathlib import Path @@ -100,33 +98,26 @@ If you would like to customize the `.env` file location and name, you can manual # Load the specified .env file load_dotenv(env_file) ``` + Refer to [Python-dotenv documentation](https://saurabh-kumar.com/python-dotenv/reference/) for further information. !!! warning "Don't share your secret API key!" - You should avoid committing the `.env` file to version control. You can do this for Git by adding `.env` to your `.gitignore` file. - __Method 2: Set an environment variable for all projects__ -To make the OpenAI API key available for all projects, you can set it as a system environment -variable. Refer to the section ["Set up your API key for all projects"](https://platform.openai.com/docs/quickstart/step-2-setup-your-api-key?context=python) -in the OpenAI documentation. (It is under the dropdown of "Step 2: Set up your API key"). +To make the OpenAI API key available for all projects, you can set it as a system environment variable. Refer to the section ["Set up your API key for all projects"](https://platform.openai.com/docs/quickstart/step-2-setup-your-api-key?context=python) in the OpenAI documentation. (It is under the dropdown of "Step 2: Set up your API key"). -The documentation gives step-by-step instructions for setting up the API key as an environment -variable, on operating systems including Windows and MacOS. +The documentation gives step-by-step instructions for setting up the API key as an environment variable, on operating systems including Windows and MacOS. -!!!note +!!! note Sometimes setting up the `.env` file can be fiddly. If necessary, you can supply the API key directly to the instantiated model. See [our user guide](./customize-vizro-ai.md#setting-model-via-class-for-additional-configuration) for this option. Remember not to commit this API key to any public space! __Set the base URL (optional)__ You might need to give the base URL if you are using a custom OpenAI resource endpoint. -The API base URL used for the OpenAI connector is set to `https://api.openai.com/v1` by default. -If you are using a custom API endpoint, for example, if your organization has a designated API gateway, -you can change the base URL by setting it as an environment variable. - +The API base URL used for the OpenAI connector is set to `https://api.openai.com/v1` by default. If you are using a custom API endpoint, for example, if your organization has a designated API gateway, you can change the base URL by setting it as an environment variable. Follow the approach above in Method 2 to add the environment variable `OPENAI_API_BASE` for use by all projects. diff --git a/vizro-ai/docs/pages/user-guides/retrieve-dashboard-code.md b/vizro-ai/docs/pages/user-guides/retrieve-dashboard-code.md deleted file mode 100644 index a95d1159a..000000000 --- a/vizro-ai/docs/pages/user-guides/retrieve-dashboard-code.md +++ /dev/null @@ -1,139 +0,0 @@ -# Retrieve the code of Vizro-AI generated dashboard - -This guide shows how to retrieve the code of a Vizro-AI generated dashboard. The code can be used for further iterations, improvements, and deployment. - -While Vizro-AI can follow complex user requirements well and generate high-quality dashboards, due to the nature of LLMs, the generated dashboards often only partly match user expectations. Besides refining the user prompt and re-running Vizro-AI, you can also extract the code and iterate manually to achieve the desired result. - - -## 1. Prepare the data and user prompt -```py -import vizro.plotly.express as px - -df = px.data.tips() - -user_question = """ -Create a one-page dashboard layout with the following components: - -1. Card: - - Position: Left of the page - - Size: Takes up 1/4 of the total page width - - Content: Display the text "This is Tips dataset" - -2. Table: - - Position: Right of the card - - Size: Takes up the remaining 3/4 of the page width - - Content: Display the Tips dataset -""" -``` - -## 2. Generate and launch the dashboard -```py -from vizro_ai import VizroAI - -vizro_ai = VizroAI(model="gpt-4o-mini") -result = vizro_ai.dashboard([df], user_question, return_elements=True) -``` -This triggers the dashboard building process. Once Vizro-AI finishes the dashboard generation process, you can now launch the dashboard. - -!!! example "Generated dashboard" - - === "Code" - ```py - import vizro.plotly.express as px - from vizro_ai import VizroAI - from vizro import Vizro - - df = px.data.tips() - user_question = """ - Create a one-page dashboard layout with the following components: - - 1. Card: - - Position: Left of the page - - Size: Takes up 1/4 of the total page width - - Content: Display the text "This is Tips dataset" - - 1. Table: - - Position: Right of the card - - Size: Takes up the remaining 3/4 of the page width - - Content: Display the Tips dataset - """ - vizro_ai = VizroAI(model="gpt-4o-mini") - result = vizro_ai.dashboard([df], user_question, return_elements=True) - Vizro().build(result.dashboard).run() - ``` - - === "Result" - [![VizroAIDashboardPage1]][VizroAIDashboardPage1] - - - [VizroAIDashboardPage1]: ../../assets/user_guides/dashboard/dashboard2_page1.png - -## 3. Retrieve the Python code of the dashboard -!!! example "View dashboard code" - - === "Code" - ```py - import vizro.plotly.express as px - from vizro_ai import VizroAI - from vizro import Vizro - - df = px.data.tips() - user_question = """ - Create a one-page dashboard layout with the following components: - - 1. Card: - - Position: Left of the page - - Size: Takes up 1/4 of the total page width - - Content: Display the text "This is Tips dataset" - - 1. Table: - - Position: Right of the card - - Size: Takes up the remaining 3/4 of the page width - - Content: Display the Tips dataset - """ - vizro_ai = VizroAI(model="gpt-4o-mini") - result = vizro_ai.dashboard([df], user_question, return_elements=True) - - print(result.code) - ``` - === "Result" - ```py - ######## Module Imports ########## - from vizro import Vizro - from vizro.managers import data_manager - from vizro.models.types import capture - import vizro.models as vm - from vizro.tables import dash_ag_grid - - ########## Data Imports ########## - #####!!! UNCOMMENT BELOW !!!###### - # data_manager["restaurant_bills"] = ===> Fill in here <=== - - ###### Callable definitions ###### - - - ########## Object code ########### - dashboard = vm.Dashboard( - pages=[ - vm.Page( - id="Tips Data Visualization", - components=[ - vm.Card( - id="tips_card_tips_data_visualization", - type="card", - text="This is Tips dataset", - href="", - ), - vm.AgGrid( - id="tips_table_tips_data_visualization", - figure=dash_ag_grid(data_frame="restaurant_bills"), - ), - ], - title="Tips Data Visualization", - layout=vm.Layout(grid=[[0, 1, 1, 1]]), - controls=[], - ) - ], - title="Tips Dataset Overview", - ) - ``` diff --git a/vizro-ai/docs/pages/user-guides/run-vizro-ai-dashboard.md b/vizro-ai/docs/pages/user-guides/run-vizro-ai-dashboard.md new file mode 100644 index 000000000..f9d5944c9 --- /dev/null +++ b/vizro-ai/docs/pages/user-guides/run-vizro-ai-dashboard.md @@ -0,0 +1,166 @@ +# How to run Vizro-AI dashboard + +This guide offers insights into different ways of running `VizroAI.dashboard` to generate a Vizro dashboards from natural language prompts. + +??? note "Note: API key" + Make sure you have followed the [LLM setup guide](../user-guides/install.md#set-up-access-to-a-large-language-model) and that your API key is set up in a `.env` file in the same folder as your Notebook file (`.ipynb`). + +## Run Vizro-AI dashboard + +!!! example "Generated dashboard" + === "Prompt" + ```py + import vizro.plotly.express as px + from vizro_ai import VizroAI + from vizro import Vizro + + df = px.data.tips() + user_question = """ + Create a one-page dashboard layout with the following components: + + 1. Card: + - Position: Left of the page + - Size: Takes up 1/4 of the total page width + - Content: Display the text "This is Tips dataset" + + 1. Table: + - Position: Right of the card + - Size: Takes up the remaining 3/4 of the page width + - Content: Display the Tips dataset + """ + vizro_ai = VizroAI(model="gpt-4o-mini") + dashboard = vizro_ai.dashboard([df], user_question) + Vizro().build(dashboard).run() + ``` + + === "Result" + ![VizroAIDashboardPage1](../../assets/user_guides/dashboard/dashboard2_page1.png) + +This triggers the dashboard building process. Once Vizro-AI finishes the dashboard generation process, you can now launch the dashboard. + +## Retrieve the Python code of the dashboard + +To illustrate the process, lets use the example above. + + + +Like the `VizroAI.plot` method, in order to produce more comprehensive output we need to set `return_elements=True`. `return_elements` is a boolean (by default `False`) which determines the return type of `VizroAI.dashboard`. + +- If set to `False` it produces a `Vizro` dashboard object. +- If set to `True`, it returns a class (a Pydantic model) containing both the dashboard object and the code string used to generate it. + +!!! example "View dashboard code" + === "Prompt" + ```py + import vizro.plotly.express as px + from vizro_ai import VizroAI + from vizro import Vizro + + df = px.data.tips() + user_question = """ + Create a one-page dashboard layout with the following components: + + 1. Card: + - Position: Left of the page + - Size: Takes up 1/4 of the total page width + - Content: Display the text "This is Tips dataset" + + 1. Table: + - Position: Right of the card + - Size: Takes up the remaining 3/4 of the page width + - Content: Display the Tips dataset + """ + vizro_ai = VizroAI(model="gpt-4o-mini") + result = vizro_ai.dashboard([df], user_question, return_elements=True) + + print(result.code) + ``` + + === "Resulting code" + ```py + ######## Module Imports ########## + from vizro import Vizro + from vizro.managers import data_manager + from vizro.models.types import capture + import vizro.models as vm + from vizro.tables import dash_ag_grid + + ########## Data Imports ########## + #####!!! UNCOMMENT BELOW !!!###### + # data_manager["restaurant_bills"] = ===> Fill in here <=== + + ###### Callable definitions ###### + + + ########## Object code ########### + dashboard = vm.Dashboard( + pages=[ + vm.Page( + id="Tips Data Visualization", + components=[ + vm.Card( + id="tips_card_tips_data_visualization", + type="card", + text="This is Tips dataset", + href="", + ), + vm.AgGrid( + id="tips_table_tips_data_visualization", + figure=dash_ag_grid(data_frame="restaurant_bills"), + ), + ], + title="Tips Data Visualization", + layout=vm.Layout(grid=[[0, 1, 1, 1]]), + controls=[], + ) + ], + title="Tips Dataset Overview", + ) + ``` + +To use the above code, you will still need to add three simple steps: + +- Import your data. + + ```py + data = pd.read_csv('data.csv') # Replace 'data.csv' with your filename or path to your data + ``` + +- After importing your data, register it in the data manager by uncommenting the data manager instance and assigning the imported data to it. See the Vizro guide on [connecting dashboard to data](https://vizro.readthedocs.io/en/stable/pages/user-guides/data/#reference-by-name/). + + ```py + data_manager["restaurant_bills"] = data + ``` + +- Launch the dashboard by adding the code below at the end of the file: + + ```py + Vizro().build(dashboard).run() + ``` + +Detailed guidance is provided in [dashboard generation tutorial](https://vizro.readthedocs.io/projects/vizro-ai/en/latest/pages/tutorials/quickstart/). + +## Available Vizro components + +The following list is a table of the Vizro components currently supported by Vizro-AI. This list is not exhaustive, and we are actively working on adding more features to Vizro-AI. + +| Feature type | Feature | Availability | +| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ------------ | +| **Components** | [Graph](https://vizro.readthedocs.io/en/stable/pages/user-guides/graph/) | ✔ | +| | [AG Grid](https://vizro.readthedocs.io/en/stable/pages/user-guides/table/#ag-grid) | ✔ | +| | [Card](https://vizro.readthedocs.io/en/stable/pages/user-guides/card-button/) | ✔ | +| | [Button](https://vizro.readthedocs.io/en/stable/pages/user-guides/card-button/) | ✖ | +| | [Tabs](https://vizro.readthedocs.io/en/stable/pages/user-guides/tabs/) | ✖ | +| | [Containers](https://vizro.readthedocs.io/en/stable/pages/user-guides/container/) | ✖ | +| **Controls** | [Filter](https://vizro.readthedocs.io/en/stable/pages/user-guides/filters/) | ✔ | +| | [Parameter](https://vizro.readthedocs.io/en/stable/pages/user-guides/parameters/) | ✖ | +| **Navigation** | [Default navigation](https://vizro.readthedocs.io/en/stable/pages/user-guides/navigation/#use-the-default-navigation) | ✔ | +| | [Custom navigation](https://vizro.readthedocs.io/en/stable/pages/user-guides/navigation/#customize-the-navigation-bar) | ✖ | +| **Layout** | [Layout](https://vizro.readthedocs.io/en/stable/pages/user-guides/layouts/) | ✔ | +| **Dashboard header** | [Dashboard title](https://vizro.readthedocs.io/en/stable/pages/user-guides/dashboard/) | ✔ | +| | [Logo](https://vizro.readthedocs.io/en/stable/pages/user-guides/dashboard/) | ✖ | +| **Actions** | [Pre-defined actions](https://vizro.readthedocs.io/en/stable/pages/user-guides/actions/#pre-defined-actions/) | ✖ | +| | [Filter interaction between charts](https://vizro.readthedocs.io/en/stable/pages/user-guides/actions/#filter-data-by-clicking-on-chart/) | ✖ | +| | [Custom actions](https://vizro.readthedocs.io/en/stable/pages/user-guides/actions/#custom-actions/) | ✖ | + +If a feature you need for your dashboard isn't currently supported by Vizro-AI you can retrieve the dashboard code and add it by hand before running the dashboard. diff --git a/vizro-ai/docs/pages/user-guides/run-vizro-ai.md b/vizro-ai/docs/pages/user-guides/run-vizro-ai.md index 192df9ea5..0c392dbed 100644 --- a/vizro-ai/docs/pages/user-guides/run-vizro-ai.md +++ b/vizro-ai/docs/pages/user-guides/run-vizro-ai.md @@ -2,19 +2,18 @@ This guide offers insights into different ways of running `VizroAI.plot` to generate Plotly charts. We cover how to use: -* [a Jupyter Notebook](#jupyter-notebook) -* [a Python script](#python-script) -* [integration into an application](#application-integration) +- [a Jupyter Notebook](#jupyter-notebook) +- [a Python script](#python-script) +- [integration into an application](#application-integration) ## Jupyter Notebook + To run Vizro-AI code in a Jupyter Notebook, create a new cell and execute the code below to render the described visualization as output. ??? note "Note: API key" - - Make sure you have followed the [LLM setup guide](../user-guides/install.md#set-up-access-to-a-large-language-model) and thatyour api key is set up in a `.env` file in the same folder as your Notebook file (`.ipynb`). + Make sure you have followed the [LLM setup guide](../user-guides/install.md#set-up-access-to-a-large-language-model) and that your api key is set up in a `.env` file in the same folder as your Notebook file (`.ipynb`). !!! example "Ask Vizro-AI to generate a bar chart" - === "Code for the cell" ```py import vizro.plotly.express as px @@ -25,18 +24,17 @@ To run Vizro-AI code in a Jupyter Notebook, create a new cell and execute the co df = px.data.gapminder() vizro_ai.plot(df, "visualize the life expectancy per continent and color each continent") ``` - === "Result" - [![BarChart]][BarChart] - [BarChart]: ../../assets/user_guides/bar_chart_gdp_per_continent.png + === "Result" + [![BarChart]][barchart] Note that if you run this code, its appearance may not precisely resemble the one displayed, as it is generated by a generative AI and can vary. ## Python script + You can use Vizro-AI in any standard development environment by creating a `.py` file and executing the code, which displays the output in a browser window. As Vizro-AI returns the `fig` object, you need to append `fig.show()` to the response object. ??? note "Note: API key" - Make sure you have followed [LLM setup guide](../user-guides/install.md#set-up-access-to-a-large-language-model) and that your API key is set up in the environment where your `.py` script is running with command as below: ```bash @@ -44,7 +42,6 @@ You can use Vizro-AI in any standard development environment by creating a `.py` ``` !!! example "Ask Vizro-AI to generate a line chart" - === "example.py" ```py import vizro.plotly.express as px @@ -56,13 +53,13 @@ You can use Vizro-AI in any standard development environment by creating a `.py` fig = vizro_ai.plot(df, "describe life expectancy per continent over time") fig.show() ``` - === "Result" - [![LineChart]][LineChart] - [LineChart]: ../../assets/user_guides/line_chart_life_expect.png + === "Result" + [![LineChart]][linechart] ## Application integration -You may prefer to integrate Vizro-AI into an application that offers a UI for users to input prompts. For that you can take the `fig` object from the `.plot` call, and use it elsewhere -in any application (you might also want to process it further, for example, by serializing it or similar). It is also possible to obtain the code that would produce the object. For any advanced usage options, refer to -[our advanced options guide](advanced-options.md). +You may prefer to integrate Vizro-AI into an application that offers a UI for users to input prompts. For that you can take the `fig` object from the `.plot` call, and use it elsewhere in any application (you might also want to process it further, for example, by serializing it or similar). It is also possible to obtain the code that would produce the object. For any advanced usage options, refer to [our advanced options guide](advanced-options.md). + +[barchart]: ../../assets/user_guides/bar_chart_gdp_per_continent.png +[linechart]: ../../assets/user_guides/line_chart_life_expect.png diff --git a/vizro-ai/docs/pages/user-guides/use-different-languages.md b/vizro-ai/docs/pages/user-guides/use-different-languages.md index ff797f1fc..7c222fa37 100644 --- a/vizro-ai/docs/pages/user-guides/use-different-languages.md +++ b/vizro-ai/docs/pages/user-guides/use-different-languages.md @@ -1,8 +1,8 @@ # Generate visualizations using different languages + Vizro-AI is versatile, supporting prompts and chart visualizations in languages other than English. In this guide you will explore this capability with two examples, starting with Chinese where we inquire about visualizing the GDP per capita over time. !!! example "Vizro-AI Chinese" - === "Code for the cell" ```py from vizro_ai import VizroAI @@ -14,15 +14,13 @@ Vizro-AI is versatile, supporting prompts and chart visualizations in languages fig = vizro_ai.plot(df, "请画一个世界年均GDP的趋势图") fig.show() ``` - === "Result" - [![ChineseChart]][ChineseChart] - [ChineseChart]: ../../assets/tutorials/chart/ChineseExample.png + === "Result" + [![ChineseChart]][chinesechart] Subsequently, we'll switch to German and prompt the visualization of life expectancy in the United States over time, comparing it to the global life expectancy trend. For this example, we'll include `return_elements=True` to obtain comprehensive insights into both the data and the generated code. !!! example "Vizro-AI German" - === "Code for the cell" ```py from vizro_ai import VizroAI @@ -36,7 +34,9 @@ Subsequently, we'll switch to German and prompt the visualization of life expect print(f"Code:\n{result.code_explanation}\n{result.code_vizro}\n" ) result.get_fig_object(df).show() ``` + === "Result" - [![GermanChart]][GermanChart] + [![GermanChart]][germanchart] - [GermanChart]: ../../assets/tutorials/chart/GermanExample.png +[chinesechart]: ../../assets/tutorials/chart/ChineseExample.png +[germanchart]: ../../assets/tutorials/chart/GermanExample.png diff --git a/vizro-ai/docs/pages/user-guides/vizro-ai-langchain-guide.md b/vizro-ai/docs/pages/user-guides/vizro-ai-langchain-guide.md index 210f36410..b43653356 100644 --- a/vizro-ai/docs/pages/user-guides/vizro-ai-langchain-guide.md +++ b/vizro-ai/docs/pages/user-guides/vizro-ai-langchain-guide.md @@ -3,9 +3,9 @@ You can use Vizro-AI's functionality within a larger LangChain application. This guide shows how to integrate Vizro-AI's chart and dashboard generation capabilities as LangChain tools. Here are the steps you need to take: 1. [Set up the environment](#1-set-up-the-environment) -2. [Define LangChain tools](#2-define-langchain-tools) -3. [Set up the tool chain](#3-set-up-the-tool-chain) -4. [Use the chain](#4-use-the-chain) +1. [Define LangChain tools](#2-define-langchain-tools) +1. [Set up the tool chain](#3-set-up-the-tool-chain) +1. [Use the chain](#4-use-the-chain) ## 1. Set up the environment @@ -51,6 +51,7 @@ def get_plot_code(df: Annotated[Any, InjectedToolArg], question: str) -> str: ) return plot_elements.code_vizro + @tool(parse_docstring=True) def get_dashboard_code(dfs: Annotated[Any, InjectedToolArg], question: str) -> str: """Generate the dashboard code. @@ -80,6 +81,7 @@ Create a chain that handles tool execution and data injection: tools = [get_plot_code, get_dashboard_code] llm_with_tools = llm.bind_tools(tools) + # Create data injection chain @chain def inject_df(ai_msg): @@ -95,13 +97,16 @@ def inject_df(ai_msg): tool_calls.append(tool_call_copy) return tool_calls + # Create tool router tool_map = {tool.name: tool for tool in tools} + @chain def tool_router(tool_call): return tool_map[tool_call["name"]] + # Combine chains chain = llm_with_tools | inject_df | tool_router.map() ``` @@ -111,26 +116,25 @@ chain = llm_with_tools | inject_df | tool_router.map() Now you can use the chain to generate charts or dashboards based on natural language queries. The chain will generate code that you can use to create visualizations. !!! example "Generate chart code" - === "Code" - ```py + ```python # Load sample data df = px.data.gapminder() plot_response = chain.invoke("Plot GDP per capita for each continent") print(plot_response[0].content) ``` + === "Vizro-AI Generated Code" - ```py + ```python import plotly.graph_objects as go from vizro.models.types import capture + @capture("graph") def custom_chart(data_frame): continent_gdp = data_frame.groupby("continent")["gdpPercap"].mean().reset_index() - fig = go.Figure( - data=[go.Bar(x=continent_gdp["continent"], y=continent_gdp["gdpPercap"])] - ) + fig = go.Figure(data=[go.Bar(x=continent_gdp["continent"], y=continent_gdp["gdpPercap"])]) fig.update_layout( title="GDP per Capita by Continent", xaxis_title="Continent", @@ -140,16 +144,18 @@ Now you can use the chain to generate charts or dashboards based on natural lang ``` !!! example "Generate dashboard code" - === "Code" - ```py + ```python dfs = [px.data.gapminder()] - dashboard_response = chain.invoke("Create a dashboard. This dashboard has a chart showing the correlation between gdpPercap and lifeExp.") + dashboard_response = chain.invoke( + "Create a dashboard. This dashboard has a chart showing the correlation between gdpPercap and lifeExp." + ) print(dashboard_response[0].content) ``` + === "Vizro-AI Generated Code" - ```py + ```python ############ Imports ############## import vizro.models as vm from vizro.models.types import capture @@ -160,9 +166,7 @@ Now you can use the chain to generate charts or dashboards based on natural lang @capture("graph") def gdp_life_exp_graph(data_frame): fig = go.Figure() - fig.add_trace( - go.Scatter(x=data_frame["gdpPercap"], y=data_frame["lifeExp"], mode="markers") - ) + fig.add_trace(go.Scatter(x=data_frame["gdpPercap"], y=data_frame["lifeExp"], mode="markers")) fig.update_layout( title="GDP per Capita vs Life Expectancy", xaxis_title="GDP per Capita", diff --git a/vizro-ai/examples/dashboard_ui/app.py b/vizro-ai/examples/dashboard_ui/app.py index 48dc43d21..aa4f27a2b 100644 --- a/vizro-ai/examples/dashboard_ui/app.py +++ b/vizro-ai/examples/dashboard_ui/app.py @@ -15,6 +15,7 @@ CodeClipboard, CustomDashboard, DropdownMenu, + FlexContainer, HeaderComponent, Icon, Modal, @@ -47,14 +48,10 @@ vm.Container.add_type("components", ToggleSwitch) vm.Container.add_type("components", UserPromptTextArea) vm.Container.add_type("components", DropdownMenu) -vm.Container.add_type("components", HeaderComponent) +vm.Page.add_type("components", HeaderComponent) +vm.Page.add_type("components", FlexContainer) -vm.Page.add_type("components", UserUpload) -vm.Page.add_type("components", MyDropdown) -vm.Page.add_type("components", OffCanvas) -vm.Page.add_type("components", CodeClipboard) -vm.Page.add_type("components", Icon) -vm.Page.add_type("components", Modal) +FlexContainer.add_type("components", DropdownMenu) SUPPORTED_MODELS = { @@ -79,120 +76,102 @@ title="Vizro-AI - create interactive charts with Plotly and Vizro", layout=vm.Layout( grid=[ - [4, 4, 4, 4], - [2, 2, 1, 1], - [2, 2, 1, 1], - [3, 3, 1, 1], - [3, 3, 1, 1], - [3, 3, 1, 1], - *[[0, 0, 1, 1]] * 8, + *[[0, 0, 0, 0]] * 1, + *[[1, 1, 2, 2]] * 11, ] ), components=[ - vm.Container( - title="", - components=[CodeClipboard(id="plot"), ToggleSwitch(id="toggle-id")], - layout=vm.Layout( - grid=[*[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]] * 7, [-1, -1, -1, -1, -1, -1, -1, -1, -1, 1, 1, 1]], - row_gap="12px", - col_gap="12px", - ), - ), - vm.Container( - title="", - layout=vm.Layout( - grid=[ - *[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]] * 10, - [-1, -1, -1, -1, -1, -1, -1, -1, -1, 1, 1], - ] - ), - components=[ - vm.Graph(id="graph-id", figure=px.scatter(pd.DataFrame())), - DropdownMenu(id="dropdown-menu"), - ], - ), - vm.Container( - id="upload-data-container", - title="Turn your data into visuals — just upload, describe, and see your chart in action", - layout=vm.Layout( - grid=[ - [1], - [0], - ], - row_gap="0px", - # row_min_height="40px", - ), + HeaderComponent(), + FlexContainer( components=[ - UserUpload( - id="data-upload-id", - actions=[ - vm.Action( - function=data_upload_action(), - inputs=["data-upload-id.contents", "data-upload-id.filename"], - outputs=["data-store-id.data", "modal-table-icon.style", "modal-table-tooltip.style"], - ), - vm.Action( - function=display_filename(), - inputs=["data-store-id.data"], - outputs=["upload-message-id.children"], - ), - vm.Action( - function=update_table(), - inputs=["data-store-id.data"], - outputs=["modal-table.children", "modal-title.children"], + vm.Container( + id="upload-data-container", + title="Turn your data into visuals — just upload, describe, and see your chart in action", + layout=vm.Layout( + grid=[[0], [1]], + row_gap="0px", + ), + components=[ + vm.Figure(id="show-data-component", figure=custom_table(data_frame=pd.DataFrame())), + UserUpload( + id="data-upload-component", + actions=[ + vm.Action( + function=data_upload_action(), + inputs=["data-upload-component.contents", "data-upload-component.filename"], + outputs=["data-store.data", "modal-table-icon.style", "modal-table-tooltip.style"], + ), + vm.Action( + function=display_filename(), + inputs=["data-store.data"], + outputs=["upload-message.children"], + ), + vm.Action( + function=update_table(), + inputs=["data-store.data"], + outputs=["modal-table.children", "modal-title.children"], + ), + ], ), ], ), - vm.Figure(id="show-data-component", figure=custom_table(data_frame=pd.DataFrame())), - ], - ), - vm.Container( - title="", - layout=vm.Layout( - grid=[ - [3, 3, 3, 3, 3, 3, 3, 3, 3], - [3, 3, 3, 3, 3, 3, 3, 3, 3], - [3, 3, 3, 3, 3, 3, 3, 3, 3], - [2, -1, -1, -1, -1, 1, 1, 0, 0], - ], - row_gap="10px", - col_gap="4px", - ), - components=[ - vm.Button( - id="trigger-button-id", - text="Run Vizro-AI", - actions=[ - vm.Action( - function=run_vizro_ai(), - inputs=[ - "text-area-id.value", - "trigger-button-id.n_clicks", - "data-store-id.data", - "model-dropdown-id.value", - "settings-api-key.value", - "settings-api-base.value", - "settings-dropdown.value", + vm.Container( + title="", + layout=vm.Layout( + grid=[ + *[[0, 0, 0, 0, 0, 0, 0, 0, 0]] * 3, + [3, -1, -1, -1, -1, 1, 1, 2, 2], + ], + row_gap="12px", + col_gap="4px", + ), + components=[ + UserPromptTextArea(id="text-area"), + MyDropdown( + options=SUPPORTED_MODELS["OpenAI"], value="gpt-4o-mini", multi=False, id="model-dropdown" + ), + vm.Button( + id="trigger-button", + text="Run Vizro-AI", + actions=[ + vm.Action( + function=run_vizro_ai(), + inputs=[ + "text-area.value", + "trigger-button.n_clicks", + "data-store.data", + "model-dropdown.value", + "settings-api-key.value", + "settings-api-base.value", + "settings-dropdown.value", + ], + outputs=["plot-code-markdown.children", "graph.figure", "code-output-store.data"], + ), ], - outputs=["plot-code-markdown.children", "graph-id.figure", "code-output-store-id.data"], + ), + OffCanvas( + id="settings", + options=["OpenAI", "Anthropic", "Mistral", "xAI"], + value="OpenAI", ), ], ), - MyDropdown( - options=SUPPORTED_MODELS["OpenAI"], value="gpt-4o-mini", multi=False, id="model-dropdown-id" - ), - OffCanvas( - id="settings", - options=["OpenAI", "Anthropic", "Mistral", "xAI"], - value="OpenAI", + vm.Container( + title="", + components=[CodeClipboard(id="plot"), ToggleSwitch()], + layout=vm.Layout( + grid=[*[[0]] * 7, [1]], + row_gap="12px", + col_gap="12px", + ), ), - UserPromptTextArea(id="text-area-id"), - # Modal(id="modal"), - ], + ] ), - vm.Container( - title="", - components=[HeaderComponent()], + FlexContainer( + components=[ + vm.Graph(id="graph", figure=px.scatter(pd.DataFrame())), + DropdownMenu(id="dropdown-menu"), + ], ), ], ) @@ -206,7 +185,7 @@ @callback( Output("settings", "is_open"), - Input("open-settings-id", "n_clicks"), + Input("open-settings", "n_clicks"), [State("settings", "is_open")], ) def open_settings(n_clicks, is_open): @@ -235,7 +214,7 @@ def show_api_base(value): @callback( Output("plot-code-markdown", "children"), Input("toggle-switch", "value"), - [State("code-output-store-id", "data")], + [State("code-output-store", "data")], ) def toggle_code(value, data): """Callback for switching between vizro and plotly code.""" @@ -253,7 +232,7 @@ def toggle_code(value, data): Output("data-modal", "is_open"), Input("modal-table-icon", "n_clicks"), State("data-modal", "is_open"), - State("data-store-id", "data"), + State("data-store", "data"), ) def open_modal(n_clicks, is_open, data): """Callback for opening modal component.""" @@ -267,7 +246,7 @@ def open_modal(n_clicks, is_open, data): @callback( Output("download-file", "data"), [Input("dropdown-menu-html", "n_clicks"), Input("dropdown-menu-json", "n_clicks")], - State("code-output-store-id", "data"), + State("code-output-store", "data"), prevent_initial_call=True, ) def download_fig(n_clicks_html, n_clicks_json, data): @@ -290,9 +269,7 @@ def download_fig(n_clicks_html, n_clicks_json, data): return dcc.send_string(plotly_json, "plotly_fig.json") -@callback( - [Output("model-dropdown-id", "options"), Output("model-dropdown-id", "value")], Input("settings-dropdown", "value") -) +@callback([Output("model-dropdown", "options"), Output("model-dropdown", "value")], Input("settings-dropdown", "value")) def update_model_dropdown(value): """Callback for updating available models.""" available_models = SUPPORTED_MODELS[value] @@ -302,21 +279,13 @@ def update_model_dropdown(value): app = Vizro().build(dashboard) app.dash.layout.children.append( - html.Div( - [ - html.Div( - [ - "Made using ", - html.Img(src=get_asset_url("logo.svg"), id="banner", alt="Vizro logo"), - dbc.NavLink("vizro", href="https://github.com/mckinsey/vizro", target="_blank"), - ], - style={"display": "flex", "flexDirection": "row"}, - ), - ], + dbc.NavLink( + ["Made with ", html.Img(src=get_asset_url("logo.svg"), id="banner", alt="Vizro logo"), "vizro"], + href="https://github.com/mckinsey/vizro", + target="_blank", className="anchor-container", ) ) - server = app.dash.server if __name__ == "__main__": app.run() diff --git a/vizro-ai/examples/dashboard_ui/assets/custom_css.css b/vizro-ai/examples/dashboard_ui/assets/custom.css similarity index 69% rename from vizro-ai/examples/dashboard_ui/assets/custom_css.css rename to vizro-ai/examples/dashboard_ui/assets/custom.css index dea230d73..cddc9eaac 100644 --- a/vizro-ai/examples/dashboard_ui/assets/custom_css.css +++ b/vizro-ai/examples/dashboard_ui/assets/custom.css @@ -3,12 +3,12 @@ } .card-body { - color: var(--text-light-mode-secondary); + color: var(--text-secondary-inverted); } .textbox { border-radius: 24px; - font-size: var(--text-size-02); + font-size: 0.875rem; margin-bottom: 20px; max-width: 60%; padding: 4px 12px; @@ -17,16 +17,16 @@ .user_input:focus { background: var(--field-enabled); - box-shadow: 0 0 0 2px var(--focus-focus) inset; + box-shadow: 0 0 0 2px var(--focus) inset; color: var(--text-primary); outline-width: 0; } -#text-area-id { +#text-area { background-color: inherit; - border: 1px solid var(--border-subtle-alpha-01); + border: 1px solid var(--border-subtleAlpha01); color: var(--text-primary); - min-height: 13.5vh; + min-height: 14vh; padding: 8px; width: 100%; } @@ -46,7 +46,7 @@ background: var(--surfaces-bg-card); font-family: monospace; height: 100%; - max-height: 470px; + max-height: 42vh; overflow: auto; padding: 1rem; position: relative; @@ -66,20 +66,16 @@ font-size: 12px; } -#model-dropdown-id .Select-menu-outer { +#model-dropdown .Select-menu-outer { font-size: 12px; - - /* top: 0; */ - - /* transform: translateY(3px) translateY(-100%); */ } -#model-dropdow-idn .dash-dropdown { +#model-dropdow .dash-dropdown { background-color: inherit; font-size: 12px; } -#trigger-button-id { +#trigger-button { width: 100%; } @@ -87,7 +83,7 @@ background-color: inherit; } -#model-dropdown-id .Select-clear { +#model-dropdown .Select-clear { display: none; } @@ -95,7 +91,7 @@ width: 50%; } -.card:has(#upload-message-id) { +.card:has(#upload-message) { background-color: inherit; box-shadow: none; font-size: 12px; @@ -121,24 +117,14 @@ width: 20px; } -#data-upload-id { - border: 1px dashed var(--border-subtle-alpha-01); - border-radius: 5px; +#data-upload-component { + border: 1px dashed var(--border-subtleAlpha01); color: var(--text-primary); - height: 46px; - line-height: 46px; + height: 5vh; + line-height: 5vh; text-align: center; } -/* -#settings-api-key-toggle .form-check-input { - border-radius: 8px; -} - -#settings-api-base-toggle .form-check-input { - border-radius: 8px; -} -*/ #toggle-div-api-base, #toggle-div-api-key { align-items: center; @@ -147,18 +133,6 @@ justify-content: center; } -.anchor-container { - background: #060a17; - bottom: 0; - display: flex; - font-weight: 600; - gap: 2rem; - padding: 4px; - place-content: baseline center; - position: fixed; - width: 100%; -} - .toggle-div { display: flex; flex-direction: row; @@ -190,17 +164,19 @@ .btn-primary:enabled:has(#dropdown-menu-icon) { background-color: inherit; - color: var(--text-active); + color: var(--text-primary); + font-size: 1.5rem; } .btn-primary:disabled:has(#dropdown-menu-icon) { background-color: inherit; - color: var(--text-active); + color: var(--text-primary); + font-size: 1.5rem; } .download-button { background-color: inherit; - color: var(--text-active); + color: var(--text-primary); width: 100%; } @@ -210,17 +186,17 @@ .dropdown-menu-toggle-class { background-color: inherit; - color: var(--text-active); + color: var(--text-primary); height: 2rem; - scale: 90%; + transform: scale(0.9); } .dropdown-menu-toggle-class:hover { background-color: inherit; - color: var(--text-active); + color: var(--text-primary); cursor: pointer; height: 2rem; - scale: 90%; + transform: scale(0.9); } .dropdown-menu-toggle-class.btn-primary:focus:not( @@ -229,7 +205,7 @@ .disabled ) { background-color: inherit; - color: var(--text-active); + color: var(--text-primary); } .modal-class { @@ -245,7 +221,7 @@ .custom_header { align-items: center; - border-bottom: 1px solid var(--border-subtle-alpha-01); + border-bottom: 1px solid var(--border-subtleAlpha01); display: flex; flex-direction: row; height: 60px; @@ -267,14 +243,17 @@ padding-top: 4px; } -#dropdown-menu-id { +#dropdown-menu-div { align-items: center; + align-self: flex-end; border: 0.5px solid gray; border-radius: 8px; display: flex; flex-direction: row; justify-content: center; - width: 100%; + min-width: 130px; + padding-left: 4px; + width: 130px; } #custom-header-div { @@ -303,7 +282,7 @@ color: var(--text-secondary); } -#open-settings-id:hover { +#open-settings:hover { cursor: pointer; } @@ -316,3 +295,59 @@ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); transform: translateY(-2px); } + +.anchor-container { + align-items: center; + background: var(--text-primary); + border-top-left-radius: 8px; + bottom: 0; + color: var(--text-primary-inverted); + 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; +} + +.dropdown-menu-outer-div { + align-items: end; + display: flex; + justify-content: end; + width: 100%; +} + +.flex-container { + display: flex; + flex-flow: column wrap; + gap: 12px; + justify-content: flex-start; + overflow: hidden; /* check and remove comment */ +} + +.flex-container h4 { + color: var(--text-secondary); + margin: 0; + padding-top: 12px; + text-align: center; +} + +.flex-container .code-clipboard-container { + height: 500px; + width: 100%; +} + +.flex-container #graph { + height: 712px; +} diff --git a/vizro-ai/examples/dashboard_ui/components.py b/vizro-ai/examples/dashboard_ui/components.py index 1e8397b3f..793f27dcc 100644 --- a/vizro-ai/examples/dashboard_ui/components.py +++ b/vizro-ai/examples/dashboard_ui/components.py @@ -89,12 +89,17 @@ def build(self): markdown_code = "\n".join(["```python", code, "```"]) - return html.Div( - [ - dcc.Clipboard(target_id=f"{self.id}-code-markdown", className="code-clipboard"), - dcc.Markdown(markdown_code, id=f"{self.id}-code-markdown"), - ], - className="code-clipboard-container", + return dcc.Loading( + html.Div( + [ + dcc.Clipboard(target_id=f"{self.id}-code-markdown", className="code-clipboard"), + dcc.Markdown(markdown_code, id=f"{self.id}-code-markdown"), + ], + className="code-clipboard-container", + ), + color="grey", + parent_className="loading-container", + overlay_style={"visibility": "visible", "opacity": 0.3}, ) @@ -293,8 +298,8 @@ class CustomDashboard(vm.Dashboard): def build(self): """Returns custom dashboard.""" dashboard_build_obj = super().build() - dashboard_build_obj.children.append(dcc.Store(id="data-store-id", storage_type="session")) - dashboard_build_obj.children.append(dcc.Store(id="code-output-store-id", storage_type="session")) + dashboard_build_obj.children.append(dcc.Store(id="data-store", storage_type="session")) + dashboard_build_obj.children.append(dcc.Store(id="code-output-store", storage_type="session")) return dashboard_build_obj @@ -340,7 +345,7 @@ def custom_table(data_frame): id="modal-table-tooltip", ), html.P( - id="upload-message-id", children=["Upload your data file (csv or excel)"], style={"paddingTop": "10px"} + id="upload-message", children=["Upload your data file (csv or excel)"], style={"paddingTop": "10px"} ), dbc.Modal( id="data-modal", @@ -392,12 +397,11 @@ def build(self): dbc.Tooltip( "Download this plot to your device as a plotly JSON or interactive HTML file " "for easy sharing or future use.", - target="dropdown-menu-icon", + target="dropdown-menu-div", ), ], - id="dropdown-menu-id", + id="dropdown-menu-div", ) - return download_div @@ -415,10 +419,23 @@ def build(self): ) icon = html.Div( children=[ - html.Span("settings", className="material-symbols-outlined", id="open-settings-id"), - dbc.Tooltip("Settings", target="open-settings-id"), + html.Span("settings", className="material-symbols-outlined", id="open-settings"), + dbc.Tooltip("Settings", target="open-settings"), ], className="settings-div", ) return html.Div(children=[header, icon], className="custom_header") + + +class FlexContainer(vm.Container): + """Custom flex `Container`.""" + + type: Literal["flex_container"] = "flex_container" + title: str = None # Title exists in vm.Container but we don't want to use it here. + + def build(self): + """Returns a flex container.""" + return html.Div( + id=self.id, children=[component.build() for component in self.components], className="flex-container" + ) diff --git a/vizro-ai/examples/dashboard_ui/requirements.in b/vizro-ai/examples/dashboard_ui/requirements.in index 2e9c8c58a..c8da40972 100644 --- a/vizro-ai/examples/dashboard_ui/requirements.in +++ b/vizro-ai/examples/dashboard_ui/requirements.in @@ -1,5 +1,6 @@ gunicorn vizro-ai>=0.3.2 +vizro==0.1.28 black openpyxl langchain_anthropic diff --git a/vizro-ai/examples/dashboard_ui/requirements.txt b/vizro-ai/examples/dashboard_ui/requirements.txt index 47db784ee..143f56ebc 100644 --- a/vizro-ai/examples/dashboard_ui/requirements.txt +++ b/vizro-ai/examples/dashboard_ui/requirements.txt @@ -15,13 +15,20 @@ anyio==4.4.0 # anthropic # httpx # openai +async-timeout==4.0.3 + # via + # aiohttp + # langchain attrs==24.2.0 # via aiohttp autoflake==2.3.1 - # via vizro-ai + # via + # vizro + # vizro-ai black==24.8.0 # via # -r requirements.in + # vizro # vizro-ai blinker==1.8.2 # via flask @@ -63,6 +70,8 @@ distro==1.9.0 # openai et-xmlfile==1.1.0 # via openpyxl +exceptiongroup==1.2.2 + # via anyio filelock==3.16.1 # via huggingface-hub flask==3.0.3 @@ -77,6 +86,8 @@ frozenlist==1.4.1 # aiosignal fsspec==2024.10.0 # via huggingface-hub +greenlet==3.1.1 + # via sqlalchemy gunicorn==23.0.0 # via -r requirements.in h11==0.14.0 @@ -100,7 +111,9 @@ idna==3.8 # requests # yarl importlib-metadata==8.5.0 - # via dash + # via + # dash + # flask itsdangerous==2.2.0 # via flask jinja2==3.1.4 @@ -182,7 +195,9 @@ pathspec==0.12.1 platformdirs==4.3.2 # via black plotly==5.24.1 - # via dash + # via + # dash + # vizro pydantic==2.9.1 # via # anthropic @@ -217,8 +232,6 @@ requests==2.32.3 # tiktoken retrying==1.3.4 # via dash -ruff==0.6.4 - # via vizro setuptools==74.1.2 # via dash six==1.16.0 @@ -246,6 +259,10 @@ tokenizers==0.20.1 # via # anthropic # langchain-mistralai +tomli==2.1.0 + # via + # autoflake + # black tqdm==4.66.5 # via # huggingface-hub @@ -253,9 +270,12 @@ tqdm==4.66.5 typing-extensions==4.12.2 # via # anthropic + # anyio + # black # dash # huggingface-hub # langchain-core + # multidict # openai # pydantic # pydantic-core @@ -264,8 +284,10 @@ tzdata==2024.1 # via pandas urllib3==2.2.3 # via requests -vizro==0.1.23 - # via vizro-ai +vizro==0.1.28 + # via + # -r requirements.in + # vizro-ai vizro-ai==0.3.2 # via -r requirements.in werkzeug==3.0.4 diff --git a/vizro-ai/mkdocs.yml b/vizro-ai/mkdocs.yml index c95fbe9e0..9bd285a2a 100644 --- a/vizro-ai/mkdocs.yml +++ b/vizro-ai/mkdocs.yml @@ -16,8 +16,8 @@ nav: - Advanced charts: pages/user-guides/create-advanced-charts.md - Add Vizro-AI charts to a Vizro dashboard: pages/user-guides/add-generated-chart-usecase.md - DASHBOARDS: - - Generate a complex dashboard: pages/user-guides/create-complex-dashboard.md - - Retrieve code for a generated dashboard: pages/user-guides/retrieve-dashboard-code.md + - Run vizro_ai.dashboard: pages/user-guides/run-vizro-ai-dashboard.md + - Advanced dashboards: pages/user-guides/create-complex-dashboard.md - Use Vizro-AI methods as Langchain tools: pages/user-guides/vizro-ai-langchain-guide.md - API Reference: - VizroAI: pages/API-reference/vizro-ai.md @@ -26,8 +26,9 @@ nav: - Disclaimer: pages/explanation/disclaimer.md - Safeguard code execution: pages/explanation/safeguard.md - Safety in Vizro-AI: pages/explanation/safety-in-vizro-ai.md + - Why use Vizro-AI: pages/explanation/why-use-vizro-ai.md - Examples: - - Chart examples: pages/user-guides/chart-examples.md + - Gallery: https://vizro.mckinsey.com #- Contribute: # - Contributing: pages/contribute/contributing.md - Vizro: diff --git a/vizro-ai/pyproject.toml b/vizro-ai/pyproject.toml index 015e302aa..791d2ab72 100644 --- a/vizro-ai/pyproject.toml +++ b/vizro-ai/pyproject.toml @@ -21,7 +21,7 @@ dependencies = [ "langchain>=0.1.0, <0.3.0", # TODO update to pydantic v2 and remove upper bound "langgraph>=0.1.2, <0.2.17", # TODO update to pydantic v2 and remove upper bound (latest version break pydantic v1 compatibility) "python-dotenv>=1.0.0", # TODO decide env var management to see if we need this - "vizro>=0.1.23", + "vizro<=0.1.29", # Limited to current version of vizro, until the pydantic V2 changes of the main repo have been into here "langchain_openai", # Base dependency, ie minimum model working "black", "autoflake" diff --git a/vizro-ai/src/vizro_ai/plot/_response_models.py b/vizro-ai/src/vizro_ai/plot/_response_models.py index 17333373a..8daa0f4de 100644 --- a/vizro-ai/src/vizro_ai/plot/_response_models.py +++ b/vizro-ai/src/vizro_ai/plot/_response_models.py @@ -196,7 +196,8 @@ def _test_execute_chart_code(v, values): fig = custom_chart(data_frame.sample(10, replace=True)) except Exception as e: raise ValueError( - f"Produced code execution failed the following error: <{e}>. Please check the code and try again." + f"Produced code execution failed the following error: <{e}>. Please check the code and try again, " + f"alternatively try with a more powerful model." ) assert isinstance( fig, go.Figure diff --git a/vizro-ai/src/vizro_ai/plot/_utils/_constants.py b/vizro-ai/src/vizro_ai/plot/_utils/_constants.py index 86935f633..30db3eaf8 100644 --- a/vizro-ai/src/vizro_ai/plot/_utils/_constants.py +++ b/vizro-ai/src/vizro_ai/plot/_utils/_constants.py @@ -22,7 +22,7 @@ # '__build_class__', # '__import__', "abs", - # 'all', + "all", "any", # 'ascii', # 'bin', @@ -45,7 +45,7 @@ # 'input', # 'isinstance', # 'issubclass', - # 'iter', + "iter", # 'aiter', "len", # 'locals', @@ -60,7 +60,7 @@ # 'repr', "round", # 'setattr', - # 'sorted', + "sorted", "sum", # 'vars', "None", @@ -76,7 +76,7 @@ # 'complex', "dict", "enumerate", - # 'filter', + "filter", "float", # 'frozenset', # 'property', @@ -85,7 +85,7 @@ "map", # 'object', "range", - # 'reversed', + "reversed", "set", # 'slice', # 'staticmethod', diff --git a/vizro-core/CHANGELOG.md b/vizro-core/CHANGELOG.md index d001e8da5..ef31d9d93 100644 --- a/vizro-core/CHANGELOG.md +++ b/vizro-core/CHANGELOG.md @@ -11,6 +11,50 @@ See the fragment files in the [changelog.d directory](https://github.com/mckinse +
+ +# 0.1.29 — 2024-12-03 + +## Highlights ✨ + +- Filters update automatically when underlying dynamic data changes. See the [user guide on dynamic filters](https://vizro.readthedocs.io/en/stable/pages/user-guides/data/#filters) for more information. ([#879](https://github.com/mckinsey/vizro/pull/879)) + +## Changed + +- Custom controls can be nested arbitrarily deep inside `Page.controls`. ([#903](https://github.com/mckinsey/vizro/pull/903)) + +- Replace `dmc.Switch` with `dbc.Switch` and change CSS selectors accordingly. ([#907](https://github.com/mckinsey/vizro/pull/907)) + + + +# 0.1.28 — 2024-11-27 + +## Removed + +- Removed all CSS variables from `variables.css` and `token_names.css`, replacing them with CSS variables from `vizro-bootstrap.min.css`. Refer to [`vizro-bootstrap.min.css`](https://github.com/mckinsey/vizro/blob/main/vizro-core/src/vizro/static/css/vizro-bootstrap.min.css) for the updated CSS variables. ([#886](https://github.com/mckinsey/vizro/pull/886)) + +## Added + +- Enable `href` inside `vm.Button`. ([#881](https://github.com/mckinsey/vizro/pull/881)) + +## Changed + +- Replace `dmc.Tabs` with `dbc.Tabs` and change CSS selectors accordingly. ([#895](https://github.com/mckinsey/vizro/pull/895)) + + + +# 0.1.27 — 2024-11-14 + +## Changed + +- Improve performance of data loading. ([#850](https://github.com/mckinsey/vizro/pull/850), [#857](https://github.com/mckinsey/vizro/pull/857)) + +- Upper bound dependency `dash<3`. ([#877](https://github.com/mckinsey/vizro/pull/877)) + +## Fixed + +- Fix 404 error page and page flickering on refresh. ([#865](https://github.com/mckinsey/vizro/pull/865)) + # 0.1.26 — 2024-10-30 @@ -273,10 +317,7 @@ See the fragment files in the [changelog.d directory](https://github.com/mckinse ### Highlights ✨ -- Introduce `AgGrid` as a new `Page` component, allowing the usage of - [AG Grid](https://www.ag-grid.com/) in - `Vizro`. See the [user guide on tables](https://vizro.readthedocs.io/en/stable/pages/user_guides/table/) - for more information. ([#289](https://github.com/mckinsey/vizro/pull/289),[#268](https://github.com/mckinsey/vizro/pull/268),[#324](https://github.com/mckinsey/vizro/pull/324)) +- Introduce `AgGrid` as a new `Page` component, allowing the usage of [AG Grid](https://www.ag-grid.com/) in `Vizro`. See the [user guide on tables](https://vizro.readthedocs.io/en/stable/pages/user_guides/table/) for more information. ([#289](https://github.com/mckinsey/vizro/pull/289),[#268](https://github.com/mckinsey/vizro/pull/268),[#324](https://github.com/mckinsey/vizro/pull/324)) ## Changed @@ -521,10 +562,10 @@ See the fragment files in the [changelog.d directory](https://github.com/mckinse - Optimize the client-server communication ([#34](https://github.com/mckinsey/vizro/pull/34)) - - Eliminate most server side callbacks in favor of client-side callbacks - - Add tests for client-side callbacks written in Node.js framework called `jest`. - - Add hatch command `hatch run test-js` that runs unit tests written in `jest`. - - Logging information now only displayed for action function carried out (no trigger or finished information) + - Eliminate most server side callbacks in favor of client-side callbacks + - Add tests for client-side callbacks written in Node.js framework called `jest`. + - Add hatch command `hatch run test-js` that runs unit tests written in `jest`. + - Logging information now only displayed for action function carried out (no trigger or finished information) - Replaced all screenshots in the docs to reflect new navigation designs ([#48](https://github.com/mckinsey/vizro/pull/48)) @@ -568,21 +609,21 @@ See the fragment files in the [changelog.d directory](https://github.com/mckinse - Add the following pydantic models: - - Action - - Button - - Card - - Checklist - - Dashboard - - Dropdown - - Filter - - Graph - - Layout - - Navigation - - Page - - Parameter - - RadioItems - - RangeSlider - - Slider - - VizroBaseModel + - Action + - Button + - Card + - Checklist + - Dashboard + - Dropdown + - Filter + - Graph + - Layout + - Navigation + - Page + - Parameter + - RadioItems + - RangeSlider + - Slider + - VizroBaseModel - Enable the addition and usage of custom components and custom charts diff --git a/vizro-core/README.md b/vizro-core/README.md index 3a0cb5bc6..283717094 100644 --- a/vizro-core/README.md +++ b/vizro-core/README.md @@ -11,19 +11,13 @@
-[![Python version](https://img.shields.io/badge/python-3.9%20%7C%203.10%20%7C%203.11%20%7C%203.12%20%7C%203.13-blue.svg)](https://pypi.org/project/vizro/) -[![PyPI version](https://badge.fury.io/py/vizro.svg)](https://badge.fury.io/py/vizro) -[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/mckinsey/vizro/blob/main/LICENSE.md) -[![Documentation](https://readthedocs.org/projects/vizro/badge/?version=stable)](https://vizro.readthedocs.io/) -[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/7858/badge)](https://www.bestpractices.dev/projects/7858) +[![Python version](https://img.shields.io/badge/python-3.9%20%7C%203.10%20%7C%203.11%20%7C%203.12%20%7C%203.13-blue.svg)](https://pypi.org/project/vizro/) [![PyPI version](https://badge.fury.io/py/vizro.svg)](https://badge.fury.io/py/vizro) [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/mckinsey/vizro/blob/main/LICENSE.md) [![Documentation](https://readthedocs.org/projects/vizro/badge/?version=stable)](https://vizro.readthedocs.io/) [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/7858/badge)](https://www.bestpractices.dev/projects/7858)
-Documentation | -Get Started | -Vizro examples gallery +Documentation | Get Started | Vizro examples gallery
@@ -100,9 +94,7 @@ You can see Vizro in action by clicking on the following image or by visiting [t ## Visual vocabulary -Our visual vocabulary dashboard helps you to select and create various types of charts. It helps you decide when to use -each chart type, and offers sample Python code to create these charts with [Plotly](https://plotly.com/python/) and -embed them into a Vizro dashboard. +Our visual vocabulary dashboard helps you to select and create various types of charts. It helps you decide when to use each chart type, and offers sample Python code to create these charts with [Plotly](https://plotly.com/python/) and embed them into a Vizro dashboard. @@ -138,8 +130,7 @@ We encourage you to ask and answer technical questions via the [GitHub Issues](h ## Contributing -To learn more about making a contribution, -please see the [contributing guide](https://vizro.readthedocs.io/en/stable/pages/development/contributing/) for more information +To learn more about making a contribution, please see the [contributing guide](https://vizro.readthedocs.io/en/stable/pages/development/contributing/) for more information You can also view current and former [contributors](https://vizro.readthedocs.io/en/stable/pages/development/authors/) diff --git a/vizro-core/changelog.d/20241106_104745_antony.milne_dynamic_filter_2.md b/vizro-core/changelog.d/20241106_104745_antony.milne_dynamic_filter_2.md deleted file mode 100644 index 6108280fd..000000000 --- a/vizro-core/changelog.d/20241106_104745_antony.milne_dynamic_filter_2.md +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - -### Changed - -- Improve performance of data loading. ([#850](https://github.com/mckinsey/vizro/pull/850), [#857](https://github.com/mckinsey/vizro/pull/857)) - - - - diff --git a/vizro-core/changelog.d/20241112_123240_nadija_ratkusic_graca_add_banner_to_demo_dashboards.md b/vizro-core/changelog.d/20241112_123240_nadija_ratkusic_graca_add_banner_to_demo_dashboards.md deleted file mode 100644 index 7c0d58d4f..000000000 --- a/vizro-core/changelog.d/20241112_123240_nadija_ratkusic_graca_add_banner_to_demo_dashboards.md +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - diff --git a/vizro-core/changelog.d/20241030_170000_antony.milne_vvv_link_targets.md b/vizro-core/changelog.d/20241202_150655_huong_li_nguyen_refactor_bs_example.md similarity index 99% rename from vizro-core/changelog.d/20241030_170000_antony.milne_vvv_link_targets.md rename to vizro-core/changelog.d/20241202_150655_huong_li_nguyen_refactor_bs_example.md index 7c0d58d4f..4abc0f11e 100644 --- a/vizro-core/changelog.d/20241030_170000_antony.milne_vvv_link_targets.md +++ b/vizro-core/changelog.d/20241202_150655_huong_li_nguyen_refactor_bs_example.md @@ -10,36 +10,42 @@ Uncomment the section that is right (remove the HTML comment wrapper). - A bullet item for the Highlights ✨ category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX. ([#1](https://github.com/mckinsey/vizro/pull/1)) --> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vizro-core/changelog.d/20241203_150134_antony.milne_mdformat.md b/vizro-core/changelog.d/20241203_150134_antony.milne_mdformat.md new file mode 100644 index 000000000..4abc0f11e --- /dev/null +++ b/vizro-core/changelog.d/20241203_150134_antony.milne_mdformat.md @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + diff --git a/vizro-core/changelog.d/20241204_135618_huong_li_nguyen_remove_custom_img_css.md b/vizro-core/changelog.d/20241204_135618_huong_li_nguyen_remove_custom_img_css.md new file mode 100644 index 000000000..750a8d283 --- /dev/null +++ b/vizro-core/changelog.d/20241204_135618_huong_li_nguyen_remove_custom_img_css.md @@ -0,0 +1,48 @@ + + + + +### Removed + +- Remove built-in CSS shortcuts `#floating-*` to float images. These can still be provided manually. ([#919](https://github.com/mckinsey/vizro/pull/919)) + + + + + + + diff --git a/vizro-core/docs/assets/tutorials/dashboard/dashboard-first-page.png b/vizro-core/docs/assets/tutorials/dashboard/dashboard-first-page.png new file mode 100644 index 000000000..ce421fca7 Binary files /dev/null and b/vizro-core/docs/assets/tutorials/dashboard/dashboard-first-page.png 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 new file mode 100644 index 000000000..ee48af4d9 Binary files /dev/null and b/vizro-core/docs/assets/tutorials/dashboard/dashboard-second-page.png differ diff --git a/vizro-core/docs/assets/user_guides/components/button_text.png b/vizro-core/docs/assets/user_guides/components/button_text.png new file mode 100644 index 000000000..90e53590e Binary files /dev/null and b/vizro-core/docs/assets/user_guides/components/button_text.png differ diff --git a/vizro-core/docs/index.md b/vizro-core/docs/index.md index 1cc99a4ce..e9886376b 100644 --- a/vizro-core/docs/index.md +++ b/vizro-core/docs/index.md @@ -4,58 +4,56 @@ Vizro is a toolkit for creating modular data visualization applications.
-- :fontawesome-solid-forward-fast:{ .lg .middle } __New to Vizro?__ +- :fontawesome-solid-forward-fast:{ .lg .middle } __New to Vizro?__ --- - [:octicons-arrow-right-24: Quickstart tutorial](pages/tutorials/first-dashboard.md)
+ [:octicons-arrow-right-24: Quickstart tutorial](pages/tutorials/first-dashboard.md) + [:octicons-arrow-right-24: Install Vizro](pages/user-guides/install.md) +- :fontawesome-solid-keyboard:{ .lg .middle } __Vizro features__ + --- + [:octicons-arrow-right-24: Fundamentals](pages/user-guides/dashboard.md) -- :fontawesome-solid-keyboard:{ .lg .middle } __Vizro features__ + [:octicons-arrow-right-24: Components overview](pages/user-guides/components.md) - --- + [:octicons-arrow-right-24: Filters, parameters and selectors](pages/user-guides/filters.md) - [:octicons-arrow-right-24: Fundamentals](pages/user-guides/dashboard.md)
- [:octicons-arrow-right-24: Components overview](pages/user-guides/components.md)
- [:octicons-arrow-right-24: Filters, parameters and selectors](pages/user-guides/filters.md)
[:octicons-arrow-right-24: Visual formatting](pages/user-guides/visual-formatting.md) - - -- :fontawesome-solid-microscope:{ .lg .middle } __Hands-on code__ +- :fontawesome-solid-microscope:{ .lg .middle } __Hands-on code__ --- - [:octicons-arrow-right-24: Tutorials](pages/tutorials/explore-components.md)
- [:octicons-arrow-right-24: Examples](https://vizro.mckinsey.com) - + [:octicons-arrow-right-24: Tutorials](pages/tutorials/explore-components.md) + [:octicons-arrow-right-24: Examples](https://vizro.mckinsey.com) -- :material-scale-balance:{ .lg .middle } __FAQs__ +- :material-scale-balance:{ .lg .middle } __FAQs__ --- - [:octicons-arrow-right-24: How is Vizro different to Streamlit?](pages/explanation/faq.md/#how-does-vizro-differ-from-dash-or-streamlit)
- [:octicons-arrow-right-24: Where can I ask a question about Vizo?](pages/explanation/faq.md/#i-still-have-a-question-where-can-i-ask-it)
- [:octicons-arrow-right-24: Other FAQs](pages/explanation/faq.md)
+ [:octicons-arrow-right-24: How is Vizro different to Streamlit?](pages/explanation/faq.md/#how-does-vizro-differ-from-dash-or-streamlit) + [:octicons-arrow-right-24: Where can I ask a question?](pages/explanation/faq.md/#i-still-have-a-question-where-can-i-ask-it) + [:octicons-arrow-right-24: Other FAQs](pages/explanation/faq.md) -- :fontawesome-solid-hands-holding-circle:{ .lg .middle } __Get involved__ +- :fontawesome-solid-hands-holding-circle:{ .lg .middle } __Get involved__ --- - [:octicons-arrow-right-24: Contribute code](pages/explanation/contributing.md)
+ [:octicons-arrow-right-24: Contribute code](pages/explanation/contributing.md) + [:octicons-arrow-right-24: Contribute to our docs](pages/explanation/documentation-style-guide.md) -- :fontawesome-solid-chart-column:{ .lg .middle } __Vizro-AI__ +- :fontawesome-solid-chart-column:{ .lg .middle } __Vizro-AI__ --- [:octicons-arrow-right-24: Vizro-AI: Use natural language queries to build Plotly charts](https://vizro.readthedocs.io/projects/vizro-ai/) -
diff --git a/vizro-core/docs/pages/explanation/authors.md b/vizro-core/docs/pages/explanation/authors.md index c59700a4a..d88f73ea6 100644 --- a/vizro-core/docs/pages/explanation/authors.md +++ b/vizro-core/docs/pages/explanation/authors.md @@ -1,57 +1,21 @@ ## Current team members + -[Alexey Snigir](https://github.com/l0uden), -[Antony Milne](https://github.com/antonymilne), -[Dan Dumitriu](https://github.com/dandumitriu1), -[Huong Li Nguyen](https://github.com/huong-li-nguyen), -[Jo Stichbury](https://github.com/stichbury), -[Joseph Perkins](https://github.com/Joseph-Perkins), -[Lingyi Zhang](https://github.com/lingyielia), -[Maximilian Schulz](https://github.com/maxschulz-COL), -[Nadija Graca](https://github.com/nadijagraca), -[Petar Pejovic](https://github.com/petar-qb) - +[Alexey Snigir](https://github.com/l0uden), [Antony Milne](https://github.com/antonymilne), [Dan Dumitriu](https://github.com/dandumitriu1), [Huong Li Nguyen](https://github.com/huong-li-nguyen), [Jo Stichbury](https://github.com/stichbury), [Joseph Perkins](https://github.com/Joseph-Perkins), [Lingyi Zhang](https://github.com/lingyielia), [Maximilian Schulz](https://github.com/maxschulz-COL), [Nadija Graca](https://github.com/nadijagraca), and [Petar Pejovic](https://github.com/petar-qb) + + ## Previous team members and code contributors + -[Ann Marie Ward](https://github.com/AnnMarieW), -[Ned Letcher](https://github.com/ned2), -Natalia Kurakina, -[Leon Nallamuthu](https://github.com/leonnallamuthu), -[axa99](https://github.com/axa99), -[Juan Luis Cano Rodríguez](https://github.com/astrojuanlu), -[Denis Lebedev](https://github.com/DenisLebedevMcK), -[Qiuyi Chen](https://github.com/Qiuyi-Chen), -[Elena Fridman](https://github.com/EllenWie), -[Bo Xu](https://github.com/boxuboxu), -[Jingjing Guo](https://github.com/jjguo-mck), -[Oleksandr Serdiuk](https://github.com/oserdiuk-lohika), -[Prateek Bajaj](https://github.com/prateekdev552), -[Nikolaos Tsaousis](https://github.com/tsanikgr), -[Annie Wachsmuth](https://github.com/anniecwa), -[Hamza Oza](https://github.com/hamzaoza), -[Kee Wen Ng](https://github.com/KeeWenNgQB), -[Rashida Kanchwala](https://github.com/rashidakanchwala), -[Juan Luis Cano Rodríguez](https://github.com/astrojuanlu), -[Anna Xiong](https://github.com/Anna-Xiong), -[Chiara Pullem](https://github.com/chiara-sophie), -[Sylvie Zhang](https://github.com/sylviezhang37), -[Bhavana Sundar](https://github.com/bhavanaeh), -[Ferida Mohammed](https://github.com/feridaaa), -[Lydia Pitts](https://github.com/LydiaPitts), -[Riley Dou](https://github.com/rilieo), -[Upekesha Ngugi](https://github.com/upekesha), -[Hansaem Park](https://github.com/sammitako), -[Rosheen C.](https://github.com/rc678), -[Hilary Ivy](https://github.com/hxe00570), -[Jasmine Wu](https://github.com/jazwu), -[njmcgrat](https://github.com/njmcgrat) + +[Ann Marie Ward](https://github.com/AnnMarieW), [Anna Xiong](https://github.com/Anna-Xiong), [Annie Wachsmuth](https://github.com/anniecwa), [ataraexia](https://github.com/ataraexia), [axa99](https://github.com/axa99), [Bhavana Sundar](https://github.com/bhavanaeh), [Bo Xu](https://github.com/boxuboxu), [Chiara Pullem](https://github.com/chiara-sophie), [Denis Lebedev](https://github.com/DenisLebedevMcK), [Elena Fridman](https://github.com/EllenWie), [Ferida Mohammed](https://github.com/feridaaa), [Hamza Oza](https://github.com/hamzaoza), [Hansaem Park](https://github.com/sammitako), [Hilary Ivy](https://github.com/hxe00570), [Jasmine Wu](https://github.com/jazwu), [Jenelle Yonkman](https://github.com/yonkmanjl), [Jingjing Guo](https://github.com/jjguo-mck), [Juan Luis Cano Rodríguez](https://github.com/astrojuanlu), [Kee Wen Ng](https://github.com/KeeWenNgQB), [Leon Nallamuthu](https://github.com/leonnallamuthu), [Lydia Pitts](https://github.com/LydiaPitts), [Ned Letcher](https://github.com/ned2), [Nikolaos Tsaousis](https://github.com/tsanikgr), [njmcgrat](https://github.com/njmcgrat), [Oleksandr Serdiuk](https://github.com/oserdiuk-lohika), [Prateek Bajaj](https://github.com/prateekdev552), [Qiuyi Chen](https://github.com/Qiuyi-Chen), [Rashida Kanchwala](https://github.com/rashidakanchwala), [Riley Dou](https://github.com/rilieo), [Rosheen C.](https://github.com/rc678), [Sylvie Zhang](https://github.com/sylviezhang37), and [Upekesha Ngugi](https://github.com/upekesha). with thanks to Sam Bourton and Kevin Staight for sponsorship, inspiration and guidance, -and special thanks to -[Wesley Leong](https://github.com/wesleyleong), [Jonas Kemper](https://github.com/jonasrk) and team for origination and support +and special thanks to [Wesley Leong](https://github.com/wesleyleong), [Jonas Kemper](https://github.com/jonasrk) and team for origination and support (plus everyone else who helped to test, guide, support and encourage development) + diff --git a/vizro-core/docs/pages/explanation/contributing.md b/vizro-core/docs/pages/explanation/contributing.md index 7e532ae3b..bf71bfb39 100644 --- a/vizro-core/docs/pages/explanation/contributing.md +++ b/vizro-core/docs/pages/explanation/contributing.md @@ -4,22 +4,20 @@ Contributions of all experience levels are welcome! There are many ways to contr Our development follows a standard [GitHub flow](https://docs.github.com/en/get-started/using-github/github-flow). To be merged, your PR must meet all the following requirements: -* two approving reviews (including a code owner) -* Continuous Integration (CI) checks pass -* code is up-to-date with `main` +- two approving reviews (including a code owner) +- Continuous Integration (CI) checks pass +- code is up-to-date with `main` If you are a first-time contributor with a new GitHub account then you may also need to [wait for CI workflows to be approved](https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-workflow-runs/approving-workflow-runs-from-public-forks). We aim to make the contribution process as easy as possible by having only one direct development dependency: [Hatch](https://hatch.pypa.io/). There are two ways to develop on Vizro: -* [GitHub Codespaces](https://docs.github.com/en/codespaces). This is the recommended method if you are a new contributor. It is the quickest and easiest way to get started. All development can be done in your browser in a temporary environment; you do not need to set up anything on your computer. The [Develop on GitHub Codespaces](#develop-on-github-codespaces) section has full instructions on how to do this. -* Local machine. If you are more experienced then you might prefer to develop on your own computer. The [Develop locally](#develop-locally) section has full instructions on how to do this. +- [GitHub Codespaces](https://docs.github.com/en/codespaces). This is the recommended method if you are a new contributor. It is the quickest and easiest way to get started. All development can be done in your browser in a temporary environment; you do not need to set up anything on your computer. The [Develop on GitHub Codespaces](#develop-on-github-codespaces) section has full instructions on how to do this. +- Local machine. If you are more experienced then you might prefer to develop on your own computer. The [Develop locally](#develop-locally) section has full instructions on how to do this. !!! note - For either method, Hatch is the _only development dependency_. You do not need to manually install Python or create any virtual environments to develop Vizro; all this will be handled for you behind the scenes by Hatch. We have also configured our codespace to pre-install Hatch. If you develop on GitHub Codespaces you don't need to install anything at all! - ## Develop on GitHub Codespaces There is no need to manually create a fork of the Vizro code if you use GitHub Codespaces. A fork is [automatically created for you](https://docs.github.com/en/codespaces/developing-in-a-codespace/using-source-control-in-your-codespace#about-automatic-forking). @@ -27,17 +25,17 @@ There is no need to manually create a fork of the Vizro code if you use GitHub C To develop on [GitHub Codespaces](https://docs.github.com/en/codespaces), follow the below steps: 1. [Create a codespace for our repository](https://codespaces.new/mckinsey/vizro). Leave the settings on their defaults and click "Create codespace" to start your codespace. It should take 1-2 minutes to fully launch and automatically start an example dashboard on port 8050. In the rare event that the codespace fails to start correctly and enters recovery mode, you should [rebuild the container](https://docs.github.com/en/codespaces/developing-in-a-codespace/rebuilding-the-container-in-a-codespace#rebuilding-a-container) or start a whole new codespace. -2. Make changes to Vizro code in your codespace. See the [GitHub Codespaces documentation on developing in a codespace](https://docs.github.com/en/codespaces/developing-in-a-codespace/developing-in-a-codespace) for more information. -3. Add your name to the [list of contributors](authors.md) (source file `vizro-core/docs/pages/explanation/authors.md`). -4. [Create a pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork). +1. Make changes to Vizro code in your codespace. See the [GitHub Codespaces documentation on developing in a codespace](https://docs.github.com/en/codespaces/developing-in-a-codespace/developing-in-a-codespace) for more information. +1. Add your name to the [list of contributors](authors.md) (source file `vizro-core/docs/pages/explanation/authors.md`). +1. [Create a pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork). ## Develop locally 1. Install Hatch. There are [several ways to do this](https://hatch.pypa.io/latest/install/). -2. [Fork the Vizro repository](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo) and clone it to your local machine. -3. Make changes to Vizro code in your fork. -4. Add your name to the [list of contributors](authors.md) (source file `vizro-core/docs/pages/explanation/authors.md`). -5. [Create a pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork). +1. [Fork the Vizro repository](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo) and clone it to your local machine. +1. Make changes to Vizro code in your fork. +1. Add your name to the [list of contributors](authors.md) (source file `vizro-core/docs/pages/explanation/authors.md`). +1. [Create a pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork). ## How to use Hatch @@ -45,16 +43,18 @@ Regardless of whether you are developing locally or in a codespace, everything y The Hatch commands you need most commonly are as follows. These must be executed with `vizro-core` as your current working directory: -* [`hatch run pypath`](#hatch-run-pypath) shows the path to the Python interpreter. -* [`hatch run example`](#hatch-run-example) runs an example dashboard on port 8050 that hot-reloads while you edit it. On GitHub Codespaces, this runs automatically on startup. -* [`hatch run lint`](#hatch-run-lint) checks and fixes code quality and formatting. This is included in CI checks. -* [`hatch run changelog:add`](#hatch-run-changelogadd) generates a new changelog fragment. Changelog inclusion is checked by CI and required for all changes to source code. -* [`hatch run test-unit`](#hatch-run-test-unit) runs the test suite. This is included in CI checks. -* [`hatch run docs:serve`](#hatch-run-docsserve) builds and displays documentation that hot-reloads while you edit it. Documentation is also built automatically in your PR and can be previewed on Read The Docs. -* [`hatch run pip`](#hatch-run-pip) provides a [pip-compatible interface using uv](https://docs.astral.sh/uv/pip/). You should not need to use this much. +- [`hatch run pypath`](#hatch-run-pypath) shows the path to the Python interpreter. +- [`hatch run example`](#hatch-run-example) runs an example dashboard on port 8050 that hot-reloads while you edit it. On GitHub Codespaces, this runs automatically on startup. +- [`hatch run lint`](#hatch-run-lint) checks and fixes code quality and formatting. This is included in CI checks. +- [`hatch run changelog:add`](#hatch-run-changelogadd) generates a new changelog fragment. Changelog inclusion is checked by CI and required for all changes to source code. +- [`hatch run test-unit`](#hatch-run-test-unit) runs the test suite. This is included in CI checks. +- [`hatch run docs:serve`](#hatch-run-docsserve) builds and displays documentation that hot-reloads while you edit it. Documentation is also built automatically in your PR and can be previewed on Read The Docs. +- [`hatch run pip`](#hatch-run-pip) provides a [pip-compatible interface using uv](https://docs.astral.sh/uv/pip/). You should not need to use this much. + To save yourself from repeatedly typing `hatch run` you might like to [set up an alias](https://www.tecmint.com/create-alias-in-linux/): + ```console @@ -67,10 +67,10 @@ This enables you to run, for example, `hr lint` instead of `hatch run lint`. On `hatch run pypath` shows the path to the Python interpreter. This is useful for setting a Python interpreter in your IDE to navigate the codebase. For example, in GitHub Codespaces and VS Code: -* Run `hatch run pypath` and copy the output to your clipboard. -* Open the Command Palette (++ctrl+shift+p++). -* Run the "Python: Select Interpreter" command and select the "Enter interpreter path..." option. -* Paste the path. +- Run `hatch run pypath` and copy the output to your clipboard. +- Open the Command Palette (++ctrl+shift+p++). +- Run the "Python: Select Interpreter" command and select the "Enter interpreter path..." option. +- Paste the path. ### `hatch run example` @@ -82,16 +82,15 @@ You can run any example in `vizro-core/examples` or its subdirectories by runnin Examples are run with the following settings: -* [Dash dev tools](https://dash.plotly.com/devtools) enabled. This includes hot reloading, so that any changes to the example app or Vizro source code should automatically show in your dashboard without needing refresh or restart anything. -* The environment variable `VIZRO_LOG_LEVEL = "DEBUG"` to show log messages of level `DEBUG` and above. +- [Dash dev tools](https://dash.plotly.com/devtools) enabled. This includes hot reloading, so that any changes to the example app or Vizro source code should automatically show in your dashboard without needing refresh or restart anything. +- The environment variable `VIZRO_LOG_LEVEL = "DEBUG"` to show log messages of level `DEBUG` and above. ### `hatch run lint` `hatch run lint` checks and fixes code quality and formatting. This is included in CI checks. All linting and associated dependencies are controlled by [pre-commit](https://pre-commit.com/) hooks. We use the [pre-commit.ci](https://pre-commit.ci/) to automatically fix all the linting checks that we can when a PR is pushed. Other linting failures (such as `mypy`) need manual intervention from the developer. !!! note - - The first time you run `hatch run lint` it may take a couple of minutes, since pre-commit needs to setup linting environments. Subsequent runs reuse these environments and are much faster. + The first time you run `hatch run lint` it may take a couple of minutes, since pre-commit needs to setup linting environments. Further runs reuse these environments and are much faster. `hatch run lint` runs the pre-commit hooks on all (not only staged) files. You can run an individual hook, for example `mypy`, on all files by running `hatch run lint mypy`. @@ -106,8 +105,7 @@ The format of our changelog is based on [Keep a Changelog](https://keepachangelo Run `hatch run changelog:add` to create a changelog fragment and then uncomment the relevant section(s). If you are uncertain about what to add or whether to add anything at all, refer to [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). The rule of thumb is that if Vizro users would be affected in any way then the changes should be described in the changelog. !!! note - - Changes that do not affect source code do not need a changelog fragment. This facilitates simple modifications to documentation [made directly on GitHub](https://docs.github.com/en/repositories/working-with-files/managing-files/editing-files) or with the [github.dev](https://docs.github.com/en/codespaces/the-githubdev-web-based-editor), where no terminal is available to run `hatch changelog:add`. Any changes to source code require a changelog fragment to be generated. If your changes do not require a changelog entry then you still need to generate the fragment but can leave it all commented out. + Changes that do not affect source code do not need a changelog fragment. This simplifies modifications to documentation [made directly on GitHub](https://docs.github.com/en/repositories/working-with-files/managing-files/editing-files) or within the [github.dev](https://docs.github.com/en/codespaces/the-githubdev-web-based-editor), where no terminal is available to run `hatch changelog:add`. Any changes to source code require a changelog fragment to be generated. If your changes do not require a changelog entry then you still need to generate the fragment but can leave it all commented out. ### `hatch run test-unit` @@ -127,9 +125,9 @@ hatch run all.py3.10:test-unit-coverage In addition to running unit tests with code coverage, CI also performs the following checks: -* `hatch run test-integration` runs integration tests that include checking that the example apps in `vizro-core/examples` run. -* `hatch run test-js` runs Javascript tests using [jest](https://jestjs.io/). Arguments are passed through to the underlying `npx jest` command, for example `hatch run test-js --help`. -* QA tests. These are run on a separate private `vizro-qa` repository and not triggered by PRs coming from forks. +- `hatch run test-integration` runs integration tests that include checking that the example apps in `vizro-core/examples` run. +- `hatch run test-js` runs Javascript tests using [jest](https://jestjs.io/). Arguments are passed through to the underlying `npx jest` command, for example `hatch run test-js --help`. +- QA tests. These are run on a separate private `vizro-qa` repository and not triggered by PRs coming from forks. ### `hatch run docs:serve` @@ -146,7 +144,6 @@ Vizro's dependencies are described by the `dependencies` section in `vizro-core/ We have [configured Hatch to use uv](https://hatch.pypa.io/1.12/how-to/environment/select-installer/) for rapid virtual environment creation, dependency resolution and installation. !!! note - If you have installed unwanted dependencies in your Hatch environment then the simplest solution is to delete the environment (`hatch env remove` to remove one environment or `hatch env prune` to remove all environments). Your next `hatch run` command will recreate the environment and install all the dependencies it needs. If for some reason you do need to use `pip` then the correct way to do so is through `hatch run pip`. For example, you could run `hatch run pip show plotly`. This will use the version of uv that Hatch itself uses under the hood. If you already have uv installed globally then `uv pip show plotly` would also work. diff --git a/vizro-core/docs/pages/explanation/documentation-style-guide.md b/vizro-core/docs/pages/explanation/documentation-style-guide.md index 9c397754e..40f989ac0 100644 --- a/vizro-core/docs/pages/explanation/documentation-style-guide.md +++ b/vizro-core/docs/pages/explanation/documentation-style-guide.md @@ -14,8 +14,8 @@ The names of our products are **Vizro** and **Vizro-AI**. We refer to other products using their preferred capitalization. For example: -* Dash and Pydantic are always capitalized, except where given as Python package names `dash` and `pydantic`. -* pandas DataFrame has a lowercase "p" and camelcase "DataFrame". +- Dash and Pydantic are always capitalized, except where given as Python package names `dash` and `pydantic`. +- pandas DataFrame has a lowercase "p" and camelcase "DataFrame". Vizro components are named using lower case: @@ -28,46 +28,42 @@ Use code font when referring to the component as a class or object: Avoid referring to data using terms like "dataset" or "connector". Prefer to use just "data" or, where that does not feel natural, "data source". ## Bullets -* Capitalize the first word, and end the bullet with a period. -* Don't use numbered bullets except for a sequence of instructions, or where you have to refer back to one of them in the text (or a diagram). + +- Capitalize the first word, and end the bullet with a period. +- Don't use numbered bullets except for a sequence of instructions, or where you have to refer back to one of them in the text (or a diagram). ## Call out boxes Keep the amount of text, and the number and variety of callouts used, to a minimum. There is a [broad set available](https://squidfunk.github.io/mkdocs-material/reference/admonitions/#supported-types) for use in the Vizro docs, but we limit usage to notes, warnings, details and examples: !!! note "note" - - For notable information. + For notable information. !!! warning "warning" - - To indicate a potential gotcha. + To indicate a potential gotcha. ??? details "See more details" - A side note (used sparingly) !!! example "example" - - For example code. - + For example code. Callout boxes can be made collapsible: if you use them, add them to the page so they are initially collapsed. ???+ note "Limit the use of collapsible callouts to secondary information only" - Don't use expanded-on-load collapsibles like this one. If the callout contains important information and needs to be shown as expanded on page load, it should simply be non-collapsible. ## Capitalization -* Only capitalize proper nouns such as the names of technology products, other tools and services. -* Don't capitalize cloud, internet, machine learning, or advanced analytics. Take a look at the [Microsoft style guide](https://docs.microsoft.com/en-us/style-guide/a-z-word-list-term-collections/term-collections/accessibility-terms) if you're unsure. -* Follow sentence case, which capitalizes only the first word of a title/subtitle. We prefer "An introduction to data visualization" to "An Introduction to Data Visualization". +- Only capitalize proper nouns such as the names of technology products, other tools and services. +- Don't capitalize cloud, internet, machine learning, or advanced analytics. Take a look at the [Microsoft style guide](https://docs.microsoft.com/en-us/style-guide/a-z-word-list-term-collections/term-collections/accessibility-terms) if you're unsure. +- Follow sentence case, which capitalizes only the first word of a title/subtitle. We prefer "An introduction to data visualization" to "An Introduction to Data Visualization". ## Code formatting -* Mark code blocks with the appropriate language to enable syntax highlighting. -* We use a `bash` lexer for all codeblocks that represent the terminal, and we don't include the prompt. -* Use the code format for Python package names such as `pandas` or `pydantic`. + +- Mark code blocks with the appropriate language to enable syntax highlighting. +- We use a `bash` lexer for all codeblocks that represent the terminal, and we don't include the prompt. +- Use the code format for Python package names such as `pandas` or `pydantic`. ## Headings and subheadings @@ -77,43 +73,45 @@ Aim to avoid use of gerunds (verb+ing) where you can. So your page should be "Ge In Vizro, when you are working on a how-to guide, there are a few additional guidelines to follow for consistency: -* Don't use "how to" in the file name: keep that as short as possible. -* The title (H1 header) should start with "How to". -* Don't use "how to" in the subsections that follow (H2 - H5) unless you consider the alternative to be confusing. -* Don't use gerund form in the subsections either. +- Don't use "how to" in the file name: keep that as short as possible. +- The title (H1 header) should start with "How to". +- Don't use "how to" in the subsections that follow (H2 - H5) unless you consider the alternative to be confusing. +- Don't use gerund form in the subsections either. Example: In a page called "Filters" you would have the following: -* H1: "How to use filters" -* H2 subsection: "Use a custom filter" and not "How to use a custom filter" nor "Using a custom filter". - +- H1: "How to use filters" +- H2 subsection: "Use a custom filter" and not "How to use a custom filter" nor "Using a custom filter". ## Instructions Prefer to use imperatives to make instructions. For example: + > Complete the configuration steps You don't need to use the word "please" -- readers want less to read and don't think it's rude if you omit it. You can also use second person: + > You should complete the configuration steps. Don't use the passive tense: -> The configuration steps should be completed. -!!!note "What is passive tense?" +> The configuration steps should be completed. +!!! note "What is passive tense?" If you can add "by zombies" to the end of any sentence, it is passive. - * For example: "The configuration steps should be completed." can also be read as: "The configuration should be completed BY ZOMBIES". - * Instead, you'd write this: "You should complete the configuration steps" or better still, "Complete the configuration steps". - + - For example: "The configuration steps should be completed." can also be read as: "The configuration should be completed BY ZOMBIES". + - Instead, you'd write this: "You should complete the configuration steps" or better still, "Complete the configuration steps". ## Language -* Use US English. + +- Use US English. ## Links -* Make hyperlink descriptions as descriptive as you can. This is a good description: + +- Make hyperlink descriptions as descriptive as you can. This is a good description: > Learn how to [contribute to Vizro](https://vizro.readthedocs.io/en/stable/pages/development/contributing/). @@ -125,16 +123,16 @@ Don't write this: > To learn how to contribute to Vizro, see [here](https://vizro.readthedocs.io/en/stable/pages/development/contributing/). - ### Internal cross-referencing -We use internal cross-references as follows: -* For each documentation page, if it helps the reader, we link to narrative documentation (non-API documentation) about each Vizro topic where it is first introduced. -* On any single page, we limit the repetition of links: do not re-link to the same page again unless there is good reason to do so (for example, linking to a specific sub-section to illustrate a point). -* Add links to relevant API documentation where it is useful for the reader, and consider how they will navigate from where they land in the API documentation back to the narrative content. Consider adding a link in the relevant docstring back to your page. +We use internal cross-references as follows: +- For each documentation page, if it helps the reader, we link to narrative documentation (non-API documentation) about each Vizro topic where it is first introduced. +- On any single page, we limit the repetition of links: do not re-link to the same page again unless there is good reason to do so (for example, linking to a specific sub-section to illustrate a point). +- Add links to relevant API documentation where it is useful for the reader, and consider how they will navigate from where they land in the API documentation back to the narrative content. Consider adding a link in the relevant docstring back to your page. ## Oxford commas + Use these in lists to avoid confusion. This is confusing: > The ice cream comes in a range of flavors including banana and strawberry, mango and raspberry and blueberry. @@ -158,6 +156,7 @@ Simple is not vague, verbose or full of jargon: > We leverage your existing organizational resources to synthesize novel competencies. ### Friendly + Friendly is approachable and open, and it makes discussions flow: > Vizro: Let’s make cool visualizations happen. Together. @@ -178,9 +177,10 @@ Functional is not try-hard, cliched or hyperbolic: ## Things to avoid -* **Gerunds in headings**. What are these? They are the "-ing" forms of verbs. If you find yourself writing "Getting started" in a heading, then consider "Get started" or "How to get started" instead. In fact, in general, it's better to avoid gerund-forms of verbs where you can. -* **Plagiarism**. Link to their text and credit them. -* **Colloquialisms**. Avoid them "like the plague" because they may not translate to other regions/languages. -* **Technical terminology**. This applies particularly to acronyms that do not pass the "Google test". If it is not possible to find their meaning from a simple Google search, don't use them, or explain them with a link or some text. -* **Business speak**. You can explain simply without using words like "leverage", "utilize" or "facilitate" and still sound clever. +- **Gerunds in headings**. What are these? They are the "-ing" forms of verbs. If you find yourself writing "Getting started" in a heading, then consider "Get started" or "How to get started" instead. In fact, in general, it's better to avoid gerund-forms of verbs where you can. +- **Plagiarism**. Link to their text and credit them. +- **Colloquialisms**. Avoid them "like the plague" because they may not translate to other regions/languages. +- **Technical terminology**. This applies particularly to acronyms that do not pass the "Google test". If it is not possible to find their meaning from a simple Google search, don't use them, or explain them with a link or some text. +- **Business speak**. You can explain simply without using words like "leverage", "utilize" or "facilitate" and still sound clever. + diff --git a/vizro-core/docs/pages/explanation/faq.md b/vizro-core/docs/pages/explanation/faq.md index 98dd6f051..084687701 100644 --- a/vizro-core/docs/pages/explanation/faq.md +++ b/vizro-core/docs/pages/explanation/faq.md @@ -9,72 +9,60 @@ Here are some answers to frequently asked questions: -* [Which browsers does Vizro support?](#which-browsers-does-vizro-support) -* [What is the Vizro versioning policy?](#what-is-the-vizro-versioning-policy) -* [Where can I find example dashboards?](#where-can-i-find-example-dashboards) -* [Why should I use Vizro?](#why-should-i-use-vizro) -* [How does Vizro differ from Dash or Streamlit?](#how-does-vizro-differ-from-dash-or-streamlit) -* [How does Vizro compare with Python packages and business intelligence (BI) tools?](#how-does-vizro-compare-with-python-packages-and-business-intelligence-bi-tools) -* [When would an alternative to Vizro be more suitable?](#when-would-an-alternative-to-vizro-be-more-suitable) -* [How can I report a bug?](#how-can-i-report-a-bug) -* [How can I request a feature?](#how-can-i-request-a-feature) -* [I still have a question. Where can I ask it?](#i-still-have-a-question-where-can-i-ask-it) +- [Which browsers does Vizro support?](#which-browsers-does-vizro-support) +- [What is the Vizro versioning policy?](#what-is-the-vizro-versioning-policy) +- [Where can I find example dashboards?](#where-can-i-find-example-dashboards) +- [Why should I use Vizro?](#why-should-i-use-vizro) +- [How does Vizro differ from Dash or Streamlit?](#how-does-vizro-differ-from-dash-or-streamlit) +- [How does Vizro compare with Python packages and business intelligence (BI) tools?](#how-does-vizro-compare-with-python-packages-and-business-intelligence-bi-tools) +- [When would an alternative to Vizro be more suitable?](#when-would-an-alternative-to-vizro-be-more-suitable) +- [How can I report a bug?](#how-can-i-report-a-bug) +- [How can I request a feature?](#how-can-i-request-a-feature) +- [I still have a question. Where can I ask it?](#i-still-have-a-question-where-can-i-ask-it) ## Which browsers does Vizro support? -Vizro supports the [Chrome browser](https://www.google.com/intl/en_us/chrome/). -Other browsers may work, but are not officially supported. + +Vizro supports the [Chrome browser](https://www.google.com/intl/en_us/chrome/). Other browsers may work, but are not officially supported. ## What is the Vizro versioning policy? -This project adheres to [semantic versioning](https://semver.org/spec/v2.0.0.html). -We do not consider frontend changes (such as changing the appearance of a component) to be breaking changes. -!!! note +This project adheres to [semantic versioning](https://semver.org/spec/v2.0.0.html). We do not consider frontend changes (such as changing the appearance of a component) to be breaking changes. +!!! note While being in version `0.x.x`, we may introduce breaking changes in minor versions. - ## Where can I find example dashboards? For a gallery of examples showing Vizro in action, take a look at [vizro.mckinsey.com](https://vizro.mckinsey.com). The gallery links to the [Vizro HuggingFace collection](https://huggingface.co/vizro), which includes complete code accessed for each example by selecting "Files" in the top right menu. We also maintain a separate, curated page of [videos, blog posts, and examples of Vizro usage from our community](your-examples.md). - ## Why should I use Vizro? -Vizro is a high-level framework built on top of Dash and Pydantic, -which makes it easier to build advanced dashboards -since it automates many of the otherwise complex and time-consuming tasks -traditionally associated with designing, building and deploying front-end applications, -from prototypes to production. +Vizro is a high-level framework built on top of Dash and Pydantic, which makes it easier to build advanced dashboards since it automates many of the otherwise complex and time-consuming tasks traditionally associated with designing, building and deploying front-end applications, from prototypes to production. ### You can build beautiful & powerful dashboards, quickly & easily Users can configure Vizro dashboards without needing to know advanced software development principles, nor how to build front-end applications. -??? details "See more details" +??? details "See more details"
Image title
less than 30 lines of configuration can create dashboards with multiple charts and filters
- - **Beautiful** - inbuilt visual design best practices are applied automatically, - so users can create beautiful dashboards without needing to know any HTML, CSS or design principles. + - **Beautiful** - inbuilt visual design best practices are applied automatically, so users can create beautiful dashboards without needing to know any HTML, CSS or design principles. - **Powerful** - advanced functionality and interactions come out-of-the-box, by using just a few lines of simple configuration. - - **Quick and easy** - the simple configuration follows an intuitive "grammar of dashboards" which is quick to learn and easy to use. - This removes most of the "glue code" that would otherwise need to be written. Thousands of lines of code are reduced to tens of lines of configuration. - Users can configure Vizro dashboards without needing to know any advanced software development principles of how to build front-end applications. + - **Quick and easy** - the simple configuration follows an intuitive "grammar of dashboards" which is quick to learn and easy to use. This removes most of the "glue code" that would otherwise need to be written. Thousands of lines of code are reduced to tens of lines of configuration. Users can configure Vizro dashboards without needing to know any advanced software development principles of how to build front-end applications. ### You can extend and customize infinitely Users benefit from the power of the Dash framework and the flexibility of React. ??? details "See more details" - - - **Dash** - since Vizro is built on top of Dash, then users benefit from all the underlying power and customizations offered by the Dash framework, - including the ability to use extension libraries related to Dash. + - **Dash** - since Vizro is built on top of Dash, then users benefit from all the underlying power and customizations offered by the Dash framework, including the ability to use extension libraries related to Dash. - **React** - since Dash enables JavaScript React components to be incorporated into Dash applications, Vizro users can create custom charts and UI components which offer the infinite flexibility of React. - **Vizro extensions** - adding extensions such as user defined custom charts, components, actions and data connectors is intuitively incorporated into the configuration language of Vizro. @@ -83,74 +71,54 @@ Users benefit from the power of the Dash framework and the flexibility of React. Consistency and re-usability designed for scale. ??? details "See more details" - - - **Prototype rapidly** - even complex dashboards can be created within minutes using Vizro, - which enables prototype dashboards to be created and iterated on quickly and easily, with very low barrier to entry. + - **Prototype rapidly** - even complex dashboards can be created within minutes using Vizro, which enables prototype dashboards to be created and iterated on quickly and easily, with very low barrier to entry. - **Deploy easily** - since Vizro is built on Dash which uses Flask, it is simple to deploy Vizro like any other Dash application, and use application servers such as Gunicorn to scale to multiple users. - - **Scale** - since Vizro offers standardization of visual design, application architecture and configuration language, - it is easier to scale across multiple developers, projects and implementations in a consistent and reusable way. + - **Scale** - since Vizro offers standardization of visual design, application architecture and configuration language, it is easier to scale across multiple developers, projects and implementations in a consistent and reusable way. ## How does Vizro differ from Dash or Streamlit? -Potential users sometimes request comparisons between Vizro and similar tools such as Dash and Streamlit. -In many ways a direct comparison is not possible as these products tackle somewhat different use cases and their relative pros and cons change depending on the particular requirements of each different user. +Potential users sometimes request comparisons between Vizro and similar tools such as Dash and Streamlit. In many ways a direct comparison is not possible as these products tackle somewhat different use cases and their relative pros and cons change depending on the particular requirements of each different user. Any attempt at a high-level explanation must rely on an oversimplification that misses many important nuances. With the caveat that it's not possible to "compare apples with pears", and that any comparison will have a different conclusion for different users, an oversimplified view could be: ??? details "Streamlit is great for rapid prototyping" - - - **rapid prototyping** - Streamlit's architecture allows you to write apps the same way you write plain Python scripts. - To unlock this, Streamlit apps have a unique data flow: any time something must be updated on the screen, Streamlit reruns your entire Python script from top to bottom. [[1]](https://docs.streamlit.io/library/get-started/main-concepts) - This turns data scripts into sharable web apps in minutes. [[2]](https://streamlit.io/) - Adding a widget is the same as declaring a variable. - (No need to write a backend, define routes, handle HTTP requests, connect a frontend, write HTML, CSS, JavaScript, etc. [[3]](https://streamlit.io/)) + - **rapid prototyping** - Streamlit's architecture allows you to write apps the same way you write plain Python scripts. To unlock this, Streamlit apps have a unique data flow: any time something must be updated on the screen, Streamlit reruns your entire Python script from top to bottom. [\[1\]](https://docs.streamlit.io/library/get-started/main-concepts) This turns data scripts into sharable web apps in minutes. [\[2\]](https://streamlit.io/) Adding a widget is the same as declaring a variable. (No need to write a backend, define routes, handle HTTP requests, connect a frontend, write HTML, CSS, JavaScript, etc. [\[3\]](https://streamlit.io/)) ??? details "Dash is great for customization and scalability" - - - **customization** - one of the great things about Dash is that it is built on top of React.js, a JavaScript library for building web components. - Thousands of components have been built and released with open source licenses by the React community, any of which could be adapted into a Dash component. [[4]](https://dash.plotly.com/plugins) - Dash supports adding custom CSS [[5]](https://dash.plotly.com/external-resources) and HTML, callbacks for custom behavior, - and many component libraries such as Dash Bootstrap components [[6]](https://dash-bootstrap-components.opensource.faculty.ai/) - - **scalability** - based on Flask which is widely adopted by the Python community and deployed in production environments - everywhere [[7]](https://medium.com/plotly/introducing-dash-5ecf7191b503) Dash was designed to be a stateless framework. Stateless frameworks are more scalable and robust [[8]](https://dash.plotly.com/sharing-data-between-callbacks#why-share-state?) + - **customization** - one of the great things about Dash is that it is built on top of React.js, a JavaScript library for building web components. Thousands of components have been built and released with open source licenses by the React community, any of which could be adapted into a Dash component. [\[4\]](https://dash.plotly.com/plugins) Dash supports adding custom CSS [\[5\]](https://dash.plotly.com/external-resources) and HTML, callbacks for custom behavior, and many component libraries such as Dash Bootstrap components [\[6\]](https://dash-bootstrap-components.opensource.faculty.ai/) + - **scalability** - based on Flask which is widely adopted by the Python community and deployed in production environments everywhere [\[7\]](https://medium.com/plotly/introducing-dash-5ecf7191b503) Dash was designed to be a stateless framework. Stateless frameworks are more scalable and robust [\[8\]](https://dash.plotly.com/sharing-data-between-callbacks#why-share-state?) ??? details "Vizro is great for combining rapid prototyping with customization and scalability" - - **rapid prototyping** - since Vizro is a high-level framework providing declarative configuration, it is quick and easy to create powerful interactive apps in minutes, without needing to write callbacks, HTML, CSS, or JavaScript. Key topics such as applying state management, application architecture, and testing are done automatically by Vizro. - **customization and scalability** - since Vizro is built on top of Dash, then users benefit from all the underlying power of the Dash framework for customization and scalability - **beauty and robustness** - since Vizro uses inbuilt visual design and software development best practices, it automatically generates dashboards which look beautiful and can go from prototype to production quickly and easily -All are great entry points to the world of data apps. -If you prefer a top-down scripting style, then Streamlit is a powerful approach. -If you prefer full control and customization over callbacks and layouts, then Dash is a powerful approach. -If you prefer a configuration approach with in-built best practices, and the potential for customization and scalability through Dash, then Vizro is a powerful approach. +All are great entry points to the world of data apps. If you prefer a top-down scripting style, then Streamlit is a powerful approach. If you prefer full control and customization over callbacks and layouts, then Dash is a powerful approach. If you prefer a configuration approach with in-built best practices, and the potential for customization and scalability through Dash, then Vizro is a powerful approach. -For a more detailed comparison, it may help to visit the introductory articles of [Dash](https://medium.com/plotly/introducing-dash-5ecf7191b503), [Streamlit](https://towardsdatascience.com/coding-ml-tools-like-you-code-ml-models-ddba3357eace) and [Vizro](https://quantumblack.medium.com/introducing-vizro-a-toolkit-for-creating-modular-data-visualization-applications-3a42f2bec4db), -to see how each tool serves a distinct purpose, and could be the best tool of choice. +For a more detailed comparison, it may help to visit the introductory articles of [Dash](https://medium.com/plotly/introducing-dash-5ecf7191b503), [Streamlit](https://towardsdatascience.com/coding-ml-tools-like-you-code-ml-models-ddba3357eace) and [Vizro](https://quantumblack.medium.com/introducing-vizro-a-toolkit-for-creating-modular-data-visualization-applications-3a42f2bec4db), to see how each tool serves a distinct purpose, and could be the best tool of choice. ## How does Vizro compare with Python packages and business intelligence (BI) tools? + There are a number of Python packages and BI tools which offer support for visualization applications (such as Streamlit, Plotly/Dash, Tableau and PowerBI). Vizro is intended to support several niches between the benefits from those tools, rather than being in direct comparison with any single tool. Therefore, direct comparisons are often only partially suitable, given the many features offered across this landscape. -However, in general, there are several areas of functionality where Vizro can be particularly useful, such as in providing a simple configuration to speed up the assembly of components, leveraging inbuilt visual design, application architecture, and coding standards, -along with the ability to scale easily across multiple developers and implementations. +However, in general, there are several areas of functionality where Vizro can be particularly useful, such as in providing a simple configuration to speed up the assembly of components, leveraging inbuilt visual design, application architecture, and coding standards, along with the ability to scale easily across multiple developers and implementations. ??? details "See more details" - - | Functionality | Benefits | In context of Python packages | In context of BI tools | - | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | - | **Assembly system** | | Many Python packages still require a moderate understanding of the coding required to assemble higher-level dashboard concepts, which necessitates the creation of “glue code” to combine lower-level components.

Vizro primarily offers configuration to simplify that assembly of components offered by existing packages (currently leveraging Plotly/Dash), and so occupies a slightly different niche from libraries offering primarily the lower-level components themselves. It also removes the requirement from users to implement certain code standards for the assembled code themselves, and therefore saves time on often time consuming things such as writing unit tests and ensuring linting coverage. | Many BI tools incorporate the assembly of higher-level concepts automatically from GUI (drop-and-drag) interfaces, and occupy a slightly different niche to the configuration driven assembly offered by Vizro. | - | **Inbuilt visual design decisions** | | Many Python packages offer inbuilt color choices, and visual design choices for certain components.

Vizro applies that to a wide range of component combinations and complex user flows, offering a holistic and comprehensive approach to automatically enable beautiful visual design best practices, whilst allowing customization and flexibility. | Many BI tools supply inbuilt color choices and visual design choices for certain components.

Vizro offers inbuilt visual design for components in addition to the ability to customize them in a flexible way where needed (for example through CSS), and automatic arrangement of components on the screen. | - | **Inbuilt application architecture decisions** | | Many Python packages supply inbuilt application architecture choices for certain functionalities.

Vizro applies that to a wide range of component combinations and complex user flows, offering a holistic and comprehensive approach to the entire application architecture, whilst allowing customization and flexibility where relevant. | Many BI tools apply proprietary application architecture by default.

Vizro enables the user to view and understand the application architecture by directly viewing the code. | - | **Declarative configuration, in multiple formats** | | Many Python packages give a mostly declarative configuration, with limited ability to leverage multiple formats such as Pydantic models and JSON.

Vizro simplifies this approach, and facilitates the extension of the assembly system to enable integration with other tools such as Kedro to programmatically and dynamically generate the relevant dashboard configurations in a streamlined way. | Many BI tools solely use a GUI and/or drag-and-drop interface for defining dashboards, and leverage proprietary configuration formats.

Vizro enables the user to view and understand the configuration by viewing it directly, in addition to being able to edit it directly. | - | **Mostly tech agnostic “grammar of dashboards”** | | Many Python packages supply an effective “grammar of charts” and Python specific declaration.

Vizro offers a “grammar of dashboards” with declaration which is largely tech agnostic and can be extended to non-Python languages. | Many BI tools utilize an implicit internal “grammar of dashboards” which is specific to the proprietary language(s) on with which they are built.

Vizro offers an explicit configuration grammar and allows users to leverage a mostly tech agnostic approach (which can be extended in future). | - | **Inbuilt validation** | | Many Python packages supply individual components or non-Pydantic based models.

Vizro offers the advantages of Pydantic based models for many elements of the configuration process, which leverages the validation and guidance inherent in that process to facilitate implementation by users. | Many BI tools have inbuilt guideline systems to ensure components are combined in a valid way, and give feedback to the manual user to help guide through that process.

Vizro utilizes a flexible system which can be extended to give feedback to programmatic generation of configuration. | - | **Modularity** | | Many Python packages support modularity of components such as charts and controls.

Vizro supports modularity of groups of components (such as their implementation together as a dashboard screen), which can be easily transferred between implementations as configuration. | Many BI tools supply extensions and plugins which support modularity of visualizations.

Vizro supports modularity of groups of components (such as their implementation together as a dashboard screen), which can be easily transferred between implementations as configuration. | - | **Flexibility** | | Many Python packages offer some low-code and/or high-code approaches, which offer varying degrees of flexibility.

Vizro supports a holistic combination of low-code and high-code approaches which support less technical and more technical users individually while allowing them much flexibility in implementation according to their technical level. | Many BI tools offer a no-code (or at least low-code) approach to creating charts and dashboards, along with varying forms of plugins to increase flexibility.

Vizro unlocks Python custom functions to be able to power this user driven flexibility in a technically advanced way, with a high degree of control and visibility over the code, whilst also supporting less technical users through low-code configuration. | - | **Scaling** | | Many Python packages benefit from the ability to propagate updates programmatically, while applying that to “glue code”.

Vizro makes it easy to scale by replicating the relatively small amount of configuration in 1 file between usages, rather than replicating a large amount of code across many files between usages.

Updating configuration programmatically from a central location is often easier than updating code. When sharing between users, it can be easier to inherit and understand configuration than the underlying code. The visual consistency makes it easier to scale within or between projects while maintaining visual coherence. | Many BI tools allow scaling through GUI drag-and-drop interfaces (having some functionality for duplication or propagating changes) for single users.

Vizro makes it easy to leverage tools such as Git to enable almost any number of users to collaborate effectively, therefore allowing the number of developers on a single project to scale easily.

Since updates can be propagated programmatically easily from a central location, it allows scaling across almost any number of related implementations which can be kept up to date and aligned without the need to manually adjust each implementation when updates are required. | - | **Python first** | | Python packages are already Python focused.

Vizro is no different in this respect. By leveraging Plotly/Dash, Vizro is also able to benefit from the power and flexibility offered by JavaScript via React, whilst still presenting a format that is Python first to the user (by making use of the ability offered by Dash to effectively wrap those components into Python). | Many BI tools do not offer direct or full integration with Python.

Vizro supports a Python first approach which leverages the power and flexibility of Python, and the open source community supporting that wide ranging functionality. | - | **Open source** | | Many Python packages also use an open source license, and offer ongoing development and maintenance.

Vizro is no different in this respect. | Many BI tools follow a license fee model and/or charge for ongoing development and maintenance.

Vizro requires no license fee, and offers ongoing development and support, which helps to remove some barriers to usage. | + | Functionality | Benefits | In context of Python packages | In context of BI tools | + | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | + | **Assembly system** | | Many Python packages still require a moderate understanding of the coding required to assemble higher-level dashboard concepts, which necessitates the creation of “glue code” to combine lower-level components.

Vizro primarily offers configuration to simplify that assembly of components offered by existing packages (currently leveraging Plotly/Dash), and so occupies a slightly different niche from libraries offering primarily the lower-level components themselves. It also removes the requirement from users to implement certain code standards for the assembled code themselves, and therefore saves time on often time consuming things such as writing unit tests and ensuring linting coverage. | Many BI tools incorporate the assembly of higher-level concepts automatically from GUI (drop-and-drag) interfaces, and occupy a slightly different niche to the configuration driven assembly offered by Vizro. | + | **Inbuilt visual design decisions** | | Many Python packages offer inbuilt color choices, and visual design choices for certain components.

Vizro applies that to a wide range of component combinations and complex user flows, offering a holistic and comprehensive approach to automatically enable beautiful visual design best practices, whilst allowing customization and flexibility. | Many BI tools supply inbuilt color choices and visual design choices for certain components.

Vizro offers inbuilt visual design for components in addition to the ability to customize them in a flexible way where needed (for example through CSS), and automatic arrangement of components on the screen. | + | **Inbuilt application architecture decisions** | | Many Python packages supply inbuilt application architecture choices for certain functionalities.

Vizro applies that to a wide range of component combinations and complex user flows, offering a holistic and comprehensive approach to the entire application architecture, whilst allowing customization and flexibility where relevant. | Many BI tools apply proprietary application architecture by default.

Vizro enables the user to view and understand the application architecture by directly viewing the code. | + | **Declarative configuration, in multiple formats** | | Many Python packages give a mostly declarative configuration, with limited ability to leverage multiple formats such as Pydantic models and JSON.

Vizro simplifies this approach, and facilitates the extension of the assembly system to enable integration with other tools such as Kedro to programmatically and dynamically generate the relevant dashboard configurations in a streamlined way. | Many BI tools solely use a GUI and/or drag-and-drop interface for defining dashboards, and leverage proprietary configuration formats.

Vizro enables the user to view and understand the configuration by viewing it directly, in addition to being able to edit it directly. | + | **Mostly tech agnostic “grammar of dashboards”** | | Many Python packages supply an effective “grammar of charts” and Python specific declaration.

Vizro offers a “grammar of dashboards” with declaration which is largely tech agnostic and can be extended to non-Python languages. | Many BI tools utilize an implicit internal “grammar of dashboards” which is specific to the proprietary language(s) on with which they are built.

Vizro offers an explicit configuration grammar and allows users to leverage a mostly tech agnostic approach (which can be extended in future). | + | **Inbuilt validation** | | Many Python packages supply individual components or non-Pydantic based models.

Vizro offers the advantages of Pydantic based models for many elements of the configuration process, which leverages the validation and guidance inherent in that process to facilitate implementation by users. | Many BI tools have inbuilt guideline systems to ensure components are combined in a valid way, and give feedback to the manual user to help guide through that process.

Vizro utilizes a flexible system which can be extended to give feedback to programmatic generation of configuration. | + | **Modularity** | | Many Python packages support modularity of components such as charts and controls.

Vizro supports modularity of groups of components (such as their implementation together as a dashboard screen), which can be easily transferred between implementations as configuration. | Many BI tools supply extensions and plugins which support modularity of visualizations.

Vizro supports modularity of groups of components (such as their implementation together as a dashboard screen), which can be easily transferred between implementations as configuration. | + | **Flexibility** | | Many Python packages offer some low-code and/or high-code approaches, which offer varying degrees of flexibility.

Vizro supports a holistic combination of low-code and high-code approaches which support less technical and more technical users individually while allowing them much flexibility in implementation according to their technical level. | Many BI tools offer a no-code (or at least low-code) approach to creating charts and dashboards, along with varying forms of plugins to increase flexibility.

Vizro unlocks Python custom functions to be able to power this user driven flexibility in a technically advanced way, with a high degree of control and visibility over the code, whilst also supporting less technical users through low-code configuration. | + | **Scaling** | | Many Python packages benefit from the ability to propagate updates programmatically, while applying that to “glue code”.

Vizro makes it easy to scale by replicating the relatively small amount of configuration in 1 file between usages, rather than replicating a large amount of code across many files between usages.

Updating configuration programmatically from a central location is often easier than updating code. When sharing between users, it can be easier to inherit and understand configuration than the underlying code. The visual consistency makes it easier to scale within or between projects while maintaining visual coherence. | Many BI tools allow scaling through GUI drag-and-drop interfaces (having some functionality for duplication or propagating changes) for single users.

Vizro makes it easy to leverage tools such as Git to enable almost any number of users to collaborate effectively, therefore allowing the number of developers on a single project to scale easily.

Since updates can be propagated programmatically easily from a central location, it allows scaling across almost any number of related implementations which can be kept up to date and aligned without the need to manually adjust each implementation when updates are required. | + | **Python first** | | Python packages are already Python focused.

Vizro is no different in this respect. By leveraging Plotly/Dash, Vizro is also able to benefit from the power and flexibility offered by JavaScript via React, whilst still presenting a format that is Python first to the user (by making use of the ability offered by Dash to effectively wrap those components into Python). | Many BI tools do not offer direct or full integration with Python.

Vizro supports a Python first approach which leverages the power and flexibility of Python, and the open source community supporting that wide ranging functionality. | + | **Open source** | | Many Python packages also use an open source license, and offer ongoing development and maintenance.

Vizro is no different in this respect. | Many BI tools follow a license fee model and/or charge for ongoing development and maintenance.

Vizro requires no license fee, and offers ongoing development and support, which helps to remove some barriers to usage. | ## When would an alternative to Vizro be more suitable? @@ -162,18 +130,14 @@ There are a number of cases where alternatives to Vizro may be more suitable, in - where Python developers are already very comfortable leveraging other Python packages - - ## How can I report a bug? Head over to our [GitHub issues](https://github.com/mckinsey/vizro/issues) and [create a new bug report](https://github.com/mckinsey/vizro/issues/new/choose). We will try to reproduce the bug you've reported and follow up with the next steps. - ## How can I request a feature? To raise a feature request, head to our [GitHub issues](https://github.com/mckinsey/vizro/issues) and [create a new feature request](https://github.com/mckinsey/vizro/issues/new/choose). The team will then try to understand the request in more detail, explore the feasibility and prioritize it in relation to the current roadmap. We will get back to you as soon as possible with an estimate of whether and when this feature could be released. - ## I still have a question. Where can I ask it? We are happy to receive general questions around Vizro. Take a look at our [GitHub issues](https://github.com/mckinsey/vizro/issues) and [create a new issue](https://github.com/mckinsey/vizro/issues/new/choose) by clicking "General question". diff --git a/vizro-core/docs/pages/explanation/your-examples.md b/vizro-core/docs/pages/explanation/your-examples.md index 6f1da044a..91a0baa2b 100644 --- a/vizro-core/docs/pages/explanation/your-examples.md +++ b/vizro-core/docs/pages/explanation/your-examples.md @@ -4,37 +4,43 @@ This page lists videos, blog posts, and examples of Vizro usage in repositories If you have something you'd like us to include on the list, or spot something that we should include, let us know: -* you can [raise an issue](https://github.com/mckinsey/vizro/issues) on the Vizro repository, -* better still, you can [make a PR to contribute](../explanation/contributing.md) to this page. - +- you can [raise an issue](https://github.com/mckinsey/vizro/issues) on the Vizro repository, +- better still, you can [make a PR to contribute](../explanation/contributing.md) to this page. !!! note - The Vizro team and QuantumBlack, AI by McKinsey, do not take responsibility for third party content. In curating the list below, we may inspect or test an example at time of inclusion, but cannot guarantee the content thereafter. ## Videos -* From [Charming Data on YouTube](https://www.youtube.com/@CharmingData): - * [Build Python Data Apps with Vizro Dash](https://www.youtube.com/watch?v=wmQ6_GZ0zSk). - * [Introduction to Vizro Actions - Plotly Dash](https://www.youtube.com/watch?v=bom-9275Cic&t=8s). - * [Use Dash AG Grid within a Vizro app](https://www.youtube.com/watch?v=YvtVcXwQw0E). - * [Visual Vocabulary Dashboard showcasing charting options in Vizro and Plotly](https://www.youtube.com/watch?v=OZNAokBKT-M). +- From [Charming Data on YouTube](https://www.youtube.com/@CharmingData): + - [Build Python Data Apps with Vizro Dash](https://www.youtube.com/watch?v=wmQ6_GZ0zSk). + - [Introduction to Vizro Actions - Plotly Dash](https://www.youtube.com/watch?v=bom-9275Cic&t=8s). + - [Use Dash AG Grid within a Vizro app](https://www.youtube.com/watch?v=YvtVcXwQw0E). + - [Visual Vocabulary Dashboard showcasing charting options in Vizro and Plotly](https://www.youtube.com/watch?v=OZNAokBKT-M). ## Blog posts -* [Introducing Vizro](https://quantumblack.medium.com/introducing-vizro-a-toolkit-for-creating-modular-data-visualization-applications-3a42f2bec4db). -* [Creating Custom Dashboards with Vizro: A Comprehensive Guide](https://medium.com/@saffand03/creating-custom-dashboards-with-vizro-a-comprehensive-guide-73c69c6f851e). + +- [Introducing Vizro](https://quantumblack.medium.com/introducing-vizro-a-toolkit-for-creating-modular-data-visualization-applications-3a42f2bec4db). +- [Creating Custom Dashboards with Vizro: A Comprehensive Guide](https://medium.com/@saffand03/creating-custom-dashboards-with-vizro-a-comprehensive-guide-73c69c6f851e). + -* [I built a reusable dashboard read-the-docs traffic analytics using vizro](https://medium.com/towards-data-science/i-built-a-reusable-dashboard-for-read-the-docs-traffic-analytics-using-vizro-47dc15dc04f8). + +- [I built a reusable dashboard read-the-docs traffic analytics using vizro](https://medium.com/towards-data-science/i-built-a-reusable-dashboard-for-read-the-docs-traffic-analytics-using-vizro-47dc15dc04f8). + -* [Visualizing data science insights](https://medium.com/quantumblack/visualizing-data-science-insights-dfc8ad0646b6). + +- [Visualizing data science insights](https://medium.com/quantumblack/visualizing-data-science-insights-dfc8ad0646b6). + -* [A low-code, attractive, sharable data dashboard: Illustrating my LinkedIn connections in 100 lines of Python](https://medium.com/design-bootcamp/a-low-code-attractive-sharable-data-dashboard-a60badba2a03). + +- [A low-code, attractive, sharable data dashboard: Illustrating my LinkedIn connections in 100 lines of Python](https://medium.com/design-bootcamp/a-low-code-attractive-sharable-data-dashboard-a60badba2a03). ## Examples on GitHub or PyCafe -* [Personal Vizro app demos by Vizro team member `huong-li-nguyen`](https://github.com/huong-li-nguyen/vizro-app-demos). -* [Proof of concept example by `viiviandias`](https://github.com/viiviandias/poc-vizro/blob/main/brasil_stocks.ipynb). -* [Amazon sales analysis by `Bottleneck44`](https://github.com/Bottleneck44/Amazon-Sales-Analysis/blob/main/Amazon-analysis.ipynb). -* [Insight-AI: Chart and business insight generation by `micky091` using vizro-ai](https://github.com/micky0919/insight-ai) -* [Music trend analysis using Vizro](https://py.cafe/app/KhushaliP/vizro-music-trend-analysis) by [`KhushaliP`](https://github.com/KhushaliP) +- [Personal Vizro app demos by Vizro team member `huong-li-nguyen`](https://github.com/huong-li-nguyen/vizro-app-demos). +- [Proof of concept example by `viiviandias`](https://github.com/viiviandias/poc-vizro/blob/main/brasil_stocks.ipynb). +- [Amazon sales analysis by `Bottleneck44`](https://github.com/Bottleneck44/Amazon-Sales-Analysis/blob/main/Amazon-analysis.ipynb). +- [Insight-AI: Chart and business insight generation by `micky091` using vizro-ai](https://github.com/micky0919/insight-ai) +- [Music trend analysis using Vizro](https://py.cafe/app/KhushaliP/vizro-music-trend-analysis) by [`KhushaliP`](https://github.com/KhushaliP) + diff --git a/vizro-core/docs/pages/tutorials/explore-components.md b/vizro-core/docs/pages/tutorials/explore-components.md index 52a0ef3e3..f59cbf052 100644 --- a/vizro-core/docs/pages/tutorials/explore-components.md +++ b/vizro-core/docs/pages/tutorials/explore-components.md @@ -23,12 +23,10 @@ Vizro uses [`Graph`][vizro.models.Graph] objects and [Plotly Express functions]( The code below shows the steps necessary to add a box plot to the page: 1. Add a Vizro [`Graph`][vizro.models.Graph] to the `components` list. -2. 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. - +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. !!! example "First component" === "app.py" - ```{.python pycafe-link} from vizro import Vizro import vizro.models as vm @@ -55,23 +53,19 @@ The code below shows the steps necessary to add a box plot to the page: ``` === "Result" - - [![FirstPage1]][FirstPage1] - - [FirstPage1]: ../../assets/tutorials/dashboard/dashboard21.png - + [![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. --- + If you prefer to use Python scripts to Notebooks, here's how to try out the dashboard: 1. Create a new script called `app.py`. - 2. Copy the code above into the script. - 3. Navigate to the directory where `app.py` file is located using your terminal. - 4. Run the script by executing the command `python 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 go to `localhost:8050`. You should see the dashboard page with the gapminder data displayed, as shown in the `Result` tab above. @@ -83,11 +77,10 @@ You can combine and arrange various types of `components` on a dashboard page. T The code below adds two components to the page: -* 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. +- 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. !!! 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. !!! example "Add components" @@ -103,6 +96,7 @@ The code below adds two components to the page: ) ``` + === "Code second component" ```py @@ -110,11 +104,11 @@ The code below adds two components to the page: id="line_gdp", figure=px.line(gapminder_data, x="year", y="gdpPercap", color="continent", labels={"year": "Year", "continent": "Continent", - "gdpPercap":"GDP Per Cap"}, title=''), + "gdpPercap":"GDP Per Cap"}), ) ``` - === "app.py" + === "app.py" ```{.python pycafe-link} from vizro import Vizro import vizro.models as vm @@ -143,7 +137,7 @@ The code below adds two components to the page: vm.Graph( figure=px.line(gapminder_data, x="year", y="gdpPercap", color="continent", labels={"year": "Year", "continent": "Continent", - "gdpPercap":"GDP Per Cap"}, title=''), + "gdpPercap":"GDP Per Cap"}), ), ], @@ -152,15 +146,13 @@ The code below adds two components to the page: dashboard = vm.Dashboard(pages=[first_page]) Vizro().build(dashboard).run() ``` - === "Result" - [![FirstPage2]][FirstPage2] - [FirstPage2]: ../../assets/tutorials/dashboard/dashboard22.png + === "Result" + [![FirstPage2]][firstpage2] 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. !!! 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. Find out more in the [Vizro-AI documentation](https://vizro.readthedocs.io/projects/vizro-ai/)! @@ -169,32 +161,23 @@ As you explore the dashboard, you may notice that the current layout could be fu By default, Vizro places each element in the order it was added to `components` list, and spaces them equally. -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). +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 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: +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, 0], [1, 2], [1, 2], [1, 2]] ``` -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: +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: ![image1](../../assets/tutorials/dashboard/dashboard231.png) -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. +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. ![image2](../../assets/tutorials/dashboard/dashboard233.png) -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. +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. ![image3](../../assets/tutorials/dashboard/dashboard232.png) @@ -205,8 +188,8 @@ Run the code below to apply the layout to the dashboard page: ```py layout=vm.Layout(grid=[[0, 0], [1, 2], [1, 2], [1, 2]]) ``` - === "app.py" + === "app.py" ```{.python pycafe-link} from vizro import Vizro import vizro.models as vm @@ -244,32 +227,24 @@ Run the code below to apply the layout to the dashboard page: dashboard = vm.Dashboard(pages=[first_page]) Vizro().build(dashboard).run() ``` - === "Result" - [![FirstPage3]][FirstPage3] - - [FirstPage3]: ../../assets/tutorials/dashboard/dashboard23.png + === "Result" + [![FirstPage3]][firstpage3] ### 2.4. Add a control for dashboard interactivity -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. +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. There are two types of control: -* [`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. - +- [`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. 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). 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. -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. +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. !!! example "Configure filter" === "Code" @@ -278,8 +253,8 @@ are listed in the `targets` parameter, meaning that the filter is be applied to vm.Filter(column="continent", targets=["box_cont", "line_gdp"]), ] ``` - === "app.py" + === "app.py" ```{.python pycafe-link} from vizro import Vizro import vizro.models as vm @@ -324,16 +299,14 @@ are listed in the `targets` parameter, meaning that the filter is be applied to ``` === "Result" - [![FirstPage4]][FirstPage4] - - [FirstPage4]: ../../assets/tutorials/dashboard/dashboard24.png + [![FirstPage4]][firstpage4] Fantastic job! You have completed first dashboard page and gained valuable skills to: 1. [Create an initial figure on a dashboard page](#2-create-a-first-dashboard-page) -2. [Add extra components](#22-add-further-components) -3. [Arrange them in a layout configuration](#23-configure-the-layout) -4. [Set up an interactive dashboard control](#24-add-a-control-for-dashboard-interactivity). +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). ## 3. Create a second dashboard page @@ -343,16 +316,10 @@ Every [`Page`][vizro.models.Page] that you want to display needs to be added to In creating a [`Parameter`][vizro.models.Parameter] object, you define the `target` it applies to. In the code below: -* 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`. +- 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`. - -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). +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). !!! example "Second page" === "Code" @@ -392,8 +359,8 @@ for parameters](../user-guides/parameters.md). ], ) ``` - === "app.py" + === "app.py" ```{.python pycafe-link} from vizro import Vizro import vizro.models as vm @@ -470,25 +437,19 @@ for parameters](../user-guides/parameters.md). dashboard = vm.Dashboard(pages=[first_page,second_page]) Vizro().build(dashboard).run() ``` - === "Result" - [![SecondPage]][SecondPage] - [SecondPage]: ../../assets/tutorials/dashboard/dashboard3.png + === "Result" + [![SecondPage]][secondpage] ### 3.1. Customize with selectors -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. +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. -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. +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. 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`. -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: +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: - [`Parameter`][vizro.models.Parameter]: - [`Checklist`][vizro.models.Checklist] @@ -499,87 +460,24 @@ You can apply selectors to configure [`Filters`][vizro.models.Filter] and ## 4. The final touches -This section puts everything together by adding a -homepage to the example for navigation between the two separate pages. - -For easy navigation within your dashboard, we'll create a page that serves as the entry point for the user. -On this homepage are two [`Cards`][vizro.models.Card] which serve as tiles that can be customized with a title, some text, and an -image. These cards link to the subpages within your dashboard using their `href` attributes as `href="/first-page"` and `href="/second-page"`. This -establishes the navigation links from the homepage to each of the subpages. - -Each page is added to the dashboard using the following line of code: -`vm.Dashboard(pages=[home_page, first_page, second_page])`. This ensures that all the pages are accessible. +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. -The code below illustrates a functional dashboard where you can navigate from the homepage to each -of the subpages. Additionally, you can use the navigation panel on the left side to switch between the three pages. +By default, a navigation panel on the left side enables the user to switch between the two pages. !!! example "Final dashboard" - === "Code" - ```py - home_page = vm.Page( - title="Homepage", - components=[ - vm.Card( - text=""" - ![](https://raw.githubusercontent.com/mckinsey/vizro/786167c822cce65fe85ffad8ed000d8553a5ef44/vizro-core/docs/assets/images/collections.svg#icon-top) - - ### First Page - - Exemplary first dashboard page. - """, - href="/first-page", - ), - vm.Card( - text=""" - ![](https://raw.githubusercontent.com/mckinsey/vizro/786167c822cce65fe85ffad8ed000d8553a5ef44/vizro-core/docs/assets/images/features.svg#icon-top) - - ### Second Page - - Exemplary second dashboard page. - """, - href="/second-page", - ), - ], - ) - ``` - ```py + ```python dashboard = vm.Dashboard(pages=[home_page, first_page, second_page]) + Vizro().build(dashboard).run() ``` - === "app.py" + === "app.py" ```{.python pycafe-link} from vizro import Vizro import vizro.models as vm import vizro.plotly.express as px - home_page = vm.Page( - title="Homepage", - components=[ - vm.Card( - text=""" - ![](https://raw.githubusercontent.com/mckinsey/vizro/786167c822cce65fe85ffad8ed000d8553a5ef44/vizro-core/docs/assets/images/collections.svg) - - ### First Page - - Exemplary first dashboard page. - """, - href="/first-page", - ), - vm.Card( - text=""" - ![](https://raw.githubusercontent.com/mckinsey/vizro/786167c822cce65fe85ffad8ed000d8553a5ef44/vizro-core/docs/assets/images/features.svg#icon-top) - - ### Second Page - - Exemplary second dashboard page. - """, - href="/second-page", - ), - ], - ) - df = px.data.gapminder() gapminder_data = ( df.groupby(by=["continent", "year"]). @@ -648,34 +546,23 @@ of the subpages. Additionally, you can use the navigation panel on the left side ], ) - dashboard = vm.Dashboard(pages=[home_page, first_page, second_page]) + dashboard = vm.Dashboard(pages=[first_page, second_page]) Vizro().build(dashboard).run() ``` - === "Homepage" - [![FinalPage]][FinalPage] - - [FinalPage]: ../../assets/tutorials/dashboard/dashboard4.png === "Subpage1" - [![FinalPage1]][FinalPage1] - - [FinalPage1]: ../../assets/tutorials/dashboard/dashboard2.png + [![FinalPage1]][finalpage1] === "Subpage2" - [![FinalPage2]][FinalPage2] + [![FinalPage2]][finalpage2] - [FinalPage2]: ../../assets/tutorials/dashboard/dashboard3.png - -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. +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. ## 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. -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 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). Vizro doesn't end here, and we only covered the key features, but there is still much more to explore! You can learn: @@ -683,3 +570,11 @@ Vizro doesn't end here, and we only covered the key features, but there is still - 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 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 diff --git a/vizro-core/docs/pages/tutorials/first-dashboard.md b/vizro-core/docs/pages/tutorials/first-dashboard.md index 0f69ee34d..a33bb4521 100644 --- a/vizro-core/docs/pages/tutorials/first-dashboard.md +++ b/vizro-core/docs/pages/tutorials/first-dashboard.md @@ -16,8 +16,8 @@ Click on the **Run and edit this code in PyCafe** link below to live-edit the da page = vm.Page( title="My first dashboard", components=[ - vm.Graph(id="scatter_chart", figure=px.scatter(df, x="sepal_length", y="petal_width", color="species")), - vm.Graph(id="hist_chart", figure=px.histogram(df, x="sepal_width", color="species")), + vm.Graph(figure=px.scatter(df, x="sepal_length", y="petal_width", color="species")), + vm.Graph(figure=px.histogram(df, x="sepal_width", color="species")), ], controls=[ vm.Filter(column="species", selector=vm.Dropdown(value=["ALL"])), @@ -29,22 +29,28 @@ Click on the **Run and edit this code in PyCafe** link below to live-edit the da ``` === "Result" - - [![FirstDash]][FirstDash] - - [FirstDash]: ../../assets/tutorials/dashboard/first-dashboard.png + [![FirstDash]][firstdash] + ## Can I break this code? + + When you click the link to "Edit live on PyCafe" the dashboard is running inside your browser. Any changes you make are local and you don't need to worry about breaking the code for others. Nobody else sees the changes you make unless you save a copy of the project as your own Vizro PyCafe project. + ## How can I make my own dashboards? + + You can use PyCafe to experiment with your own Vizro dashboards by dropping code onto a new project. Check out the [PyCafe documentation](https://py.cafe/docs/apps/vizro) for more information. If you need inspiration or a starting point, we make all our examples available for you to try out on PyCafe. Throughout our documentation, follow the "**Run and edit this code in PyCafe**" link below the code snippets to open them in PyCafe. ## Where next? + You are now ready to explore Vizro further, by working through the ["Explore Vizro" tutorial](explore-components.md) or by consulting the [how-to guides](../user-guides/dashboard.md). + +[firstdash]: ../../assets/tutorials/dashboard/first-dashboard.png diff --git a/vizro-core/docs/pages/user-guides/actions.md b/vizro-core/docs/pages/user-guides/actions.md index 3c811eb48..6a41ee25d 100644 --- a/vizro-core/docs/pages/user-guides/actions.md +++ b/vizro-core/docs/pages/user-guides/actions.md @@ -1,7 +1,6 @@ # How to use actions -This guide shows you how to use actions, an idea that is similar to [callbacks](https://dash.plotly.com/basic-callbacks) in `Dash`. -Many components of a dashboard (for example, [`Graph`][vizro.models.Graph] or [`Button`][vizro.models.Button]) have an optional `actions` argument, where you can enter the [`Action`][vizro.models.Action] model. +This guide shows you how to use actions, an idea that is similar to [callbacks](https://dash.plotly.com/basic-callbacks) in `Dash`. Many components of a dashboard (for example, [`Graph`][vizro.models.Graph] or [`Button`][vizro.models.Button]) have an optional `actions` argument, where you can enter the [`Action`][vizro.models.Action] model. By combining the [`Action`][vizro.models.Action] model with an action function, you can create complex dashboard interactions triggered by various events. @@ -12,22 +11,18 @@ There are already a few action functions you can reuse: ## Pre-defined actions -To attach an action to a component, you must enter the [`Action`][vizro.models.Action] model into the component's `action` argument. You can then -add a desired pre-defined action function into the `function` argument of the [`Action`][vizro.models.Action]. +To attach an action to a component, you must enter the [`Action`][vizro.models.Action] model into the component's `action` argument. You can then add a desired pre-defined action function into the `function` argument of the [`Action`][vizro.models.Action]. ??? note "Note on `Trigger`" - Currently each component has one pre-defined trigger property. A trigger property is an attribute of the component that triggers a configured action (for example, for the `Button` it is `n_click`). The below sections are guides on how to use pre-defined action functions. ### Export data -To enable downloading data, you can add the [`export_data`][vizro.actions.export_data] action function to the [`Button`][vizro.models.Button] component. -Hence, as a result, when a dashboard user now clicks the button, all data on the page will be downloaded. +To enable downloading data, you can add the [`export_data`][vizro.actions.export_data] action function to the [`Button`][vizro.models.Button] component. Hence, as a result, when a dashboard user now clicks the button, all data on the page will be downloaded. !!! example "`export_data`" - === "app.py" ```{.python pycafe-link} import vizro.models as vm @@ -61,60 +56,55 @@ Hence, as a result, when a dashboard user now clicks the button, all data on the 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: - - type: graph - figure: - _target_: scatter - data_frame: iris - color: sepal_width - x: petal_length - y: sepal_length - - type: graph - figure: - _target_: histogram - data_frame: iris - color: species - x: petal_length - - type: button - text: Export data - id: export_data_button - actions: - - function: - _target_: export_data + - type: graph + figure: + _target_: scatter + data_frame: iris + color: sepal_width + x: petal_length + y: sepal_length + - type: graph + figure: + _target_: histogram + data_frame: iris + color: species + x: petal_length + - type: button + text: Export data + id: export_data_button + actions: + - function: + _target_: export_data title: Exporting ``` - === "Result" - [![Graph]][Graph] - - [Graph]: ../../assets/user_guides/actions/actions_export.png + === "Result" + [![Graph]][graph] !!! note - - Note that exported data only reflects the original dataset and any native data modifications defined with [`vm.Filter`](filters.md), [`vm.Parameter`](data.md/#parametrize-data-loading) or [`filter_interaction`](actions.md/#filter-data-by-clicking-on-chart) action. - Filters from the chart itself, such as ag-grid filters, are not included, and neither are other chart modifications, nor any data transformations in custom charts. + Note that exported data only reflects the original dataset and any native data modifications defined with [`vm.Filter`](filters.md), [`vm.Parameter`](data.md/#parametrize-data-loading) or [`filter_interaction`](actions.md/#filter-data-by-clicking-on-chart) action. Filters from the chart itself, such as ag-grid filters, are not included, and neither are other chart modifications, nor any data transformations in custom charts. ### Filter data by clicking on chart -To enable filtering when clicking on data in a source chart, you can add the -[`filter_interaction`][vizro.actions.filter_interaction] action function to the [`Graph`][vizro.models.Graph], -[`Table`][vizro.models.Table] or [`AgGrid`][vizro.models.AgGrid] components. -The [`filter_interaction`][vizro.actions.filter_interaction] is currently configured -to be triggered on click only. +To enable filtering when clicking on data in a source chart, you can add the [`filter_interaction`][vizro.actions.filter_interaction] action function to the [`Graph`][vizro.models.Graph], [`Table`][vizro.models.Table] or [`AgGrid`][vizro.models.AgGrid] components. The [`filter_interaction`][vizro.actions.filter_interaction] is currently configured to be triggered on click only. To configure this chart interaction follow the steps below: -1. Add the action function to the source [`Graph`][vizro.models.Graph], [`Table`][vizro.models.Table] or [`AgGrid`][vizro.models.AgGrid] -component and a list of IDs of the target charts into `targets`. +1. Add the action function to the source [`Graph`][vizro.models.Graph], [`Table`][vizro.models.Table] or [`AgGrid`][vizro.models.AgGrid] component and a list of IDs of the target charts into `targets`. + ```py actions=[vm.Action(function=filter_interaction(targets=["scatter_relation_2007"]))] ``` -2. If the source chart is [`Graph`][vizro.models.Graph], enter the filter columns in the `custom_data` argument of the underlying source chart `function`. + +1. If the source chart is [`Graph`][vizro.models.Graph], enter the filter columns in the `custom_data` argument of the underlying source chart `function`. + ```py Graph(figure=px.scatter(..., custom_data=["continent"])) ``` @@ -128,7 +118,6 @@ Selecting a data point with a corresponding value of "Africa" in the continent c Here is an example of how to configure a chart interaction when the source is a [`Graph`][vizro.models.Graph] component. !!! example "Graph `filter_interaction`" - === "app.py" ```{.python pycafe-link} import vizro.models as vm @@ -171,50 +160,47 @@ Here is an example of how to configure a chart interaction when the source is a 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: - - type: graph - figure: - _target_: box - data_frame: gapminder - color: continent - x: continent - y: lifeExp - custom_data: - - continent - actions: - - function: - _target_: filter_interaction - targets: - - scatter_relation_2007 - - type: graph - id: scatter_relation_2007 - figure: - _target_: scatter - data_frame: gapminder - color: continent - x: gdpPercap - y: lifeExp - size: pop + - type: graph + figure: + _target_: box + data_frame: gapminder + color: continent + x: continent + y: lifeExp + custom_data: + - continent + actions: + - function: + _target_: filter_interaction + targets: + - scatter_relation_2007 + - type: graph + id: scatter_relation_2007 + figure: + _target_: scatter + data_frame: gapminder + color: continent + x: gdpPercap + y: lifeExp + size: pop controls: - column: continent type: filter title: Filter interaction ``` - === "Result" - [![Graph2]][Graph2] - - [Graph2]: ../../assets/user_guides/actions/actions_filter_interaction.png + === "Result" + [![Graph2]][graph2] !!! note "`filter_interaction` with custom charts" - - If `filter_interaction` is assigned to a [custom chart](custom-charts.md), ensure that `custom_data` is an argument of the custom chart function, and that this argument is then passed to the underlying plotly function. - When then adding the custom chart in `vm.Graph`, ensure that `custom_data` is passed. + If `filter_interaction` is assigned to a [custom chart](custom-charts.md), ensure that `custom_data` is an argument of the custom chart function, and that this argument is then passed to the underlying plotly function. When then adding the custom chart in `vm.Graph`, ensure that `custom_data` is passed. ```py @capture("graph") @@ -227,11 +213,9 @@ Here is an example of how to configure a chart interaction when the source is a ``` - Here is an example of how to configure a chart interaction when the source is an [`AgGrid`][vizro.models.AgGrid] component. !!! example "AgGrid `filter_interaction`" - === "app.py" ```{.python pycafe-link} import vizro.models as vm @@ -271,57 +255,53 @@ Here is an example of how to configure a chart interaction when the source is an 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: - - type: ag_grid - figure: - _target_: dash_ag_grid - data_frame: gapminder_2007 - actions: - - function: - _target_: filter_interaction - targets: - - scatter_relation_2007 - - type: graph - id: scatter_relation_2007 - figure: - _target_: scatter - data_frame: gapminder_2007 - color: continent - x: gdpPercap - y: lifeExp - size: pop + - type: ag_grid + figure: + _target_: dash_ag_grid + data_frame: gapminder_2007 + actions: + - function: + _target_: filter_interaction + targets: + - scatter_relation_2007 + - type: graph + id: scatter_relation_2007 + figure: + _target_: scatter + data_frame: gapminder_2007 + color: continent + x: gdpPercap + y: lifeExp + size: pop controls: - column: continent type: filter title: Filter interaction ``` - === "Result" - [![Table]][Table] - - [Table]: ../../assets/user_guides/actions/actions_table_filter_interaction.png + === "Result" + [![Table]][table] ### Customize pre-defined actions + Many pre-defined actions are customizable which helps to achieve a more specific goal. Refer to the [API reference][vizro.actions] for the options available. ## Custom actions -If you require an action that isn't available as a pre-defined option, you can create a custom action function. -Refer to our [user guide on custom actions](custom-actions.md) for more information. +If you require an action that isn't available as a pre-defined option, you can create a custom action function. Refer to our [user guide on custom actions](custom-actions.md) for more information. ## Chain actions -The `actions` parameter for the different screen components accepts a `list` of [`Action`][vizro.models.Action] models. -This means that it's possible to chain together a list of actions that are executed by triggering only one component. -The order of action execution is guaranteed, and the next action in the list will start executing only when the previous one is completed. +The `actions` parameter for the different screen components accepts a `list` of [`Action`][vizro.models.Action] models. This means that it's possible to chain together a list of actions that are executed by triggering only one component. The order of action execution is guaranteed, and the next action in the list will start executing only when the previous one is completed. !!! example "Actions chaining" - === "app.py" ```{.python pycafe-link extra-requirements="openpyxl"} import vizro.models as vm @@ -368,44 +348,49 @@ The order of action execution is guaranteed, and the next action in the list wil Vizro().build(dashboard).run() ``` + === "app.yaml" ```yaml pages: - components: - - type: graph - id: scatter - figure: - _target_: scatter - data_frame: iris - color: sepal_width - x: petal_length - y: sepal_length - - type: graph - id: hist - figure: - _target_: histogram - data_frame: iris - color: species - x: petal_length - - type: button - text: Export data - id: export_data_button - actions: - - function: - _target_: export_data - targets: - - scatter - - function: - _target_: export_data - targets: - - hist - file_format: xlsx + - type: graph + id: scatter + figure: + _target_: scatter + data_frame: iris + color: sepal_width + x: petal_length + y: sepal_length + - type: graph + id: hist + figure: + _target_: histogram + data_frame: iris + color: species + x: petal_length + - type: button + text: Export data + id: export_data_button + actions: + - function: + _target_: export_data + targets: + - scatter + - function: + _target_: export_data + targets: + - hist + file_format: xlsx controls: - type: filter column: species title: Exporting ``` + === "Result" - [![Graph3]][Graph3] + [![Graph3]][graph3] - [Graph3]: ../../assets/user_guides/actions/actions_chaining.png +[graph]: ../../assets/user_guides/actions/actions_export.png +[graph2]: ../../assets/user_guides/actions/actions_filter_interaction.png +[graph3]: ../../assets/user_guides/actions/actions_chaining.png +[table]: ../../assets/user_guides/actions/actions_table_filter_interaction.png diff --git a/vizro-core/docs/pages/user-guides/assets.md b/vizro-core/docs/pages/user-guides/assets.md index ae87f5527..87c79fb07 100644 --- a/vizro-core/docs/pages/user-guides/assets.md +++ b/vizro-core/docs/pages/user-guides/assets.md @@ -1,11 +1,8 @@ # How to add static assets -This guide shows you how to add static assets to your dashboard. Static assets are images that you would like to show in your dashboard, or custom CSS and JS files -with which you would like to enhance/change the appearance of your dashboard. +This guide shows you how to add static assets to your dashboard. Static assets are images that you would like to show in your dashboard, or custom CSS and JS files with which you would like to enhance/change the appearance of your dashboard. -To add images, custom CSS or JS files, create a folder named `assets` in the root of your app directory and insert your files. -Assets included in that folder are automatically served after serving Vizro's static files via the `external_stylesheets` and `external_scripts` arguments of [Dash](https://dash.plotly.com/external-resources#adding-external-css/javascript). -The user's `assets` folder thus always takes precedence. +To add images, custom CSS or JS files, create a folder named `assets` in the root of your app directory and insert your files. Assets included in that folder are automatically served after serving Vizro's static files via the `external_stylesheets` and `external_scripts` arguments of [Dash](https://dash.plotly.com/external-resources#adding-external-css/javascript). The user's `assets` folder thus always takes precedence. ```text title="Example folder structure" ├── app.py @@ -21,15 +18,11 @@ The user's `assets` folder thus always takes precedence. ``` !!! warning "Dash Bootstrap Themes" - - Note that Vizro is currently not compatible with [Dash Bootstrap Themes](https://dash-bootstrap-components.opensource.faculty.ai/docs/themes/). - Adding a Bootstrap stylesheet will have no visual effect on the [components](https://vizro.readthedocs.io/en/stable/pages/user_guides/components/) included in Vizro. - + Note that Vizro is currently not compatible with [Dash Bootstrap Themes](https://dash-bootstrap-components.opensource.faculty.ai/docs/themes/). Adding a Bootstrap stylesheet will have no visual effect on the [components](https://vizro.readthedocs.io/en/stable/pages/user_guides/components/) included in Vizro. ## Change 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 the [Dash documentation](https://dash.plotly.com/external-resources#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 the [Dash documentation](https://dash.plotly.com/external-resources#changing-the-favicon). ## Add a logo image @@ -41,52 +34,36 @@ If an image named `logo.` is present in the assets folder, Vizro auto ### Theme-specific logos -You can also supply two images named `logo_dark.` and `logo_light.` to switch logos -based on the theme (dark or light). +You can also supply two images named `logo_dark.` and `logo_light.` to switch logos based on the theme (dark or light). Note that both `logo_light.` and `logo_dark.` must be supplied together, unless a single `logo.` is supplied for both light and dark themes. That is, the valid configurations are as follows: -* Single logo: Supply only `logo.`, which is used for dark and light themes. **Do not include light and dark theme logos**. -* Theme logos: Supply both `logo_light.` and `logo_dark.` for light/dark themes. **Do not include `logo.`**. -* No logo: No logo images supplied. +- Single logo: Supply only `logo.`, which is used for dark and light themes. **Do not include light and dark theme logos**. +- Theme logos: Supply both `logo_light.` and `logo_dark.` for light/dark themes. **Do not include `logo.`**. +- No logo: No logo images supplied. -![Logo dark](../../assets/user_guides/assets/logo-dark.png) -![Logo light](../../assets/user_guides/assets/logo-light.png) +![Logo dark](../../assets/user_guides/assets/logo-dark.png) ![Logo light](../../assets/user_guides/assets/logo-light.png) ## Change the `assets` folder path -If you do not want to place your `assets` folder in the root directory of your app, you can -specify an alternative path through the `assets_folder` argument of the [`Vizro`][vizro.Vizro] class. - -```python -from vizro import Vizro -import vizro.models as vm - -page = -dashboard = vm.Dashboard(pages=[page]) -app = Vizro(assets_folder="path/to/assets/folder").build(dashboard).run() +If you do not want to place your `assets` folder in the root directory of your app, you can specify an alternative path through the `assets_folder` argument of the [`Vizro`][vizro.Vizro] class. +```python +Vizro(assets_folder="path/to/assets/folder").build(dashboard).run() ``` -Note that in the example above, you still need to configure your [`Page`][vizro.models.Page]. -See more information in the [Pages User Guide](pages.md). - - ## Include a meta tags image -Vizro automatically adds [meta tags](https://metatags.io/) to display a preview card when your app is shared on social media and chat -clients. To include an image in the preview, place an image file in the assets folder named `app.` or -`logo.`. Vizro searches the assets folder and uses the first one it finds. +Vizro automatically adds [meta tags](https://metatags.io/) to display a preview card when your app is shared on social media and chat clients. To include an image in the preview, place an image file in the assets folder named `app.` or `logo.`. Vizro searches the assets folder and uses the first one it finds. Image types of `apng`, `avif`, `gif`, `jpeg`, `jpg`, `png`, `svg`, and `webp` are supported. - ## Order of serving CSS files CSS properties will be applied with the last served file taking precedence. The order of serving is: 1. Dash built-in stylesheets -2. Vizro built-in stylesheets -3. User assets folder stylesheets +1. Vizro built-in stylesheets +1. User assets folder stylesheets Within each of these categories, individual files are served in alphanumeric order. diff --git a/vizro-core/docs/pages/user-guides/card-button.md b/vizro-core/docs/pages/user-guides/card-button.md index c4f0218d2..7da72d8f1 100755 --- a/vizro-core/docs/pages/user-guides/card-button.md +++ b/vizro-core/docs/pages/user-guides/card-button.md @@ -4,12 +4,10 @@ This guide shows you how to use cards and buttons to visualize and interact with ## Cards -The [`Card`][vizro.models.Card] is a flexible and extensible component that enables customization via markdown text. -Refer to any online guide for [basic markdown usage](https://markdown-guide.readthedocs.io/en/latest/). +The [`Card`][vizro.models.Card] is a flexible and extensible component that enables customization via markdown text. Refer to any online guide for [basic markdown usage](https://markdown-guide.readthedocs.io/en/latest/). You can add a [`Card`][vizro.models.Card] to your dashboard by inserting the [`Card`][vizro.models.Card] into the `components` argument of the [`Page`][vizro.models.Page]. - !!! example "Card" === "app.py" ```{.python pycafe-link} @@ -32,28 +30,26 @@ You can add a [`Card`][vizro.models.Card] to your dashboard by inserting the [`C Vizro().build(dashboard).run() ``` + === "app.yaml" ```yaml # Still requires a .py to add data to the data manager and parse YAML configuration # See from_yaml example pages: - - components: - - text: | - Commodi repudiandae consequuntur voluptatum. - title: Card Title - type: card - title: Card + - components: + - text: | + Commodi repudiandae consequuntur voluptatum. + title: Card Title + type: card + title: Card ``` - === "Result" - [![Card]][Card] - [Card]: ../../assets/user_guides/components/card.png + === "Result" + [![Card]][card] ### Customize card text -The [`Card`][vizro.models.Card] uses the `dcc.Markdown` component from Dash as its underlying text component. -For more details on customizing the markdown text, refer to the [`dcc.Markdown` component documentation](https://dash.plotly.com/dash-core-components/markdown). -Based on examples from Dash, the [`Card`][vizro.models.Card] model supports the following: +The [`Card`][vizro.models.Card] uses the `dcc.Markdown` component from Dash as its underlying text component. For more details on customizing the markdown text, refer to the [`dcc.Markdown` component documentation](https://dash.plotly.com/dash-core-components/markdown). Based on examples from Dash, the [`Card`][vizro.models.Card] model supports the following: - Headers - Emphasis @@ -130,58 +126,58 @@ Based on examples from Dash, the [`Card`][vizro.models.Card] model supports the Vizro().build(dashboard).run() ``` + === "app.yaml" ```yaml # Still requires a .py to add data to the data manager and parse YAML configuration # See from_yaml example pages: - - components: - - text: | - # Header level 1

- - ## Header level 2

- - ### Header level 3

- - #### Header level 4

- type: card - - text: | - Commodi repudiandae consequuntur voluptatum laborum numquam blanditiis harum quisquam eius sed odit. - - Fugiat iusto fuga praesentium option, eaque rerum! Provident similique accusantium nemo autem. - - Obcaecati tenetur iure eius earum ut molestias architecto voluptate aliquam nihil, eveniet aliquid. - - Culpa officia aut! Impedit sit sunt quaerat, odit, tenetur error, harum nesciunt ipsum debitis quas. - title: Paragraphs - type: card - - text: | - > - > A block quote is a long quotation, indented to create a separate block of text. - > - title: Block Quotes - type: card - - text: | - * Item A - * Sub Item 1 - * Sub Item 2 - * Item B - title: Lists - type: card - - text: | - This word will be *italic* - - This word will be **bold** - - This word will be _**bold and italic**_ - title: Emphasis - type: card - title: Customizing Text + - components: + - text: | + # Header level 1

+ + ## Header level 2

+ + ### Header level 3

+ + #### Header level 4

+ type: card + - text: | + Commodi repudiandae consequuntur voluptatum laborum numquam blanditiis harum quisquam eius sed odit. + + Fugiat iusto fuga praesentium option, eaque rerum! Provident similique accusantium nemo autem. + + Obcaecati tenetur iure eius earum ut molestias architecto voluptate aliquam nihil, eveniet aliquid. + + Culpa officia aut! Impedit sit sunt quaerat, odit, tenetur error, harum nesciunt ipsum debitis quas. + title: Paragraphs + type: card + - text: | + > + > A block quote is a long quotation, indented to create a separate block of text. + > + title: Block Quotes + type: card + - text: | + * Item A + * Sub Item 1 + * Sub Item 2 + * Item B + title: Lists + type: card + - text: | + This word will be *italic* + + This word will be **bold** + + This word will be _**bold and italic**_ + title: Emphasis + type: card + title: Customizing Text ``` - === "Result" - [![CardText]][CardText] - [CardText]: ../../assets/user_guides/components/card_text.png + === "Result" + [![CardText]][cardtext] ### Place an image on a card @@ -189,11 +185,10 @@ Images can be added to the `text` parameter by using the standard markdown synta `![Image ALT text](Image URL)` -An image ALT text offers a description to your image and serves as a text placeholder or to improve the -accessibility of your app. Providing an image ALT text is optional. +An image ALT text offers a description to your image and serves as a text placeholder or to improve the accessibility of your app. Providing an image ALT text is optional. 1. To use a relative Image URL, place an image of your choice into your `assets` folder first -2. Use markdown to render your image by using one of the following syntax: +1. Use markdown to render your image by using one of the following syntax: - Relative Image URL: `![Image ALT text](/path/to/image.png)` - Absolute Image URL: `![Image ALT text](https://XXXXXX)` @@ -228,55 +223,50 @@ accessibility of your app. Providing an image ALT text is optional. 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 from_yaml example pages: - - components: - - text: | - ![continent](assets/images/continents/africa.svg) + - components: + - text: | + ![continent](assets/images/continents/africa.svg) - Commodi repudiandae consequuntur voluptatum laborum numquam blanditiis harum quisquam eius sed odit. + Commodi repudiandae consequuntur voluptatum laborum numquam blanditiis harum quisquam eius sed odit. - Fugiat iusto fuga praesentium option, eaque rerum! Provident similique accusantium nemo autem. + Fugiat iusto fuga praesentium option, eaque rerum! Provident similique accusantium nemo autem. - Obcaecati tenetur iure eius earum ut molestias architecto voluptate aliquam nihil, eveniet aliquid. - title: My card with image! - type: card - title: Placing Images + Obcaecati tenetur iure eius earum ut molestias architecto voluptate aliquam nihil, eveniet aliquid. + title: My card with image! + type: card + title: Placing Images ``` - === "Result" - [![CardImageDefault]][CardImageDefault] - [CardImageDefault]: ../../assets/user_guides/components/card_image_default.png + === "Result" + [![CardImageDefault]][cardimagedefault] !!! note - - Note that inserting images using html is by default turned off by the `dcc.Markdown` to prevent users being exposed - to cross-site scripting attacks. If you need to turn it on, a custom component would have to be created. + Note that inserting images using HTML is by default turned off by the `dcc.Markdown` to prevent users being exposed to cross-site scripting attacks. If you need to turn it on, a custom component would have to be created. You might notice that the image is quite large. You'll find out how to style images in terms of their position and size in the next section. - ### Style a card image To change the size or position of the image, add a URL hash to your image like this: `![Image ALT text](Image URL#my-image)` -Note the added URL hash `#my-image`. Now create a CSS file placed in your `assets` folder -and give an attribute selector to select images with that matching URL hash. +Note the added URL hash `#my-image`. Now create a CSS file placed in your `assets` folder and give an attribute selector to select images with that matching URL hash. !!! example "Card with styled image" === "images.css" - ```css - img[src*="#my-image"] { - width: 120px; - height: 120px; - } - ``` + ```css + img[src*="#my-image"] { + width: 120px; + height: 120px; + } + ``` + === "app.py" ```py import vizro.models as vm @@ -313,40 +303,42 @@ and give an attribute selector to select images with that matching URL hash. # Still requires a .py to add data to the data manager and parse YAML configuration # See from_yaml example pages: - - components: - - text: | - ![](assets/images/continents/europe.svg#my-image) + - components: + - text: | + ![](assets/images/continents/europe.svg#my-image) - Commodi repudiandae consequuntur voluptatum laborum numquam blanditiis harum quisquam eius sed odit. + Commodi repudiandae consequuntur voluptatum laborum numquam blanditiis harum quisquam eius sed odit. - Fugiat iusto fuga praesentium option, eaque rerum! Provident similique accusantium nemo autem. + Fugiat iusto fuga praesentium option, eaque rerum! Provident similique accusantium nemo autem. - Obcaecati tenetur iure eius earum ut molestias architecto voluptate aliquam nihil, eveniet aliquid. - title: My card with image! - type: card - title: Styling Images + Obcaecati tenetur iure eius earum ut molestias architecto voluptate aliquam nihil, eveniet aliquid. + title: My card with image! + type: card + title: Styling Images ``` - === "Result" - [![CardImageStyled]][CardImageStyled] - [CardImageStyled]: ../../assets/user_guides/components/card_image_styled.png + === "Result" + [![CardImageStyled]][cardimagestyled] Use the following pre-defined URL hashes in your image path to apply Vizro's default styling. -#### To float the image next to the text: +**To float the image next to the text:** + +To float an image for example to the right of the text, use the `src` attribute as a CSS selector. Follow these steps: -- floating-left: `![](my_image.png#floating-left)` -- floating-right: `![](my_image.png#floating-right)` -- floating-center: `![](my_image.png#floating-center)` +1. Add a hash (#) to the image URL, e.g., `![](assets/images/continents/europe.svg#my-image)` +1. Target that hash in your custom CSS file `img[src*="#my-image"] { float: right; }` !!! example "Card with floating image" === "images.css" - ```css - img[src*="#my-image"] { - width: 120px; - height: 120px; - } - ``` + ```css + img[src*="#my-image"] { + width: 120px; + height: 120px; + float: right; + } + ``` + === "app.py" ```py import vizro.models as vm @@ -359,7 +351,7 @@ Use the following pre-defined URL hashes in your image path to apply Vizro's def text=""" ### My card with floating image! - ![](assets/images/continents/europe.svg#my-image#floating-right) + ![](assets/images/continents/europe.svg#my-image) Commodi repudiandae consequuntur voluptatum laborum numquam blanditiis harum quisquam eius sed odit. @@ -389,91 +381,40 @@ Use the following pre-defined URL hashes in your image path to apply Vizro's def # Still requires a .py to add data to the data manager and parse YAML configuration # See from_yaml example pages: - - components: - - text: | - ![](assets/images/continents/europe.svg#my-image#floating-right) + - components: + - text: | + ![](assets/images/continents/europe.svg#my-image) - Commodi repudiandae consequuntur voluptatum laborum numquam blanditiis harum quisquam eius sed odit. + Commodi repudiandae consequuntur voluptatum laborum numquam blanditiis harum quisquam eius sed odit. - Fugiat iusto fuga praesentium option, eaque rerum! Provident similique accusantium nemo autem. + Fugiat iusto fuga praesentium option, eaque rerum! Provident similique accusantium nemo autem. - Obcaecati tenetur iure eius earum ut molestias architecto voluptate aliquam nihil, eveniet aliquid. + Obcaecati tenetur iure eius earum ut molestias architecto voluptate aliquam nihil, eveniet aliquid. - Culpa officia aut! Impedit sit sunt quaerat, odit, tenetur error, harum nesciunt ipsum debitis quas. + Culpa officia aut! Impedit sit sunt quaerat, odit, tenetur error, harum nesciunt ipsum debitis quas. - Obcaecati tenetur iure eius earum ut molestias architecto voluptate aliquam nihil, eveniet aliquid. + Obcaecati tenetur iure eius earum ut molestias architecto voluptate aliquam nihil, eveniet aliquid. - Culpa officia aut! Impedit sit sunt quaerat, odit, tenetur error, harum nesciunt ipsum debitis quas. - title: My card with floating image! - type: card - title: Floating Images + Culpa officia aut! Impedit sit sunt quaerat, odit, tenetur error, harum nesciunt ipsum debitis quas. + title: My card with floating image! + type: card + title: Floating Images ``` - === "Result" - [![CardImageFloating]][CardImageFloating] - - [CardImageFloating]: ../../assets/user_guides/components/card_image_floating.png - -#### Card with icon - -- default icon styling (`icon-top`): `![](my_image.png#icon-top)` -!!! example "Card with icon" - === "app.py" - ```{.python pycafe-link} - import vizro.models as vm - from vizro import Vizro - - page = vm.Page( - title="Card with icon", - components=[ - vm.Card( - text=""" - ![](https://raw.githubusercontent.com/mckinsey/vizro/d24a6f0d4efdf3c47392458e64b190fa1f92b2a7/vizro-core/docs/assets/images/hypotheses.svg#icon-top) - - ### Card Title - - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut fringilla dictum lacus eget fringilla. - Maecenas in various nibh, quis venenatis nulla. Integer et libero ultrices, scelerisque velit sed. - """, - ), - ], - ) - - 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 from_yaml example - pages: - - components: - - text: | - ![](assets/images/icons/hypotheses.svg#icon-top) - - ### Card Title - - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut fringilla dictum lacus eget fringilla. - Maecenas in various nibh, quis venenatis nulla. Integer et libero ultrices, scelerisque velit sed. - type: card - title: Card with icon - ``` === "Result" - [![CardIcon]][CardIcon] + [![CardImageFloating]][cardimagefloating] - [CardIcon]: ../../assets/user_guides/components/card_icon.png +### Make an icon responsive to theme switch +To make an icon responsive to theme switching, override its [`filter` CSS property](https://developer.mozilla.org/en-US/docs/Web/CSS/filter). -### Make an icon responsive to theme switch +In this example, we use the `--fill-icon-image-card` CSS variable from the `vizro-bootstrap` CSS file. It uses the `invert()` function to flip the icon's color during a theme switch. -To make an icon responsive to the theme switch, override the value of the [`filter` CSS property](https://developer.mozilla.org/en-US/docs/Web/CSS/filter). -The `filter` CSS property lets you add visual effects to elements using different functions. In our example, we're using the `--inverse-color` CSS variable from the Vizro theme. -It uses the CSS `invert()` function to flip the color of the icon when you switch themes. Note that this only works if your initial icon has a white fill color. If your icon is not white, you can change its color by adding `fill="white"` to the SVG code. -Assign the predefined CSS variable `--inverse-color` to the `filter` property of your selected icon. +This approach works if your icon initially has a white fill color. If not, modify the SVG code by adding `fill="white"`. ```css img[src*="#my-image"] { - filter: var(--inverse-color); + filter: var(--fill-icon-image-card); } ``` @@ -482,15 +423,15 @@ img[src*="#my-image"] { ### Create a navigation card -This section describes how to use the [`Card`][vizro.models.Card] component to create a navigation card. To configure the navigation panel on the left hand side of the screen, refer to the [guide on navigation](navigation.md). +This section describes how to use the [`Card`][vizro.models.Card] component to create a navigation card, enabling users to navigate to another page by clicking on the card area. -A navigation card enables you to navigate to a different page via a click on the card area. +For a button-style link navigation component, see the [separate guide on creating a link button](#create-a-link-button). To configure the navigation panel on the left hand side of the screen, refer to the [separate guide on navigation](navigation.md). To create a navigation card: 1. Insert the [`Card`][vizro.models.Card] into the `components` argument of the [`Page`][vizro.models.Page]. -2. Pass your markdown text to the `Card.text`. -3. Pass a relative or absolute URL to the `Card.href`. +1. Pass your markdown text to the `Card.text`. +1. Pass a relative or absolute URL to the `Card.href`. !!! example "Navigation Card" === "app.py" @@ -526,7 +467,7 @@ To create a navigation card: page_2 = vm.Page( title="Filters and parameters", components=[ - vm.Graph(id="scatter", figure=px.scatter(iris, x="sepal_length", y="petal_width", color="sepal_width")), + vm.Graph(figure=px.scatter(iris, x="sepal_length", y="petal_width", color="sepal_width")), ], ) @@ -534,76 +475,110 @@ To create a navigation card: Vizro().build(dashboard).run() ``` + === "app.yaml" ```yaml # Still requires a .py to add data to the data manager and parse YAML configuration # See from_yaml example pages: - - components: - - text: | - ### Filters and parameters - - Leads to the first page on click - href: /filters-and-parameters - type: card - - text: | - ### Google - External Link - - Leads to an external link on click. - href: https://google.com - type: card - title: Homepage - - components: - - figure: - _target_: scatter - color: sepal_width - data_frame: iris - x: sepal_length - y: petal_width - id: scatter - type: graph - title: Filters and parameters + - components: + - text: | + ### Filters and parameters + + Leads to the first page on click + href: /filters-and-parameters + type: card + - text: | + ### Google - External Link + + Leads to an external link on click. + href: https://google.com + type: card + title: Homepage + - components: + - figure: + _target_: scatter + color: sepal_width + data_frame: iris + x: sepal_length + y: petal_width + type: graph + title: Filters and parameters ``` - === "Result" - [![NavCard]][NavCard] - [NavCard]: ../../assets/user_guides/components/nav_card.png + === "Result" + [![NavCard]][navcard] If you now click on the card area, you should automatically be redirected to the relevant `href`. !!! note - When using the [`Card`][vizro.models.Card], keep the following in mind: - If the href given is a relative URL, it should match the `path` of the [`Page`][vizro.models.Page] that the [`Card`][vizro.models.Card] should navigate to. - If the href given is an absolute link, it should start with `https://` or an equivalent protocol. - ### Create a KPI card -To create a KPI card, you can use the existing KPI card functions from [`vizro.figures`](../API-reference/figure-callables.md). -Unlike the static text card `vm.Card`, a KPI card must be created using a figure function, -which enables the text content of the KPI to change based on input from controls or actions. + +To create a KPI card, you can use the existing KPI card functions from [`vizro.figures`](../API-reference/figure-callables.md). Unlike the static text card `vm.Card`, a KPI card must be created using a figure function, which enables the text content of the KPI to change based on input from controls or actions. For detailed examples on how to create a KPI card, refer to the [figure user guide on KPI cards](figure.md#key-performance-indicator-kpi-cards). ## Buttons -To enhance dashboard interactions, you can use the [`Button`][vizro.models.Button] component to trigger any pre-defined -action functions such as exporting chart data. To use the currently available options for the [`Actions`][vizro.models.Action] -component, check out the [API reference][vizro.actions]. +The Button component is commonly used for interactive dashboard interactions such as form submissions, navigation links, and other action triggers. -To add a [`Button`][vizro.models.Button], insert it into the `components` argument of the -[`Page`][vizro.models.Page]. +To add a [`Button`][vizro.models.Button], insert it into the `components` argument of the [`Page`][vizro.models.Page]. -You can configure the `text` argument to alter the display text of the [`Button`][vizro.models.Button] and the -`actions` argument to define which action function should be executed on button click. +### Customize button text -In the below example we show how to configure a button to export the filtered data of a target chart using -[export_data][vizro.actions.export_data], a pre-defined action function. +You can configure the `text` argument to alter the display text of the [`Button`][vizro.models.Button]. +!!! example "Customize text" + === "app.py" + ```{.python pycafe-link} + import vizro.models as vm + from vizro import Vizro -!!! example "Button" + page = vm.Page( + title="Button with text", + components=[vm.Button(text="I'm a button!")], + ) + 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 from_yaml example + pages: + - components: + - type: button + text: I'm a button! + title: Button with text + ``` + + === "Result" + [![ButtonText]][buttontext] + +### Create a link button + +To navigate to a different page using a button with an anchor tag, assign an absolute or relative URL to the `Button.href`. + +```python +import vizro.models as vm + +vm.Button(text="Leave us a star! ⭐", href="https://github.com/mckinsey/vizro") +``` + +### Attach an action + +You can use the [`Button`][vizro.models.Button] to trigger predefined action functions, such as exporting data. To explore the available options for [`Actions`][vizro.models.Action], refer to our [API reference][vizro.actions]. Use the `Button.actions` argument to specify which action function executes when the button is clicked. + +The example below demonstrates how to configure a button to export the filtered data of a target chart using the [export_data][vizro.actions.export_data] action function. + +!!! example "Button with action" === "app.py" ```{.python pycafe-link} import vizro.models as vm @@ -632,36 +607,37 @@ In the below example we show how to configure a button to export the filtered da actions=[vm.Action(function=export_data(targets=["scatter_chart"]))], ), ], - controls=[vm.Filter(column="species", selector=vm.Dropdown(title="Species"))], + controls=[vm.Filter(column="species")], ) 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 from_yaml example pages: - components: - - figure: - _target_: scatter - x: sepal_width - y: sepal_length - color: species - size: petal_length - data_frame: iris - id: scatter_chart - type: graph - - type: button - text: Export data - id: export_data - actions: - - function: - _target_: export_data - targets: - - scatter_chart + - figure: + _target_: scatter + x: sepal_width + y: sepal_length + color: species + size: petal_length + data_frame: iris + id: scatter_chart + type: graph + - type: button + text: Export data + id: export_data + actions: + - function: + _target_: export_data + targets: + - scatter_chart controls: - column: species selector: @@ -677,13 +653,13 @@ In the below example we show how to configure a button to export the filtered da - [1] title: My first page ``` + === "Result" - [![Button]][Button] + [![Button]][button] - [Button]: ../../assets/user_guides/components/button.png +### Use as a control -The [`Button`][vizro.models.Button] component is currently reserved to be used inside the main panel (right-side) of the dashboard. -However, there might be use cases where one would like to place the `Button` inside the control panel (left-side) with the other controls. +The [`Button`][vizro.models.Button] component is currently reserved to be used inside the main panel (right-side) of the dashboard. However, there might be use cases where one would like to place the `Button` inside the control panel (left-side) with the other controls. In this case, follow the user-guide outlined for [creating custom components](custom-components.md) and manually add the `Button` as a valid type to the `controls` argument by running the following lines before your dashboard configurations: @@ -696,3 +672,12 @@ vm.Page.add_type("controls", vm.Button) # Add dashboard configurations below ... ``` + +[button]: ../../assets/user_guides/components/button.png +[buttontext]: ../../assets/user_guides/components/button_text.png +[card]: ../../assets/user_guides/components/card.png +[cardimagedefault]: ../../assets/user_guides/components/card_image_default.png +[cardimagefloating]: ../../assets/user_guides/components/card_image_floating.png +[cardimagestyled]: ../../assets/user_guides/components/card_image_styled.png +[cardtext]: ../../assets/user_guides/components/card_text.png +[navcard]: ../../assets/user_guides/components/nav_card.png diff --git a/vizro-core/docs/pages/user-guides/components.md b/vizro-core/docs/pages/user-guides/components.md index 106bf6a0b..45ea5a123 100755 --- a/vizro-core/docs/pages/user-guides/components.md +++ b/vizro-core/docs/pages/user-guides/components.md @@ -1,8 +1,6 @@ # Components -The [`Page`][vizro.models.Page] model accepts the `components` argument, where you can enter any of the components -listed below to fill your dashboard with visuals. - +The [`Page`][vizro.models.Page] model accepts the `components` argument, where you can enter any of the components listed below to fill your dashboard with visuals.
@@ -38,7 +36,6 @@ listed below to fill your dashboard with visuals. [:octicons-arrow-right-24: View user guide](card-button.md) - - :octicons-table-16:{ .lg .middle } __Containers__ --- diff --git a/vizro-core/docs/pages/user-guides/container.md b/vizro-core/docs/pages/user-guides/container.md index c7e327c5f..2f379709e 100755 --- a/vizro-core/docs/pages/user-guides/container.md +++ b/vizro-core/docs/pages/user-guides/container.md @@ -2,30 +2,22 @@ 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 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. -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. +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. !!! note "Displaying multiple containers inside Tabs" - An alternative way to display multiple containers on one page is to place them inside [Tabs](tabs.md). - [`Tabs`][vizro.models.Tabs] organize and separate groups of related content in a dashboard, letting users switch between different sections or views. - They are a way of putting multiple containers into the same screen space, and letting the user switch between them. + [`Tabs`][vizro.models.Tabs] organize and separate groups of related content in a dashboard, letting users switch between different sections or views. They are a way of putting multiple containers into the same screen space, and letting the user switch between them. ![tabs](../../assets/user_guides/components/tabs-info.png){ width="500" } - - ## When to use containers -In general, any arbitrarily granular layout can already be achieved by [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. -`Page.layout` has a `grid` argument that sets the overall layout of the page. -`Container.layout` also has a `grid` argument. This enables you to insert a further `grid` into a component's space on the page, -enabling more granular control by breaking the overall page grid into subgrids. +In general, any arbitrarily granular layout can already be achieved by [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. + +`Page.layout` has a `grid` argument that sets the overall layout of the page. `Container.layout` also has a `grid` argument. This enables you to insert a further `grid` into a component's space on the page, enabling more granular control by breaking the overall page grid into subgrids. Here are a few cases where you might want to use a `Container` instead of `Page.layout`: @@ -34,14 +26,14 @@ Here are a few cases where you might want to use a `Container` instead of `Page. - If you want different row and column spacing between subgrids - If you want to apply controls to selected subgrids (will be supported soon) - ## Basic containers + To add a [`Container`][vizro.models.Container] to your page, do the following: 1. Insert the `Container` into the `components` argument of the [`Page`][vizro.models.Page] -2. Set a `title` for your `Container` -3. Configure your `components`, [read the overview page for various options](components.md) -4. (optional) Configure your `layout`, see [the guide on `Layout`](layouts.md) +1. Set a `title` for your `Container` +1. Configure your `components`, [read the overview page for various options](components.md) +1. (optional) Configure your `layout`, see [the guide on `Layout`](layouts.md) !!! example "Container" === "app.py" @@ -105,7 +97,7 @@ To add a [`Container`][vizro.models.Container] to your page, do the following: ``` 1. Note that the `Page.layout` argument is not specified here and will therefore defaults to `[[0], [1]]`, meaning the containers will be **vertically stacked** down the page in one column. - 2. **Horizontally stack** the components side-by-side inside this `Container` in one row. + 1. **Horizontally stack** the components side-by-side inside this `Container` in one row. === "app.yaml" ```yaml @@ -149,27 +141,20 @@ To add a [`Container`][vizro.models.Container] to your page, do the following: title: Container II title: Containers ``` - === "Result" - [![Container]][Container] - [Container]: ../../assets/user_guides/components/containers.png + === "Result" + [![Container]][container] Note that an almost identical layout can also be achieved using solely the [`Page.layout`](layouts.md) by configuring the `Page.layout` as `vm.Layout(grid = [[0, 1], [2, 2]])`. ## Nested containers -Containers can be nested, providing a hierarchical structure for organizing components. -This nesting capability enables users to create more complex layouts and manage related components at any level of granularity. + +Containers can be nested, providing a hierarchical structure for organizing components. This nesting capability enables users to create more complex layouts and manage related components at any level of granularity. 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()])]) ``` + +[container]: ../../assets/user_guides/components/containers.png diff --git a/vizro-core/docs/pages/user-guides/custom-actions.md b/vizro-core/docs/pages/user-guides/custom-actions.md index e5fa3afd8..2fb66cd2c 100644 --- a/vizro-core/docs/pages/user-guides/custom-actions.md +++ b/vizro-core/docs/pages/user-guides/custom-actions.md @@ -1,17 +1,16 @@ # How to create custom actions -This guide demonstrates the usage of custom actions, an idea that shares similarities with, but is not identical to [callbacks](https://dash.plotly.com/basic-callbacks) in `Dash`. -If you want to use the [`Action`][vizro.models.Action] model to perform functions that are not available in the [pre-defined action functions][vizro.actions], you can create your own custom action. -Like other [actions](actions.md), custom actions could also be added as an element inside the [actions chain](actions.md#chain-actions), and it can be triggered with one of many dashboard components. +This guide demonstrates the usage of custom actions, an idea that shares similarities with, but is not identical to [callbacks](https://dash.plotly.com/basic-callbacks) in `Dash`. If you want to use the [`Action`][vizro.models.Action] model to perform functions that are not available in the [pre-defined action functions][vizro.actions], you can create your own custom action. Like other [actions](actions.md), custom actions could also be added as an element inside the [actions chain](actions.md#chain-actions), and it can be triggered with one of many dashboard components. + ## Simple custom action Custom actions enable you to implement your own action function. Simply do the following: 1. define a function -2. decorate it with the `@capture("action")` decorator -3. add it as a `function` argument inside the [`Action`][vizro.models.Action] model +1. decorate it with the `@capture("action")` decorator +1. add it as a `function` argument inside the [`Action`][vizro.models.Action] model The following example shows how to create a custom action that postpones execution of the next action in the chain for `t` seconds. @@ -58,27 +57,24 @@ The following example shows how to create a custom action that postpones executi Vizro().build(dashboard).run() ``` + === "app.yaml" - ```yaml - # Custom actions are currently only possible via Python configuration - ``` - + Custom actions are currently only possible via Python configuration. + ## Interact with inputs and outputs + When a custom action needs to interact with the dashboard, it is possible to define `inputs` and `outputs` for the custom action. -- `inputs` represents dashboard component properties whose values are passed to the custom action function as arguments. -It is a list of strings in the format `"."` (for example, `"my_selector.value`"). -- `outputs` represents dashboard component properties corresponding to the custom action function return value(s). -Similar to `inputs`, it is a list of strings in the format `"."` (for example, `"my_card.children"`). +- `inputs` represents dashboard component properties whose values are passed to the custom action function as arguments. It is a list of strings in the format `"."` (for example, `"my_selector.value`"). +- `outputs` represents dashboard component properties corresponding to the custom action function return value(s). Similar to `inputs`, it is a list of strings in the format `"."` (for example, `"my_card.children"`). ### Example of `value` as input -The following example shows a custom action that takes the `value` of the `vm.RadioItem` and returns it inside a -[`Card`][vizro.models.Card] component. -!!! example "Display `value` in Card" +The following example shows a custom action that takes the `value` of the `vm.RadioItem` and returns it inside a [`Card`][vizro.models.Card] component. +!!! example "Display `value` in Card" === "app.py" ```{.python pycafe-link} import vizro.models as vm @@ -113,23 +109,18 @@ The following example shows a custom action that takes the `value` of the `vm.Ra dashboard = vm.Dashboard(pages=[page]) Vizro().build(dashboard).run() ``` - === "app.yaml" - ```yaml - # Custom actions are currently only possible via Python configuration - ``` - === "Result" - [![ValueAction]][ValueAction] - [ValueAction]: ../../assets/user_guides/custom_actions/value_as_input.png + === "app.yaml" + Custom actions are currently only possible via Python configuration. + === "Result" + [![ValueAction]][valueaction] ### Example of `clickData` as input -The following example shows how to create a custom action that shows the `clickData` of a chart in a -[`Card`][vizro.models.Card] component. For further information on the structure and content of the `clickData` -property, refer to the Dash documentation on [interactive visualizations](https://dash.plotly.com/interactive-graphing). -!!! example "Display `clickData` in Card" +The following example shows how to create a custom action that shows the `clickData` of a chart in a [`Card`][vizro.models.Card] component. For further information on the structure and content of the `clickData` property, refer to the Dash documentation on [interactive visualizations](https://dash.plotly.com/interactive-graphing). +!!! example "Display `clickData` in Card" === "app.py" ```{.python pycafe-link} import vizro.models as vm @@ -173,21 +164,18 @@ property, refer to the Dash documentation on [interactive visualizations](https: ``` 1. Just as for any Python function, the names of the arguments `show_species` and `points_data` are arbitrary and do not need to match on to the names of `inputs` in any particular way. - 2. We _bind_ (set) the argument `show_species` to the value `True` in the initial specification of the `function` field. These are static values that are fixed when the dashboard is _built_. - 3. The content of `inputs` will "fill in the gaps" by setting values for the remaining unbound arguments in `my_custom_action`. Here there is one such argument, named `points_data`. Values for these are bound _dynamically at runtime_ to reflect the live state of your dashboard. + 1. We _bind_ (set) the argument `show_species` to the value `True` in the initial specification of the `function` field. These are static values that are fixed when the dashboard is _built_. + 1. The content of `inputs` will "fill in the gaps" by setting values for the remaining unbound arguments in `my_custom_action`. Here there is one such argument, named `points_data`. Values for these are bound _dynamically at runtime_ to reflect the live state of your dashboard. + === "app.yaml" - ```yaml - # Custom actions are currently only possible via Python configuration - ``` - === "Result" - [![CustomAction]][CustomAction] + Custom actions are currently only possible via Python configuration. - [CustomAction]: ../../assets/user_guides/custom_actions/clickdata_as_input.png + === "Result" + [![CustomAction]][customaction] ## Multiple return values -The return value of the custom action function is propagated to the dashboard components that are defined in the `outputs` argument of the [`Action`][vizro.models.Action] model. -If there is a single `output` defined then the function return value is directly assigned to the component property. -If there are multiple `outputs` defined then the return value is iterated through and each part is assigned to each component property given in `outputs` in turn. This behavior is identical to Python's flexibility in managing multiple return values. + +The return value of the custom action function is propagated to the dashboard components that are defined in the `outputs` argument of the [`Action`][vizro.models.Action] model. If there is a single `output` defined then the function return value is directly assigned to the component property. If there are multiple `outputs` defined then the return value is iterated through and each part is assigned to each component property given in `outputs` in turn. This behavior is identical to Python's flexibility in managing multiple return values. !!! example "Multiple return values" === "app.py" @@ -246,17 +234,17 @@ If there are multiple `outputs` defined then the return value is iterated throug ``` 1. `my_custom_action` returns two values (which will be in Python tuple). - 2. These values are assigned to the `outputs` in the same order. - === "app.yaml" - ```yaml - # Custom actions are currently only possible via Python configuration - ``` - === "Result" - [![CustomAction2]][CustomAction2] + 1. These values are assigned to the `outputs` in the same order. - [CustomAction2]: ../../assets/user_guides/custom_actions/custom_action_multiple_return_values.png + === "app.yaml" + Custom actions are currently only possible via Python configuration. + === "Result" + [![CustomAction2]][customaction2] !!! warning + Note that users of this package are responsible for the content of any custom action function that they write. Take care to avoid leaking any sensitive information or exposing to any security threat during implementation. You should always [treat the content of user input as untrusted](https://community.plotly.com/t/writing-secure-dash-apps-community-thread/54619). - Note that users of this package are responsible for the content of any custom action function that they write - especially with regard to leaking any sensitive information or exposing to any security threat during implementation. You should always [treat the content of user input as untrusted](https://community.plotly.com/t/writing-secure-dash-apps-community-thread/54619). +[customaction]: ../../assets/user_guides/custom_actions/clickdata_as_input.png +[customaction2]: ../../assets/user_guides/custom_actions/custom_action_multiple_return_values.png +[valueaction]: ../../assets/user_guides/custom_actions/value_as_input.png diff --git a/vizro-core/docs/pages/user-guides/custom-charts.md b/vizro-core/docs/pages/user-guides/custom-charts.md index cb26614c8..1b77d0869 100644 --- a/vizro-core/docs/pages/user-guides/custom-charts.md +++ b/vizro-core/docs/pages/user-guides/custom-charts.md @@ -1,9 +1,9 @@ # How to create custom charts -This guide shows you how to create custom charts and how to add them to your dashboard. -The [`Graph`][vizro.models.Graph] model accepts the `figure` argument, where you can enter _any_ [`plotly.express`](https://plotly.com/python/plotly-express/) chart as explained in the [user guide on graphs](graph.md). +This guide shows you how to create custom charts and how to add them to your dashboard. The [`Graph`][vizro.models.Graph] model accepts the `figure` argument, where you can enter _any_ [`plotly.express`](https://plotly.com/python/plotly-express/) chart as explained in the [user guide on graphs](graph.md). ## When to use a custom chart + In general, you should use the custom chart decorator `@capture("graph")` if your plotly chart needs any post-update calls or customization. For example: - You want to use any of the post figure update calls by `plotly` such as `update_layout`, `update_xaxes`, `update_traces` (for more details, see the docs on [plotly's update calls](https://plotly.com/python/creating-and-updating-figures/#other-update-methods)) @@ -12,11 +12,10 @@ In general, you should use the custom chart decorator `@capture("graph")` if you ## Steps to create a custom chart 1. Define a function that returns a `go.Figure()`. -2. Decorate it with `@capture("graph")`. -3. The function must accept a `data_frame` argument (of type `pandas.DataFrame`). -4. The visualization should be derived from and require only one `pandas.DataFrame`. Dataframes from other arguments -will not react to dashboard controls such as [`Filter`](filters.md). -5. Pass your function to the `figure` argument of the [`Graph`][vizro.models.Graph] model. +1. Decorate it with `@capture("graph")`. +1. The function must accept a `data_frame` argument (of type `pandas.DataFrame`). +1. The visualization should be derived from and require only one `pandas.DataFrame`. Dataframes from other arguments will not react to dashboard controls such as [`Filter`](filters.md). +1. Pass your function to the `figure` argument of the [`Graph`][vizro.models.Graph] model. The minimal example below can be used as a base to build more sophisticated charts. @@ -39,12 +38,10 @@ To alter the data in the `data_frame` argument, consider using a [Filter](filter ## Enhanced `plotly.express` chart with reference line -The below examples shows a case where we enhance an existing `plotly.express` chart. We add a new argument (`hline`), that is used to draw a grey reference line at the height determined by the value of `hline`. The important thing to note is that we then -add a `Parameter` that enables the dashboard user to interact with the argument, and hence move the line in this case. See the `Result` tab for an animation. +The below examples shows a case where we enhance an existing `plotly.express` chart. We add a new argument (`hline`), that is used to draw a grey reference line at the height determined by the value of `hline`. The important thing to note is that we then add a `Parameter` that enables the dashboard user to interact with the argument, and hence move the line in this case. See the `Result` tab for an animation. !!! example "Custom `plotly.express` scatter chart with a `Parameter`" === "app.py" - ```{.python pycafe-link} import vizro.models as vm import vizro.plotly.express as px @@ -86,17 +83,14 @@ add a `Parameter` that enables the dashboard user to interact with the argument, Vizro().build(dashboard).run() ``` - 1. Note that arguments of the custom chart can be parametrized. Here we choose to parametrize the `hline` argument (see below). - 2. Here we parametrize the `hline` argument, but any other argument can be parametrized as well. Since there is complete flexibility regarding what can be derived from such arguments, the dashboard user has a wide range of customization options. - === "app.yaml" - ```yaml - # Custom charts are currently only possible via Python configuration - ``` - === "Result" - [![Graph2]][Graph2] + 1. Note that arguments of the custom chart can be parametrized. Here we choose to parametrize the `hline` argument (see below). + 1. Here we parametrize the `hline` argument, but any other argument can be parametrized as well. Since there is complete flexibility regarding what can be derived from such arguments, the dashboard user has a wide range of customization options. - [Graph2]: ../../assets/user_guides/custom_charts/custom_chart_showcase_parameter.gif + === "app.yaml" + Custom charts are currently only possible via Python configuration. + === "Result" + [![Graph2]][graph2] ## New Waterfall chart based on `go.Figure()` @@ -104,7 +98,6 @@ The below examples shows a more involved use-case. We create and style a waterfa !!! example "Custom `go.Figure()` waterfall chart with a `Parameter`" === "app.py" - ```{.python pycafe-link} import pandas as pd import plotly.graph_objects as go @@ -162,10 +155,10 @@ The below examples shows a more involved use-case. We create and style a waterfa ``` === "app.yaml" - ```yaml - # Custom charts are currently only possible via Python configuration - ``` + Custom charts are currently only possible via Python configuration. + === "Result" - [![Graph3]][Graph3] + [![Graph3]][graph3] - [Graph3]: ../../assets/user_guides/custom_charts/custom_chart_waterfall.png +[graph2]: ../../assets/user_guides/custom_charts/custom_chart_showcase_parameter.gif +[graph3]: ../../assets/user_guides/custom_charts/custom_chart_waterfall.png diff --git a/vizro-core/docs/pages/user-guides/custom-components.md b/vizro-core/docs/pages/user-guides/custom-components.md index bd73a5d72..1b958f5e1 100644 --- a/vizro-core/docs/pages/user-guides/custom-components.md +++ b/vizro-core/docs/pages/user-guides/custom-components.md @@ -234,7 +234,6 @@ vm.Page.add_type("components", Jumbotron) title="Custom Component", components=[ Jumbotron( # (6)! - id="my_jumbotron", title="Jumbotron", subtitle="This is a subtitle to summarize some content.", text="This is the main body of text of the Jumbotron.", @@ -364,7 +363,6 @@ Add the custom action `open_offcanvas` as a `function` argument inside the [`Act ], ), OffCanvas( - id="offcanvas", content="OffCanvas content", title="Offcanvas Title", ), diff --git a/vizro-core/docs/pages/user-guides/custom-css.md b/vizro-core/docs/pages/user-guides/custom-css.md index 44e3a3621..d5bc5f7dc 100755 --- a/vizro-core/docs/pages/user-guides/custom-css.md +++ b/vizro-core/docs/pages/user-guides/custom-css.md @@ -1,25 +1,20 @@ # Customizing Vizro dashboard CSS -Vizro is opinionated about visual formatting, and some elements, such as the layout of the navigation and controls, -are fixed. You can customize some settings such as background colors, fonts, and other styles via CSS overrides. +Vizro is opinionated about visual formatting, and some elements, such as the layout of the navigation and controls, are fixed. You can customize some settings such as background colors, fonts, and other styles via CSS overrides. To make customizations, you need to: 1. **Add a CSS file to your `assets` folder**. Refer to our user guide on [adding static assets](assets.md#how-to-add-static-assets). -2. **Identify the correct CSS selector** for the component you want to style. -3. **Change the relevant CSS properties** in your CSS file. - - +1. **Identify the correct CSS selector** for the component you want to style. +1. **Change the relevant CSS properties** in your CSS file. ## Introduction to Vizro CSS + For a short introduction to CSS, we recommend reading this article: [Get Started with CSS in 5 Minutes](https://www.freecodecamp.org/news/get-started-with-css-in-5-minutes-e0804813fc3e/). -For a more comprehensive tutorial, refer to the [W3Schools CSS tutorial](https://www.w3schools.com/css/default.asp). -The entire tutorial is beneficial, but the section on [CSS selectors](https://www.w3schools.com/css/css_selectors.asp) -will be particularly useful. +For a more comprehensive tutorial, refer to the [W3Schools CSS tutorial](https://www.w3schools.com/css/default.asp). The entire tutorial is beneficial, but the section on [CSS selectors](https://www.w3schools.com/css/css_selectors.asp) will be particularly useful. -In Vizro, the CSS file is read in as an external stylesheet. The most common way of applying any styling to -Vizro is therefore through the use of CSS selectors: +In Vizro, the CSS file is read in as an external stylesheet. The most common way of applying any styling to Vizro is therefore through the use of CSS selectors: - **Element Selector**: Applies the style to all elements inside the Vizro app. @@ -33,8 +28,7 @@ Vizro is therefore through the use of CSS selectors: } ``` -- **Class selector:** Targets all elements with the given class for styling. All CSS classes must be preceded with a -`.` symbol. +- **Class selector:** Targets all elements with the given class for styling. All CSS classes must be preceded with a `.` symbol. ``` .card { @@ -50,30 +44,25 @@ Vizro is therefore through the use of CSS selectors: } ``` - ## Identify the correct CSS selector Use Chrome DevTools or a similar tool (Web Inspector, Web Developer Tools, etc.) to inspect the HTML document in your browser. -1. **Open DevTools:** In Google Chrome, right-click on the app and select "Inspect" from the context menu. This opens the -HTML document of your Vizro app. +1. **Open DevTools:** In Google Chrome, right-click on the app and select "Inspect" from the context menu. This opens the HTML document of your Vizro app. ![Inspect panel](../../assets/user_guides/custom_css/inspect-panel.png) - -2. **Select an element:** Suppose you want to change the background color of your cards. Click the -"Select an element in the page to inspect it" icon in the top left corner of the inspect panel. +1. **Select an element:** Suppose you want to change the background color of your cards. Click the "Select an element in the page to inspect it" icon in the top left corner of the inspect panel. ![Inspect icon](../../assets/user_guides/custom_css/inspect-icon.png) -3. **Find the HTML Block:** Hover over the component you want to style. The corresponding HTML block will be -highlighted in the HTML document. +1. **Find the HTML Block:** Hover over the component you want to style. The corresponding HTML block will be highlighted in the HTML document. ![Highlighted element](../../assets/user_guides/custom_css/highlighted-element.png) Notice that the selected HTML block corresponds to the container of the card and has a CSS class, here it is `card`. -4. **Apply CSS:** Use this CSS class to style the card component. In your CSS file, you can write: +1. **Apply CSS:** Use this CSS class to style the card component. In your CSS file, you can write: ``` .card { @@ -83,25 +72,25 @@ highlighted in the HTML document. This changes the background color for any HTML element with the `card` class. -**Tip:** You can also test your CSS live by editing the CSS attributes in the "Elements" panel. -For example, temporarily add `background: blue;`. Note that this change will be lost upon reloading the page. +**Tip:** You can also test your CSS live by editing the CSS attributes in the "Elements" panel. For example, temporarily add `background: blue;`. Note that this change will be lost upon reloading the page. ![Temporary changes](../../assets/user_guides/custom_css/temporary-changes.png) - ## CSS overwrites ### Overwrite CSS globally -To overwrite any global CSS property, you need to target the element selector and place your CSS file with the -overwrites in the `assets` folder. + +To overwrite any global CSS property, you need to target the element selector and place your CSS file with the overwrites in the `assets` folder. !!! example "Overwrite CSS globally" === "my_css_file.css" - ```css - h1, h2 { - color: hotpink; - } - ``` + ```css + h1, + h2 { + color: hotpink; + } + ``` + === "app.py" ```py import os @@ -134,44 +123,41 @@ overwrites in the `assets` folder. # Still requires a .py to add data to the data manager and parse YAML configuration # See yaml_version example pages: - - components: - - text: | - # This is an

tag + - components: + - text: | + # This is an

tag - ## This is an

tag + ## This is an

tag - ###### This is an

tag - type: card - title: Changing the header color + ###### This is an
tag + type: card + title: Changing the header color ``` - === "Result" - [![AssetsCSS]][AssetsCSS] - - [AssetsCSS]: ../../assets/user_guides/assets/css_change.png + === "Result" + [![AssetsCSS]][assetscss] ### Overwrite CSS for selected pages -To style components for a specific page, use the page's `id` in the CSS selectors. By default, this is the -[same as the page `title`](pages.md), but such a value might not be a valid CSS identifier. -A suitable `id` must be unique across all models in the dashboard and should contain only alphanumeric -characters, hyphens (`-`) and underscores (`_`). In particular, note that spaces are _not_ allowed. +To style components for a specific page, use the page's `id` in the CSS selectors. By default, this is the [same as the page `title`](pages.md), but such a value might not be a valid CSS identifier. + +A suitable `id` must be unique across all models in the dashboard and should contain only alphanumeric characters, hyphens (`-`) and underscores (`_`). In particular, note that spaces are _not_ allowed. Suppose you want to hide the page title on one page only. Here's how you can achieve this: 1. Give a valid `id` to the `Page`, for example `Page(id="page-with-hidden-title", title="Page with hidden title", ...)`. -2. Identify the CSS class or CSS `id` you need to target. To hide the page title, you need to hide the parent container with the id `right-header`. -3. Use the `id` to hide the content. -4. Add your custom css file to the `assets` folder. - +1. Identify the CSS class or CSS `id` you need to target. To hide the page title, you need to hide the parent container with the id `right-header`. +1. Use the `id` to hide the content. +1. Add your custom css file to the `assets` folder. !!! example "Hide page title on selected pages" === "my_css_file.css" - ```css - #page-with-hidden-title #right-header { - display: none; - } - ``` + ```css + #page-with-hidden-title #right-header { + display: none; + } + ``` + === "app.py" ```py import vizro.models as vm @@ -199,25 +185,24 @@ Suppose you want to hide the page title on one page only. Here's how you can ach # Still requires a .py to add data to the data manager and parse YAML configuration # See yaml_version example pages: - - components: - - text: | - # Placeholder - type: card - title: Page with hidden title - id: page-with-hidden-title - - components: - - text: | - # Placeholder - type: card - title: Page with shown title + - components: + - text: | + # Placeholder + type: card + title: Page with hidden title + id: page-with-hidden-title + - components: + - text: | + # Placeholder + type: card + title: Page with shown title ``` - === "Result" - [![PageTitle]][PageTitle] - - [PageTitle]: ../../assets/user_guides/assets/css_page_title.png + === "Result" + [![PageTitle]][pagetitle] ### Overwrite CSS for selected components + To adjust CSS properties for specific components, such as altering the appearance of a selected [`Card`][vizro.models.Card] rather than all Cards, you need to supply an `id` to the component you want to style. Let's say we want to change the `background-color` and `color` of a specific `Card`. @@ -225,41 +210,43 @@ Let's say we want to change the `background-color` and `color` of a specific `Ca Here's how you can do it: 1. Assign a unique `id` to the relevant `Card`, for example: `Card(id="custom-card", ...)` -2. Run your dashboard and open it in your browser -3. View the HTML document to find the appropriate CSS class or element you need to target, as explained in the section -on [identifying the correct CSS selector](#identify-the-correct-css-selector). +1. Run your dashboard and open it in your browser +1. View the HTML document to find the appropriate CSS class or element you need to target, as explained in the section on [identifying the correct CSS selector](#identify-the-correct-css-selector). -It's essential to understand the relationship between the targeted CSS class or element and the component assigned -`id`, for example: +It's essential to understand the relationship between the targeted CSS class or element and the component assigned `id`, for example: -```html title="HTML structure of a `Card`" + +```html title="HTML structure of a card"
-
-

Lorem ipsum dolor sit amet consectetur adipisicing elit.

-
+
+

+ Lorem ipsum dolor sit amet consectetur adipisicing elit. +

+
``` - -* **Main element with `id`:** There is a `
` with our `id="custom-card"`. -* **Parent element:** That `
` is wrapped inside a parent `
` with the class name `"card"`. This is the element we need to target to change the background color. -* **Child element:** The card text is wrapped inside a `

` that is a child of the `

` with our `id`. This is the element we need to target to change the font color. + +- **Main element with `id`:** There is a `
` with our `id="custom-card"`. +- **Parent element:** That `
` is wrapped inside a parent `
` with the class name `"card"`. This is the element we need to target to change the background color. +- **Child element:** The card text is wrapped inside a `

` that is a child of the `

` with our `id`. This is the element we need to target to change the font color. !!! example "Customizing CSS properties in selective components" === "my_css_file.css" - ```css - /* Apply styling to parent */ - .card:has(#custom-card) { - background-color: white; - } + ```css + /* Apply styling to parent */ + .card:has(#custom-card) { + background-color: white; + } + + /* Apply styling to child */ + #custom-card p { + color: black; + } + ``` - /* Apply styling to child */ - #custom-card p { - color: black; - } - ``` === "app.py" ```py import vizro.models as vm @@ -285,69 +272,65 @@ It's essential to understand the relationship between the targeted CSS class or # Still requires a .py to add data to the data manager and parse YAML configuration # See yaml_version example pages: - - components: - - text: | - Lorem ipsum dolor sit amet consectetur adipisicing elit. - type: card - id: custom-card - - text: | - Lorem ipsum dolor sit amet consectetur adipisicing elit. - type: card - title: Changing the card color + - components: + - text: | + Lorem ipsum dolor sit amet consectetur adipisicing elit. + type: card + id: custom-card + - text: | + Lorem ipsum dolor sit amet consectetur adipisicing elit. + type: card + title: Changing the card color ``` - === "Result" - [![CardCSS]][CardCSS] - - [CardCSS]: ../../assets/user_guides/assets/css_change_card.png + === "Result" + [![CardCSS]][cardcss] !!! note "Relationship between model ID and CSS ID" + Some Vizro components produce a single HTML element with an ID that matches the model ID, allowing you to target it directly using the CSS #id selector. Other components generate multiple HTML elements. Within these, the "core" element will have an ID matching the model ID, while non-core elements may have IDs that are variations of it, such as `{model-id}-title`. - Some Vizro components produce a single HTML element with an ID that matches the model ID, allowing you to target it - directly using the CSS #id selector. Other components generate multiple HTML elements. Within these, the "core" - element will have an ID matching the model ID, while non-core elements may have IDs that are variations of it, - such as `{model-id}-title`. + In all instances, you can determine the correct selector by using Chrome DevTools or a similar tool after setting the appropriate model ID. - In all instances, you can determine the correct selector by using Chrome DevTools or a similar tool after setting the - appropriate model ID. +## Common examples +### Make your CSS responsive to theme switches with variables -## Common examples +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/). Note that our Bootstrap stylesheet is still under development, so not all Bootstrap variables are currently available. 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). ### Turn off page title + See the example above on [hiding the page title on selected pages](#overwrite-css-for-selected-pages). ### Change the background color of a card + See the example above on [customizing CSS properties in selective components](#overwrite-css-for-selected-components). ### Change the global font + The default fonts for a Vizro app are `Inter, sans-serif, Arial, serif`. If you need to change the global font, perhaps to adhere to branding guidelines, follow these steps: 1. Download the desired font from a font provider such as [Google Fonts](https://fonts.google.com/). -2. Place the font file (`.ttf`, `woff2`, etc.) into your `assets` folder. Here’s an example of what the assets folder might look like: - ![Font Change](../../assets/user_guides/custom_css/font-change.png) +1. Place the font file (`.ttf`, `woff2`, etc.) into your `assets` folder. Here’s an example of what the assets folder might look like: -3. Add the font to your CSS file using the `@font-face` rule and apply the font globally to your Vizro app, making sure -to specify fallback fonts. Add the following to your `custom.css` file: + ![Font Change](../../assets/user_guides/custom_css/font-change.png) +1. Add the font to your CSS file using the `@font-face` rule and apply the font globally to your Vizro app, making sure to specify fallback fonts. Add the following to your `custom.css` file: ```css @font-face { - font-family: PlayfairDisplay; - src: url("PlayfairDisplay-VariableFont_wght.ttf") format("truetype"); + font-family: PlayfairDisplay; + src: url("PlayfairDisplay-VariableFont_wght.ttf") format("truetype"); } * { - font-family: PlayfairDisplay, Inter, sans-serif, Arial, serif; + font-family: PlayfairDisplay, Inter, sans-serif, Arial, serif; } ``` - -4. Note that the modification above applies solely to the dashboard font. To also change the font within the -Plotly charts, you must specify this at the beginning of your `app.py` file: +1. Note that the modification above applies solely to the dashboard font. To also change the font within the Plotly charts, you must specify this at the beginning of your `app.py` file: ```python import plotly.io as pio @@ -357,34 +340,42 @@ Plotly charts, you must specify this at the beginning of your `app.py` file: ``` ### Reposition the logo -By default, the logo appears in the top left corner of the dashboard. You can move it further to the left or right by -adjusting the `padding` of the `#page-header` element. Here is an example of how to achieve this: + +By default, the logo appears in the top left corner of the dashboard. You can move it further to the left or right by adjusting the `padding` of the `#page-header` element. Here is an example of how to achieve this: ```css #page-header { - padding-left: 8px; + padding-left: 8px; } ``` ![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`. +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`. !!! example "Style a container" === "custom.css" - ```css - .page-component-container { - background: var(--surfaces-bg-card); - padding: 12px; - } - ``` + ```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 @@ -419,12 +410,12 @@ To do this, you need to change the container's CSS class. Using the DevTool, as # Still requires a .py to add data to the data manager and parse YAML configuration # See yaml_version example pages: - - title: "Page with subsections" + - title: Page with subsections layout: grid: [[0, 1]] components: - type: container - title: "Container I" + title: Container I components: - type: graph figure: @@ -434,7 +425,7 @@ To do this, you need to change the container's CSS class. Using the DevTool, as y: sepal_length color: species - type: container - title: "Container II" + title: Container II components: - type: graph figure: @@ -444,13 +435,11 @@ To do this, you need to change the container's CSS class. Using the DevTool, as y: sepal_length color: species ``` - === "Result" - [![StyleContainer]][StyleContainer] - [StyleContainer]: ../../assets/user_guides/custom_css/style-container.png + === "Result" + [![StyleContainer]][stylecontainer] -You will notice that the background colors of the charts are different. To align it with the colors of the container, -you can make the charts' background transparent. +You will notice that the background colors of the charts are different. To align it with the colors of the container, you can make the charts' background transparent. To make the background of all charts transparent: @@ -474,3 +463,8 @@ def custom_chart(data_frame): ``` ![Transparent charts](../../assets/user_guides/custom_css/transparent-charts.png) + +[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/custom-figures.md b/vizro-core/docs/pages/user-guides/custom-figures.md index f135127be..3a178932f 100644 --- a/vizro-core/docs/pages/user-guides/custom-figures.md +++ b/vizro-core/docs/pages/user-guides/custom-figures.md @@ -1,41 +1,36 @@ # How to create custom figures -This guide explains how to create custom figures, which is useful when you need a component that reacts to -[filter](filters.md) and [parameter](parameters.md) controls. +This guide explains how to create custom figures, which is useful when you need a component that reacts to [filter](filters.md) and [parameter](parameters.md) controls. -The [`Figure`][vizro.models.Figure] model accepts the `figure` argument, where you can enter _any_ custom figure function -as explained in the [user guide on figures](figure.md). +The [`Figure`][vizro.models.Figure] model accepts the `figure` argument, where you can enter _any_ custom figure function as explained in the [user guide on figures](figure.md). ## When to use a custom figure -As described in the flowchart detailing [when to use `Figure`](figure.md), custom figures should be used if -**both** of the following conditions are met: + +As described in the flowchart detailing [when to use `Figure`](figure.md), custom figures should be used if **both** of the following conditions are met: - You need a figure that doesn't fit into the existing pre-defined components ([`Graph`][vizro.models.Graph], [`Table`][vizro.models.Table] or [`AgGrid`][vizro.models.AgGrid]). - You need a figure that isn't available in our pre-defined figure functions [`vizro.figures`](../API-reference/figure-callables.md). ## Steps to create a custom figure -1. Define a function that returns a [Dash component](https://dash.plotly.com/#open-source-component-libraries). -This can, but does not need to, be based on code in our pre-defined figure functions in [`vizro.figures`](../API-reference/figure-callables.md). -2. Decorate it with `@capture("figure")`. -3. The function must accept a `data_frame` argument (of type `pandas.DataFrame`). -4. The figure should be derived from and require only one `pandas.DataFrame`. Dataframes from other arguments -will not react to dashboard controls such as [`Filter`](filters.md). -5. Pass your function to the `figure` argument of the [`Figure`][vizro.models.Figure] model. +1. Define a function that returns a [Dash component](https://dash.plotly.com/#open-source-component-libraries). This can, but does not need to, be based on code in our pre-defined figure functions in [`vizro.figures`](../API-reference/figure-callables.md). +1. Decorate it with `@capture("figure")`. +1. The function must accept a `data_frame` argument (of type `pandas.DataFrame`). +1. The figure should be derived from and require only one `pandas.DataFrame`. Dataframes from other arguments will not react to dashboard controls such as [`Filter`](filters.md). +1. Pass your function to the `figure` argument of the [`Figure`][vizro.models.Figure] model. The following examples can be used as a base to build more sophisticated figures. ## Examples of custom figures ### Custom KPI card -To change the design or content of our existing KPI (key performance indicator) cards from -[`vizro.figures`](../API-reference/figure-callables.md), you can do so by following the steps described above. -For instance, to make a KPI card with the icon positioned on the right side of the title instead of the left, -copy and paste the [source code of `kpi_card`](../API-reference/figure-callables.md#vizro.figures.kpi_card) and -adjust the return statement of the function. +To change the design or content of our existing KPI (key performance indicator) cards from [`vizro.figures`](../API-reference/figure-callables.md), you can do so by following the steps described above. + +For instance, to make a KPI card with the icon positioned on the right side of the title instead of the left, copy and paste the [source code of `kpi_card`](../API-reference/figure-callables.md#vizro.figures.kpi_card) and adjust the return statement of the function. + !!! example "Custom KPI card" === "app.py" ```{.python pycafe-link} @@ -107,29 +102,26 @@ adjust the return statement of the function. ``` 1. Here we decorate our custom figure function with the `@capture("figure")` decorator. - 2. The custom figure function needs to have a `data_frame` argument and return a `Dash` component. - 3. We adjust the return statement to include the icon on the right side of the title. This is achieved by swapping the order of the `html.H2` and `html.P` compared to the original `kpi_card`. - 4. This creates a [`layout`](layouts.md) with four rows and columns. The KPI cards are positioned in the first two cells, while the remaining cells are empty. - 5. For more information, refer to the API reference for the [`kpi_card`](../API-reference/figure-callables.md#vizro.figures.kpi_card). - 6. Our custom figure function `custom_kpi_card` now needs to be passed on to the `vm.Figure`. + 1. The custom figure function needs to have a `data_frame` argument and return a `Dash` component. + 1. We adjust the return statement to include the icon on the right side of the title. This is achieved by swapping the order of the `html.H2` and `html.P` compared to the original `kpi_card`. + 1. This creates a [`layout`](layouts.md) with four rows and columns. The KPI cards are positioned in the first two cells, while the remaining cells are empty. + 1. For more information, refer to the API reference for the [`kpi_card`](../API-reference/figure-callables.md#vizro.figures.kpi_card). + 1. Our custom figure function `custom_kpi_card` now needs to be passed on to the `vm.Figure`. === "app.yaml" - ```yaml - # Custom figures are currently only possible via Python configuration - ``` - === "Result" - [![CustomKPI]][CustomKPI] + Custom figures are currently only possible via Python configuration. - [CustomKPI]: ../../assets/user_guides/figure/custom_kpi.png + === "Result" + [![CustomKPI]][customkpi] ### Dynamic HTML header -You can create a custom figure for any [Dash component](https://dash.plotly.com/#open-source-component-libraries). -Below is an example of a custom figure that returns a `html.H2` component that dynamically updates based on the selected -name from a filter. + +You can create a custom figure for any [Dash component](https://dash.plotly.com/#open-source-component-libraries). Below is an example of a custom figure that returns a `html.H2` component that dynamically updates based on the selected name from a filter. + !!! example "Dynamic HTML header" === "app.py" ```{.python pycafe-link} @@ -159,27 +151,24 @@ name from a filter. ``` 1. Here we decorate our custom figure function with the `@capture("figure")` decorator. - 2. The custom figure function needs to have a `data_frame` argument and return a `Dash` component. - 3. We return a `html.H2` component that dynamically updates based on the selected name from the filter. - 4. Our custom figure function `dynamic_html_header` now needs to be passed on to the `vm.Figure`. + 1. The custom figure function needs to have a `data_frame` argument and return a `Dash` component. + 1. We return a `html.H2` component that dynamically updates based on the selected name from the filter. + 1. Our custom figure function `dynamic_html_header` now needs to be passed on to the `vm.Figure`. === "app.yaml" - ```yaml - # Custom figures are currently only possible via Python configuration - ``` - === "Result" - [![CustomHTML]][CustomHTML] + Custom figures are currently only possible via Python configuration. - [CustomHTML]: ../../assets/user_guides/figure/custom_html.png + === "Result" + [![CustomHTML]][customhtml] - ### Dynamic number of cards -The example below shows how to create multiple cards created from a `pandas.DataFrame` where the -number of cards displayed dynamically adjusts based on a `vm.Parameter`. + +The example below shows how to create multiple cards created from a `pandas.DataFrame` where the number of cards displayed dynamically adjusts based on a `vm.Parameter`. + !!! example "Dynamic number of cards" === "app.py" ```py @@ -239,40 +228,39 @@ number of cards displayed dynamically adjusts based on a `vm.Parameter`. ``` 1. Here we decorate our custom figure function with the `@capture("figure")` decorator. - 2. The custom figure function needs to have a `data_frame` argument and return a `Dash` component. - 3. Our decorated figure function `multiple_cards` now needs to be passed on to the `vm.Figure`. - 4. We add a [`vm.Parameter`](parameters.md) to dynamically adjust the number of cards displayed. - The parameter targets the `n_rows` argument of the `multiple_cards` function, determining the number of rows - taken from the data. + 1. The custom figure function needs to have a `data_frame` argument and return a `Dash` component. + 1. Our decorated figure function `multiple_cards` now needs to be passed on to the `vm.Figure`. + 1. We add a [`vm.Parameter`](parameters.md) to dynamically adjust the number of cards displayed. The parameter targets the `n_rows` argument of the `multiple_cards` function, determining the number of rows taken from the data. PyCafe logoRun and edit this code in PyCafe === "styling.css" ```css .multiple-cards-container { - display: flex; - flex-wrap: wrap; - gap: 12px; + display: flex; + flex-wrap: wrap; + gap: 12px; } .figure-container { - height: unset; - width: unset; + height: unset; + width: unset; } .figure-container .card { - height: 210px; - width: 240px; + height: 210px; + width: 240px; } ``` - === "app.yaml" - ```yaml - # Custom figures are currently only possible via Python configuration - ``` - === "Result" - [![CustomFigure]][CustomFigure] - [CustomFigure]: ../../assets/user_guides/figure/custom_multiple_cards.png + === "app.yaml" + Custom figures are currently only possible via Python configuration. + === "Result" + [![CustomFigure]][customfigure] + +[customfigure]: ../../assets/user_guides/figure/custom_multiple_cards.png +[customhtml]: ../../assets/user_guides/figure/custom_html.png +[customkpi]: ../../assets/user_guides/figure/custom_kpi.png diff --git a/vizro-core/docs/pages/user-guides/custom-tables.md b/vizro-core/docs/pages/user-guides/custom-tables.md index 8fa5f5252..42748820b 100644 --- a/vizro-core/docs/pages/user-guides/custom-tables.md +++ b/vizro-core/docs/pages/user-guides/custom-tables.md @@ -1,21 +1,18 @@ # How to create custom Dash AG Grids and Dash DataTables -In cases where the available arguments for the [`dash_ag_grid`][vizro.tables.dash_ag_grid] or [`dash_data_table`][vizro.tables.dash_data_table] models are not sufficient, -you can create a custom Dash AG Grid or Dash DataTable. +In cases where the available arguments for the [`dash_ag_grid`][vizro.tables.dash_ag_grid] or [`dash_data_table`][vizro.tables.dash_data_table] models are not sufficient, you can create a custom Dash AG Grid or Dash DataTable. -The [`Table`][vizro.models.Table] and the [`AgGrid`][vizro.models.AgGrid] model accept the `figure` argument, where you can enter -_any_ [`dash_ag_grid`][vizro.tables.dash_ag_grid] or [`dash_data_table`][vizro.tables.dash_data_table] chart as explained in the [user guide on tables](table.md). +The [`Table`][vizro.models.Table] and the [`AgGrid`][vizro.models.AgGrid] model accept the `figure` argument, where you can enter _any_ [`dash_ag_grid`][vizro.tables.dash_ag_grid] or [`dash_data_table`][vizro.tables.dash_data_table] chart as explained in the [user guide on tables](table.md). One reason could be that you want to create a table/grid that requires computations that can be controlled by parameters (see the example below). ### Steps to create a custom table 1. Define a function that returns a `dash_ag_grid.AgGrid` or `dash_table.DataTable` object. -2. Decorate it with `@capture("ag_grid")` or `@capture("table")`. -3. The function must accept a `data_frame` argument (of type `pandas.DataFrame`). -4. The table should be derived from and require only one `pandas.DataFrame`. Dataframes from other arguments -will not react to dashboard controls such as [`Filter`](filters.md). -5. Pass your function to the `figure` argument of the [`Table`][vizro.models.Table] or [`AgGrid`][vizro.models.AgGrid] model. +1. Decorate it with `@capture("ag_grid")` or `@capture("table")`. +1. The function must accept a `data_frame` argument (of type `pandas.DataFrame`). +1. The table should be derived from and require only one `pandas.DataFrame`. Dataframes from other arguments will not react to dashboard controls such as [`Filter`](filters.md). +1. Pass your function to the `figure` argument of the [`Table`][vizro.models.Table] or [`AgGrid`][vizro.models.AgGrid] model. The following examples show a possible version of a custom table. In this case the argument `chosen_columns` was added, which you can control with a parameter: @@ -38,10 +35,10 @@ The following examples show a possible version of a custom table. In this case t columns = [{"name": i, "id": i} for i in chosen_columns] defaults = { "style_as_list_view": True, - "style_data": {"border_bottom": "1px solid var(--border-subtle-alpha-01)", "height": "40px"}, + "style_data": {"border_bottom": "1px solid var(--border-subtleAlpha01)", "height": "40px"}, "style_header": { - "border_bottom": "1px solid var(--state-overlays-selected-hover)", - "border_top": "1px solid var(--main-container-bg-color)", + "border_bottom": "1px solid var(--stateOverlays-selectedHover)", + "border_top": "1px solid var(--right-side-bg)", "height": "32px", }, } @@ -70,14 +67,12 @@ The following examples show a possible version of a custom table. In this case t Vizro().build(dashboard).run() ``` + === "app.yaml" - ```yaml - # Custom tables are currently only possible via Python configuration - ``` - === "Result" - [![Table3]][Table3] + Custom tables are currently only possible via Python configuration. - [Table3]: ../../assets/user_guides/table/custom_table.png + === "Result" + [![Table3]][table3] ??? example "Custom Dash AgGrid" === "app.py" @@ -136,11 +131,12 @@ The following examples show a possible version of a custom table. In this case t Vizro().build(dashboard).run() ``` + === "app.yaml" - ```yaml - # Custom Ag Grids are currently only possible via Python configuration - ``` + Custom Ag Grids are currently only possible via Python configuration. + === "Result" - [![GridCustom]][GridCustom] + [![GridCustom]][gridcustom] - [GridCustom]: ../../assets/user_guides/table/custom_grid.png +[gridcustom]: ../../assets/user_guides/table/custom_grid.png +[table3]: ../../assets/user_guides/table/custom_table.png diff --git a/vizro-core/docs/pages/user-guides/dashboard.md b/vizro-core/docs/pages/user-guides/dashboard.md index 0b37e03fa..244f65256 100644 --- a/vizro-core/docs/pages/user-guides/dashboard.md +++ b/vizro-core/docs/pages/user-guides/dashboard.md @@ -1,16 +1,15 @@ # How to create a dashboard -This guide shows you how to configure and call a [`Dashboard`][vizro.models.Dashboard] using either -pydantic models, Python dictionaries, YAML, or JSON. + +This guide shows you how to configure and call a [`Dashboard`][vizro.models.Dashboard] using either pydantic models, Python dictionaries, YAML, or JSON. To create a dashboard: 1. Choose one of the possible configuration syntaxes -2. Create your `pages`, see our [guide on Pages](pages.md) -3. (optional) Choose a `theme`, see our [guide on Themes](themes.md) -4. (optional) Customize your `navigation`, see our [guide on Navigation](navigation.md) -5. (optional) Set a `title` for your dashboard -6. Add your `dashboard` to the `build` call of Vizro - +1. Create your `pages`, see our [guide on Pages](pages.md) +1. (optional) Choose a `theme`, see our [guide on Themes](themes.md) +1. (optional) Customize your `navigation`, see our [guide on Navigation](navigation.md) +1. (optional) Set a `title` for your dashboard +1. Add your `dashboard` to the `build` call of Vizro ## Use dashboard configuration options @@ -37,6 +36,7 @@ To create a dashboard: Vizro().build(dashboard).run() ``` + === "app.py - Python dict" ```py import vizro.plotly.express as px @@ -77,6 +77,7 @@ To create a dashboard: Vizro().build(dashboard).run() ``` + === "dashboard.yaml" ```yaml # Still requires a .py to add data to the data manager and parse YAML configuration @@ -101,50 +102,49 @@ To create a dashboard: type: filter title: My first dashboard ``` + === "dashboard.json" ```json { - "pages": [ + "pages": [ + { + "components": [ + { + "figure": { + "_target_": "scatter", + "color": "species", + "data_frame": "iris", + "x": "sepal_length", + "y": "petal_width" + }, + "type": "graph" + }, { - "components": [ - { - "figure": { - "_target_": "scatter", - "color": "species", - "data_frame": "iris", - "x": "sepal_length", - "y": "petal_width" - }, - "type": "graph" - }, - { - "figure": { - "_target_": "histogram", - "color": "species", - "data_frame": "iris", - "x": "sepal_width", - }, - "type": "graph" - } - ], - "controls": [ - { - "column": "species", - "type": "filter" - } - ], - "title": "My first dashboard" + "figure": { + "_target_": "histogram", + "color": "species", + "data_frame": "iris", + "x": "sepal_width" + }, + "type": "graph" } - ] + ], + "controls": [ + { + "column": "species", + "type": "filter" + } + ], + "title": "My first dashboard" + } + ] } ``` - === "Result" - [![Dashboard]][Dashboard] - [Dashboard]: ../../assets/user_guides/dashboard/dashboard.png + === "Result" + [![Dashboard]][dashboard] !!! note "Extra `.py` files for `yaml` and `json` required" - Note that in the `yaml` and `json` example an extra `.py` is required to register the data and parse the yaml/json configuration. === "app.py for yaml" @@ -164,6 +164,7 @@ To create a dashboard: Vizro().build(dashboard).run() ``` + === "app.py for json" ```py import json @@ -187,7 +188,6 @@ After running the dashboard, you can access the dashboard via `localhost:8050`. If supplied, the `title` of the [`Dashboard`][vizro.models.Dashboard] displays a heading at the top of every page. - ## Add a dashboard logo Vizro will [automatically incorporate the dashboard logo](assets.md/#add-a-logo-image) in the top-left corner of each page if an image named `logo.` is present within the assets folder. @@ -196,11 +196,10 @@ Vizro will [automatically incorporate the dashboard logo](assets.md/#add-a-logo- ## Browser title -The [website icon](assets.md/#change-the-favicon), Dashboard `title` (if supplied) and [Page `title`][vizro.models.Page] are displayed in the browser's -title bar. For example, if your Dashboard `title` is "Vizro Demo" and the Page `title` is "Homepage", then the title in the browser tab will be "Vizro Demo: Homepage". +The [website icon](assets.md/#change-the-favicon), Dashboard `title` (if supplied) and [Page `title`][vizro.models.Page] are displayed in the browser's title bar. For example, if your Dashboard `title` is "Vizro Demo" and the Page `title` is "Homepage", then the title in the browser tab will be "Vizro Demo: Homepage". ## Meta tags for social media -Vizro automatically adds [meta tags](https://metatags.io/) to display a preview card when your app is shared on social media and chat -clients. The preview includes the `URL`, `title`, plus an [image](assets.md/#include-a-meta-tags-image) and -[Page `description`][vizro.models.Page] (if supplied). To see an example, try sharing an example from the [Vizro examples gallery](https://vizro.mckinsey.com/). +Vizro automatically adds [meta tags](https://metatags.io/) to display a preview card when your app is shared on social media and chat clients. The preview includes the `URL`, `title`, plus an [image](assets.md/#include-a-meta-tags-image) and [Page `description`][vizro.models.Page] (if supplied). To see an example, try sharing an example from the [Vizro examples gallery](https://vizro.mckinsey.com/). + +[dashboard]: ../../assets/user_guides/dashboard/dashboard.png diff --git a/vizro-core/docs/pages/user-guides/data.md b/vizro-core/docs/pages/user-guides/data.md index b4fdfeda1..d19b8aafb 100644 --- a/vizro-core/docs/pages/user-guides/data.md +++ b/vizro-core/docs/pages/user-guides/data.md @@ -2,11 +2,12 @@ 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?`"] @@ -27,11 +28,10 @@ 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,14 +73,13 @@ 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]: ../../assets/user_guides/data/data_pandas_dataframe.png + === "Result" + [![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`. -!!! note +!!! 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 @@ -107,27 +106,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: + - components: + figure: _target_: box data_frame: iris # (1)! x: species y: petal_width color: species type: graph - title: Static data example + title: Static data example ``` 1. Refer to the `"iris"` data source in the data manager. - === "Result" - [![DataBasic]][DataBasic] - [DataBasic]: ../../assets/user_guides/data/data_pandas_dataframe.png + === "Result" + [![DataBasic]][databasic] -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 @@ -166,28 +165,26 @@ 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`. - 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"`. + 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"`. === "Result" - [![DynamicData]][DynamicData] - - [DynamicData]: ../../assets/user_guides/data/dynamic_data.gif + [![DynamicData]][dynamicdata] 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 @@ -220,12 +217,11 @@ 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" @@ -239,7 +235,9 @@ 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). @@ -268,16 +266,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. -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). +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). -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" @@ -314,33 +316,100 @@ 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. - 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`. + 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`. === "Result" - [![ParametrizedDynamicData]][ParametrizedDynamicData] - - [ParametrizedDynamicData]: ../../assets/user_guides/data/parametrized_dynamic_data.gif + [![ParametrizedDynamicData]][parametrizeddynamicdata] 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 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. + 1. 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. + 1. 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()), +] ``` + +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! + +[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 diff --git a/vizro-core/docs/pages/user-guides/extensions.md b/vizro-core/docs/pages/user-guides/extensions.md index 2c7efc9ac..74a740448 100644 --- a/vizro-core/docs/pages/user-guides/extensions.md +++ b/vizro-core/docs/pages/user-guides/extensions.md @@ -2,64 +2,49 @@ At its simplest, Vizro enables low-code configuration, but you can also customize it to go beyond its default capabilities by incorporating any Dash component, Dash callback, or Plotly chart. -* **[Vizro customizations](#vizro-customizations)**. You can customize Vizro to extend the default functionality of Vizro and create Python functions as customized Plotly charts, tables, dashboard components, actions, or reactive HTML components, and then plug them directly into the existing Vizro dashboard configuration (as explained below). +- **[Vizro customizations](#vizro-customizations)**. You can customize Vizro to extend the default functionality of Vizro and create Python functions as customized Plotly charts, tables, dashboard components, actions, or reactive HTML components, and then plug them directly into the existing Vizro dashboard configuration (as explained below). -* **[Dash customizations](#dash-customizations)**. You can add custom Dash callbacks directly to any Vizro dashboard, enabling you to code beneath the Vizro layer and control Dash directly. +- **[Dash customizations](#dash-customizations)**. You can add custom Dash callbacks directly to any Vizro dashboard, enabling you to code beneath the Vizro layer and control Dash directly. -* **[CSS customizations](#css-customizations)**. You can add custom CSS to any Vizro dashboard, enabling users to deviate from the default styling and create a unique look and feel for their dashboard. - -* **[React.js customizations](#reactjs-customizations)**. You can add custom React.js components directly to any Vizro dashboard, enabling users to go beneath both the Vizro and Dash layers and control React.js directly +- **[CSS customizations](#css-customizations)**. You can add custom CSS to any Vizro dashboard, enabling users to deviate from the default styling and create a unique look and feel for their dashboard. +- **[React.js customizations](#reactjs-customizations)**. You can add custom React.js components directly to any Vizro dashboard, enabling users to go beneath both the Vizro and Dash layers and control React.js directly Vizro offers the ability to combine ease of use of low-code configuration, with the flexibility of a range of high-code extensions to expand your dashboard's functionality. - ## Vizro customizations ### [Custom charts](custom-charts.md) -You can create custom chart functions in Vizro by wrapping Plotly chart code inside a -Vizro chart function wrapper, and then use the functions directly inside the Vizro dashboard configuration. This enables the creation of `plotly.graph_objects` charts with multiple traces, or `plotly_express` charts with post update customizations - +You can create custom chart functions in Vizro by wrapping Plotly chart code inside a Vizro chart function wrapper, and then use the functions directly inside the Vizro dashboard configuration. This enables the creation of `plotly.graph_objects` charts with multiple traces, or `plotly_express` charts with post update customizations ### [Custom tables](custom-tables.md) If the available arguments for the [`dash_ag_grid`][vizro.tables.dash_ag_grid] or [`dash_data_table`][vizro.tables.dash_data_table] models are insufficient, you can create a custom Dash AG Grid or Dash DataTable. - ### [Custom components](custom-components.md) -You can create a custom component based on any dash-compatible component (for example, [dash-core-components](https://dash.plotly.com/dash-core-components), -[dash-bootstrap-components](https://dash-bootstrap-components.opensource.faculty.ai/), [dash-html-components](https://github.com/plotly/dash/tree/dev/components/dash-html-components)). +You can create a custom component based on any dash-compatible component (for example, [dash-core-components](https://dash.plotly.com/dash-core-components), [dash-bootstrap-components](https://dash-bootstrap-components.opensource.faculty.ai/), [dash-html-components](https://github.com/plotly/dash/tree/dev/components/dash-html-components)). All Vizro's components are based on `Dash` and ship with a set of defaults that can be modified. If you would like to overwrite one of those defaults, or if you would like to use extra `args` or `kwargs` of those components, then this is the correct way to include those. You can use any existing attribute of any underlying [Dash component](https://dash.plotly.com/#open-source-component-libraries) with this method. - ### [Custom actions](custom-actions.md) -If you want to use the [`Action`][vizro.models.Action] model to perform functions that are not available in the [pre-defined action functions][vizro.actions], you can create your own custom action. -Like other [actions](actions.md), custom actions can also be added as an element inside the [actions chain](actions.md#chain-actions), and triggered with one of dashboard components. - +If you want to use the [`Action`][vizro.models.Action] model to perform functions that are not available in the [pre-defined action functions][vizro.actions], you can create your own custom action. Like other [actions](actions.md), custom actions can also be added as an element inside the [actions chain](actions.md#chain-actions), and triggered with one of dashboard components. ### [Custom figures](custom-figures.md) -Custom figures are useful when you need a component that reacts to -[filter](filters.md) and [parameter](parameters.md) controls. - -Vizro's [`Figure`][vizro.models.Figure] model accepts the `figure` argument, where you can enter _any_ custom figure function -as described in the [how-to guide for figures](figure.md). +Custom figures are useful when you need a component that reacts to [filter](filters.md) and [parameter](parameters.md) controls. +Vizro's [`Figure`][vizro.models.Figure] model accepts the `figure` argument, where you can enter _any_ custom figure function as described in the [how-to guide for figures](figure.md). ## Dash customizations -Since Vizro is built using Dash, it is possible to use [Dash callbacks](https://dash.plotly.com/basic-callbacks) directly in any Vizro dashboard. This enables you to code beneath the Vizro layer and control Dash directly, -which is especially useful when working with callbacks +Since Vizro is built using Dash, it is possible to use [Dash callbacks](https://dash.plotly.com/basic-callbacks) directly in any Vizro dashboard. This enables you to code beneath the Vizro layer and control Dash directly, which is especially useful when working with callbacks -Here is an example showing a Dash callback within Vizro, -enabling an interaction between data points in a scatter plot and the content of a text card: +Here is an example showing a Dash callback within Vizro, enabling an interaction between data points in a scatter plot and the content of a text card: !!! example "Dash callback example" - === "app.py" ```{.python pycafe-link} from dash import callback, Input, Output @@ -93,14 +78,12 @@ enabling an interaction between data points in a scatter plot and the content of ## CSS customizations -Vizro is opinionated about visual formatting, and some elements, such as the layout of the navigation and controls, -are fixed. You can customize some settings such as background colors, fonts, and other styles via CSS overrides. +Vizro is opinionated about visual formatting, and some elements, such as the layout of the navigation and controls, are fixed. You can customize some settings such as background colors, fonts, and other styles via CSS overrides. For more information, see our documentation on [customizing CSS](custom-css.md) ## React.js customizations -It is possible to create custom React.js components and add them -directly to any Vizro dashboard so enabling you to code beneath both the Vizro and Dash layers and control React.js directly +It is possible to create custom React.js components and add them directly to any Vizro dashboard so enabling you to code beneath both the Vizro and Dash layers and control React.js directly For more information, see the documentation on [using React.js components with Dash](https://dash.plotly.com/plugins) diff --git a/vizro-core/docs/pages/user-guides/figure.md b/vizro-core/docs/pages/user-guides/figure.md index 0589fad3b..03bfa412a 100644 --- a/vizro-core/docs/pages/user-guides/figure.md +++ b/vizro-core/docs/pages/user-guides/figure.md @@ -1,26 +1,19 @@ # How to use figures -This guide shows you how to add any [Dash component](https://dash.plotly.com/#open-source-component-libraries) that needs to be reactive to [filter](filters.md) and [parameter](parameters.md) controls. -If you want to add a static Dash component to your page, use [custom components](custom-components.md) instead. +This guide shows you how to add any [Dash component](https://dash.plotly.com/#open-source-component-libraries) that needs to be reactive to [filter](filters.md) and [parameter](parameters.md) controls. If you want to add a static Dash component to your page, use [custom components](custom-components.md) instead. -[`Figure`][vizro.models.Figure] provides a flexible foundation for all types of reactive Dash components in Vizro. -The [`Graph`][vizro.models.Graph], [`Table`][vizro.models.Table] and [`AgGrid`][vizro.models.AgGrid] components are -specific implementations of `Figure`. They serve as intuitive shortcuts, embedding behaviors and interactions specific -to their purposes. +[`Figure`][vizro.models.Figure] provides a flexible foundation for all types of reactive Dash components in Vizro. The [`Graph`][vizro.models.Graph], [`Table`][vizro.models.Table] and [`AgGrid`][vizro.models.AgGrid] components are specific implementations of `Figure`. They serve as intuitive shortcuts, embedding behaviors and interactions specific to their purposes. -If these more specific models already achieve what you need then they should be used in preference to -the more generic `Figure`. Remember that it is possible to supply [custom charts](custom-charts.md) to `Graph` -and [custom tables](custom-tables.md) to `Table`. +If these more specific models already achieve what you need then they should be used in preference to the more generic `Figure`. Remember that it is possible to supply [custom charts](custom-charts.md) to `Graph` and [custom tables](custom-tables.md) to `Table`. -There are already a few figure functions you can reuse, see the section on [KPI cards](#key-performance-indicator-kpi-cards) -for more details: +There are already a few figure functions you can reuse, see the section on [KPI cards](#key-performance-indicator-kpi-cards) for more details: - [`kpi_card`][vizro.figures.kpi_card] - [`kpi_card_reference`][vizro.figures.kpi_card_reference] The following flowchart shows what you need to consider when choosing which model to use: -``` mermaid +```mermaid graph TD first["`Does your desired component exist in Vizro, e.g. Graph, Table or AgGrid?`"] specific-component([Use the specific component]) @@ -40,14 +33,12 @@ graph TD classDef clickable color:#4051b5; ``` - To add a `Figure` to your page: 1. Add the `Figure` model to the components argument of the [Page][vizro.models.Page] model. -2. Use an existing figure function from [`vizro.figures`](../API-reference/figure-callables.md) and pass it to the `figure` argument of the `Figure` model. +1. Use an existing figure function from [`vizro.figures`](../API-reference/figure-callables.md) and pass it to the `figure` argument of the `Figure` model. !!! example "Use existing figure functions" - === "app.py" ```{.python pycafe-link} import vizro.models as vm @@ -99,40 +90,25 @@ To add a `Figure` to your page: selector: type: radio_items layout: - grid: - [ - [0, -1, -1, -1], - [-1, -1, -1, -1], - [-1, -1, -1, -1], - [-1, -1, -1, -1], - [-1, -1, -1, -1], - ] + grid: [[0, -1, -1, -1], [-1, -1, -1, -1], [-1, -1, -1, -1], [-1, -1, -1, -1], + [-1, -1, -1, -1]] title: KPI card ``` - === "Result" - [![Figure]][Figure] - - [Figure]: ../../assets/user_guides/figure/figure.png + === "Result" + [![Figure]][figure] ### Key Performance Indicator (KPI) cards -A KPI card is a dynamic card that can display a single value, but optionally, can also include a title, icon, and reference value. -It is a common visual component to display key metrics in a dashboard. Vizro supports two pre-defined KPI card functions: -- [`kpi_card`](../API-reference/figure-callables.md#vizro.figures.kpi_card): A KPI card that shows a single value found -by performing an aggregation function (by default, `sum`) over a specified column. -Required arguments are `data_frame` and `value_column`. +A KPI card is a dynamic card that can display a single value, but optionally, can also include a title, icon, and reference value. It is a common visual component to display key metrics in a dashboard. Vizro supports two pre-defined KPI card functions: -- [`kpi_card_with_reference`](../API-reference/figure-callables.md#vizro.figures.kpi_card_reference): A KPI card that shows a single value -and a delta comparison to a reference value found by performing an aggregation function (by default, `sum`) over the specified columns. -Required arguments are `data_frame`, `value_column` and `reference_column`. +- [`kpi_card`](../API-reference/figure-callables.md#vizro.figures.kpi_card): A KPI card that shows a single value found by performing an aggregation function (by default, `sum`) over a specified column. Required arguments are `data_frame` and `value_column`. -As described in the [API reference](../API-reference/figure-callables.md) and illustrated in the below example, these -functions have several arguments to customize your KPI cards. If you require a level of customization that is not -possible with the built-in functions then you can create a [custom figure](custom-figures.md). +- [`kpi_card_with_reference`](../API-reference/figure-callables.md#vizro.figures.kpi_card_reference): A KPI card that shows a single value and a delta comparison to a reference value found by performing an aggregation function (by default, `sum`) over the specified columns. Required arguments are `data_frame`, `value_column` and `reference_column`. -!!! example "KPI card variations" +As described in the [API reference](../API-reference/figure-callables.md) and illustrated in the below example, these functions have several arguments to customize your KPI cards. If you require a level of customization that cannot be done with the built-in functions then you can create a [custom figure](custom-figures.md). +!!! example "KPI card variations" === "app.py" ```{.python pycafe-link} import pandas as pd @@ -258,8 +234,8 @@ possible with the built-in functions then you can create a [custom figure](custo value_column: Actual reference_column: Reference title: KPI reference with formatting - value_format: "{value:.2f}€" - reference_format: "{delta:+.2f}€ vs. last year ({reference:.2f}€)" + value_format: '{value:.2f}€' + reference_format: '{delta:+.2f}€ vs. last year ({reference:.2f}€)' type: figure - figure: _target_: kpi_card_reference @@ -276,7 +252,9 @@ possible with the built-in functions then you can create a [custom figure](custo grid: [[0, 1, 2, 3], [4, 5, 6, 7], [-1, -1, -1, -1], [-1, -1, -1, -1]] title: KPI cards ``` + === "Result" - [![KPICards]][KPICards] + [![KPICards]][kpicards] - [KPICards]: ../../assets/user_guides/figure/kpi_cards.png +[figure]: ../../assets/user_guides/figure/figure.png +[kpicards]: ../../assets/user_guides/figure/kpi_cards.png diff --git a/vizro-core/docs/pages/user-guides/filters.md b/vizro-core/docs/pages/user-guides/filters.md index e5dd2ed61..8228ecbe7 100644 --- a/vizro-core/docs/pages/user-guides/filters.md +++ b/vizro-core/docs/pages/user-guides/filters.md @@ -2,19 +2,18 @@ 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 -2. configure the `column` argument, which denotes the target column to be filtered +1. 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" @@ -39,6 +38,7 @@ automatically based on the target column, for example, a dropdown for categorica Vizro().build(dashboard).run() ``` + === "app.yaml" ```yaml # Still requires a .py to add data to the data manager and parse YAML configuration @@ -57,18 +57,86 @@ automatically based on the target column, for example, a dropdown for categorica type: filter title: My first page ``` + === "Result" + [![Filter]][filter] - [![Filter]][Filter] +The selector is configured automatically based on the target column type data as follows: - [Filter]: ../../assets/user_guides/control/control1.png +- 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). -## Changing selectors +The following example demonstrates these default selector types. -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 "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 -!!! example "Filter with custom Selector" + 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] + +## Change 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 @@ -91,6 +159,7 @@ Currently available selectors are [`Checklist`][vizro.models.Checklist], [`Dropd Vizro().build(dashboard).run() ``` + === "app.yaml" ```yaml # Still requires a .py to add data to the data manager and parse YAML configuration @@ -110,19 +179,16 @@ Currently available selectors are [`Checklist`][vizro.models.Checklist], [`Dropd type: filter title: My first page ``` - === "Result" - - [![Selector]][Selector] - [Selector]: ../../assets/user_guides/control/control2.png + === "Result" + [![Selector]][selector] ## 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`. @@ -139,10 +205,10 @@ Below is an advanced example where we only target one page component, and where title="My first page", components=[ vm.Graph(id="scatter_chart", figure=px.scatter(iris, x="sepal_length", y="petal_width", color="species")), - vm.Graph(id="scatter_chart2", figure=px.scatter(iris, x="petal_length", y="sepal_width", color="species")), + 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)), ], ) @@ -150,6 +216,7 @@ 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 @@ -170,20 +237,23 @@ Below is an advanced example where we only target one page component, and where x: petal_length y: sepal_width color: species - id: scatter_chart2 type: graph 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]][Advanced] +To further customize selectors, see our [how-to-guide on creating custom components](custom-components.md). - [Advanced]: ../../assets/user_guides/control/control3.png +[advanced]: ../../assets/user_guides/control/control3.png +[filter]: ../../assets/user_guides/control/control1.png +[selector]: ../../assets/user_guides/control/control2.png diff --git a/vizro-core/docs/pages/user-guides/graph.md b/vizro-core/docs/pages/user-guides/graph.md index 36c670997..1c3ffc8bc 100755 --- a/vizro-core/docs/pages/user-guides/graph.md +++ b/vizro-core/docs/pages/user-guides/graph.md @@ -6,25 +6,18 @@ The [`Graph`][vizro.models.Graph] model is the most used component in many dashb To add a [`Graph`][vizro.models.Graph] to your page, do the following: -1. insert the [`Graph`][vizro.models.Graph] model into the `components` argument of the -[`Page`][vizro.models.Page] model -2. enter any of the currently available charts of the open source library [`plotly.express`](https://plotly.com/python/plotly-express/) into the `figure` argument +1. insert the [`Graph`][vizro.models.Graph] model into the `components` argument of the [`Page`][vizro.models.Page] model +1. enter any of the currently available charts of the open source library [`plotly.express`](https://plotly.com/python/plotly-express/) into the `figure` argument !!! note + To use the [`plotly.express`](https://plotly.com/python/plotly-express/) chart in a Vizro dashboard, you need to import it as `import vizro.plotly.express as px`. This leaves any of the [`plotly.express`](https://plotly.com/python/plotly-express/) functionality untouched yet enables _direct insertion_ into the [`Graph`][vizro.models.Graph] model _as is_. - In order to use the [`plotly.express`](https://plotly.com/python/plotly-express/) chart in a Vizro dashboard, you need to import it as `import vizro.plotly.express as px`. - This leaves any of the [`plotly.express`](https://plotly.com/python/plotly-express/) functionality untouched, but allows _direct insertion_ into the [`Graph`][vizro.models.Graph] model _as is_. - - Note also that the `plotly.express` chart needs to have a `data_frame` argument. In case you require a chart without - a `data_frame` argument (for example, the [`imshow` chart](https://plotly.com/python/imshow/)), refer to our - [guide on custom charts](custom-charts.md). - + Note also that the `plotly.express` chart needs to have a `data_frame` argument. In case you require a chart without a `data_frame` argument (for example, the [`imshow` chart](https://plotly.com/python/imshow/)), refer to our [guide on custom charts](custom-charts.md). ## Insert Plotly chart !!! example "Graph" === "app.py" - ```{.python pycafe-link} import vizro.models as vm import vizro.plotly.express as px @@ -47,25 +40,24 @@ To add a [`Graph`][vizro.models.Graph] to your page, do the following: 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_: scatter_matrix - color: species - data_frame: iris - dimensions: ["sepal_length", "sepal_width", "petal_length", "petal_width"] - type: graph - title: My first page + - components: + - figure: + _target_: scatter_matrix + color: species + data_frame: iris + dimensions: [sepal_length, sepal_width, petal_length, petal_width] + type: graph + title: My first page ``` - === "Result" - [![Graph]][Graph] - - [Graph]: ../../assets/user_guides/components/graph.png + === "Result" + [![Graph]][graph] In the Python example we directly inserted the pandas DataFrame `df` into `figure=px.scatter_matrix(df, ...)`. This is [one way to supply data to a chart](data.md#supply-directly). For the YAML version, we [refer to the data source by name](data.md#reference-by-name) as `data_frame: iris`. For a full explanation of the different methods you can use to send data to your dashboard, see [our guide to using data in Vizro](data.md). @@ -73,31 +65,24 @@ In the Python example we directly inserted the pandas DataFrame `df` into `figur You will need to create a custom chart if you want to customize the Plotly chart beyond a function call, for example by: -* using post-update methods like `update_layout`, `update_xaxes`, `update_traces`, or -* by creating a custom `plotly.graph_objects.Figure()` object and manually adding traces with `add_trace`. - -For more details, refer to our [user guide on custom chart](custom-charts.md) and the -[Plotly documentation on updating figures](https://plotly.com/python/creating-and-updating-figures/). +- using post-update methods like `update_layout`, `update_xaxes`, `update_traces`, or +- by creating a custom `plotly.graph_objects.Figure()` object and manually adding traces with `add_trace`. +For more details, refer to our [user guide on custom chart](custom-charts.md) and the [Plotly documentation on updating figures](https://plotly.com/python/creating-and-updating-figures/). ## Add title, header, and footer -The [`Graph`][vizro.models.Graph] accepts a `title`, `header` and `footer` argument. This is useful for providing -context to the data being displayed, or for adding a description of the data. +The [`Graph`][vizro.models.Graph] accepts a `title`, `header` and `footer` argument. This is useful for context on the data being displayed, or for adding a description of the data. - **title**: Displayed as an [H3 header](https://dash.plotly.com/dash-html-components/h3), useful for summarizing the main topic or insight of the component. - **header**: Accepts markdown text, ideal for extra descriptions, subtitles, or detailed data insights. - **footer**: Accepts markdown text, commonly used for citing data sources, providing information on the last update, or adding disclaimers. - !!! note - - Although you can directly provide a `title` to the Plotly Express chart, we recommend using `Graph.title` for - proper alignment with other components on the screen. + Although you can directly give a `title` to the Plotly Express chart, we recommend using `Graph.title` for proper alignment with other components on the screen. !!! example "Formatted Graph" === "app.py" - ```{.python pycafe-link} import vizro.models as vm @@ -126,31 +111,34 @@ context to the data being displayed, or for adding a description of the data. 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_: scatter - x: sepal_width - y: sepal_length - color: species - data_frame: iris - title: Relationships between Sepal Width and Sepal Length - header: | - Each point in the scatter plot represents one of the 150 iris flowers, with colors indicating their - types. The Setosa type is easily identifiable by its short and wide sepals. - - However, there is still overlap between the Versicolor and Virginica types when considering only sepal - width and length. - footer: | - SOURCE: **Plotly iris data set, 2024** - type: graph - title: Formatted Graph + - components: + - figure: + _target_: scatter + x: sepal_width + y: sepal_length + color: species + data_frame: iris + title: Relationships between Sepal Width and Sepal Length + header: | + Each point in the scatter plot represents one of the 150 iris flowers, with colors indicating their + types. The Setosa type is easily identifiable by its short and wide sepals. + + However, there is still overlap between the Versicolor and Virginica types when considering only sepal + width and length. + footer: | + SOURCE: **Plotly iris data set, 2024** + type: graph + title: Formatted Graph ``` + === "Result" - [![FormattedGraph]][FormattedGraph] + [![FormattedGraph]][formattedgraph] - [FormattedGraph]: ../../assets/user_guides/components/formatted_graph.png +[formattedgraph]: ../../assets/user_guides/components/formatted_graph.png +[graph]: ../../assets/user_guides/components/graph.png diff --git a/vizro-core/docs/pages/user-guides/install.md b/vizro-core/docs/pages/user-guides/install.md index 4d4c01239..6a07911e5 100644 --- a/vizro-core/docs/pages/user-guides/install.md +++ b/vizro-core/docs/pages/user-guides/install.md @@ -8,8 +8,8 @@ We recommend that you create a virtual environment for each Vizro project you wo ## Prerequisites to working locally -* **Python**: Vizro supports macOS, Linux, and Windows. It works with Python 3.9 and later. -* **Virtual environment**: You specify the version of Python to use with Vizro when you set up the virtual environment. See the following references to learn more about [Python virtual environments](https://realpython.com/python-virtual-environments-a-primer/), [Conda virtual environments](https://docs.conda.io/projects/conda/en/latest/user-guide/getting-started.html#starting-conda) or [watch an explainer video about them](https://youtu.be/YKfAwIItO7M). +- **Python**: Vizro supports macOS, Linux, and Windows. It works with Python 3.9 and later. +- **Virtual environment**: You specify the version of Python to use with Vizro when you set up the virtual environment. See the following references to learn more about [Python virtual environments](https://realpython.com/python-virtual-environments-a-primer/), [Conda virtual environments](https://docs.conda.io/projects/conda/en/latest/user-guide/getting-started.html#starting-conda) or [watch an explainer video about them](https://youtu.be/YKfAwIItO7M). ### How to create a virtual environment for your Vizro project diff --git a/vizro-core/docs/pages/user-guides/kedro-data-catalog.md b/vizro-core/docs/pages/user-guides/kedro-data-catalog.md index 0e2b0954e..ce4b060fa 100644 --- a/vizro-core/docs/pages/user-guides/kedro-data-catalog.md +++ b/vizro-core/docs/pages/user-guides/kedro-data-catalog.md @@ -3,6 +3,7 @@ This page describes how to integrate Vizro with [Kedro](https://docs.kedro.org/en/stable/index.html), an open-source Python framework to create reproducible, maintainable, and modular data science code. For Pandas datasets registered in a Kedro data catalog, Vizro provides a convenient way to visualize them. ## Installation + If you already have Kedro installed then you do not need to install any extra dependencies. If you do not have Kedro installed then you should run: ```bash @@ -10,7 +11,9 @@ pip install vizro[kedro] ``` ## Use datasets from the Kedro Data Catalog + `vizro.integrations.kedro` provides functions to help generate and process a [Kedro Data Catalog](https://docs.kedro.org/en/stable/data/index.html). Given a Kedro Data Catalog `catalog`, the general pattern to add datasets into the Vizro data manager is: + ```python from vizro.integrations import kedro as kedro_integration from vizro.managers import data_manager @@ -25,15 +28,14 @@ This imports all datasets of type [`kedro_datasets.pandas`](https://docs.kedro.o The `catalog` variable may have been created in a number of different ways: 1. Kedro project path. Vizro exposes a helper function `vizro.integrations.kedro.catalog_from_project` to generate a `catalog` given the path to a Kedro project. -2. [Kedro Jupyter session](https://docs.kedro.org/en/stable/notebooks_and_ipython/kedro_and_notebooks.html). This automatically exposes `catalog`. -3. Data Catalog configuration file (`catalog.yaml`). This can create a `catalog` entirely independently of a Kedro project using [`kedro.io.DataCatalog.from_config`](https://docs.kedro.org/en/stable/kedro.io.DataCatalog.html#kedro.io.DataCatalog.from_config). +1. [Kedro Jupyter session](https://docs.kedro.org/en/stable/notebooks_and_ipython/kedro_and_notebooks.html). This automatically exposes `catalog`. +1. Data Catalog configuration file (`catalog.yaml`). This can create a `catalog` entirely independently of a Kedro project using [`kedro.io.DataCatalog.from_config`](https://docs.kedro.org/en/stable/kedro.io.DataCatalog.html#kedro.io.DataCatalog.from_config). The full code for these different cases is given below. !!! example "Import a Kedro Data Catalog into the Vizro data manager" - === "app.py (Kedro project path)" - ```py + ```python from vizro.integrations import kedro as kedro_integration from vizro.managers import data_manager @@ -43,16 +45,18 @@ The full code for these different cases is given below. for dataset_name, dataset in kedro_integration.datasets_from_catalog(catalog).items(): data_manager[dataset_name] = dataset ``` + === "app.ipynb (Kedro Jupyter session)" - ```py + ```python from vizro.managers import data_manager for dataset_name, dataset in kedro_integration.datasets_from_catalog(catalog).items(): data_manager[dataset_name] = dataset ``` + === "app.py (Data Catalog configuration file)" - ```py + ```python from kedro.io import DataCatalog import yaml diff --git a/vizro-core/docs/pages/user-guides/layouts.md b/vizro-core/docs/pages/user-guides/layouts.md index 8361560db..af66ec301 100644 --- a/vizro-core/docs/pages/user-guides/layouts.md +++ b/vizro-core/docs/pages/user-guides/layouts.md @@ -3,11 +3,10 @@ The [`Page`][vizro.models.Page] model accepts a `layout` argument that enables custom arrangement of charts and components on the screen. This guide shows how to customize the grid specifications in the [`Layout`][vizro.models.Layout]. ## The default layout -The `layout` argument of the [`Page`][vizro.models.Page] model is optional. If no layout is specified, all charts/components are automatically [**stacked vertically**](layouts.md#vertical-and-horizontal-stacking) on the page in one column. -If you are happy with that arrangement, you can create your charts/components without providing a [`Layout`][vizro.models.Layout]. -!!! example "Default Layout" +The `layout` argument of the [`Page`][vizro.models.Page] model is optional. If no layout is specified, all charts/components are automatically [**stacked vertically**](layouts.md#vertical-and-horizontal-stacking) on the page in one column. If you are happy with that arrangement, you can create your charts/components without providing a [`Layout`][vizro.models.Layout]. +!!! example "Default Layout" === "app.py" ```{.python pycafe-link} from vizro import Vizro @@ -23,35 +22,33 @@ If you are happy with that arrangement, you can create your charts/components wi 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: - - text: | - # Component 0 - type: card - - text: | - # Component 1 - type: card - title: two_left + - components: + - text: | + # Component 0 + type: card + - text: | + # Component 1 + type: card + title: two_left ``` - === "Result" - [![Layout]][Layout] - - [Layout]: ../../assets/user_guides/layout/two_left.png - + === "Result" + [![Layout]][layout] ## Configure the grid + To customize the grid arrangement, configure the `grid` parameter of the [`Layout`][vizro.models.Layout] model. The example below shows an example of a valid `grid`: ```python title="Basic example" -grid = [[0, 1], - [0, 2]] +grid = [[0, 1], [0, 2]] ``` - The `grid` must be provided as `list[list[int]]` (for example, `grid = [[0, 1], [0, 2]]`). @@ -64,16 +61,12 @@ grid = [[0, 1], - The area spanned by a chart/component in the grid must be rectangular. - The grid can be arbitrarily large, allowing arbitrarily granular control of the grid. - ## Vertical and horizontal stacking + As described above, when no `Layout` is specified, components are presented **vertically** as a single-column stack. If you have three components, the default `Layout.grid` will be as follows, with three equally sized rows, each containing a component spanning the entire width: ```python title="Vertical stacking" -grid = [ - [0], - [1], - [2] - ] +grid = [[0], [1], [2]] ``` To present components **horizontally** in one row: @@ -89,8 +82,8 @@ This defines a single row that occupies the entire width and height, divided int ## Grid - basic example -!!! example "Grid Arrangement - Basic Example" +!!! example "Grid Arrangement - Basic Example" === "app.py" ```{.python pycafe-link} import vizro.models as vm @@ -109,39 +102,37 @@ This defines a single row that occupies the entire width and height, divided int 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: - - text: | - # Component 0 - type: card - - text: | - # Component 1 - type: card - - text: | - # Component 2 - type: card - layout: - grid: [[0, 1], [0, 2]] - title: one_left_two_right + - components: + - text: | + # Component 0 + type: card + - text: | + # Component 1 + type: card + - text: | + # Component 2 + type: card + layout: + grid: [[0, 1], [0, 2]] + title: one_left_two_right ``` - === "Result" - [![Grid]][Grid] - [Grid]: ../../assets/user_guides/layout/one_left_two_right.png + === "Result" + [![Grid]][grid] ## 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. -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. +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. !!! example "Grid Arrangement - Advanced Example" - === "app.py" ```{.python pycafe-link} import vizro.models as vm @@ -208,6 +199,7 @@ allowing arbitrarily granular control of the grid by creating larger grids. 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 @@ -260,44 +252,44 @@ allowing arbitrarily granular control of the grid by creating larger grids. grid: [[0, 1, 3, 4], [2, 2, 3, 4]] title: Custom Layout - Advanced Example ``` - === "Result" - [![GridAdv]][GridAdv] - [GridAdv]: ../../assets/user_guides/layout/grid_advanced.png + === "Result" + [![GridAdv]][gridadv] ## Custom layout examples + Here is a reference table of example layouts: + one row with one component, second row with two components stacked horizontally -| Layout needed | Grid | Code | -|--------------------------------|----------------------------|-------------------------------------------------------------------------------------| -| one component | layout=vm.Layout(grid=[[0]]) | `layout=vm.Layout(grid=[[0]])` | -| two horizontally stacked rows, each with one component | | `layout=vm.Layout(grid=[[0],[1]])` | -| one row with two components set horizontally | layout=vm.Layout(grid=[[0],[1]]) | `layout=vm.Layout(grid=[[0,1]])` | -| three horizontally stacked rows, each with one component | layout=vm.Layout(grid=[[0],[1],[2]] | `layout=vm.Layout(grid=[[0],[1],[2]])` or
`layout=None` | -| one row divided into two separate columns where the left column is one component and the right is two stacked components | layout=vm.Layout(grid=[[0,1],[0,2]]) | `layout=vm.Layout(grid=[[0,1],[0,2]])` | -| two rows with the top as a single component and the bottom divided into two components | layout=vm.Layout(grid=[[0,0],[1,2]]) | `layout=vm.Layout(grid=[[0,0],[1,2]])` | -| two rows with the top divided into two columns where each holds one component, and the bottom as a single component | layout=vm.Layout(grid=[[0,1],[2,2]]) | `layout=vm.Layout(grid=[[0,1],[2,2]])` | -| two columns where the left is a single component and the right is a set of three horizontally stacked components | layout=vm.Layout(grid=[[0,1],[0,2],[0,3]]) | `layout=vm.Layout(grid=[[0,1],[0,2],[0,3]])` | -| two rows where each row is two components | layout=vm.Layout(grid=[[0,1],[2,3]]) | `layout=vm.Layout(grid=[[0,1],[2,3]])` | -| two columns where the left is a set of three horizontally stacked components and the right is a single component | layout=vm.Layout(grid=[[0,3],[1,3],[2,3]]) | `layout=vm.Layout(grid=[[0,3],[1,3],[2,3]])` | -| two rows where the top is a single component and the bottom is three separate components | layout=vm.Layout(grid=[[0,0,0],[1,2,3]]) | `layout=vm.Layout(grid=[[0,0,0],[1,2,3]])` | -| two rows where the top is three separate components and the bottom is a single component | layout=vm.Layout(grid=[[0,1,2],[3,3,3]]) | `layout=vm.Layout(grid=[[0,1,2],[3,3,3]])` | +| Layout needed | Grid | Code | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | +| one component | layout=vm.Layout(grid=[[0]]) | `layout=vm.Layout(grid=[[0]])` | +| two horizontally stacked rows, each with one component | | `layout=vm.Layout(grid=[[0],[1]])` | +| one row with two components set horizontally | layout=vm.Layout(grid=[[0],[1]]) | `layout=vm.Layout(grid=[[0,1]])` | +| three horizontally stacked rows, each with one component | layout=vm.Layout(grid=[[0],[1],[2]] | `layout=vm.Layout(grid=[[0],[1],[2]])` or
`layout=None` | +| one row divided into two separate columns where the left column is one component and the right is two stacked components | layout=vm.Layout(grid=[[0,1],[0,2]]) | `layout=vm.Layout(grid=[[0,1],[0,2]])` | +| two rows with the top as a single component and the bottom divided into two components | layout=vm.Layout(grid=[[0,0],[1,2]]) | `layout=vm.Layout(grid=[[0,0],[1,2]])` | +| two rows with the top divided into two columns where each holds one component, and the bottom as a single component | layout=vm.Layout(grid=[[0,1],[2,2]]) | `layout=vm.Layout(grid=[[0,1],[2,2]])` | +| two columns where the left is a single component and the right is a set of three horizontally stacked components | layout=vm.Layout(grid=[[0,1],[0,2],[0,3]]) | `layout=vm.Layout(grid=[[0,1],[0,2],[0,3]])` | +| two rows where each row is two components | layout=vm.Layout(grid=[[0,1],[2,3]]) | `layout=vm.Layout(grid=[[0,1],[2,3]])` | +| two columns where the left is a set of three horizontally stacked components and the right is a single component | layout=vm.Layout(grid=[[0,3],[1,3],[2,3]]) | `layout=vm.Layout(grid=[[0,3],[1,3],[2,3]])` | +| two rows where the top is a single component and the bottom is three separate components | layout=vm.Layout(grid=[[0,0,0],[1,2,3]]) | `layout=vm.Layout(grid=[[0,0,0],[1,2,3]])` | +| two rows where the top is three separate components and the bottom is a single component | layout=vm.Layout(grid=[[0,1,2],[3,3,3]]) | `layout=vm.Layout(grid=[[0,1,2],[3,3,3]])` | ## Add empty sections to the grid + One approach to organize the dashboard's layout involves integrating empty sections by specifying `-1` within the grid layout. ```python title="Example" -grid = [[0, 1, -1], - [0, 2, -1]] +grid = [[0, 1, -1], [0, 2, -1]] ``` !!! example "Adding Empty Spaces" - === "app.py" ```{.python pycafe-link} import vizro.models as vm @@ -316,38 +308,38 @@ grid = [[0, 1, -1], 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: - - text: | - # Component 0 - type: card - - text: | - # Component 1 - type: card - - text: | - # Component 2 - type: card - layout: - grid: [[0, 1, -1], [0, 2, -1]] - title: Adding empty spaces + - components: + - text: | + # Component 0 + type: card + - text: | + # Component 1 + type: card + - text: | + # Component 2 + type: card + layout: + grid: [[0, 1, -1], [0, 2, -1]] + title: Adding empty spaces ``` - === "Result" - [![GridEmpty]][GridEmpty] - [GridEmpty]: ../../assets/user_guides/layout/layout_empty_spaces.png + === "Result" + [![GridEmpty]][gridempty] ## Control the scroll behavior + By default, the grid fits all charts/components on the screen. This can lead to distortions such that the chart/component looks squashed. To control the scroll behavior, you can specify the following: - `row_min_height`: Sets a chart/component's minimum height. Defaults to 0px. - `col_min_width`: Sets a chart/component's minimum width. Defaults to 0px. !!! example "Activate Scrolling" - === "app.py" ```{.python pycafe-link} import vizro.models as vm @@ -371,59 +363,63 @@ By default, the grid fits all charts/components on the screen. This can lead to 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: - - text: | - # Component 0 - type: card - - text: | - # Component 1 - type: card - - text: | - # Component 2 - type: card - - text: | - # Component 2 - type: card - - text: | - # Component 4 - type: card - - text: | - # Component 5 - type: card - - text: | - # Component 6 - type: card - - text: | - # Component 7 - type: card - layout: - grid: [[0], [1], [2], [3], [4], [5], [6], [7]] - row_min_height: 240px - title: Activate scrolling + - components: + - text: | + # Component 0 + type: card + - text: | + # Component 1 + type: card + - text: | + # Component 2 + type: card + - text: | + # Component 2 + type: card + - text: | + # Component 4 + type: card + - text: | + # Component 5 + type: card + - text: | + # Component 6 + type: card + - text: | + # Component 7 + type: card + layout: + grid: [[0], [1], [2], [3], [4], [5], [6], [7]] + row_min_height: 240px + title: Activate scrolling ``` - === "Result" - [![GridScroll]][GridScroll] - - [GridScroll]: ../../assets/user_guides/layout/grid_scroll.png + === "Result" + [![GridScroll]][gridscroll] ## Further customization -For further customization, such as changing the gap between row and column, refer to the -documentation of the [`Layout`][vizro.models.Layout] model. + +For further customization, such as changing the gap between row and column, refer to the documentation of the [`Layout`][vizro.models.Layout] model. ## Alternative layout approaches + 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] provide 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] 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). 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" } + +[grid]: ../../assets/user_guides/layout/one_left_two_right.png +[gridadv]: ../../assets/user_guides/layout/grid_advanced.png +[gridempty]: ../../assets/user_guides/layout/layout_empty_spaces.png +[gridscroll]: ../../assets/user_guides/layout/grid_scroll.png +[layout]: ../../assets/user_guides/layout/two_left.png diff --git a/vizro-core/docs/pages/user-guides/navigation.md b/vizro-core/docs/pages/user-guides/navigation.md index 41c6bb9dc..75a661470 100644 --- a/vizro-core/docs/pages/user-guides/navigation.md +++ b/vizro-core/docs/pages/user-guides/navigation.md @@ -2,8 +2,7 @@ This guide shows you how to use and customize the navigation that appears on the left of your dashboard. -The [`Dashboard`][vizro.models.Dashboard] model accepts a `navigation` argument, where you can enter a [`Navigation`][vizro.models.Navigation] model. This enables you to group pages together and customize how they appear in your navigation. -The dashboard includes a collapsible side panel that users can minimize or expand by a button click. The collapse button, located in the top right corner of the side panel, is visible by default for user convenience. +The [`Dashboard`][vizro.models.Dashboard] model accepts a `navigation` argument, where you can enter a [`Navigation`][vizro.models.Navigation] model. This enables you to group pages together and customize how they appear in your navigation. The dashboard includes a collapsible side panel that users can minimize or expand by a button click. The collapse button, located in the top right corner of the side panel, is visible by default for user convenience. ## Use the default navigation @@ -56,8 +55,8 @@ By default, if the `navigation` argument is not specified, Vizro creates a navig type: graph title: My first page - components: - - text: My text here - type: card + - text: My text here + type: card title: My second page - components: - figure: @@ -69,10 +68,9 @@ By default, if the `navigation` argument is not specified, Vizro creates a navig type: graph title: My third page ``` - === "Result" - [![DefaultNavigation]][DefaultNavigation] - [DefaultNavigation]: ../../assets/user_guides/navigation/default_navigation.png + === "Result" + [![DefaultNavigation]][defaultnavigation] ## Include a subset of pages @@ -132,17 +130,15 @@ To include only some of your dashboard pages in your navigation then list them i - My first page - My second page ``` - === "Result" - [![OnlySomePages]][OnlySomePages] - [OnlySomePages]: ../../assets/user_guides/navigation/only_some_pages.png + === "Result" + [![OnlySomePages]][onlysomepages] ## Group pages You can also group your pages together by specifying `pages` as a dictionary: !!! example "Grouping pages" - === "snippet.py" ```py # page_1, page_2, page_3 defined as in default example @@ -154,7 +150,6 @@ You can also group your pages together by specifying `pages` as a dictionary: ``` === "app.py" - ```{.python pycafe-link} from vizro import Vizro import vizro.plotly.express as px @@ -187,6 +182,7 @@ You can also group your pages together by specifying `pages` as a dictionary: ) Vizro().build(dashboard).run() ``` + === "app.yaml" ```yaml # Still requires a .py to add data to the data manager and parse YAML configuration @@ -200,18 +196,15 @@ You can also group your pages together by specifying `pages` as a dictionary: Group B: - My third page ``` - === "Result" - [![GroupedNavigation]][GroupedNavigation] - - [GroupedNavigation]: ../../assets/user_guides/navigation/grouped_navigation.png + === "Result" + [![GroupedNavigation]][groupednavigation] ## Use a navigation bar with icons Another way to group together pages in the navigation is to use a [`NavBar`][vizro.models.NavBar] with icons. The simplest way to use this is to change the `nav_selector` specified in [`Navigation`][vizro.models.Navigation]: !!! example "Using `NavBar`" - === "snippet.py" ```py # page_1, page_2, page_3 defined as in default example @@ -223,8 +216,8 @@ Another way to group together pages in the navigation is to use a [`NavBar`][viz ) Vizro().build(dashboard).run() ``` - === "app.py" + === "app.py" ```{.python pycafe-link} from vizro import Vizro import vizro.plotly.express as px @@ -276,14 +269,11 @@ Another way to group together pages in the navigation is to use a [`NavBar`][viz nav_selector: type: nav_bar ``` - === "Result" - [![NavBar]][NavBar] - - [NavBar]: ../../assets/user_guides/navigation/nav_bar.png + === "Result" + [![NavBar]][navbar] -Here, the first level of the navigation hierarchy ("Group A" and "Group B") is represented by an icon in a navigation bar, and the second level of the navigation (the pages) is represented by an accordion. -By default, the set of icons used are the [`filter` icons from the Google Material icons library](https://fonts.google.com/icons?icon.query=filter). The icon label ("Group A" and "Group B") appears as a tooltip on hovering over the icon. +Here, the first level of the navigation hierarchy ("Group A" and "Group B") is represented by an icon in a navigation bar, and the second level of the navigation (the pages) is represented by an accordion. By default, the set of icons used are the [`filter` icons from the Google Material icons library](https://fonts.google.com/icons?icon.query=filter). The icon label ("Group A" and "Group B") appears as a tooltip on hovering over the icon. ## Customize the navigation bar @@ -314,7 +304,6 @@ The same configuration for [grouping pages](#group-pages) applies inside a `NavL ``` === "app.py" - ```{.python pycafe-link} from vizro import Vizro import vizro.plotly.express as px @@ -375,10 +364,9 @@ The same configuration for [grouping pages](#group-pages) applies inside a `NavL Group B: - My third page ``` - === "Result" - [![AccordionInsideNavBar]][AccordionInsideNavBar] - [AccordionInsideNavBar]: ../../assets/user_guides/navigation/accordion_inside_nav_bar.png + === "Result" + [![AccordionInsideNavBar]][accordioninsidenavbar] You can alter the icons used by specifying the name of the icon in the [Google Material icons library](https://fonts.google.com/icons): @@ -402,8 +390,8 @@ You can alter the icons used by specifying the name of the icon in the [Google M ), ) ``` - === "app.py" + === "app.py" ```{.python pycafe-link} from vizro import Vizro import vizro.plotly.express as px @@ -468,7 +456,13 @@ You can alter the icons used by specifying the name of the icon in the [Google M pages: - My third page ``` - === "Result" - [![CustomIcons]][CustomIcons] - [CustomIcons]: ../../assets/user_guides/navigation/custom_icons.png + === "Result" + [![CustomIcons]][customicons] + +[accordioninsidenavbar]: ../../assets/user_guides/navigation/accordion_inside_nav_bar.png +[customicons]: ../../assets/user_guides/navigation/custom_icons.png +[defaultnavigation]: ../../assets/user_guides/navigation/default_navigation.png +[groupednavigation]: ../../assets/user_guides/navigation/grouped_navigation.png +[navbar]: ../../assets/user_guides/navigation/nav_bar.png +[onlysomepages]: ../../assets/user_guides/navigation/only_some_pages.png diff --git a/vizro-core/docs/pages/user-guides/pages.md b/vizro-core/docs/pages/user-guides/pages.md index 7ae944e9b..71e01e9f9 100644 --- a/vizro-core/docs/pages/user-guides/pages.md +++ b/vizro-core/docs/pages/user-guides/pages.md @@ -1,7 +1,6 @@ # How to use pages -This guide shows you how to add pages to your dashboard and customize the URL paths if needed. -A [`Page`][vizro.models.Page] lets you place and arrange your dashboard content (for example, chart/components, tables, and text) -and configure your dashboard interactions (such as filters and parameters). + +This guide shows you how to add pages to your dashboard and customize the URL paths if needed. A [`Page`][vizro.models.Page] lets you place and arrange your dashboard content (for example, chart/components, tables, and text) and configure your dashboard interactions (such as filters and parameters). The [`Dashboard`][vizro.models.Dashboard] model accepts the `pages` argument, where you can insert your [`Page`][vizro.models.Page]. @@ -10,19 +9,19 @@ The [`Dashboard`][vizro.models.Dashboard] model accepts the `pages` argument, wh A [`Page`][vizro.models.Page] is split up into four main containers: 1. The **navigation container** where you can customize your `navigation` (see [Dashboard](dashboard.md) and [Navigation](navigation.md) for more information). Note that the navigation container needs to be configured via the Dashboard. -2. The **control container** where you can add your `controls` (see [Filters](filters.md) or [Parameters](parameters.md)) to interact with the dashboard -3. The **page header** that contains the page title and the theme toggle switch button -4. The **component container** where you can add your [components](components.md) to visualize your data +1. The **control container** where you can add your `controls` (see [Filters](filters.md) or [Parameters](parameters.md)) to interact with the dashboard +1. The **page header** that contains the page title and the theme toggle switch button +1. The **component container** where you can add your [components](components.md) to visualize your data ![Page Container](../../assets/user_guides/pages/page_containers.png) To create and add a page to your dashboard, do the following steps: 1. Set a `title` for your [`Page`][vizro.models.Page] -2. Configure your `components`, see our guide on the [various options](components.md) -3. (optional) Configure your `controls` , see our guides on [Filters](filters.md) and [Parameters](parameters.md) -4. (optional) Configure your `layout` , see our guide on [Layouts](layouts.md) -5. (optional) Give a `description` of your `Page` to the app's [meta tags](https://metatags.io/) +1. Configure your `components`, see our guide on the [various options](components.md) +1. (optional) Configure your `controls` , see our guides on [Filters](filters.md) and [Parameters](parameters.md) +1. (optional) Configure your `layout` , see our guide on [Layouts](layouts.md) +1. (optional) Give a `description` of your `Page` to the app's [meta tags](https://metatags.io/) !!! example "Page" === "app.py" @@ -51,51 +50,47 @@ To create and add a page to your dashboard, do the following steps: 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_: sunburst - path: ['continent', 'country'] - values: pop - color: lifeExp - data_frame: gapminder - id: sunburst - type: graph - controls: - - column: continent - targets: - - sunburst - type: filter - - selector: - options: ['lifeExp', 'pop'] - title: Color - type: radio_items - targets: - - sunburst.color - type: parameter - title: Page Title - description: "Longer description of the page content" + - components: + - figure: + _target_: sunburst + path: [continent, country] + values: pop + color: lifeExp + data_frame: gapminder + id: sunburst + type: graph + controls: + - column: continent + targets: [sunburst] + type: filter + - selector: + options: [lifeExp, pop] + title: Color + type: radio_items + targets: [sunburst.color] + type: parameter + title: Page Title + description: Longer description of the page content ``` - === "Result" - [![Page]][Page] - [Page]: ../../assets/user_guides/pages/page_sunburst.png + === "Result" + [![Page]][page] An accordion page selector is automatically added to your dashboard in the top-left of the control container for through the different pages. It will not be added if your dashboard consists of only one page. You can additionally navigate through the different pages by going directly to the relevant page URL (more details in next section). - ## Customize the page URL -By default, the page URL is automatically generated based on the `id` of the page. For example, if `id="This is my first page"` -the generated page URL will be `path=this-is-my-first-page`. You can then access the page via `localhost:/this-is-my-first-page`. -Note that the page `id` defaults to be the same as the page `title` if not set. -If you have multiple pages with the same `title` then you must assign a unique `id`. +By default, the page URL is automatically generated based on the `id` of the page. For example, if `id="This is my first page"` the generated page URL will be `path=this-is-my-first-page`. You can then access the page via `localhost:/this-is-my-first-page`. + +Note that the page `id` defaults to be the same as the page `title` if not set. If you have multiple pages with the same `title` then you must assign a unique `id`. The first page always has the URL prefix `/` assigned. A custom URL can, therefore, not be created for the first page. @@ -137,39 +132,40 @@ To customize the page URL, pass a valid URL name to the `path` argument of [`Pag 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: - - text: | - Commodi repudiandae consequuntur voluptatum. - type: card - title: Page 1 - - components: - - figure: - _target_: sunburst - path: ['continent', 'country'] - values: pop - color: lifeExp - data_frame: gapminder - id: sunburst - type: graph - controls: - - column: continent - targets: - - sunburst - type: filter - - selector: - options: ['lifeExp', 'pop'] - title: Color - type: radio_items - targets: - - sunburst.color - type: parameter - title: Page 2 - path: my-custom-url + - components: + - text: | + Commodi repudiandae consequuntur voluptatum. + type: card + title: Page 1 + - components: + - figure: + _target_: sunburst + path: [continent, country] + values: pop + color: lifeExp + data_frame: gapminder + id: sunburst + type: graph + controls: + - column: continent + targets: [sunburst] + type: filter + - selector: + options: [lifeExp, pop] + title: Color + type: radio_items + targets: [sunburst.color] + type: parameter + title: Page 2 + path: my-custom-url ``` You can now access the first page via `localhost:/` and the second page via `localhost:/my-custom-url`. + +[page]: ../../assets/user_guides/pages/page_sunburst.png diff --git a/vizro-core/docs/pages/user-guides/parameters.md b/vizro-core/docs/pages/user-guides/parameters.md index bb39a7a0c..c82707d4a 100644 --- a/vizro-core/docs/pages/user-guides/parameters.md +++ b/vizro-core/docs/pages/user-guides/parameters.md @@ -9,8 +9,8 @@ The [`Page`][vizro.models.Page] model accepts the `controls` argument, where you To add a parameter to your page, do the following: 1. add the [`Parameter`][vizro.models.Parameter] model into the `controls` argument of the [`Page`][vizro.models.Page] model. -2. add the `targets` argument -3. add a selector model to the `selector` argument. +1. add the `targets` argument +1. add a selector model to the `selector` argument. In the `targets` argument, you can specify the component and function argument that the parameter should be applied to in the form of `.` (for example, `scatter_chart.title`). @@ -48,6 +48,7 @@ Unlike for the [`Filter`][vizro.models.Filter] model, you also have to configure Vizro().build(dashboard).run() ``` + === "app.yaml" ```yaml # Still requires a .py to add data to the data manager and parse YAML configuration @@ -64,18 +65,17 @@ Unlike for the [`Filter`][vizro.models.Filter] model, you also have to configure type: graph controls: - selector: - options: ["My scatter chart", "A better title!", "Another title..."] - multi: False + options: [My scatter chart, A better title!, Another title...] + multi: false type: dropdown targets: - scatter_chart.title type: parameter title: My first page ``` - === "Result" - [![Parameter]][Parameter] - [Parameter]: ../../assets/user_guides/control/control4.png + === "Result" + [![Parameter]][parameter] If you would like to pass `None` as a parameter and make a parameter optional, you can specify the string `"NONE"` in the `options` or `value` field. @@ -133,6 +133,7 @@ If you want to change nested parameters, you can specify the `targets` argument Vizro().build(dashboard).run() ``` + === "app.yaml" ```yaml # Still requires a .py to add data to the data manager and parse YAML configuration @@ -146,7 +147,7 @@ If you want to change nested parameters, you can specify the `targets` argument y: sepal_length size: petal_length color: species - color_discrete_map: {"setosa": "#00b4ff", "versicolor": "#ff9222"} + color_discrete_map: {setosa: '#00b4ff', versicolor: '#ff9222'} id: scatter_chart type: graph - figure: @@ -155,23 +156,22 @@ If you want to change nested parameters, you can specify the `targets` argument x: sepal_width y: sepal_length color: species - color_discrete_map: {"setosa": "#00b4ff", "versicolor": "#ff9222"} + color_discrete_map: {setosa: '#00b4ff', versicolor: '#ff9222'} id: bar_chart type: graph controls: - selector: - options: ["#ff5267", "#3949ab"] + options: ['#ff5267', '#3949ab'] value: #3949ab - multi: False + multi: false type: dropdown - targets: ["scatter_chart.color_discrete_map.virginica", "bar_chart.color_discrete_map.virginica"] + targets: [scatter_chart.color_discrete_map.virginica, bar_chart.color_discrete_map.virginica] type: parameter title: My first page ``` - === "Result" - [![Nested]][Nested] - [Nested]: ../../assets/user_guides/control/control5.png + === "Result" + [![Nested]][nested] In the above example, the object passed to the function argument `color_discrete_map` is a dictionary which maps the different flower species to fixed colors (for example, `{"virginica":"blue"}`). In this case, only the value `blue` should be changed instead of the entire dictionary. This can be achieved by specifying a target as `scatter.color_discrete_map.virginica`. @@ -180,3 +180,6 @@ Note that in the above example, one parameter affects multiple targets. ## Dynamic data parameters If you use [dynamic data](data.md/#dynamic-data) that can be updated while the dashboard is running then you can pass parameters to the dynamic data function to alter the data loaded into your dashboard. For detailed instructions, refer to the section on [parametrized data loading](data.md/#parametrize-data-loading). + +[nested]: ../../assets/user_guides/control/control5.png +[parameter]: ../../assets/user_guides/control/control4.png diff --git a/vizro-core/docs/pages/user-guides/run.md b/vizro-core/docs/pages/user-guides/run.md index 29ada2935..028eb5027 100644 --- a/vizro-core/docs/pages/user-guides/run.md +++ b/vizro-core/docs/pages/user-guides/run.md @@ -15,7 +15,6 @@ Most of our examples have a link below the code, [Run and edit this code in PyCa You can use [PyCafe](https://py.cafe/snippet/vizro/v1) snippet mode to experiment with your own Vizro dashboards by dropping code into a new project. - ## Default built-in Flask development server !!! example "Default built-in Flask development server" @@ -38,9 +37,10 @@ You can use [PyCafe](https://py.cafe/snippet/vizro/v1) snippet mode to experimen Vizro().build(dashboard).run() ``` + 1. create a Python file named `app.py`. -2. type the command `python app.py` into your terminal. -3. information below will be displayed in your terminal, go to [http://127.0.0.1:8050/](http://127.0.0.1:8050/). +1. type the command `python app.py` into your terminal. +1. information below will be displayed in your terminal, go to [http://127.0.0.1:8050/](http://127.0.0.1:8050/). ``` Dash is running on http://127.0.0.1:8050/ @@ -51,8 +51,7 @@ INFO:werkzeug:WARNING: This is a development server. Do not use it in a producti ``` !!! warning "In production" - - As per the above warning message, which is [further explained in the Flask documentation](https://flask.palletsprojects.com/en/3.0.x/deploying/), the Flask development server is intended for use only during local development and **should not** be used when deploying to production. Instead, you should instead use a production-ready solution such as [Gunicorn](#gunicorn). + The above warning message is [further explained in the Flask documentation](https://flask.palletsprojects.com/en/3.0.x/deploying/). The Flask development server is intended for use only during local development and **should not** be used when deploying to production. Instead, you should instead use a production-ready solution such as [Gunicorn](#gunicorn). ### Automatic reloading and debugging @@ -64,11 +63,11 @@ Setting `debug=True` enables [Dash Dev Tools](https://dash.plotly.com/devtools). In addition, some errors generated at run time can also be viewed via the browser console (for example in `Chrome` see `View > Developer > Developer Tools > Console`). - ## Jupyter + The dashboard application can be launched in a Jupyter environment in `inline`, `external`, and `jupyterlab` mode. -!!! example "Run in a Jupyter Notebook in inline mode" +!!! example "Run in a Jupyter Notebook in inline mode" === "app.ipynb" ```py linenums="1" from vizro import Vizro @@ -80,23 +79,23 @@ The dashboard application can be launched in a Jupyter environment in `inline`, page = vm.Page( title="My first page", components=[ - vm.Graph(id="scatter_chart", figure=px.scatter(iris, x="sepal_length", y="petal_width", color="species")), + vm.Graph(figure=px.scatter(iris, x="sepal_length", y="petal_width", color="species")), ], ) dashboard = vm.Dashboard(pages=[page]) Vizro().build(dashboard).run(jupyter_mode="external") ``` + - by default, the mode is set to `inline` in `run()` and the dashboard will be displayed inside your Jupyter environment. - you can specify `jupyter_mode="external"` and a link will be displayed to direct you to the localhost where the dashboard is running. - you can use tab mode by `jupyter_mode="tab"` to automatically open the app in a new browser !!! note "Reloading and debugging" - Code reloading and hot reloading do not work within a Jupyter Notebook. Instead, there are two methods to reload the dashboard: - * Restart the Jupyter kernel and re-run your notebook. - * Add a cell containing `from vizro import Vizro; Vizro._reset()` to the top of your notebook and re-run it. With this method, there is no need to restart the Jupyter kernel. + - Restart the Jupyter kernel and re-run your notebook. + - Add a cell containing `from vizro import Vizro; Vizro._reset()` to the top of your notebook and re-run it. With this method, there is no need to restart the Jupyter kernel. ## Gunicorn @@ -114,7 +113,7 @@ The dashboard application can be launched in a Jupyter environment in `inline`, page = vm.Page( title="My first page", components=[ - vm.Graph(id="scatter_chart", figure=px.scatter(iris, x="sepal_length", y="petal_width", color="species")), + vm.Graph(figure=px.scatter(iris, x="sepal_length", y="petal_width", color="species")), ], ) @@ -126,17 +125,18 @@ The dashboard application can be launched in a Jupyter environment in `inline`, ``` 1. The Vizro `app` object is a WSGI application that exposes the underlying Flask app; 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. + 1. 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 gunicorn app:app --workers 4 ``` + in the command line. For more Gunicorn configuration options, refer to [Gunicorn documentation](https://docs.gunicorn.org/). !!! warning "In production" - - If your dashboard uses [dynamic data](data.md#dynamic-data) that can be refreshed while the dashboard is running then you should [configure your data manager cache](data.md#configure-cache) to use a backend that supports multiple processes. + If your dashboard uses [dynamic data](data.md#dynamic-data) that can be refreshed while the dashboard is running then you should [configure your data manager cache](data.md#configure-cache) to use a back end that supports multiple processes. ## Deployment @@ -149,6 +149,6 @@ Internally, `app = Vizro()` contains a Flask app in `app.dash.server`. However, [`Vizro`][vizro.Vizro] accepts `**kwargs` that are passed through to `Dash`. This enables you to configure the underlying Dash app using the same [arguments that are available](https://dash.plotly.com/reference#dash.dash) in `Dash`. For example, in a deployment context, these arguments may be useful: -- `url_base_pathname`: serve your Vizro app at a specific path rather than at the domain root. For example, if you host your dashboard at http://www.example.com/my_dashboard/ then you would set `url_base_pathname="/my_dashboard/"` or an environment variable `DASH_URL_BASE_PATHNAME="/my_dashboard/"`. +- `url_base_pathname`: serve your Vizro app at a specific path rather than at the domain root. For example, if you host your dashboard at `http://www.example.com/my_dashboard/` then you would set `url_base_pathname="/my_dashboard/"` or an environment variable `DASH_URL_BASE_PATHNAME="/my_dashboard/"`. - `serve_locally`: set to `False` to [serve Dash component libraries from a Content Delivery Network (CDN)](https://dash.plotly.com/external-resources#serving-dash's-component-libraries-locally-or-from-a-cdn), which reduces load on the server and can improve performance. Vizro uses [jsDeliver](https://www.jsdelivr.com/) as a CDN for CSS and JavaScript sources. - `assets_external_path`: when `serve_locally=False`, you can also set `assets_external_path` or an environment variable `DASH_ASSETS_EXTERNAL_PATH` to [serve your own assets from a CDN](https://dash.plotly.com/external-resources#load-assets-from-a-folder-hosted-on-a-cdn). diff --git a/vizro-core/docs/pages/user-guides/selectors.md b/vizro-core/docs/pages/user-guides/selectors.md index 944515ab6..a1bd4be20 100644 --- a/vizro-core/docs/pages/user-guides/selectors.md +++ b/vizro-core/docs/pages/user-guides/selectors.md @@ -6,10 +6,7 @@ The [`Filter`][vizro.models.Filter] or the [`Parameter`][vizro.models.Parameter] ## Categorical selectors -Within the categorical selectors, a clear distinction exists between multi-option and single-option selectors. -For instance, the [`Checklist`][vizro.models.Checklist] functions as a multi-option selector by default while -the [`RadioItem`][vizro.models.RadioItems] serves as a single-option selector by default. However, the -[`Dropdown`][vizro.models.Dropdown] can function as both a multi-option or single-option selector. +Within the categorical selectors, a clear distinction exists between multi-option and single-option selectors. For instance, the [`Checklist`][vizro.models.Checklist] functions as a multi-option selector by default while the [`RadioItem`][vizro.models.RadioItems] serves as a single-option selector by default. However, the [`Dropdown`][vizro.models.Dropdown] can function as both a multi-option or single-option selector. For more information, refer to the API reference of the selector, or the documentation of its underlying Dash component: @@ -18,15 +15,12 @@ For more information, refer to the API reference of the selector, or the documen - [`RadioItems`][vizro.models.RadioItems] based on [`dcc.RadioItems`](https://dash.plotly.com/dash-core-components/radioitems) !!! note - - When configuring the `options` of the categorical selectors, you can either provide: + When configuring the `options` of the categorical selectors, you can either give: - a list of values `options = ['Value A', 'Value B', 'Value C']` - or a dictionary of label-value mappings `options=[{'label': 'True', 'value': True}, {'label': 'False', 'value': False}]` - The later is required if you want to provide different display labels to your option values or in case you want to - provide boolean values as options. In this case, you need to provide a string label for your boolean values as - boolean values cannot be displayed properly as labels in the underlying Dash components. + The later is required if you want to provide different display labels to your option values or in case you want to provide boolean values as options. In this case, you need to provide a string label for your boolean values as boolean values cannot be displayed properly as labels in the underlying Dash components. ## Numerical selectors @@ -36,11 +30,7 @@ For more information, refer to the API reference of the selector, or the documen - [`RangeSlider`][vizro.models.RangeSlider] based on [`dcc.RangeSlider`](https://dash.plotly.com/dash-core-components/rangeslider) !!! note - - When configuring the [`Slider`][vizro.models.Slider] and the [`RangeSlider`][vizro.models.RangeSlider] with float values, and using `step` with an integer value, you may notice - unexpected behavior, such as the drag value being outside its indicated marks. - To our knowledge, this is a current bug in the underlying [`dcc.Slider`](https://dash.plotly.com/dash-core-components/slider) and - [`dcc.RangeSlider`](https://dash.plotly.com/dash-core-components/rangeslider) component, which you can circumvent by adapting the `step` size accordingly. + When configuring the [`Slider`][vizro.models.Slider] and the [`RangeSlider`][vizro.models.RangeSlider] with float values, and using `step` with an integer value, you may notice unexpected behavior, such as the drag value being outside its indicated marks. To our knowledge, this is a current bug in the underlying [`dcc.Slider`](https://dash.plotly.com/dash-core-components/slider) and [`dcc.RangeSlider`](https://dash.plotly.com/dash-core-components/rangeslider) component, which you can circumvent by adapting the `step` size as needed. ## Temporal selectors @@ -49,96 +39,6 @@ For more information, refer to the API reference of the selector, or the documen - [`DatePicker`][vizro.models.DatePicker] based on [`dmc.DateRangePicker`](https://www.dash-mantine-components.com/components/datepicker#daterangepicker) and [`dmc.DatePicker`](https://www.dash-mantine-components.com/components/datepicker) !!! note - When the [`DatePicker`][vizro.models.DatePicker] is configured with `range=True` (the default), the underlying component is `dmc.DateRangePicker`. When `range=False` the underlying component is `dmc.DatePicker`. When configuring the [`DatePicker`][vizro.models.DatePicker] make sure to provide your dates for `min`, `max` and `value` arguments in `"yyyy-mm-dd"` format or as `datetime` type (for example, `datetime.datetime(2024, 01, 01)`). - -## Default selectors - -If you don't specify a selector, a default selector is applied based on the data type of the provided column. - -Default selectors for: - - - categorical data: [`Dropdown`][vizro.models.Dropdown] - - numerical data: [`RangeSlider`][vizro.models.RangeSlider] - - temporal data: [`DatePicker(range=True)`][vizro.models.DatePicker] - -Categorical selectors can be used independently of the data type of the column being filtered. - -To use numerical [`Filter`][vizro.models.Filter] selectors, the filtered column must be of `numeric` format, -indicating that [pandas.api.types.is_numeric_dtype()](https://pandas.pydata.org/docs/reference/api/pandas.api.types.is_numeric_dtype.html) must return `True` for the filtered column. - -To use temporal [`Filter`][vizro.models.Filter] selectors, the filtered column must be of `datetime` format, -indicating that [pandas.api.types.is_datetime64_any_dtype()](https://pandas.pydata.org/docs/reference/api/pandas.api.types.is_datetime64_any_dtype.html) must return `True` for the filtered column. - -`pd.DataFrame` column types can be changed to `datetime` using [pandas.to_datetime()](https://pandas.pydata.org/docs/reference/api/pandas.to_datetime.html) or - - -### Example of default Filter selectors - -!!! 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]: ../../assets/user_guides/selectors/default_filter_selectors.png - - -To enhance existing selectors, see our [how-to-guide on creating custom components](custom-components.md). diff --git a/vizro-core/docs/pages/user-guides/table.md b/vizro-core/docs/pages/user-guides/table.md index 19197c465..80e8e53dc 100755 --- a/vizro-core/docs/pages/user-guides/table.md +++ b/vizro-core/docs/pages/user-guides/table.md @@ -2,46 +2,32 @@ This guide shows you how to visualize tables in Vizro. -There are two ways to visualize tables in Vizro, using either [AG Grid](#ag-grid) or [Dash DataTable](#dash-datatable). -In general, [AG Grid](#ag-grid) is Vizro's recommended table implementation, but sometimes it may make sense to use the [Dash DataTable](#dash-datatable) instead. +There are two ways to visualize tables in Vizro, using either [AG Grid](#ag-grid) or [Dash DataTable](#dash-datatable). In general, [AG Grid](#ag-grid) is Vizro's recommended table implementation, but sometimes it may make sense to use the [Dash DataTable](#dash-datatable) instead. ## Choose between AG Grid and Dash DataTable -Vizro offers two models - the [`AgGrid`][vizro.models.AgGrid] model and the [`Table`][vizro.models.Table] model - for the above two approaches respectively. -They both visualize tabular data in similar ways. +Vizro offers two models - the [`AgGrid`][vizro.models.AgGrid] model and the [`Table`][vizro.models.Table] model - for the above two approaches respectively. They both visualize tabular data in similar ways. -The main difference between the two is that the [`AgGrid`][vizro.models.AgGrid] model is based on Plotly's [Dash AG Grid](https://dash.plotly.com/dash-ag-grid) component, -while the [`Table`][vizro.models.Table] model is based on the [Dash DataTable](https://dash.plotly.com/datatable) component. - -Both approaches have similar base features, and are configurable in similar ways. However, the AG Grid offers more advanced features out-of-the-box, is more customizable -and also ships a powerful enterprise version. This is why it is Vizro's recommended table implementation. At the same time, the Dash DataTable can be used if developers are -already familiar with it, or if some custom functionality is easier to implement using the Dash DataTable. +The main difference between the two is that the [`AgGrid`][vizro.models.AgGrid] model is based on Plotly's [Dash AG Grid](https://dash.plotly.com/dash-ag-grid) component, while the [`Table`][vizro.models.Table] model is based on the [Dash DataTable](https://dash.plotly.com/datatable) component. +Both approaches have similar base features, and are configurable in similar ways. However, the AG Grid offers more advanced features out-of-the-box, is more customizable and also ships a powerful enterprise version. This is why it is Vizro's recommended table implementation. At the same time, the Dash DataTable can be used if developers are already familiar with it, or if some custom functionality is easier to implement using the Dash DataTable. ## AG Grid -[AG Grid](https://www.ag-grid.com/) is an interactive table/grid component designed for viewing, editing, and exploring large datasets. It -is Vizro's recommended table implementation. +[AG Grid](https://www.ag-grid.com/) is an interactive table/grid component designed for viewing, editing, and exploring large datasets. It is Vizro's recommended table implementation. -The Vizro [`AgGrid`][vizro.models.AgGrid] model is based on the [Dash AG Grid](https://dash.plotly.com/dash-ag-grid), which is in turn based the -original [Javascript implementation](https://www.ag-grid.com/). +The Vizro [`AgGrid`][vizro.models.AgGrid] model is based on the [Dash AG Grid](https://dash.plotly.com/dash-ag-grid), which is in turn based the original [Javascript implementation](https://www.ag-grid.com/). ### Basic usage To add a [`AgGrid`][vizro.models.AgGrid] to your page, do the following: -1. Insert the [`AgGrid`][vizro.models.AgGrid] model into the `components` argument of the -[`Page`][vizro.models.Page] model. -2. Enter the `dash_ag_grid` function under the `figure` argument (imported via `from vizro.tables import dash_ag_grid`). - -The Vizro version of this AG Grid differs in one way from the original Dash AG Grid: it requires the user to pass a pandas DataFrame as the source of data. -As explained in [our guide to using data in Vizro](data.md), this must be entered under the argument `data_frame`. Most other [parameters of the Dash AG Grid](https://dash.plotly.com/dash-ag-grid/reference) can be entered as keyword arguments. -Note that some defaults are set for some arguments (for example, for `columnDefs`) to help with styling and usability. -Sometimes a parameter may not work because it requires a callback to function. In that case you can try [creating a custom AG Grid callable](custom-tables.md). +1. Insert the [`AgGrid`][vizro.models.AgGrid] model into the `components` argument of the [`Page`][vizro.models.Page] model. +1. Enter the `dash_ag_grid` function under the `figure` argument (imported via `from vizro.tables import dash_ag_grid`). +The Vizro version of this AG Grid differs in one way from the original Dash AG Grid: it requires the user to pass a pandas DataFrame as the source of data. As explained in [our guide to using data in Vizro](data.md), this must be entered under the argument `data_frame`. Most other [parameters of the Dash AG Grid](https://dash.plotly.com/dash-ag-grid/reference) can be entered as keyword arguments. Note that some defaults are set for some arguments (for example, for `columnDefs`) to help with styling and usability. Sometimes a parameter may not work because it requires a callback to function. In that case you can try [creating a custom AG Grid callable](custom-tables.md). !!! example "Basic Dash AG Grid" - === "app.py" ```{.python pycafe-link} import vizro.models as vm @@ -59,29 +45,28 @@ Sometimes a parameter may not work because it requires a callback to function. I Vizro().build(dashboard).run() ``` + === "app.yaml" ```yaml # Still requires a .py to add data to the data manager and parse YAML configuration # See from_yaml example pages: - - components: - - figure: - _target_: dash_ag_grid - data_frame: gapminder - type: ag_grid - title: Default Dash AG Grid + - components: + - figure: + _target_: dash_ag_grid + data_frame: gapminder + type: ag_grid + title: Default Dash AG Grid ``` - === "Result" - [![AGGrid]][AGGrid] - [AGGrid]: ../../assets/user_guides/table/aggrid.png + === "Result" + [![AGGrid]][aggrid] ### Enable pagination -Pagination is a visual alternative to using vertical scroll. It can also improve loading time if you have many rows. -You can turn it on by setting `dashGridOptions={"pagination": True}`. -!!! example "Basic Dash AG Grid" +Pagination is a visual alternative to using vertical scroll. It can also improve loading time if you have many rows. You can turn it on by setting `dashGridOptions={"pagination": True}`. +!!! example "Basic Dash AG Grid" === "app.py" ```{.python pycafe-link} import vizro.models as vm @@ -99,37 +84,34 @@ You can turn it on by setting `dashGridOptions={"pagination": True}`. Vizro().build(dashboard).run() ``` + === "app.yaml" ```yaml # Still requires a .py to add data to the data manager and parse YAML configuration # See from_yaml example pages: - - components: - - figure: - _target_: dash_ag_grid - data_frame: gapminder - dashGridOptions: - pagination: true - type: ag_grid - title: Dash AG Grid with pagination + - components: + - figure: + _target_: dash_ag_grid + data_frame: gapminder + dashGridOptions: + pagination: true + type: ag_grid + title: Dash AG Grid with pagination ``` - === "Result" - [![AGGrid]][AGGrid] - [AGGrid]: ../../assets/user_guides/table/aggrid-pagination.png + === "Result" + [![AGGrid]][aggrid] ### Formatting columns #### Numbers -One of the most common tasks when working with tables is to format the columns so that displayed numbers are more readable. -To do this, you can use the native functionality of [value formatters](https://dash.plotly.com/dash-ag-grid/value-formatters) -or the Vizro pre-defined [custom cell data types](https://dash.plotly.com/dash-ag-grid/cell-data-types#providing-custom-cell-data-types) as shown below. +One of the most common tasks when working with tables is to format the columns so that displayed numbers are more readable. To do this, you can use the native functionality of [value formatters](https://dash.plotly.com/dash-ag-grid/value-formatters) or the Vizro pre-defined [custom cell data types](https://dash.plotly.com/dash-ag-grid/cell-data-types#providing-custom-cell-data-types) as shown below. The available custom cell types for Vizro are `dollar`, `euro`, `percent` and `numeric`. -To use these, define your desired `` alongside the chosen `cellDataType` in -the `columnDefs` argument of your `dash_ag_grid` function: +To use these, define your desired `` alongside the chosen `cellDataType` in the `columnDefs` argument of your `dash_ag_grid` function: ```py columnDefs = [{"field": "", "cellDataType": "euro"}] @@ -138,7 +120,6 @@ columnDefs = [{"field": "", "cellDataType": "euro"}] In the example below we select and format some columns of the gapminder data. !!! example "AG Grid with formatted columns" - === "app.py" ```{.python pycafe-link} import vizro.models as vm @@ -168,6 +149,7 @@ In the example below we select and format some columns of the gapminder data. Vizro().build(dashboard).run() ``` + === "app.yaml" ```yaml # Still requires a .py to add data to the data manager and parse YAML configuration @@ -190,33 +172,23 @@ In the example below we select and format some columns of the gapminder data. type: ag_grid title: Example of AG Grid with formatted columns ``` - === "Result" - [![AGGrid2]][AGGrid2] - [AGGrid2]: ../../assets/user_guides/table/formatted_aggrid.png + === "Result" + [![AGGrid2]][aggrid2] #### Dates -For the [`AgGrid`][vizro.models.AgGrid] model to sort and filter dates correctly, the date must either be of -string format `yyyy-mm-dd` (see [Dash AG Grid docs](https://dash.plotly.com/dash-ag-grid/date-filters#example:-date-filter)) -or a pandas datetime object. Any pandas datetime column will be transformed into the `yyyy-mm-dd` format automatically. +For the [`AgGrid`][vizro.models.AgGrid] model to sort and filter dates correctly, the date must either be of string format `yyyy-mm-dd` (see [Dash AG Grid docs](https://dash.plotly.com/dash-ag-grid/date-filters#example:-date-filter)) or a pandas datetime object. Any pandas datetime column will be transformed into the `yyyy-mm-dd` format automatically. #### Objects and strings -No specific formatting is available for custom objects and strings, however you can make use of [Value Formatters](https://dash.plotly.com/dash-ag-grid/value-formatters) -to format displayed strings automatically. - +No specific formatting is available for custom objects and strings, however you can make use of [Value Formatters](https://dash.plotly.com/dash-ag-grid/value-formatters) to format displayed strings automatically. ### Styling and changing the AG Grid -As mentioned above, all [parameters of the Dash AG Grid](https://dash.plotly.com/dash-ag-grid/reference) can be entered as keyword arguments. Below you can find -an example of a styled AG Grid where some conditional formatting is applied, and where the columns are editable, but -not filterable or resizable. There are more ways to alter the grid beyond this showcase. AG Grid, like any other Vizro -component, can be customized using custom CSS. You can find information in -the [guide to overwriting CSS properties](custom-css.md#overwrite-css-for-selected-components). +As mentioned above, all [parameters of the Dash AG Grid](https://dash.plotly.com/dash-ag-grid/reference) can be entered as keyword arguments. Below you can find an example of a styled AG Grid where some conditional formatting is applied, and where the columns are editable, but not filterable or resizable. There are more ways to alter the grid beyond this showcase. AG Grid, like any other Vizro component, can be customized using custom CSS. You can find information in the [guide to overwriting CSS properties](custom-css.md#overwrite-css-for-selected-components). !!! example "Styled and modified Dash AG Grid" - === "app.py" ```{.python pycafe-link} import vizro.models as vm @@ -284,6 +256,7 @@ the [guide to overwriting CSS properties](custom-css.md#overwrite-css-for-select Vizro().build(dashboard).run() ``` + === "app.yaml" ```yaml # Still requires a .py to add data to the data manager and parse YAML configuration @@ -299,28 +272,28 @@ the [guide to overwriting CSS properties](custom-css.md#overwrite-css-for-select - field: year - field: lifeExp valueFormatter: - function: "d3.format('.1f')(params.value)" + function: d3.format('.1f')(params.value) - field: gdpPercap valueFormatter: - function: "d3.format('$,.1f')(params.value)" + function: d3.format('$,.1f')(params.value) cellStyle: styleConditions: - condition: params.value < 1045 style: - backgroundColor: "#ff9222" + backgroundColor: '#ff9222' - condition: params.value >= 1045 && params.value <= 4095 style: - backgroundColor: "#de9e75" + backgroundColor: '#de9e75' - condition: params.value > 4095 && params.value <= 12695 style: - backgroundColor: "#aaa9ba" + backgroundColor: '#aaa9ba' - condition: params.value > 12695 style: - backgroundColor: "#00b4ff" + backgroundColor: '#00b4ff' - field: pop type: rightAligned valueFormatter: - function: "d3.format(',.0f')(params.value)" + function: d3.format(',.0f')(params.value) defaultColDef: resizable: false filter: false @@ -329,10 +302,9 @@ the [guide to overwriting CSS properties](custom-css.md#overwrite-css-for-select type: ag_grid title: Example of a Dash AG Grid ``` - === "Result" - [![AGGrid3]][AGGrid3] - [AGGrid3]: ../../assets/user_guides/table/styled_aggrid.png + === "Result" + [![AGGrid3]][aggrid3] If the available arguments are not sufficient, there is always the option to [create a custom AG Grid callable](custom-tables.md). @@ -348,19 +320,14 @@ The Vizro [`Table`][vizro.models.Table] model is based on the [Dash DataTable](h To add a [`Table`][vizro.models.Table] to your page, do the following: -1. Insert the [`Table`][vizro.models.Table] model into the `components` argument of the -[`Page`][vizro.models.Page] model. -2. Enter the `dash_data_table` function under the `figure` argument (imported via `from vizro.tables import dash_data_table`). - +1. Insert the [`Table`][vizro.models.Table] model into the `components` argument of the [`Page`][vizro.models.Page] model. +1. Enter the `dash_data_table` function under the `figure` argument (imported via `from vizro.tables import dash_data_table`). -The Vizro version of this table differs in one way from the original table: it requires the user to pass a pandas DataFrame as the source of data. -As explained in [our guide to using data in Vizro](data.md), this must be entered under the argument `data_frame`. +The Vizro version of this table differs in one way from the original table: it requires the user to pass a pandas DataFrame as the source of data. As explained in [our guide to using data in Vizro](data.md), this must be entered under the argument `data_frame`. -All other [parameters of the Dash DataTable](https://dash.plotly.com/datatable/reference) can be entered as keyword arguments. Note that we are -setting some defaults for some arguments to help with styling. +All other [parameters of the Dash DataTable](https://dash.plotly.com/datatable/reference) can be entered as keyword arguments. Note that we are setting some defaults for some arguments to help with styling. !!! example "Dash DataTable" - === "app.py" ```{.python pycafe-link} import vizro.models as vm @@ -380,31 +347,29 @@ setting some defaults for some arguments to help with styling. 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_: dash_data_table - data_frame: gapminder_2007 - title: Dash DataTable - type: table - title: Example of a Dash DataTable + - components: + - figure: + _target_: dash_data_table + data_frame: gapminder_2007 + title: Dash DataTable + type: table + title: Example of a Dash DataTable ``` - === "Result" - [![Table]][Table] - [Table]: ../../assets/user_guides/table/table.png + === "Result" + [![Table]][table] ### Styling and changing the Dash DataTable -As mentioned above, all [parameters of the Dash DataTable](https://dash.plotly.com/datatable/reference) can be entered as keyword arguments. Below you can find -an example of a styled table where some conditional formatting is applied. There are many more ways to alter the table beyond this showcase. +As mentioned above, all [parameters of the Dash DataTable](https://dash.plotly.com/datatable/reference) can be entered as keyword arguments. Below you can find an example of a styled table where some conditional formatting is applied. There are many more ways to alter the table beyond this showcase. !!! example "Styled Dash DataTable" - === "app.py" ```{.python pycafe-link} import vizro.models as vm @@ -467,6 +432,7 @@ an example of a styled table where some conditional formatting is applied. There Vizro().build(dashboard).run() ``` + === "app.yaml" ```yaml # Still requires a .py to add data to the data manager and parse YAML configuration @@ -504,12 +470,12 @@ an example of a styled table where some conditional formatting is applied. There backgroundColor: dodgerblue color: white - if: - filter_query: "{lifeExp} < 55" + filter_query: '{lifeExp} < 55' column_id: lifeExp - backgroundColor: "#85144b" + backgroundColor: '#85144b' color: white - if: - filter_query: "{gdpPercap} > 10000" + filter_query: '{gdpPercap} > 10000' column_id: gdpPercap backgroundColor: green color: white @@ -524,26 +490,20 @@ an example of a styled table where some conditional formatting is applied. There title: Dash DataTable ``` - === "Result" - [![Table2]][Table2] - [Table2]: ../../assets/user_guides/table/styled_table.png + === "Result" + [![Table2]][table2] If the available arguments are not sufficient, there is always the option to create a [custom Dash DataTable](custom-tables.md). - ## Add title, header, and footer -The [`Table`][vizro.models.Table] and the [`AgGrid`][vizro.models.AgGrid] models accept a `title`, `header` and -`footer` argument. This is useful for providing context to the data being displayed, or for adding a description of -the data. +The [`Table`][vizro.models.Table] and the [`AgGrid`][vizro.models.AgGrid] models accept a `title`, `header` and `footer` argument. This is useful for providing context to the data being displayed, or for adding a description of the data. - **title**: Displayed as an [H3 header](https://dash.plotly.com/dash-html-components/h3), useful for summarizing the main topic or insight of the component. - **header**: Accepts markdown text, ideal for extra descriptions, subtitles, or detailed data insights. - **footer**: Accepts markdown text, commonly used for citing data sources, providing information on the last update, or adding disclaimers. - - ### Formatted AgGrid !!! example "Formatted AgGrid" @@ -571,30 +531,29 @@ the data. 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_: dash_ag_grid - data_frame: gapminder_2007 - dashGridOptions: - pagination: true - title: Gapminder Data Insights - header: | - #### An Interactive Exploration of Global Health, Wealth, and Population - footer: | - SOURCE: **Plotly gapminder data set, 2024** - type: ag_grid - title: Formatted AgGrid + - components: + - figure: + _target_: dash_ag_grid + data_frame: gapminder_2007 + dashGridOptions: + pagination: true + title: Gapminder Data Insights + header: | + #### An Interactive Exploration of Global Health, Wealth, and Population + footer: | + SOURCE: **Plotly gapminder data set, 2024** + type: ag_grid + title: Formatted AgGrid ``` - === "Result" - [![FormattedGrid]][FormattedGrid] - - [FormattedGrid]: ../../assets/user_guides/components/formatted_aggrid.png + === "Result" + [![FormattedGrid]][formattedgrid] ### Formatted DataTable @@ -623,24 +582,32 @@ the data. 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_: dash_data_table - data_frame: gapminder_2007 - title: Gapminder Data Insights - header: | - #### An Interactive Exploration of Global Health, Wealth, and Population - footer: | - SOURCE: **Plotly gapminder data set, 2024** - type: table - title: Formatted DataTable + - components: + - figure: + _target_: dash_data_table + data_frame: gapminder_2007 + title: Gapminder Data Insights + header: | + #### An Interactive Exploration of Global Health, Wealth, and Population + footer: | + SOURCE: **Plotly gapminder data set, 2024** + type: table + title: Formatted DataTable ``` - === "Result" - [![FormattedTable]][FormattedTable] - [FormattedTable]: ../../assets/user_guides/components/formatted_table.png + === "Result" + [![FormattedTable]][formattedtable] + +[aggrid]: ../../assets/user_guides/table/aggrid.png +[aggrid2]: ../../assets/user_guides/table/formatted_aggrid.png +[aggrid3]: ../../assets/user_guides/table/styled_aggrid.png +[formattedgrid]: ../../assets/user_guides/components/formatted_aggrid.png +[formattedtable]: ../../assets/user_guides/components/formatted_table.png +[table]: ../../assets/user_guides/table/table.png +[table2]: ../../assets/user_guides/table/styled_table.png diff --git a/vizro-core/docs/pages/user-guides/tabs.md b/vizro-core/docs/pages/user-guides/tabs.md index 09154177e..1811d0cac 100755 --- a/vizro-core/docs/pages/user-guides/tabs.md +++ b/vizro-core/docs/pages/user-guides/tabs.md @@ -1,8 +1,6 @@ # How to use tabs -[`Tabs`][vizro.models.Tabs] organize and separate groups of related content in a dashboard, letting users switch between different sections or views. -They are essentially a way of putting multiple [`Containers`][vizro.models.Container] in the same screen space, and letting the user switch between them. -`Containers` enable the grouping of page components into sections and subsections. See our [user guide on `Containers`](container.md) for more information. +[`Tabs`][vizro.models.Tabs] organize and separate groups of related content in a dashboard, letting users switch between different sections or views. They are essentially a way of putting multiple [`Containers`][vizro.models.Container] in the same screen space, and letting the user switch between them. `Containers` enable the grouping of page components into sections and subsections. See our [user guide on `Containers`](container.md) for more information.
![tabs](../../assets/user_guides/components/tabs-info.png){ width="400"} @@ -16,14 +14,12 @@ This guide shows you how to use tabs to organize your `Containers` into subsecti By using [`Tabs`][vizro.models.Tabs], the following applies: - [`Filters`][vizro.models.Filter] affect all components on all tabs (opened and closed) of the page if not specified otherwise inside `Filter.targets` -- The `title` of the [`Container`][vizro.models.Container] inserted into `Tabs.tabs` will be displayed as a tab label, and the title will be removed from the `Container` - +- The `title` of the [`Container`][vizro.models.Container] inserted into `Tabs.tabs` will be displayed as a tab label, and the title will be removed from the `Container` To add [`Tabs`][vizro.models.Tabs] to your page, do the following: 1. Insert the [`Tabs`][vizro.models.Tabs] into the `components` argument of the [`Page`][vizro.models.Page] -2. Insert your [`Containers`][vizro.models.Container] into the `tabs` argument of the [`Tabs`][vizro.models.Tabs] - +1. Insert your [`Containers`][vizro.models.Container] into the `tabs` argument of the [`Tabs`][vizro.models.Tabs] !!! example "Tabs" === "app.py" @@ -88,52 +84,53 @@ To add [`Tabs`][vizro.models.Tabs] to your page, do the following: Vizro().build(dashboard).run() ``` + === "app.yaml" ```yaml # Still requires a .py to add data to the data manager and parse YAML configuration # See from_yaml example pages: - - components: - - tabs: - - title: Tab I - type: container - components: - - type: graph - figure: - _target_: bar - data_frame: gapminder_2007 - title: Graph 1 - x: continent - y: lifeExp - color: continent - - type: graph - figure: - _target_: box - data_frame: gapminder_2007 - title: Graph 2 - x: continent - y: lifeExp - color: continent - - title: Tab II - type: container - components: - - type: graph - figure: - _target_: scatter - data_frame: gapminder_2007 - title: Graph 3 - x: gdpPercap - y: lifeExp - size: pop - color: continent - type: tabs + components: + - type: tabs + tabs: + - title: Tab I + type: container + components: + - type: graph + figure: + _target_: bar + data_frame: gapminder_2007 + title: Graph 1 + x: continent + y: lifeExp + color: continent + - type: graph + figure: + _target_: box + data_frame: gapminder_2007 + title: Graph 2 + x: continent + y: lifeExp + color: continent + - title: Tab II + type: container + components: + - type: graph + figure: + _target_: scatter + data_frame: gapminder_2007 + title: Graph 3 + x: gdpPercap + y: lifeExp + size: pop + color: continent controls: - - type: filter - column: continent + - type: filter + column: continent title: Tabs ``` - === "Result" - [![Tabs]][Tabs] + === "Result" + [![Tabs]][tabs] - [Tabs]: ../../assets/user_guides/components/tabs.png +[tabs]: ../../assets/user_guides/components/tabs.png diff --git a/vizro-core/docs/pages/user-guides/themes.md b/vizro-core/docs/pages/user-guides/themes.md index 6f78441d5..ac1d6fc7b 100644 --- a/vizro-core/docs/pages/user-guides/themes.md +++ b/vizro-core/docs/pages/user-guides/themes.md @@ -1,15 +1,13 @@ # How to use themes -This guide shows you how to use themes. Themes are pre-designed collections of stylings that are applied to entire charts and dashboards. -The themes provided by Vizro are infused with our design best practices that make charts and dashboards look visually consistent and professional. +This guide shows you how to use themes. Themes are pre-designed collections of stylings that are applied to entire charts and dashboards. The themes provided by Vizro are infused with our design best practices that make charts and dashboards look visually consistent and professional. ## Themes in dashboards -The [`Dashboard`][vizro.models.Dashboard] model accepts an optional `theme` argument, where you can choose between -a `vizro_dark` and a `vizro_light` theme. If not specified then `theme` defaults to `vizro_dark`. The theme is applied to the entire dashboard and its charts/components when a user first loads your dashboard. Regardless of the theme applied on first load, users can always switch between light and dark themes via the toggle button in the upper-right corner of the dashboard. + +The [`Dashboard`][vizro.models.Dashboard] model accepts an optional `theme` argument, where you can choose between a `vizro_dark` and a `vizro_light` theme. If not specified then `theme` defaults to `vizro_dark`. The theme is applied to the entire dashboard and its charts/components when a user first loads your dashboard. Regardless of the theme applied on first load, users can always switch between light and dark themes via the toggle button in the upper-right corner of the dashboard. !!! example "Change theme" === "app.py" - ```{.python pycafe-link hl_lines="18"} import vizro.models as vm import vizro.plotly.express as px @@ -32,47 +30,40 @@ a `vizro_dark` and a `vizro_light` theme. If not specified then `theme` defaults Vizro().build(dashboard).run() ``` + === "app.yaml" ```yaml hl_lines="12" # Still requires a .py to add data to the data manager and parse YAML configuration # See yaml_version example pages: - - components: - - figure: - _target_: scatter_matrix - color: species - data_frame: iris - dimensions: ["sepal_length", "sepal_width", "petal_length", "petal_width"] - type: graph - title: Changing themes + - components: + - figure: + _target_: scatter_matrix + color: species + data_frame: iris + dimensions: [sepal_length, sepal_width, petal_length, petal_width] + type: graph + title: Changing themes theme: vizro_light ``` + === "Result - vizro_light" - [![Light]][Light] + [![Light]][light] - [Light]: ../../assets/user_guides/themes/light.png === "Result - vizro_dark" - [![Dark]][Dark] - - [Dark]: ../../assets/user_guides/themes/dark.png - + [![Dark]][dark] ## Themes in plotly charts You can also use our templates for plotly charts outside the dashboard. This is useful in a few contexts: -* Creation of standalone charts to be used independently of a Vizro dashboard. -* Rapid development of charts for eventual use in a Vizro dashboard, for example in a Jupyter Notebook. +- Creation of standalone charts to be used independently of a Vizro dashboard. +- Rapid development of charts for eventual use in a Vizro dashboard, for example in a Jupyter Notebook. !!! note + Using `import vizro.plotly.express as px` is equal to using `import plotly.express as px`, but with the added benefit of being able to integrate the resulting chart code into a Vizro dashboard. Vizro offers a minimal layer on top of Plotly's existing charting library, allowing you to seamlessly use all the existing charts and functionalities provided by plotly.express without any modifications. - Using `import vizro.plotly.express as px` is equivalent to using `import plotly.express as px`, - but with the added benefit of being able to integrate the resulting chart code into a Vizro dashboard. - Vizro offers a minimal layer on top of Plotly's existing charting library, allowing you to seamlessly use all the - existing charts and functionalities provided by plotly.express without any modifications. - -Our `vizro_dark` and `vizro_light` themes are automatically registered to `plotly.io.templates` when importing Vizro. -Consult the plotly documentation for [more details on how templates work in plotly](https://plotly.com/python/templates/#theming-and-templates). +Our `vizro_dark` and `vizro_light` themes are automatically registered to `plotly.io.templates` when importing Vizro. Consult the plotly documentation for [more details on how templates work in plotly](https://plotly.com/python/templates/#theming-and-templates). By default, plots imported from `vizro.plotly.express` have the `vizro_dark` theme applied. This can be altered either globally or for individual plots. @@ -87,14 +78,20 @@ pio.templates.default = "vizro_light" ``` ### Set themes for selected charts + To change the template for a selected chart only, use the `template` parameter and run: ```python import vizro.plotly.express as px df = px.data.iris() -px.scatter_matrix(df, - dimensions=["sepal_length", "sepal_width", "petal_length", "petal_width"], - color="species", - template="vizro_light") +px.scatter_matrix( + df, + dimensions=["sepal_length", "sepal_width", "petal_length", "petal_width"], + color="species", + template="vizro_light", +) ``` + +[dark]: ../../assets/user_guides/themes/dark.png +[light]: ../../assets/user_guides/themes/light.png diff --git a/vizro-core/docs/pages/user-guides/visual-formatting.md b/vizro-core/docs/pages/user-guides/visual-formatting.md index 7b384f3e9..56d1d2d52 100644 --- a/vizro-core/docs/pages/user-guides/visual-formatting.md +++ b/vizro-core/docs/pages/user-guides/visual-formatting.md @@ -1,17 +1,13 @@ # How to customize the style of Vizro dashboards -Vizro has a default styling to help users with no design experience get started. -There are several options if you want to customize the style: +Vizro has a default styling to help users with no design experience get started. There are several options if you want to customize the style: -* **[Configure the `Layout`](layouts.md)**: Customize the arrangement of your components inside your Vizro dashboard. +- **[Configure the `Layout`](layouts.md)**: Customize the arrangement of your components inside your Vizro dashboard. -* **[Apply a theme](themes.md)**: Choose between a dark or light theme. +- **[Apply a theme](themes.md)**: Choose between a dark or light theme. -* **[Manage assets](assets.md)**: Enhance your dashboard by adding images, scripts, and stylesheets to your assets folder. +- **[Manage assets](assets.md)**: Enhance your dashboard by adding images, scripts, and stylesheets to your assets folder. -* **[Customize CSS](custom-css.md)**: Incorporate custom CSS to deviate from the default styling and create a -unique appearance for your Vizro dashboard. +- **[Customize CSS](custom-css.md)**: Incorporate custom CSS to deviate from the default styling and create a unique appearance for your Vizro dashboard. -* **[Customize your `component`](components.md)**: Change the appearance of components like the [Graph](graph.md), the -[Table](table.md) and the [AgGrid](table.md), by passing extra arguments. Refer to the relevant user guide for -more details. +- **[Customize your `component`](components.md)**: Change the appearance of components like the [Graph](graph.md), the [Table](table.md) and the [AgGrid](table.md), by passing extra arguments. Refer to the relevant user guide for more details. diff --git a/vizro-core/examples/README.md b/vizro-core/examples/README.md index 72146bfb7..53cf932b7 100644 --- a/vizro-core/examples/README.md +++ b/vizro-core/examples/README.md @@ -4,8 +4,7 @@ Please note that this folder contains only example dashboards that are still **i ### Vizro examples gallery -To view a comprehensive list of available demos, please visit our [examples gallery](http://vizro.mckinsey.com/). -There, you can explore a wide range of dashboards and applications created with Vizro. +To view a comprehensive list of available demos, please visit our [examples gallery](http://vizro.mckinsey.com/). There, you can explore a wide range of dashboards and applications created with Vizro. diff --git a/vizro-core/examples/dev/app.py b/vizro-core/examples/dev/app.py index aa56f9ff1..1265ab20c 100644 --- a/vizro-core/examples/dev/app.py +++ b/vizro-core/examples/dev/app.py @@ -8,7 +8,7 @@ import plotly.graph_objects as go import vizro.models as vm import vizro.plotly.express as px -from dash import dash_table, dcc, html +from dash import dash_table, dcc, get_asset_url, html from vizro import Vizro from vizro.actions import export_data, filter_interaction from vizro.figures import kpi_card, kpi_card_reference @@ -606,10 +606,10 @@ def my_custom_table(data_frame=None, chosen_columns: Optional[list[str]] = None) columns = [{"name": i, "id": i} for i in chosen_columns] defaults = { "style_as_list_view": True, - "style_data": {"border_bottom": "1px solid var(--border-subtle-alpha-01)", "height": "40px"}, + "style_data": {"border_bottom": "1px solid var(--border-subtleAlpha01)", "height": "40px"}, "style_header": { - "border_bottom": "1px solid var(--state-overlays-selected-hover)", - "border_top": "1px solid var(--main-container-bg-color)", + "border_bottom": "1px solid var(--stateOverlays-selectedHover)", + "border_top": "1px solid var(--right-side-bg)", "height": "32px", }, } @@ -817,5 +817,16 @@ def multiple_cards(data_frame: pd.DataFrame, n_rows: Optional[int] = 1) -> html. ), ) + if __name__ == "__main__": - Vizro().build(dashboard).run() + app = Vizro().build(dashboard) + app.dash.layout.children.append( + dbc.NavLink( + ["Made with ", html.Img(src=get_asset_url("logo.svg"), id="banner", alt="Vizro logo"), "vizro"], + href="https://github.com/mckinsey/vizro", + target="_blank", + className="anchor-container", + ) + ) + server = app.dash.server + app.run() diff --git a/vizro-core/examples/dev/assets/images/app.svg b/vizro-core/examples/dev/assets/app.svg similarity index 100% rename from vizro-core/examples/dev/assets/images/app.svg rename to vizro-core/examples/dev/assets/app.svg diff --git a/vizro-core/examples/dev/assets/css/custom.css b/vizro-core/examples/dev/assets/css/custom.css index f6c0002d2..749289dba 100644 --- a/vizro-core/examples/dev/assets/css/custom.css +++ b/vizro-core/examples/dev/assets/css/custom.css @@ -17,3 +17,34 @@ height: 210px; width: 240px; } + +.anchor-container { + align-items: center; + background: var(--text-primary); + border-top-left-radius: 8px; + bottom: 0; + color: var(--text-primary-inverted); + 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; +} + +img[src*="icon-top"] { + filter: var(--fill-icon-image-card); + height: 36px; + width: 36px; +} diff --git a/vizro-core/examples/dev/assets/images/logo.svg b/vizro-core/examples/dev/assets/logo.svg similarity index 100% rename from vizro-core/examples/dev/assets/images/logo.svg rename to vizro-core/examples/dev/assets/logo.svg diff --git a/vizro-core/examples/dev/requirements.in b/vizro-core/examples/dev/requirements.in new file mode 100644 index 000000000..e6027a462 --- /dev/null +++ b/vizro-core/examples/dev/requirements.in @@ -0,0 +1,4 @@ +# This file is only used if you don't have hatch installed. +gunicorn +openpyxl +vizro==0.1.28 diff --git a/vizro-core/examples/dev/requirements.txt b/vizro-core/examples/dev/requirements.txt new file mode 100644 index 000000000..f43e7125e --- /dev/null +++ b/vizro-core/examples/dev/requirements.txt @@ -0,0 +1,130 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile requirements.in -o requirements.txt +annotated-types==0.7.0 + # via pydantic +autoflake==2.3.1 + # via vizro +black==24.4.2 + # via vizro +blinker==1.8.2 + # via flask +cachelib==0.9.0 + # via flask-caching +certifi==2024.8.30 + # via requests +charset-normalizer==3.4.0 + # via requests +click==8.1.7 + # via + # black + # flask +dash==2.18.1 + # via + # dash-ag-grid + # dash-bootstrap-components + # vizro +dash-ag-grid==31.2.0 + # via vizro +dash-bootstrap-components==1.6.0 + # via vizro +dash-core-components==2.0.0 + # via dash +dash-html-components==2.0.0 + # via dash +dash-mantine-components==0.12.1 + # via vizro +dash-table==5.0.0 + # via dash +et-xmlfile==2.0.0 + # via openpyxl +flask==3.0.3 + # via + # dash + # flask-caching +flask-caching==2.3.0 + # via vizro +gunicorn==23.0.0 + # via -r requirements.in +idna==3.10 + # via requests +importlib-metadata==8.5.0 + # via + # dash + # flask +itsdangerous==2.2.0 + # via flask +jinja2==3.1.4 + # via flask +markupsafe==3.0.2 + # via + # jinja2 + # werkzeug +mypy-extensions==1.0.0 + # via black +nest-asyncio==1.6.0 + # via dash +numpy==2.0.2 + # via pandas +openpyxl==3.1.5 + # via -r requirements.in +packaging==24.1 + # via + # black + # gunicorn + # plotly +pandas==2.2.3 + # via vizro +pathspec==0.12.1 + # via black +platformdirs==4.2.2 + # via black +plotly==5.24.1 + # via + # dash + # vizro +pydantic==2.9.2 + # via vizro +pydantic-core==2.23.4 + # via pydantic +pyflakes==3.2.0 + # via autoflake +python-dateutil==2.9.0.post0 + # via pandas +pytz==2024.2 + # via pandas +requests==2.32.3 + # via dash +retrying==1.3.4 + # via dash +setuptools==75.3.0 + # via dash +six==1.16.0 + # via + # python-dateutil + # retrying +tenacity==9.0.0 + # via plotly +tomli==2.1.0 + # via + # autoflake + # black +typing-extensions==4.12.2 + # via + # black + # dash + # pydantic + # pydantic-core +tzdata==2024.2 + # via pandas +urllib3==2.2.3 + # via requests +vizro==0.1.28 + # via -r requirements.in +werkzeug==3.0.6 + # via + # dash + # flask +wrapt==1.16.0 + # via vizro +zipp==3.20.2 + # via importlib-metadata diff --git a/vizro-core/examples/scratch_dev/app.py b/vizro-core/examples/scratch_dev/app.py index 84e70e6aa..56413e831 100644 --- a/vizro-core/examples/scratch_dev/app.py +++ b/vizro-core/examples/scratch_dev/app.py @@ -1,49 +1,89 @@ """Dev app to try things out.""" -import pandas as pd +from vizro import Vizro import vizro.models as vm import vizro.plotly.express as px -from vizro import Vizro -from vizro._themes._color_values import COLORS -pastry = pd.DataFrame( - { - "pastry": [ - "Scones", - "Bagels", - "Muffins", - "Cakes", - "Donuts", - "Cookies", - "Croissants", - "Eclairs", - "Brownies", - "Tarts", - "Macarons", - "Pies", - ], - "Profit Ratio": [-0.10, -0.15, -0.05, 0.10, 0.05, 0.20, 0.15, -0.08, 0.08, -0.12, 0.02, -0.07], - } +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]]), + 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( + 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"]), + ], ) - -page = vm.Page( - title="Charts UI", +iris_data = px.data.iris() +second_page = vm.Page( + title="Second Page", components=[ vm.Graph( - figure=px.bar( - pastry.sort_values("Profit Ratio"), - orientation="h", - x="Profit Ratio", - y="pastry", - color="Profit Ratio", - color_continuous_scale=COLORS["DIVERGING_RED_CYAN"], + 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"}, + ), + ), + 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"}, ), ), ], + 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"), + ), + vm.Parameter( + targets=["scatter_iris.opacity"], + selector=vm.Slider(min=0, max=1, value=0.8, title="Opacity"), + ), + ], ) -dashboard = vm.Dashboard(pages=[page]) +dashboard = vm.Dashboard(pages=[first_page, second_page]) if __name__ == "__main__": Vizro().build(dashboard).run() diff --git a/vizro-core/examples/scratch_dev/data.yaml b/vizro-core/examples/scratch_dev/data.yaml new file mode 100644 index 000000000..d8b0aea90 --- /dev/null +++ b/vizro-core/examples/scratch_dev/data.yaml @@ -0,0 +1,12 @@ +# Choose between 0-50 +setosa: 5 +versicolor: 10 +virginica: 15 + +# Choose between: 4.3 to 7.4 +min: 5 +max: 7 + +# Choose between: 2020-01-01 to 2020-05-29 +date_min: 2024-01-01 +date_max: 2024-05-29 diff --git a/vizro-core/examples/visual-vocabulary/README.md b/vizro-core/examples/visual-vocabulary/README.md index 667742e8c..e59b3103c 100644 --- a/vizro-core/examples/visual-vocabulary/README.md +++ b/vizro-core/examples/visual-vocabulary/README.md @@ -2,14 +2,11 @@ ### Welcome to our visual vocabulary dashboard! 🎨 -This dashboard serves as a comprehensive guide for selecting and creating various types of charts. It helps you decide -when to use each chart type, and offers sample Python code using [Plotly](https://plotly.com/python/), and -instructions for embedding these charts into a [Vizro](https://github.com/mckinsey/vizro) dashboard. +This dashboard serves as a comprehensive guide for selecting and creating various types of charts. It helps you decide when to use each chart type, and offers sample Python code using [Plotly](https://plotly.com/python/), and instructions for embedding these charts into a [Vizro](https://github.com/mckinsey/vizro) dashboard. The charts in this dashboard are designed to make it easy for anyone to create beautiful and sophisticated visuals. -Our goal is to help you understand best practices in data visualization, ensure your charts effectively communicate -your message, and streamline the creation of high-quality, interactive visualizations. +Our goal is to help you understand best practices in data visualization, ensure your charts effectively communicate your message, and streamline the creation of high-quality, interactive visualizations. Created by: @@ -19,12 +16,9 @@ Created by: Inspired by: -- [The FT Visual Vocabulary](https://github.com/Financial-Times/chart-doctor/blob/main/visual-vocabulary/README.md): - [Alan Smith](https://github.com/alansmithy), [Chris Campbell](https://github.com/digitalcampbell), Ian Bott, - Liz Faunce, Graham Parrish, Billy Ehrenberg, Paul McCallum, [Martin Stabe](https://github.com/martinstabe). +- [The FT Visual Vocabulary](https://github.com/Financial-Times/chart-doctor/blob/main/visual-vocabulary/README.md): [Alan Smith](https://github.com/alansmithy), [Chris Campbell](https://github.com/digitalcampbell), Ian Bott, Liz Faunce, Graham Parrish, Billy Ehrenberg, Paul McCallum, [Martin Stabe](https://github.com/martinstabe). -- [The Graphic Continuum](https://www.informationisbeautifulawards.com/showcase/611-the-graphic-continuum): - Jon Swabish and Severino Ribecca +- [The Graphic Continuum](https://www.informationisbeautifulawards.com/showcase/611-the-graphic-continuum): Jon Swabish and Severino Ribecca Credits and sources: @@ -36,80 +30,78 @@ Credits and sources: The dashboard is still in development. Below is an overview of the chart types for which a completed page is available. -| Chart Type | Status | Category | Credits & sources | API | -| --------------------- | ------ | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Arc | ❌ | Part-to-whole | | | -| Area | ✅ | Time | [Filled area plot with px](https://plotly.com/python/filled-area-plots/) | [px.area](https://plotly.com/python-api-reference/generated/plotly.express.area) | -| Bar | ✅ | Magnitude | [Bar chart with px](https://plotly.com/python/bar-charts/) | [px.bar](https://plotly.com/python-api-reference/generated/plotly.express.bar) | -| Barcode | ❌ | Distribution | | | -| Beeswarm | ❌ | Distribution | | | -| Boxplot | ✅ | Distribution | [Box plot with px](https://plotly.com/python/box-plots/) | [px.box](https://plotly.github.io/plotly.py-docs/generated/plotly.express.box) | -| Bubble | ✅ | Correlation | [Scatter plot with px](https://plotly.com/python/line-and-scatter/) | [px.scatter](https://plotly.com/python-api-reference/generated/plotly.express.scatter) | -| Bubble map | ✅ | Spatial | [Bubble map in px](https://plotly.com/python/bubble-maps/) | [px.scatter_map](https://plotly.github.io/plotly.py-docs/generated/plotly.express.scatter_map) | -| Bubble timeline | ❌ | Time | | | -| Bullet | ❌ | Magnitude | | | -| Bump | ❌ | Ranking | | | -| Butterfly | ✅ | Deviation, Distribution | [Pyramid charts in Plotly](https://plotly.com/python/v3/population-pyramid-charts/) | [go.Bar](https://plotly.com/python-api-reference/generated/plotly.graph_objects.Bar.html) | -| Chord | ❌ | Flow | | | -| Choropleth | ✅ | Spatial | [Choropleth map with px](https://plotly.com/python/choropleth-maps/) | [px.choropleth](https://plotly.github.io/plotly.py-docs/generated/plotly.express.choropleth.html) | -| Column | ✅ | Magnitude, Time | [Bar chart with px](https://plotly.com/python/bar-charts/) | [px.bar](https://plotly.com/python-api-reference/generated/plotly.express.bar.html) | -| Column and line | ✅ | Correlation, Time | [Multiple chart types in Plotly](https://plotly.com/python/graphing-multiple-chart-types/) | [go.Bar](https://plotly.com/python-api-reference/generated/plotly.graph_objects.Bar.html) and [go.Scatter](https://plotly.com/python-api-reference/generated/plotly.graph_objects.Scatter.html) | -| Connected scatter | ✅ | Correlation, Time | [Line plot with px](https://plotly.com/python/line-charts/) | [px.line](https://plotly.com/python-api-reference/generated/plotly.express.line) | -| Cumulative curve | ❌ | Distribution | | | -| Diverging bar | ✅ | Deviation | [Bar chart with px](https://plotly.com/python/bar-charts/) | [px.bar](https://plotly.com/python-api-reference/generated/plotly.express.bar) | -| Diverging stacked bar | ✅ | Deviation | [Plotly forum - diverging stacked bar](https://community.plotly.com/t/need-help-in-making-diverging-stacked-bar-charts/34023/2) | [go.Bar](https://plotly.com/python-api-reference/generated/plotly.graph_objects.Bar.html) | -| Donut | ✅ | Part-to-whole | [Pie chart with px](https://plotly.com/python/pie-charts/) | [px.pie](https://plotly.com/python-api-reference/generated/plotly.express.pie) | -| Dot map | ✅ | Spatial | [Bubble map in px](https://plotly.com/python/bubble-maps/) | [px.scatter_map](https://plotly.github.io/plotly.py-docs/generated/plotly.express.scatter_map) | -| Dumbbell | ✅ | Distribution | [Dumbbell plots in Plotly](https://community.plotly.com/t/how-to-make-dumbbell-plots-in-plotly-python/47762) | [px.scatter](https://plotly.com/python-api-reference/generated/plotly.express.scatter.html) and [add_shape](https://plotly.com/python/shapes/) | -| Fan | ❌ | Time | | | -| Flow map | ❌ | Spatial | | | -| Funnel | ✅ | Part-to-whole | [Funnel plot with px](https://plotly.com/python/funnel-charts/) | [px.funnel](https://plotly.com/python/funnel-charts/) | -| Gantt | ✅ | Time | [Gantt chart with px](https://plotly.com/python/gantt/) | [px.timeline](https://plotly.com/python-api-reference/generated/plotly.express.timeline.html) | -| Gridplot | ❌ | Part-to-whole | | | -| Heatmap | ✅ | Time | [Heatmaps with px](https://plotly.com/python/heatmaps/) | [px.density_heatmap](https://plotly.com/python-api-reference/generated/plotly.express.density_heatmap.html) | -| Correlation matrix | ❌ | Correlation | | | -| Histogram | ✅ | Distribution | [Histograms with px](https://plotly.com/python/histograms/) | [px.histogram](https://plotly.github.io/plotly.py-docs/generated/plotly.express.histogram) | -| Line | ✅ | Time | [Line plot with px](https://plotly.com/python/line-charts/) | [px.line](https://plotly.com/python-api-reference/generated/plotly.express.line) | -| Lollipop | ❌ | Ranking, Magnitude | | | -| Marimekko | ❌ | Magnitude, Part-to-whole | | | -| Network | ❌ | Flow | | | -| Ordered bar | ✅ | Ranking | [Bar chart with px](https://plotly.com/python/bar-charts/) | [px.bar](https://plotly.com/python-api-reference/generated/plotly.express.bar.html) | -| Ordered bubble | ❌ | Ranking | | | -| Ordered column | ✅ | Ranking | [Bar chart with px](https://plotly.com/python/bar-charts/) | [px.bar](https://plotly.com/python-api-reference/generated/plotly.express.bar.html) | -| Paired bar | ✅ | Magnitude | [Histograms with px](https://plotly.com/python/histograms/) | [px.histogram](https://plotly.github.io/plotly.py-docs/generated/plotly.express.histogram) | -| Paired column | ✅ | Magnitude | [Histograms with px](https://plotly.com/python/histograms/) | [px.histogram](https://plotly.github.io/plotly.py-docs/generated/plotly.express.histogram) | -| Parallel coordinates | ✅ | Magnitude | [Parallel coordinates plot with px](https://plotly.com/python/parallel-coordinates-plot/) | [px.parallel_coordinates](https://plotly.com/python-api-reference/generated/plotly.express.parallel_coordinates.html) | -| Pictogram | ❌ | Magnitude | | | -| Pie | ✅ | Part-to-whole | [Pie chart with px](https://plotly.com/python/pie-charts/) | [px.pie](https://plotly.com/python-api-reference/generated/plotly.express.pie) | -| Radar | ✅ | Magnitude | [Radar chart with px](https://plotly.com/python/radar-chart/) | [px.line_polar](https://plotly.com/python-api-reference/generated/plotly.express.line_polar) | -| Radial | ❌ | Magnitude | | | -| Sankey | ✅ | Flow | [Sankey diagram in Plotly](https://plotly.com/python/sankey-diagram/) | [go.Sankey](https://plotly.github.io/plotly.py-docs/generated/plotly.graph_objects.Sankey.html) | -| Scatter | ✅ | Correlation | [Scatter plot with px](https://plotly.com/python/line-and-scatter/) | [px.scatter](https://plotly.com/python-api-reference/generated/plotly.express.scatter) | -| Scatter matrix | ✅ | Correlation | [Scatter matrix with px](https://plotly.com/python/splom/) | [px.scatter_matrix](https://plotly.github.io/plotly.py-docs/generated/plotly.express.scatter_matrix.html) | -| Slope | ❌ | Ranking, Time | | | -| Sparkline | ❌ | Time | | | -| Stacked bar | ✅ | Part-to-whole | [Histograms with px](https://plotly.com/python/histograms/) | [px.histogram](https://plotly.github.io/plotly.py-docs/generated/plotly.express.histogram) | -| Stacked column | ✅ | Part-to-whole | [Histograms with px](https://plotly.com/python/histograms/) | [px.histogram](https://plotly.github.io/plotly.py-docs/generated/plotly.express.histogram) | -| Stepped line | ✅ | Time | [Line plot with px](https://plotly.com/python/line-charts/) | [px.line](https://plotly.com/python-api-reference/generated/plotly.express.line) | -| Surplus deficit line | ❌ | Deviation | | | -| Treemap | ✅ | Part-to-whole | [Treemap with px](https://plotly.com/python/treemaps/) | [px.treemap](https://plotly.com/python-api-reference/generated/plotly.express.treemap.html) | -| Venn | ❌ | Part-to-whole | | | -| Violin | ✅ | Distribution | [Violin plot with px](https://plotly.com/python/violin/) | [px.violin](https://plotly.com/python-api-reference/generated/plotly.express.violin.html) | -| Waterfall | ✅ | Part-to-whole, Flow | [Waterfall charts in Plotly](https://plotly.com/python/waterfall-charts/) | [go.Waterfall](https://plotly.github.io/plotly.py-docs/generated/plotly.graph_objects.Waterfall.html) | +| Chart Type | Done | Category | Credits & sources | API | +| --------------------- | ---- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Arc | ❌ | Part-to-whole | | | +| Area | ✅ | Time | [Filled area plot with px](https://plotly.com/python/filled-area-plots/) | [px.area](https://plotly.com/python-api-reference/generated/plotly.express.area) | +| Bar | ✅ | Magnitude | [Bar chart with px](https://plotly.com/python/bar-charts/) | [px.bar](https://plotly.com/python-api-reference/generated/plotly.express.bar) | +| Barcode | ❌ | Distribution | | | +| Beeswarm | ❌ | Distribution | | | +| Boxplot | ✅ | Distribution | [Box plot with px](https://plotly.com/python/box-plots/) | [px.box](https://plotly.github.io/plotly.py-docs/generated/plotly.express.box) | +| Bubble | ✅ | Correlation | [Scatter plot with px](https://plotly.com/python/line-and-scatter/) | [px.scatter](https://plotly.com/python-api-reference/generated/plotly.express.scatter) | +| Bubble map | ✅ | Spatial | [Bubble map in px](https://plotly.com/python/bubble-maps/) | [px.scatter_map](https://plotly.github.io/plotly.py-docs/generated/plotly.express.scatter_map) | +| Bubble timeline | ❌ | Time | | | +| Bullet | ❌ | Magnitude | | | +| Bump | ❌ | Ranking | | | +| Butterfly | ✅ | Deviation, Distribution | [Pyramid charts in Plotly](https://plotly.com/python/v3/population-pyramid-charts/) | [go.Bar](https://plotly.com/python-api-reference/generated/plotly.graph_objects.Bar.html) | +| Chord | ❌ | Flow | | | +| Choropleth | ✅ | Spatial | [Choropleth map with px](https://plotly.com/python/choropleth-maps/) | [px.choropleth](https://plotly.github.io/plotly.py-docs/generated/plotly.express.choropleth.html) | +| Column | ✅ | Magnitude, Time | [Bar chart with px](https://plotly.com/python/bar-charts/) | [px.bar](https://plotly.com/python-api-reference/generated/plotly.express.bar.html) | +| Column and line | ✅ | Correlation, Time | [Multiple chart types in Plotly](https://plotly.com/python/graphing-multiple-chart-types/) | [go.Bar](https://plotly.com/python-api-reference/generated/plotly.graph_objects.Bar.html) and [go.Scatter](https://plotly.com/python-api-reference/generated/plotly.graph_objects.Scatter.html) | +| Connected scatter | ✅ | Correlation, Time | [Line plot with px](https://plotly.com/python/line-charts/) | [px.line](https://plotly.com/python-api-reference/generated/plotly.express.line) | +| Cumulative curve | ❌ | Distribution | | | +| Diverging bar | ✅ | Deviation | [Bar chart with px](https://plotly.com/python/bar-charts/) | [px.bar](https://plotly.com/python-api-reference/generated/plotly.express.bar) | +| Diverging stacked bar | ✅ | Deviation | [Plotly forum - diverging stacked bar](https://community.plotly.com/t/need-help-in-making-diverging-stacked-bar-charts/34023/2) | [go.Bar](https://plotly.com/python-api-reference/generated/plotly.graph_objects.Bar.html) | +| Donut | ✅ | Part-to-whole | [Pie chart with px](https://plotly.com/python/pie-charts/) | [px.pie](https://plotly.com/python-api-reference/generated/plotly.express.pie) | +| Dot map | ✅ | Spatial | [Bubble map in px](https://plotly.com/python/bubble-maps/) | [px.scatter_map](https://plotly.github.io/plotly.py-docs/generated/plotly.express.scatter_map) | +| Dumbbell | ✅ | Distribution | [Dumbbell plots in Plotly](https://community.plotly.com/t/how-to-make-dumbbell-plots-in-plotly-python/47762) | [px.scatter](https://plotly.com/python-api-reference/generated/plotly.express.scatter.html) and [add_shape](https://plotly.com/python/shapes/) | +| Fan | ❌ | Time | | | +| Flow map | ❌ | Spatial | | | +| Funnel | ✅ | Part-to-whole | [Funnel plot with px](https://plotly.com/python/funnel-charts/) | [px.funnel](https://plotly.com/python/funnel-charts/) | +| Gantt | ✅ | Time | [Gantt chart with px](https://plotly.com/python/gantt/) | [px.timeline](https://plotly.com/python-api-reference/generated/plotly.express.timeline.html) | +| Gridplot | ❌ | Part-to-whole | | | +| Heatmap | ✅ | Time | [Heatmaps with px](https://plotly.com/python/heatmaps/) | [px.density_heatmap](https://plotly.com/python-api-reference/generated/plotly.express.density_heatmap.html) | +| Correlation matrix | ❌ | Correlation | | | +| Histogram | ✅ | Distribution | [Histograms with px](https://plotly.com/python/histograms/) | [px.histogram](https://plotly.github.io/plotly.py-docs/generated/plotly.express.histogram) | +| Line | ✅ | Time | [Line plot with px](https://plotly.com/python/line-charts/) | [px.line](https://plotly.com/python-api-reference/generated/plotly.express.line) | +| Lollipop | ✅ | Ranking, Magnitude | [Lollipop & Dumbbell Charts with Plotly](https://towardsdatascience.com/lollipop-dumbbell-charts-with-plotly-696039d5f85) | [px.scatter](https://plotly.com/python-api-reference/generated/plotly.express.scatter) | +| Marimekko | ❌ | Magnitude, Part-to-whole | | | +| Network | ❌ | Flow | | | +| Ordered bar | ✅ | Ranking | [Bar chart with px](https://plotly.com/python/bar-charts/) | [px.bar](https://plotly.com/python-api-reference/generated/plotly.express.bar.html) | +| Ordered bubble | ❌ | Ranking | | | +| Ordered column | ✅ | Ranking | [Bar chart with px](https://plotly.com/python/bar-charts/) | [px.bar](https://plotly.com/python-api-reference/generated/plotly.express.bar.html) | +| Paired bar | ✅ | Magnitude | [Histograms with px](https://plotly.com/python/histograms/) | [px.histogram](https://plotly.github.io/plotly.py-docs/generated/plotly.express.histogram) | +| Paired column | ✅ | Magnitude | [Histograms with px](https://plotly.com/python/histograms/) | [px.histogram](https://plotly.github.io/plotly.py-docs/generated/plotly.express.histogram) | +| Parallel coordinates | ✅ | Magnitude | [Parallel coordinates plot with px](https://plotly.com/python/parallel-coordinates-plot/) | [px.parallel_coordinates](https://plotly.com/python-api-reference/generated/plotly.express.parallel_coordinates.html) | +| Pictogram | ❌ | Magnitude | | | +| Pie | ✅ | Part-to-whole | [Pie chart with px](https://plotly.com/python/pie-charts/) | [px.pie](https://plotly.com/python-api-reference/generated/plotly.express.pie) | +| Radar | ✅ | Magnitude | [Radar chart with px](https://plotly.com/python/radar-chart/) | [px.line_polar](https://plotly.com/python-api-reference/generated/plotly.express.line_polar) | +| Radial | ❌ | Magnitude | | | +| Sankey | ✅ | Flow | [Sankey diagram in Plotly](https://plotly.com/python/sankey-diagram/) | [go.Sankey](https://plotly.github.io/plotly.py-docs/generated/plotly.graph_objects.Sankey.html) | +| Scatter | ✅ | Correlation | [Scatter plot with px](https://plotly.com/python/line-and-scatter/) | [px.scatter](https://plotly.com/python-api-reference/generated/plotly.express.scatter) | +| Scatter matrix | ✅ | Correlation | [Scatter matrix with px](https://plotly.com/python/splom/) | [px.scatter_matrix](https://plotly.github.io/plotly.py-docs/generated/plotly.express.scatter_matrix.html) | +| Slope | ❌ | Ranking, Time | | | +| Sparkline | ✅ | Time | [Sparklines with px](https://plotly.com/python/line-charts/#sparklines-with-plotly-express) | [px.line](https://plotly.com/python-api-reference/generated/plotly.express.line) or [px.area](https://plotly.com/python-api-reference/generated/plotly.express.area) | +| Stacked bar | ✅ | Part-to-whole | [Histograms with px](https://plotly.com/python/histograms/) | [px.histogram](https://plotly.github.io/plotly.py-docs/generated/plotly.express.histogram) | +| Stacked column | ✅ | Part-to-whole | [Histograms with px](https://plotly.com/python/histograms/) | [px.histogram](https://plotly.github.io/plotly.py-docs/generated/plotly.express.histogram) | +| Stepped line | ✅ | Time | [Line plot with px](https://plotly.com/python/line-charts/) | [px.line](https://plotly.com/python-api-reference/generated/plotly.express.line) | +| Surplus deficit line | ❌ | Deviation | | | +| Treemap | ✅ | Part-to-whole | [Treemap with px](https://plotly.com/python/treemaps/) | [px.treemap](https://plotly.com/python-api-reference/generated/plotly.express.treemap.html) | +| Venn | ❌ | Part-to-whole | | | +| Violin | ✅ | Distribution | [Violin plot with px](https://plotly.com/python/violin/) | [px.violin](https://plotly.com/python-api-reference/generated/plotly.express.violin.html) | +| Waterfall | ✅ | Part-to-whole, Flow | [Waterfall charts in Plotly](https://plotly.com/python/waterfall-charts/) | [go.Waterfall](https://plotly.github.io/plotly.py-docs/generated/plotly.graph_objects.Waterfall.html) | ## How to contribute Contributions are welcome! To contribute a chart, follow the steps below: 1. Check that a `svg` file named after the chart type is contained in the [assets](https://github.com/mckinsey/vizro/tree/main/vizro-core/examples/visual-vocabulary/assets/images/charts) folder. If not, [raise an issue](https://github.com/mckinsey/vizro/issues) in the repository. -2. Add the data set to `_pages_utils.py` if it doesn't already exist. Use existing data sets preferably or any other data set that is publicly available e.g. [plotly.express.data](https://plotly.com/python-api-reference/generated/plotly.express.data.html) -3. Create a new page for the chart type and add it to the relevant category `.py` file such as `correlation.py`, - `deviation.py`, `distribution.py`, etc. Ensure you add the page to the list of `pages` at the end of the `.py` file. -4. Add a `.py` file containing a code example of the chart type in the `pages/examples` folder, for instance, `area.py`. Take a look at existing examples. -5. Remove the `IncompletePage(..)` entry for that chart type in `chart_groups.py`. -6. Update this `README.md` with the new chart type, its status, category, and API links. +1. Add a `.py` file containing a code example of the chart type in the `pages/examples` folder, for instance, `area.py`. Take a look at existing examples. +1. Create a new page for the chart type and add it to the relevant category `.py` file such as `correlation.py`, `deviation.py`, `distribution.py`, etc. Ensure you add the page to the list of `pages` at the end of the `.py` file. +1. Remove the `IncompletePage(..)` entry for that chart type in `chart_groups.py`. +1. Update this `README.md` with the new chart type, its status, category, and API links. ## How to run the example locally 1. Run the example with the command `hatch run example visual-vocabulary`. -2. You should now be able to access the app locally via http://127.0.0.1:8050/. +1. You should now be able to access the app locally via http://127.0.0.1:8051/. diff --git a/vizro-core/examples/visual-vocabulary/assets/css/custom.css b/vizro-core/examples/visual-vocabulary/assets/css/custom.css index ef2fce294..c69bbe58c 100644 --- a/vizro-core/examples/visual-vocabulary/assets/css/custom.css +++ b/vizro-core/examples/visual-vocabulary/assets/css/custom.css @@ -34,9 +34,7 @@ img[src*="#chart-icon"] { position: relative; } -.code-clipboard-container .pycafe-link, -.code-clipboard-container .pycafe-link:focus { - line-height: unset; +.code-clipboard-container .pycafe-link { margin-bottom: 12px; } @@ -96,7 +94,7 @@ img[src*="#chart-icon"] { background: var(--text-primary); border-top-left-radius: 8px; bottom: 0; - color: var(--text-contrast-primary); + color: var(--text-primary-inverted); display: flex; font-size: 0.8rem; font-weight: 500; @@ -109,7 +107,7 @@ img[src*="#chart-icon"] { .anchor-container:focus, .anchor-container:hover { background: var(--text-secondary); - color: var(--text-contrast-primary); + color: var(--text-primary-inverted); } img#banner { diff --git a/vizro-core/examples/visual-vocabulary/chart_groups.py b/vizro-core/examples/visual-vocabulary/chart_groups.py index 2896306f8..527ae2d8b 100644 --- a/vizro-core/examples/visual-vocabulary/chart_groups.py +++ b/vizro-core/examples/visual-vocabulary/chart_groups.py @@ -81,7 +81,6 @@ class ChartGroup: incomplete_pages=[ IncompletePage("Ordered bubble"), IncompletePage("Slope"), - IncompletePage("Lollipop"), IncompletePage("Bump"), ], icon="Stacked Bar Chart", @@ -117,7 +116,6 @@ class ChartGroup: pages=pages.magnitude.pages, incomplete_pages=[ IncompletePage("Marimekko"), - IncompletePage("Lollipop"), IncompletePage("Pictogram"), IncompletePage("Bullet"), IncompletePage("Radial"), @@ -138,7 +136,6 @@ class ChartGroup: IncompletePage("Slope"), IncompletePage("Fan"), IncompletePage("Bubble timeline"), - IncompletePage("Sparkline"), ], icon="Timeline", intro_text=time_intro_text, diff --git a/vizro-core/examples/visual-vocabulary/custom_charts.py b/vizro-core/examples/visual-vocabulary/custom_charts.py index 7e1f26948..6a7aae68b 100644 --- a/vizro-core/examples/visual-vocabulary/custom_charts.py +++ b/vizro-core/examples/visual-vocabulary/custom_charts.py @@ -290,12 +290,18 @@ def diverging_stacked_bar(data_frame: pd.DataFrame, **kwargs) -> go.Figure: orientation = fig.data[0].orientation x_or_y = "x" if orientation == "h" else "y" - for trace_idx in range(len(fig.data) // 2): + for trace_idx in range(len(fig.data) // 2, len(fig.data)): fig.update_traces({f"{x_or_y}axis": f"{x_or_y}2"}, selector=trace_idx) + # Add ticksuffix and range limitations on both sids for correct interpretation of diverging stacked bar + # with percentage data + fig.update_layout({f"{x_or_y}axis": {"ticksuffix": "%"}}) fig.update_layout({f"{x_or_y}axis2": fig.layout[f"{x_or_y}axis"]}) fig.update_layout( - {f"{x_or_y}axis": {"autorange": "reversed", "domain": [0, 0.5]}, f"{x_or_y}axis2": {"domain": [0.5, 1]}} + { + f"{x_or_y}axis": {"domain": [0, 0.5], "range": [100, 0]}, + f"{x_or_y}axis2": {"domain": [0.5, 1], "range": [0, 100]}, + } ) if orientation == "h": @@ -304,3 +310,51 @@ def diverging_stacked_bar(data_frame: pd.DataFrame, **kwargs) -> go.Figure: fig.add_hline(y=0, line_width=2, line_color="grey") return fig + + +@capture("graph") +def lollipop(data_frame: pd.DataFrame, **kwargs): + """Creates a lollipop based on px.scatter. + + A lollipop chart is a variation of a bar chart where each data point is represented by a line and a dot at the end + to mark the value. + + Inspired by: https://towardsdatascience.com/lollipop-dumbbell-charts-with-plotly-696039d5f85 + + Args: + data_frame: DataFrame for the chart. Can be long form or wide form. + See https://plotly.com/python/wide-form/. + **kwargs: Keyword arguments to pass into px.scatter (e.g. x, y, labels). + See https://plotly.com/python-api-reference/generated/plotly.scatter.html. + + Returns: + go.Figure: Lollipop chart. + """ + # Plots the dots of the lollipop chart + fig = px.scatter(data_frame, **kwargs) + + # Enables the orientation of the chart to be either horizontal or vertical + orientation = fig.data[0].orientation + x_or_y = "x" if orientation == "h" else "y" + y_or_x = "y" if orientation == "h" else "x" + + # Plots the lines of the lollipop chart + for x_or_y_value, y_or_x_value in zip(fig.data[0][x_or_y], fig.data[0][y_or_x]): + fig.add_trace(go.Scatter({x_or_y: [0, x_or_y_value], y_or_x: [y_or_x_value, y_or_x_value], "mode": "lines"})) + + # Styles the lollipop chart and makes it uni-colored + fig.update_traces( + marker_size=12, + line_width=3, + line_color=fig.layout.template.layout.colorway[0], + ) + + fig.update_layout( + { + "showlegend": False, + f"{x_or_y}axis_showgrid": True, + f"{y_or_x}axis_showgrid": False, + f"{x_or_y}axis_rangemode": "tozero", + }, + ) + return fig diff --git a/vizro-core/examples/visual-vocabulary/pages/_factories.py b/vizro-core/examples/visual-vocabulary/pages/_factories.py index 97b4615b5..9fa0deb77 100644 --- a/vizro-core/examples/visual-vocabulary/pages/_factories.py +++ b/vizro-core/examples/visual-vocabulary/pages/_factories.py @@ -7,7 +7,7 @@ import vizro.models as vm from pages._pages_utils import PAGE_GRID, make_code_clipboard_from_py_file -from pages.examples import butterfly, column_and_line, connected_scatter, waterfall +from pages.examples import butterfly, column_and_line, connected_scatter, lollipop, waterfall def butterfly_factory(group: str): @@ -179,3 +179,48 @@ def waterfall_factory(group: str): ), ], ) + + +def lollipop_factory(group: str): + """Reusable function to create the page content for the lollipop chart with a unique ID.""" + return vm.Page( + id=f"{group}-lollipop", + path=f"{group}/lollipop", + title="Lollipop", + layout=vm.Layout(grid=PAGE_GRID), + components=[ + vm.Card( + text=""" + + #### What is a lollipop chart? + + A lollipop chart is a variation of a bar chart where each data point is represented by a line and a + dot at the end to mark the value. It functions like a bar chart but offers a cleaner visual, + especially useful when dealing with a large number of high values, to avoid the clutter of tall columns. + However, it can be less precise due to the difficulty in judging the exact center of the circle. + +   + + #### When should I use it? + + Use a lollipop chart to compare values across categories, especially when dealing with many high values. + It highlights differences and trends clearly without the visual bulk of a bar chart. Ensure clarity by + limiting categories, using consistent scales, and clearly labeling axes. Consider alternatives if + precise value representation is crucial. + """ + ), + vm.Graph(figure=lollipop.fig), + vm.Tabs( + tabs=[ + vm.Container( + title="Vizro dashboard", + components=[make_code_clipboard_from_py_file("lollipop.py", mode="vizro")], + ), + vm.Container( + title="Plotly figure", + components=[make_code_clipboard_from_py_file("lollipop.py", mode="plotly")], + ), + ] + ), + ], + ) diff --git a/vizro-core/examples/visual-vocabulary/pages/examples/diverging_stacked_bar.py b/vizro-core/examples/visual-vocabulary/pages/examples/diverging_stacked_bar.py index ae08c6629..e6ab63c0a 100644 --- a/vizro-core/examples/visual-vocabulary/pages/examples/diverging_stacked_bar.py +++ b/vizro-core/examples/visual-vocabulary/pages/examples/diverging_stacked_bar.py @@ -24,12 +24,16 @@ def diverging_stacked_bar(data_frame: pd.DataFrame, **kwargs) -> go.Figure: orientation = fig.data[0].orientation x_or_y = "x" if orientation == "h" else "y" - for trace_idx in range(len(fig.data) // 2): + for trace_idx in range(len(fig.data) // 2, len(fig.data)): fig.update_traces({f"{x_or_y}axis": f"{x_or_y}2"}, selector=trace_idx) + fig.update_layout({f"{x_or_y}axis": {"ticksuffix": "%"}}) fig.update_layout({f"{x_or_y}axis2": fig.layout[f"{x_or_y}axis"]}) fig.update_layout( - {f"{x_or_y}axis": {"autorange": "reversed", "domain": [0, 0.5]}, f"{x_or_y}axis2": {"domain": [0.5, 1]}} + { + f"{x_or_y}axis": {"domain": [0, 0.5], "range": [100, 0]}, + f"{x_or_y}axis2": {"domain": [0.5, 1], "range": [0, 100]}, + } ) if orientation == "h": @@ -63,6 +67,6 @@ def diverging_stacked_bar(data_frame: pd.DataFrame, **kwargs) -> go.Figure: data_frame=pastries, x=["Strongly Disagree", "Disagree", "Agree", "Strongly Agree"], y="pastry", - labels={"value": "Response count", "variable": "Opinion"}, + labels={"value": "", "variable": "", "pastry": ""}, title="I would recommend this pastry to my friends", ) diff --git a/vizro-core/examples/visual-vocabulary/pages/examples/lollipop.py b/vizro-core/examples/visual-vocabulary/pages/examples/lollipop.py new file mode 100644 index 000000000..9c483706a --- /dev/null +++ b/vizro-core/examples/visual-vocabulary/pages/examples/lollipop.py @@ -0,0 +1,42 @@ +import pandas as pd +import plotly.express as px +import plotly.graph_objects as go +from vizro.models.types import capture + + +@capture("graph") +def lollipop(data_frame: pd.DataFrame, **kwargs): + """Creates a lollipop chart using Plotly.""" + fig = px.scatter(data_frame, **kwargs) + + orientation = fig.data[0].orientation + x_or_y = "x" if orientation == "h" else "y" + y_or_x = "y" if orientation == "h" else "x" + + for x_or_y_value, y_or_x_value in zip(fig.data[0][x_or_y], fig.data[0][y_or_x]): + fig.add_trace(go.Scatter({x_or_y: [0, x_or_y_value], y_or_x: [y_or_x_value, y_or_x_value], "mode": "lines"})) + + fig.update_traces( + marker_size=12, + line_width=3, + line_color=fig.layout.template.layout.colorway[0], + ) + + fig.update_layout( + { + "showlegend": False, + f"{x_or_y}axis_showgrid": True, + f"{y_or_x}axis_showgrid": False, + f"{x_or_y}axis_rangemode": "tozero", + }, + ) + return fig + + +gapminder = ( + px.data.gapminder() + .query("year == 2007 and country.isin(['United States', 'Pakistan', 'India', 'China', 'Indonesia'])") + .sort_values("pop") +) + +fig = lollipop(gapminder, y="country", x="pop") diff --git a/vizro-core/examples/visual-vocabulary/pages/examples/sparkline.py b/vizro-core/examples/visual-vocabulary/pages/examples/sparkline.py new file mode 100644 index 000000000..3d8b192d5 --- /dev/null +++ b/vizro-core/examples/visual-vocabulary/pages/examples/sparkline.py @@ -0,0 +1,16 @@ +import plotly.express as px +from vizro.models.types import capture + +stocks = px.data.stocks() + + +@capture("graph") +def sparkline(data_frame, **kwargs): + fig = px.line(data_frame, **kwargs) + fig.update_xaxes(ticks="", showgrid=False, title="") + fig.update_yaxes(visible=False) + fig.update_layout(showlegend=False) + return fig + + +fig = sparkline(stocks, x="date", y=["GOOG", "AMZN", "AAPL"], labels={"variable": "stock"}, facet_row="variable") diff --git a/vizro-core/examples/visual-vocabulary/pages/magnitude.py b/vizro-core/examples/visual-vocabulary/pages/magnitude.py index fc3b23337..983e190e8 100644 --- a/vizro-core/examples/visual-vocabulary/pages/magnitude.py +++ b/vizro-core/examples/visual-vocabulary/pages/magnitude.py @@ -2,6 +2,7 @@ import vizro.models as vm +from pages._factories import lollipop_factory from pages._pages_utils import PAGE_GRID, make_code_clipboard_from_py_file from pages.examples import bar, magnitude_column, paired_bar, paired_column, parallel_coordinates, radar @@ -238,4 +239,13 @@ ], ) -pages = [bar_page, column_page, paired_bar_page, paired_column_page, parallel_coordinates_page, radar_page] +lollipop_page = lollipop_factory("magnitude") +pages = [ + bar_page, + column_page, + paired_bar_page, + paired_column_page, + parallel_coordinates_page, + radar_page, + lollipop_page, +] diff --git a/vizro-core/examples/visual-vocabulary/pages/ranking.py b/vizro-core/examples/visual-vocabulary/pages/ranking.py index a788223d7..3ea7bdbe1 100644 --- a/vizro-core/examples/visual-vocabulary/pages/ranking.py +++ b/vizro-core/examples/visual-vocabulary/pages/ranking.py @@ -2,6 +2,7 @@ import vizro.models as vm +from pages._factories import lollipop_factory from pages._pages_utils import PAGE_GRID, make_code_clipboard_from_py_file from pages.examples import ordered_bar, ordered_column @@ -85,4 +86,6 @@ ) -pages = [ordered_bar_page, ordered_column_page] +lollipop_page = lollipop_factory("deviation") + +pages = [ordered_bar_page, ordered_column_page, lollipop_page] diff --git a/vizro-core/examples/visual-vocabulary/pages/time.py b/vizro-core/examples/visual-vocabulary/pages/time.py index 6fda8d314..6143abcf7 100644 --- a/vizro-core/examples/visual-vocabulary/pages/time.py +++ b/vizro-core/examples/visual-vocabulary/pages/time.py @@ -7,7 +7,7 @@ PAGE_GRID, make_code_clipboard_from_py_file, ) -from pages.examples import area, gantt, heatmap, line, stepped_line, time_column +from pages.examples import area, gantt, heatmap, line, sparkline, stepped_line, time_column line_page = vm.Page( title="Line", @@ -212,19 +212,19 @@ #### What is a gantt chart? A gantt chart is a type of bar chart that visualizes a project schedule. -It shows the start and end dates of a project element, such as tasks, activities, or -events, in a timeline format. Each element is represented by a bar whose length indicates -its duration. + It shows the start and end dates of a project element, such as tasks, activities, or + events, in a timeline format. Each element is represented by a bar whose length indicates + its duration.   #### When should I use it? Gantt charts are ideal for visualizing project timelines, tracking -progress, and managing dependencies. They clearly display task start and end dates, making -it easy to monitor project status and manage interdependencies. However, they can become -complex if not regularly updated, especially for large projects. - """ + progress, and managing dependencies. They clearly display task start and end dates, making + it easy to monitor project status and manage interdependencies. However, they can become + complex if not regularly updated, especially for large projects. + """ ), vm.Graph(figure=gantt.fig), vm.Tabs( @@ -240,6 +240,46 @@ ), ], ) + + +sparkline_page = vm.Page( + title="Sparkline", + path="time/sparkline", + layout=vm.Layout(grid=PAGE_GRID), + components=[ + vm.Card( + text=""" + #### What is a sparkline chart? + + A sparkline chart is a compact line or area chart that displays multiple time series over a continuous + period. Without visible axes or labels, they are ideal for embedding within text, tables, or dashboards, + highlighting relative movement rather than precise values for a quick visual summary of trends. + +   + + #### When should I use it? + + Use sparkline charts to show trends for multiple time series sharing the same y-axis quantity over the + same x-axis time range. They emphasize relative movement rather than precise values. To keep them + effective, ensure simplicity by avoiding clutter. Use consistent scales and distinct colors for + different series. Remove labels and gridlines, limit annotations, and place sparklines near relevant + text or data. + """ + ), + vm.Graph(figure=sparkline.fig), + vm.Tabs( + tabs=[ + vm.Container( + title="Vizro dashboard", components=[make_code_clipboard_from_py_file("sparkline.py", mode="vizro")] + ), + vm.Container( + title="Plotly figure", + components=[make_code_clipboard_from_py_file("sparkline.py", mode="plotly")], + ), + ] + ), + ], +) pages = [ line_page, column_page, @@ -249,4 +289,5 @@ stepped_line_page, heatmap_page, gantt_page, + sparkline_page, ] diff --git a/vizro-core/examples/visual-vocabulary/requirements.in b/vizro-core/examples/visual-vocabulary/requirements.in index 701ad04d6..bc13bc273 100644 --- a/vizro-core/examples/visual-vocabulary/requirements.in +++ b/vizro-core/examples/visual-vocabulary/requirements.in @@ -3,5 +3,5 @@ autoflake==2.3.1 black==24.4.2 isort==5.13.2 plotly==5.24.1 -vizro==0.1.25 +vizro==0.1.28 gunicorn diff --git a/vizro-core/examples/visual-vocabulary/requirements.txt b/vizro-core/examples/visual-vocabulary/requirements.txt index 693b90a19..5981b1821 100644 --- a/vizro-core/examples/visual-vocabulary/requirements.txt +++ b/vizro-core/examples/visual-vocabulary/requirements.txt @@ -50,7 +50,9 @@ gunicorn==23.0.0 idna==3.10 # via requests importlib-metadata==8.5.0 - # via dash + # via + # dash + # flask isort==5.13.2 # via -r requirements.in itsdangerous==2.2.0 @@ -65,7 +67,7 @@ mypy-extensions==1.0.0 # via black nest-asyncio==1.6.0 # via dash -numpy==2.1.2 +numpy==2.0.2 # via pandas packaging==24.1 # via @@ -105,8 +107,13 @@ six==1.16.0 # retrying tenacity==9.0.0 # via plotly +tomli==2.1.0 + # via + # autoflake + # black typing-extensions==4.12.2 # via + # black # dash # pydantic # pydantic-core @@ -114,7 +121,7 @@ tzdata==2024.2 # via pandas urllib3==2.2.3 # via requests -vizro==0.1.25 +vizro==0.1.28 # via -r requirements.in werkzeug==3.0.6 # via diff --git a/vizro-core/hatch.toml b/vizro-core/hatch.toml index 0cecae8be..c18cc24fd 100644 --- a/vizro-core/hatch.toml +++ b/vizro-core/hatch.toml @@ -30,7 +30,10 @@ dependencies = [ "openpyxl", "jupyter", "pre-commit", - "PyGithub" + "PyGithub", + "imutils", + "opencv-python", + "pyhamcrest" ] installer = "uv" @@ -55,7 +58,7 @@ schema-check = ["python schemas/generate.py --check"] # 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-component-library = "pytest tests/component_library --headless {args}" +test-e2e-component-library = "pytest tests/e2e/test_component_library.py --headless {args}" test-integration = "pytest tests/integration --headless {args}" test-js = "./tools/run_jest.sh {args}" test-unit = "pytest tests/unit {args}" @@ -111,7 +114,7 @@ VIZRO_LOG_LEVEL = "DEBUG" [envs.lower-bounds] extra-dependencies = [ "pydantic==1.10.16", - "dash==2.17.1", + "dash==2.18.0", "plotly==5.12.0", "pandas==2.0.0", "numpy==1.23.0" # Need numpy<2 to work with pandas==2.0.0. See https://stackoverflow.com/questions/78634235/. @@ -119,14 +122,6 @@ extra-dependencies = [ features = ["kedro"] python = "3.9" -[envs.tests] -extra-dependencies = [ - "imutils", - "opencv-python", - "pyhamcrest" -] -python = "3.12" - [publish.index] disable = true diff --git a/vizro-core/mkdocs.yml b/vizro-core/mkdocs.yml index bede305e6..d09520f77 100644 --- a/vizro-core/mkdocs.yml +++ b/vizro-core/mkdocs.yml @@ -56,7 +56,7 @@ nav: - Documentation style: pages/explanation/documentation-style-guide.md - Authors: pages/explanation/authors.md - Examples: - - Vizro gallery: https://vizro.mckinsey.com/ + - Gallery: https://vizro.mckinsey.com/ - Vizro-AI: - Vizro-AI: https://vizro.readthedocs.io/projects/vizro-ai/ @@ -112,7 +112,7 @@ markdown_extensions: kwds: type: vizro requirements: | - vizro==0.1.26 + vizro==0.1.29 - pymdownx.tabbed: alternate_style: true - pymdownx.mark @@ -144,6 +144,10 @@ plugins: - git-revision-date-localized: enable_creation_date: false +extra: + meta: + - name: google-site-verification + content: "CYb3cxosCgsN2QDQVaSGQpMQCesqpsGQ3oTM02NtvkY" extra_css: - stylesheets/extra.css diff --git a/vizro-core/pyproject.toml b/vizro-core/pyproject.toml index a1992ddc8..1bf2b9577 100644 --- a/vizro-core/pyproject.toml +++ b/vizro-core/pyproject.toml @@ -15,7 +15,7 @@ classifiers = [ "Programming Language :: Python :: 3.13" ] dependencies = [ - "dash>=2.17.1", # 2.17.1 needed for no_output fix in clientside_callback + "dash>=2.18.0,<3", # 2.18.0 needed as the 'id' attribute is exposed for dcc.Loading "dash_bootstrap_components", "dash-ag-grid>=31.0.0", "pandas>=2", @@ -79,7 +79,9 @@ filterwarnings = [ # Ignore warning when using the fig.layout.title inside examples: "ignore:Using the `title` argument in your Plotly chart function may cause misalignment:UserWarning", # Ignore warning for Pydantic v1 API and Python 3.13: - "ignore:Failing to pass a value to the 'type_params' parameter of 'typing.ForwardRef._evaluate' is deprecated:DeprecationWarning" + "ignore:Failing to pass a value to the 'type_params' parameter of 'typing.ForwardRef._evaluate' is deprecated:DeprecationWarning", + # Ignore deprecation warning until this is solved: https://github.com/plotly/dash/issues/2590: + "ignore:HTTPResponse.getheader():DeprecationWarning" ] norecursedirs = ["tests/tests_utils", "tests/js"] pythonpath = ["tests/tests_utils"] diff --git a/vizro-core/schemas/0.1.26.json b/vizro-core/schemas/0.1.29.json similarity index 99% rename from vizro-core/schemas/0.1.26.json rename to vizro-core/schemas/0.1.29.json index d5002ea1f..d69755927 100644 --- a/vizro-core/schemas/0.1.26.json +++ b/vizro-core/schemas/0.1.29.json @@ -141,6 +141,12 @@ "default": "Click me!", "type": "string" }, + "href": { + "title": "Href", + "description": "URL (relative or absolute) to navigate to.", + "default": "", + "type": "string" + }, "actions": { "title": "Actions", "default": [], diff --git a/vizro-core/schemas/0.1.27.dev0.json b/vizro-core/schemas/0.1.30.dev0.json similarity index 99% rename from vizro-core/schemas/0.1.27.dev0.json rename to vizro-core/schemas/0.1.30.dev0.json index d5002ea1f..d69755927 100644 --- a/vizro-core/schemas/0.1.27.dev0.json +++ b/vizro-core/schemas/0.1.30.dev0.json @@ -141,6 +141,12 @@ "default": "Click me!", "type": "string" }, + "href": { + "title": "Href", + "description": "URL (relative or absolute) to navigate to.", + "default": "", + "type": "string" + }, "actions": { "title": "Actions", "default": [], diff --git a/vizro-core/src/vizro/__init__.py b/vizro-core/src/vizro/__init__.py index e10f21fb0..6a77ff59c 100644 --- a/vizro-core/src/vizro/__init__.py +++ b/vizro-core/src/vizro/__init__.py @@ -14,7 +14,7 @@ __all__ = ["Vizro"] -__version__ = "0.1.27.dev0" +__version__ = "0.1.30.dev0" # For the below _css_dist and _js_dist to be used by Dash, they must be retrieved by dash.resources.Css.get_all_css(). diff --git a/vizro-core/src/vizro/_vizro.py b/vizro-core/src/vizro/_vizro.py index 55d30a5fe..f648d053f 100644 --- a/vizro-core/src/vizro/_vizro.py +++ b/vizro-core/src/vizro/_vizro.py @@ -5,7 +5,7 @@ from collections.abc import Iterable from contextlib import suppress from pathlib import Path, PurePosixPath -from typing import TYPE_CHECKING, TypedDict +from typing import TYPE_CHECKING, TypedDict, cast import dash import plotly.io as pio @@ -15,7 +15,7 @@ import vizro from vizro._constants import VIZRO_ASSETS_PATH from vizro.managers import data_manager, model_manager -from vizro.models import Dashboard +from vizro.models import Dashboard, Filter logger = logging.getLogger(__name__) @@ -144,9 +144,16 @@ def _pre_build(): # changes size. # Any models that are created during the pre-build process *will not* themselves have pre_build run on them. # In future may add a second pre_build loop after the first one. + + for filter in cast(Iterable[Filter], model_manager._get_models(Filter)): + # Run pre_build on all filters first, then on all other models. This handles dependency between Filter + # and Page pre_build and ensures that filters are pre-built before the Page objects that use them. + # This is important because the Page pre_build method checks whether filters are dynamic or not, which is + # defined in the filter's pre_build method. + filter.pre_build() for model_id in set(model_manager): model = model_manager[model_id] - if hasattr(model, "pre_build"): + if hasattr(model, "pre_build") and not isinstance(model, Filter): model.pre_build() def __call__(self, environ: WSGIEnvironment, start_response: StartResponse) -> Iterable[bytes]: diff --git a/vizro-core/src/vizro/actions/_action_loop/_action_loop.py b/vizro-core/src/vizro/actions/_action_loop/_action_loop.py index d9caac0ff..2d8edefa9 100644 --- a/vizro-core/src/vizro/actions/_action_loop/_action_loop.py +++ b/vizro-core/src/vizro/actions/_action_loop/_action_loop.py @@ -1,10 +1,14 @@ """The action loop creates all the required action callbacks and its components.""" +from collections.abc import Iterable +from typing import cast + from dash import html -from vizro.actions._action_loop._action_loop_utils import _get_actions_on_registered_pages from vizro.actions._action_loop._build_action_loop_callbacks import _build_action_loop_callbacks from vizro.actions._action_loop._get_action_loop_components import _get_action_loop_components +from vizro.managers import model_manager +from vizro.models import Action class ActionLoop: @@ -37,5 +41,8 @@ def _build_actions_models(): List of required components for each `Action` in the `Dashboard` e.g. list[dcc.Download] """ - actions = _get_actions_on_registered_pages() - return html.Div([action.build() for action in actions], id="app_action_models_components_div", hidden=True) + return html.Div( + [action.build() for action in cast(Iterable[Action], model_manager._get_models(Action))], + id="app_action_models_components_div", + hidden=True, + ) diff --git a/vizro-core/src/vizro/actions/_action_loop/_action_loop_utils.py b/vizro-core/src/vizro/actions/_action_loop/_action_loop_utils.py deleted file mode 100644 index d640f24bb..000000000 --- a/vizro-core/src/vizro/actions/_action_loop/_action_loop_utils.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Contains utilities to extract the Action and ActionsChain models from registered pages only.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -import dash - -from vizro.managers import model_manager -from vizro.managers._model_manager import ModelID - -if TYPE_CHECKING: - from vizro.models import Action, Page - from vizro.models._action._actions_chain import ActionsChain - - -def _get_actions_chains_on_all_pages() -> list[ActionsChain]: - """Gets list of ActionsChain models for registered pages.""" - actions_chains: list[ActionsChain] = [] - # TODO: once dash.page_registry matches up with model_manager, change this to use purely model_manager. - # Making the change now leads to problems since there can be Action models defined that aren't used in the - # dashboard. - # See https://github.com/mckinsey/vizro/pull/366. - for registered_page in dash.page_registry.values(): - try: - page: Page = model_manager[registered_page["module"]] - except KeyError: - continue - actions_chains.extend(model_manager._get_page_actions_chains(page_id=ModelID(str(page.id)))) - return actions_chains - - -def _get_actions_on_registered_pages() -> list[Action]: - """Gets list of Action models for registered pages.""" - return [action for action_chain in _get_actions_chains_on_all_pages() for action in action_chain.actions] diff --git a/vizro-core/src/vizro/actions/_action_loop/_build_action_loop_callbacks.py b/vizro-core/src/vizro/actions/_action_loop/_build_action_loop_callbacks.py index bc76a146d..439c443e1 100644 --- a/vizro-core/src/vizro/actions/_action_loop/_build_action_loop_callbacks.py +++ b/vizro-core/src/vizro/actions/_action_loop/_build_action_loop_callbacks.py @@ -4,20 +4,20 @@ from dash import ClientsideFunction, Input, Output, State, clientside_callback -from vizro.actions._action_loop._action_loop_utils import ( - _get_actions_chains_on_all_pages, - _get_actions_on_registered_pages, -) from vizro.managers import model_manager from vizro.managers._model_manager import ModelID +from vizro.models import Action +from vizro.models._action._actions_chain import ActionsChain logger = logging.getLogger(__name__) def _build_action_loop_callbacks() -> None: """Creates all required dash callbacks for the action loop.""" - actions_chains = _get_actions_chains_on_all_pages() - actions = _get_actions_on_registered_pages() + # actions_chain and actions are not iterated over multiple times so conversion to list is not technically needed, + # but it prevents future bugs and matches _get_action_loop_components. + actions_chains: list[ActionsChain] = list(model_manager._get_models(ActionsChain)) + actions: list[Action] = list(model_manager._get_models(Action)) if not actions_chains: return diff --git a/vizro-core/src/vizro/actions/_action_loop/_get_action_loop_components.py b/vizro-core/src/vizro/actions/_action_loop/_get_action_loop_components.py index 7d34c2a4a..2d18c18df 100644 --- a/vizro-core/src/vizro/actions/_action_loop/_get_action_loop_components.py +++ b/vizro-core/src/vizro/actions/_action_loop/_get_action_loop_components.py @@ -2,10 +2,9 @@ from dash import dcc, html -from vizro.actions._action_loop._action_loop_utils import ( - _get_actions_chains_on_all_pages, - _get_actions_on_registered_pages, -) +from vizro.managers import model_manager +from vizro.models import Action +from vizro.models._action._actions_chain import ActionsChain def _get_action_loop_components() -> html.Div: @@ -15,8 +14,9 @@ def _get_action_loop_components() -> html.Div: List of dcc or html components. """ - actions_chains = _get_actions_chains_on_all_pages() - actions = _get_actions_on_registered_pages() + # actions_chain and actions are iterated over multiple times so must be realized into a list. + actions_chains: list[ActionsChain] = list(model_manager._get_models(ActionsChain)) + actions: list[Action] = list(model_manager._get_models(Action)) if not actions_chains: return html.Div(id="action_loop_components_div") diff --git a/vizro-core/src/vizro/actions/_actions_utils.py b/vizro-core/src/vizro/actions/_actions_utils.py index 484d41ed4..1873b6620 100644 --- a/vizro-core/src/vizro/actions/_actions_utils.py +++ b/vizro-core/src/vizro/actions/_actions_utils.py @@ -2,8 +2,9 @@ from __future__ import annotations +from collections.abc import Iterable from copy import deepcopy -from typing import TYPE_CHECKING, Any, Literal, Optional, TypedDict, Union +from typing import TYPE_CHECKING, Any, Literal, Optional, TypedDict, Union, cast import pandas as pd @@ -48,18 +49,18 @@ def _get_component_actions(component) -> list[Action]: def _apply_filter_controls( - data_frame: pd.DataFrame, ctds_filters: list[CallbackTriggerDict], target: ModelID + data_frame: pd.DataFrame, ctds_filter: list[CallbackTriggerDict], target: ModelID ) -> pd.DataFrame: """Applies filters from a vm.Filter model in the controls. Args: data_frame: unfiltered DataFrame. - ctds_filters: list of CallbackTriggerDict for filters. + ctds_filter: list of CallbackTriggerDict for filters. target: id of targeted Figure. Returns: filtered DataFrame. """ - for ctd in ctds_filters: + for ctd in ctds_filter: selector_value = ctd["value"] selector_value = selector_value if isinstance(selector_value, list) else [selector_value] selector_actions = _get_component_actions(model_manager[ctd["id"]]) @@ -80,15 +81,12 @@ def _apply_filter_controls( return data_frame -def _get_parent_vizro_model(_underlying_callable_object_id: str) -> VizroBaseModel: +def _get_parent_model(_underlying_callable_object_id: str) -> VizroBaseModel: from vizro.models import VizroBaseModel - for _, vizro_base_model in model_manager._items_with_type(VizroBaseModel): - if ( - hasattr(vizro_base_model, "_input_component_id") - and vizro_base_model._input_component_id == _underlying_callable_object_id - ): - return vizro_base_model + for model in cast(Iterable[VizroBaseModel], model_manager._get_models()): + if hasattr(model, "_input_component_id") and model._input_component_id == _underlying_callable_object_id: + return model raise KeyError( f"No parent Vizro model found for underlying callable object with id: {_underlying_callable_object_id}." ) @@ -164,12 +162,12 @@ def _update_nested_figure_properties( def _get_parametrized_config( - ctd_parameters: list[CallbackTriggerDict], target: ModelID, data_frame: bool + ctds_parameter: list[CallbackTriggerDict], target: ModelID, data_frame: bool ) -> dict[str, Any]: """Convert parameters into a keyword-argument dictionary. Args: - ctd_parameters: list of CallbackTriggerDicts for vm.Parameter. + ctds_parameter: list of CallbackTriggerDicts for vm.Parameter. target: id of targeted figure. data_frame: whether to return only DataFrame parameters starting "data_frame." or only non-DataFrame parameters. @@ -187,7 +185,7 @@ def _get_parametrized_config( config = deepcopy(model_manager[target].figure._arguments) del config["data_frame"] - for ctd in ctd_parameters: + for ctd in ctds_parameter: # TODO: needs to be refactored so that it is independent of implementation details parameter_value = ctd["value"] @@ -223,7 +221,7 @@ def _apply_filters( # Takes in just one target, so dataframe is filtered repeatedly for every target that uses it. # Potentially this could be de-duplicated but it's not so important since filtering is a relatively fast # operation (compared to data loading). - filtered_data = _apply_filter_controls(data_frame=data, ctds_filters=ctds_filter, target=target) + filtered_data = _apply_filter_controls(data_frame=data, ctds_filter=ctds_filter, target=target) filtered_data = _apply_filter_interaction( data_frame=filtered_data, ctds_filter_interaction=ctds_filter_interaction, target=target ) @@ -231,17 +229,17 @@ def _apply_filters( def _get_unfiltered_data( - ctds_parameters: list[CallbackTriggerDict], targets: list[ModelID] + ctds_parameter: list[CallbackTriggerDict], targets: list[ModelID] ) -> dict[ModelID, pd.DataFrame]: # Takes in multiple targets to ensure that data can be loaded efficiently using _multi_load and not repeated for # every single target. - # Getting unfiltered data requires data frame parameters. We pass in all ctd_parameters and then find the + # Getting unfiltered data requires data frame parameters. We pass in all ctds_parameter and then find the # data_frame ones by passing data_frame=True in the call to _get_paramaterized_config. Static data is also # handled here and will just have empty dictionary for its kwargs. multi_data_source_name_load_kwargs: list[tuple[DataSourceName, dict[str, Any]]] = [] for target in targets: dynamic_data_load_params = _get_parametrized_config( - ctd_parameters=ctds_parameters, target=target, data_frame=True + ctds_parameter=ctds_parameter, target=target, data_frame=True ) data_source_name = model_manager[target]["data_frame"] multi_data_source_name_load_kwargs.append((data_source_name, dynamic_data_load_params["data_frame"])) @@ -252,25 +250,45 @@ def _get_unfiltered_data( def _get_modified_page_figures( ctds_filter: list[CallbackTriggerDict], ctds_filter_interaction: list[dict[str, CallbackTriggerDict]], - ctds_parameters: list[CallbackTriggerDict], + ctds_parameter: list[CallbackTriggerDict], targets: list[ModelID], ) -> dict[ModelID, Any]: + from vizro.models import Filter + outputs: dict[ModelID, Any] = {} + control_targets = [] + figure_targets = [] + for target in targets: + if isinstance(model_manager[target], Filter): + control_targets.append(target) + else: + figure_targets.append(target) + + # TODO-NEXT: Add fetching unfiltered data for the Filter.targets as well, once dynamic filters become "targetable" + # from other actions too. For example, in future, if Parameter is targeting only a single Filter. + # Currently, it only works for the on_page_load because Filter.targets are indeed the part of the actions' targets. + # More about the limitation: https://github.com/mckinsey/vizro/pull/879/files#r1863535516 + target_to_data_frame = _get_unfiltered_data(ctds_parameter=ctds_parameter, targets=figure_targets) + # TODO: the structure here would be nicer if we could get just the ctds for a single target at one time, # so you could do apply_filters on a target a pass only the ctds relevant for that target. # Consider restructuring ctds to a more convenient form to make this possible. - - for target, unfiltered_data in _get_unfiltered_data(ctds_parameters, targets).items(): + for target, unfiltered_data in target_to_data_frame.items(): filtered_data = _apply_filters(unfiltered_data, ctds_filter, ctds_filter_interaction, target) outputs[target] = model_manager[target]( data_frame=filtered_data, - **_get_parametrized_config(ctd_parameters=ctds_parameters, target=target, data_frame=False), + **_get_parametrized_config(ctds_parameter=ctds_parameter, target=target, data_frame=False), ) - # TODO NEXT: will need to pass unfiltered_data into Filter.__call__. - # This dictionary is filtered for correct targets already selected in Filter.__call__ or that could be done here - # instead. - # {target: data_frame for target, data_frame in unfiltered_data.items() if target in self.targets} + for target in control_targets: + ctd_filter = [item for item in ctds_filter if item["id"] == model_manager[target].selector.id] + + # This only covers the case of cross-page actions when Filter in an output, but is not an input of the action. + current_value = ctd_filter[0]["value"] if ctd_filter else None + + # target_to_data_frame contains all targets, including some which might not be relevant for the filter in + # question. We filter to use just the relevant targets in Filter.__call__. + outputs[target] = model_manager[target](target_to_data_frame=target_to_data_frame, current_value=current_value) return outputs diff --git a/vizro-core/src/vizro/actions/_callback_mapping/_callback_mapping_utils.py b/vizro-core/src/vizro/actions/_callback_mapping/_callback_mapping_utils.py index 4bf82991c..ba18e4fa5 100644 --- a/vizro-core/src/vizro/actions/_callback_mapping/_callback_mapping_utils.py +++ b/vizro-core/src/vizro/actions/_callback_mapping/_callback_mapping_utils.py @@ -1,13 +1,15 @@ """Contains utilities to create the action_callback_mapping.""" -from typing import Any, Callable, Union +from collections.abc import Iterable +from typing import Any, Callable, Union, cast from dash import Output, State, dcc from vizro.actions import _parameter, filter_interaction from vizro.managers import model_manager -from vizro.managers._model_manager import ModelID -from vizro.models import Action, Page +from vizro.managers._model_manager import FIGURE_MODELS, ModelID +from vizro.models import Action, Page, VizroBaseModel +from vizro.models._action._actions_chain import ActionsChain from vizro.models._controls import Filter, Parameter from vizro.models.types import ControlType @@ -15,13 +17,11 @@ # This function can also be reused for all other inputs (filters, parameters). # Potentially this could be a way to reconcile predefined with custom actions, # and make that predefined actions see and add into account custom actions. -def _get_matching_actions_by_function( - page_id: ModelID, action_function: Callable[[Any], dict[str, Any]] -) -> list[Action]: +def _get_matching_actions_by_function(page: Page, action_function: Callable[[Any], dict[str, Any]]) -> list[Action]: """Gets list of `Actions` on triggered `Page` that match the provided `action_function`.""" return [ action - for actions_chain in model_manager._get_page_actions_chains(page_id=page_id) + for actions_chain in cast(Iterable[ActionsChain], model_manager._get_models(ActionsChain, page)) for action in actions_chain.actions if action.function._function == action_function ] @@ -32,21 +32,27 @@ def _get_inputs_of_controls(page: Page, control_type: ControlType) -> list[State """Gets list of `States` for selected `control_type` of triggered `Page`.""" return [ State(component_id=control.selector.id, component_property=control.selector._input_property) - for control in page.controls - if isinstance(control, control_type) + for control in cast(Iterable[ControlType], model_manager._get_models(control_type, page)) ] +def _get_action_trigger(action: Action) -> VizroBaseModel: # type: ignore[return] + """Gets the model that triggers the action with "action_id".""" + from vizro.models._action._actions_chain import ActionsChain + + for actions_chain in cast(Iterable[ActionsChain], model_manager._get_models(ActionsChain)): + if action in actions_chain.actions: + return model_manager[ModelID(str(actions_chain.trigger.component_id))] + + def _get_inputs_of_figure_interactions( page: Page, action_function: Callable[[Any], dict[str, Any]] ) -> list[dict[str, State]]: """Gets list of `States` for selected chart interaction `action_function` of triggered `Page`.""" - figure_interactions_on_page = _get_matching_actions_by_function( - page_id=ModelID(str(page.id)), action_function=action_function - ) + figure_interactions_on_page = _get_matching_actions_by_function(page=page, action_function=action_function) inputs = [] for action in figure_interactions_on_page: - triggered_model = model_manager._get_action_trigger(action_id=ModelID(str(action.id))) + triggered_model = _get_action_trigger(action) required_attributes = ["_filter_interaction_input", "_filter_interaction"] for attribute in required_attributes: if not hasattr(triggered_model, attribute): @@ -60,9 +66,9 @@ def _get_inputs_of_figure_interactions( # TODO: Refactor this and util functions once we implement "_get_input_property" method in VizroBaseModel models -def _get_action_callback_inputs(action_id: ModelID) -> dict[str, list[Union[State, dict[str, State]]]]: +def _get_action_callback_inputs(action: Action) -> dict[str, list[Union[State, dict[str, State]]]]: """Creates mapping of pre-defined action names and a list of `States`.""" - page: Page = model_manager[model_manager._get_model_page_id(model_id=action_id)] + page = model_manager._get_model_page(action) action_input_mapping = { "filters": _get_inputs_of_controls(page=page, control_type=Filter), @@ -76,9 +82,9 @@ def _get_action_callback_inputs(action_id: ModelID) -> dict[str, list[Union[Stat # CALLBACK OUTPUTS -------------- -def _get_action_callback_outputs(action_id: ModelID) -> dict[str, Output]: +def _get_action_callback_outputs(action: Action) -> dict[str, Output]: """Creates mapping of target names and their `Output`.""" - action_function = model_manager[action_id].function._function + action_function = action.function._function # The right solution for mypy here is to not e.g. define new attributes on the base but instead to get mypy to # recognize that model_manager[action_id] is of type Action and hence has the function attribute. @@ -86,7 +92,7 @@ def _get_action_callback_outputs(action_id: ModelID) -> dict[str, Output]: # If not then we can do the cast to Action at the point of consumption here to avoid needing mypy ignores. try: - targets = model_manager[action_id].function["targets"] + targets = action.function["targets"] except KeyError: targets = [] @@ -103,23 +109,23 @@ def _get_action_callback_outputs(action_id: ModelID) -> dict[str, Output]: } -def _get_export_data_callback_outputs(action_id: ModelID) -> dict[str, Output]: +def _get_export_data_callback_outputs(action: Action) -> dict[str, Output]: """Gets mapping of relevant output target name and `Outputs` for `export_data` action.""" - action = model_manager[action_id] - try: targets = action.function["targets"] except KeyError: targets = None - if not targets: - targets = model_manager._get_page_model_ids_with_figure( - page_id=model_manager._get_model_page_id(model_id=action_id) + targets = targets or [ + model.id + for model in cast( + Iterable[VizroBaseModel], model_manager._get_models(FIGURE_MODELS, model_manager._get_model_page(action)) ) + ] return { f"download_dataframe_{target}": Output( - component_id={"type": "download_dataframe", "action_id": action_id, "target_id": target}, + component_id={"type": "download_dataframe", "action_id": action.id, "target_id": target}, component_property="data", ) for target in targets @@ -127,21 +133,21 @@ def _get_export_data_callback_outputs(action_id: ModelID) -> dict[str, Output]: # CALLBACK COMPONENTS -------------- -def _get_export_data_callback_components(action_id: ModelID) -> list[dcc.Download]: +def _get_export_data_callback_components(action: Action) -> list[dcc.Download]: """Creates dcc.Downloads for target components of the `export_data` action.""" - action = model_manager[action_id] - try: targets = action.function["targets"] except KeyError: targets = None - if not targets: - targets = model_manager._get_page_model_ids_with_figure( - page_id=model_manager._get_model_page_id(model_id=action_id) + targets = targets or [ + model.id + for model in cast( + Iterable[VizroBaseModel], model_manager._get_models(FIGURE_MODELS, model_manager._get_model_page(action)) ) + ] return [ - dcc.Download(id={"type": "download_dataframe", "action_id": action_id, "target_id": target}) + dcc.Download(id={"type": "download_dataframe", "action_id": action.id, "target_id": target}) for target in targets ] diff --git a/vizro-core/src/vizro/actions/_callback_mapping/_get_action_callback_mapping.py b/vizro-core/src/vizro/actions/_callback_mapping/_get_action_callback_mapping.py index 10841e18b..5b797835c 100644 --- a/vizro-core/src/vizro/actions/_callback_mapping/_get_action_callback_mapping.py +++ b/vizro-core/src/vizro/actions/_callback_mapping/_get_action_callback_mapping.py @@ -15,15 +15,12 @@ from vizro.actions._filter_action import _filter from vizro.actions._on_page_load_action import _on_page_load from vizro.actions._parameter_action import _parameter -from vizro.managers import model_manager -from vizro.managers._model_manager import ModelID +from vizro.models import Action -def _get_action_callback_mapping( - action_id: ModelID, argument: str -) -> Union[list[dcc.Download], dict[str, DashDependency]]: +def _get_action_callback_mapping(action: Action, argument: str) -> Union[list[dcc.Download], dict[str, DashDependency]]: """Creates mapping of action name and required callback input/output.""" - action_function = model_manager[action_id].function._function + action_function = action.function._function action_callback_mapping: dict[str, Any] = { export_data.__wrapped__: { @@ -50,4 +47,4 @@ def _get_action_callback_mapping( } action_call = action_callback_mapping.get(action_function, {}).get(argument) default_value: Union[list[dcc.Download], dict[str, DashDependency]] = [] if argument == "components" else {} - return default_value if not action_call else action_call(action_id=action_id) + return default_value if not action_call else action_call(action=action) diff --git a/vizro-core/src/vizro/actions/_filter_action.py b/vizro-core/src/vizro/actions/_filter_action.py index f3ec21b37..d50f0125c 100644 --- a/vizro-core/src/vizro/actions/_filter_action.py +++ b/vizro-core/src/vizro/actions/_filter_action.py @@ -32,6 +32,6 @@ def _filter( return _get_modified_page_figures( ctds_filter=ctx.args_grouping["external"]["filters"], ctds_filter_interaction=ctx.args_grouping["external"]["filter_interaction"], - ctds_parameters=ctx.args_grouping["external"]["parameters"], + ctds_parameter=ctx.args_grouping["external"]["parameters"], targets=targets, ) diff --git a/vizro-core/src/vizro/actions/_on_page_load_action.py b/vizro-core/src/vizro/actions/_on_page_load_action.py index 306ed9b5e..c6611fbd5 100644 --- a/vizro-core/src/vizro/actions/_on_page_load_action.py +++ b/vizro-core/src/vizro/actions/_on_page_load_action.py @@ -25,6 +25,6 @@ def _on_page_load(targets: list[ModelID], **inputs: dict[str, Any]) -> dict[Mode return _get_modified_page_figures( ctds_filter=ctx.args_grouping["external"]["filters"], ctds_filter_interaction=ctx.args_grouping["external"]["filter_interaction"], - ctds_parameters=ctx.args_grouping["external"]["parameters"], + ctds_parameter=ctx.args_grouping["external"]["parameters"], targets=targets, ) diff --git a/vizro-core/src/vizro/actions/_parameter_action.py b/vizro-core/src/vizro/actions/_parameter_action.py index 6284481ec..bfc58014f 100644 --- a/vizro-core/src/vizro/actions/_parameter_action.py +++ b/vizro-core/src/vizro/actions/_parameter_action.py @@ -27,6 +27,6 @@ def _parameter(targets: list[str], **inputs: dict[str, Any]) -> dict[ModelID, An return _get_modified_page_figures( ctds_filter=ctx.args_grouping["external"]["filters"], ctds_filter_interaction=ctx.args_grouping["external"]["filter_interaction"], - ctds_parameters=ctx.args_grouping["external"]["parameters"], + ctds_parameter=ctx.args_grouping["external"]["parameters"], targets=target_ids, ) diff --git a/vizro-core/src/vizro/actions/filter_interaction_action.py b/vizro-core/src/vizro/actions/filter_interaction_action.py index bc6659ab9..9618d265f 100644 --- a/vizro-core/src/vizro/actions/filter_interaction_action.py +++ b/vizro-core/src/vizro/actions/filter_interaction_action.py @@ -31,6 +31,6 @@ def filter_interaction(targets: Optional[list[ModelID]] = None, **inputs: dict[s return _get_modified_page_figures( ctds_filter=ctx.args_grouping["external"]["filters"], ctds_filter_interaction=ctx.args_grouping["external"]["filter_interaction"], - ctds_parameters=ctx.args_grouping["external"]["parameters"], + ctds_parameter=ctx.args_grouping["external"]["parameters"], targets=targets or [], ) diff --git a/vizro-core/src/vizro/managers/_model_manager.py b/vizro-core/src/vizro/managers/_model_manager.py index 14081681a..fe19a1e11 100644 --- a/vizro-core/src/vizro/managers/_model_manager.py +++ b/vizro-core/src/vizro/managers/_model_manager.py @@ -4,14 +4,14 @@ import random import uuid -from collections.abc import Generator -from typing import TYPE_CHECKING, NewType, Optional, TypeVar, cast +from collections.abc import Generator, Iterable +from typing import TYPE_CHECKING, NewType, Optional, TypeVar, Union, cast from vizro.managers._managers_utils import _state_modifier if TYPE_CHECKING: - from vizro.models import VizroBaseModel - from vizro.models._action._actions_chain import ActionsChain + from vizro.models import Page, VizroBaseModel + # As done for Dash components in dash.development.base_component, fixing the random seed is required to make sure that # the randomly generated model ID for the same model matches up across workers when running gunicorn without --preload. @@ -21,6 +21,13 @@ Model = TypeVar("Model", bound="VizroBaseModel") +# Sentinel object for models that are reactive to controls. This can't be done directly by defining +# FIGURE_MODELS = (Graph, ...) due to circular imports. Done as class for mypy. +# https://stackoverflow.com/questions/69239403/type-hinting-parameters-with-a-sentinel-value-as-the-default +class FIGURE_MODELS: + pass + + class DuplicateIDError(ValueError): """Useful for providing a more explicit error message when a model has id set automatically, e.g. Page.""" @@ -50,90 +57,71 @@ def __iter__(self) -> Generator[ModelID, None, None]: Note this yields model IDs rather key/value pairs to match the interface for a dictionary. """ + # TODO: should this yield models rather than model IDs? Should model_manager be more like set with a special + # lookup by model ID or more like dictionary? yield from self.__models - # TODO: Consider adding an option to iterate only through specific page - "in_page_with_id=None" - def _items_with_type(self, model_type: type[Model]) -> Generator[tuple[ModelID, Model], None, None]: - """Iterates through all models of type `model_type` (including subclasses).""" - for model_id in self: - if isinstance(self[model_id], model_type): - yield model_id, cast(Model, self[model_id]) - - # TODO: Consider returning with yield - # TODO: Make collection of model ids (throughout this file) to be set[ModelID]. - def _get_model_children(self, model_id: ModelID, all_model_ids: Optional[list[ModelID]] = None) -> list[ModelID]: - if all_model_ids is None: - all_model_ids = [] - - all_model_ids.append(model_id) - model = self[model_id] - if hasattr(model, "components"): - for child_model in model.components: - self._get_model_children(child_model.id, all_model_ids) - if hasattr(model, "tabs"): - for child_model in model.tabs: - self._get_model_children(child_model.id, all_model_ids) - return all_model_ids - - # TODO: Consider moving this method in the Dashboard model or some other util file - def _get_model_page_id(self, model_id: ModelID) -> ModelID: # type: ignore[return] - """Gets the id of the page containing the model with "model_id".""" + def _get_models( + self, + model_type: Optional[Union[type[Model], tuple[type[Model], ...], type[FIGURE_MODELS]]] = None, + page: Optional[Page] = None, + ) -> Generator[Model, None, None]: + """Iterates through all models of type `model_type` (including subclasses). + + If `model_type` not given then look at all models. If `page` specified then only give models from that page. + """ + import vizro.models as vm + + if model_type is FIGURE_MODELS: + model_type = (vm.Graph, vm.AgGrid, vm.Table, vm.Figure) + models = self.__get_model_children(page) if page is not None else self.__models.values() + + # Convert to list to avoid changing size when looping through at runtime. + for model in list(models): + if model_type is None or isinstance(model, model_type): + yield model + + def __get_model_children(self, model: Model) -> Generator[Model, None, None]: + """Iterates through children of `model`. + + Currently looks only through certain fields so might miss some children models. + """ + from vizro.models import VizroBaseModel + + if isinstance(model, VizroBaseModel): + yield model + + # TODO: in future this list should not be maintained manually. Instead we should look through all model children + # by looking at model.model_fields. + model_fields = ["components", "tabs", "controls", "actions", "selector"] + + for model_field in model_fields: + if (model_field_value := getattr(model, model_field, None)) is not None: + if isinstance(model_field_value, list): + # For fields like components that are list of models. + for single_model_field_value in model_field_value: + yield from self.__get_model_children(single_model_field_value) + else: + # For fields that have single model like selector. + yield from self.__get_model_children(model_field_value) + # We don't handle dicts of models at the moment. See below TODO for how this will all be improved in + # future. + + # TODO: Add navigation, accordions and other page objects. Won't be needed once have made whole model + # manager work better recursively and have better ways to navigate the hierarchy. In pydantic v2 this would use + # model_fields. Maybe we'd also use Page (or sometimes Dashboard) as the central model for navigating the + # hierarchy rather than it being so generic. + + def _get_model_page(self, model: Model) -> Page: # type: ignore[return] + """Gets the page containing `model`.""" from vizro.models import Page - for page_id, page in model_manager._items_with_type(Page): - page_model_ids = [page_id, self._get_model_children(model_id=page_id)] - - for actions_chain in self._get_page_actions_chains(page_id=page_id): - page_model_ids.append(actions_chain.id) - for action in actions_chain.actions: - page_model_ids.append(action.id) # noqa: PERF401 - - for control in page.controls: - page_model_ids.append(control.id) - if hasattr(control, "selector") and control.selector: - page_model_ids.append(control.selector.id) - - # TODO: Add navigation, accordions and other page objects - - if model_id in page_model_ids: - return cast(ModelID, page.id) - - # TODO: Increase the genericity of this method - def _get_page_actions_chains(self, page_id: ModelID) -> list[ActionsChain]: - """Gets all ActionsChains present on the page.""" - page = self[page_id] - page_actions_chains = [] - - for model_id in self._get_model_children(model_id=page_id): - model = self[model_id] - if hasattr(model, "actions"): - page_actions_chains.extend(model.actions) - - for control in page.controls: - if hasattr(control, "actions") and control.actions: - page_actions_chains.extend(control.actions) - if hasattr(control, "selector") and control.selector and hasattr(control.selector, "actions"): - page_actions_chains.extend(control.selector.actions) - - return page_actions_chains - - # TODO: Consider moving this one to the _callback_mapping_utils.py since it's only used there - def _get_action_trigger(self, action_id: ModelID) -> VizroBaseModel: # type: ignore[return] - """Gets the model that triggers the action with "action_id".""" - from vizro.models._action._actions_chain import ActionsChain - - for _, actions_chain in model_manager._items_with_type(ActionsChain): - if action_id in [action.id for action in actions_chain.actions]: - return self[ModelID(str(actions_chain.trigger.component_id))] - - def _get_page_model_ids_with_figure(self, page_id: ModelID) -> list[ModelID]: - """Gets ids of all components from the page that have a 'figure' registered.""" - return [ - model_id - for model_id in self._get_model_children(model_id=page_id) - # Optimally this statement should be: "if isinstance(model, Figure)" - if hasattr(model_manager[model_id], "figure") - ] + if isinstance(model, Page): + return model + + for page in cast(Iterable[Page], self._get_models(Page)): + if model in self.__get_model_children(page): + return page @staticmethod def _generate_id() -> ModelID: diff --git a/vizro-core/src/vizro/models/_action/_action.py b/vizro-core/src/vizro/models/_action/_action.py index 9aac00ca1..0aef7303b 100644 --- a/vizro-core/src/vizro/models/_action/_action.py +++ b/vizro-core/src/vizro/models/_action/_action.py @@ -11,7 +11,6 @@ except ImportError: # pragma: no cov from pydantic import Field, validator -from vizro.managers._model_manager import ModelID from vizro.models import VizroBaseModel from vizro.models._models_utils import _log_call from vizro.models.types import CapturedCallable @@ -79,7 +78,7 @@ def _get_callback_mapping(self): if self.inputs: callback_inputs = [State(*input.split(".")) for input in self.inputs] else: - callback_inputs = _get_action_callback_mapping(action_id=ModelID(str(self.id)), argument="inputs") + callback_inputs = _get_action_callback_mapping(self, argument="inputs") callback_outputs: Union[list[Output], dict[str, Output]] if self.outputs: @@ -91,9 +90,9 @@ def _get_callback_mapping(self): if len(callback_outputs) == 1: callback_outputs = callback_outputs[0] else: - callback_outputs = _get_action_callback_mapping(action_id=ModelID(str(self.id)), argument="outputs") + callback_outputs = _get_action_callback_mapping(self, argument="outputs") - action_components = _get_action_callback_mapping(action_id=ModelID(str(self.id)), argument="components") + action_components = _get_action_callback_mapping(self, argument="components") return callback_inputs, callback_outputs, action_components diff --git a/vizro-core/src/vizro/models/_base.py b/vizro-core/src/vizro/models/_base.py index 40b00deb6..d2f47470e 100644 --- a/vizro-core/src/vizro/models/_base.py +++ b/vizro-core/src/vizro/models/_base.py @@ -127,7 +127,7 @@ def _extract_captured_callable_source() -> set[str]: # Check to see if the captured callable does use a cleaned module string, if yes then # we can assume that the source code can be imported via Vizro, and thus does not need to be defined value.__repr_clean__().startswith(new) - for _, new in REPLACEMENT_STRINGS.items() + for new in REPLACEMENT_STRINGS.values() ): try: source = textwrap.dedent(inspect.getsource(value._function)) diff --git a/vizro-core/src/vizro/models/_components/ag_grid.py b/vizro-core/src/vizro/models/_components/ag_grid.py index 25c8aef18..52b1e36a5 100644 --- a/vizro-core/src/vizro/models/_components/ag_grid.py +++ b/vizro-core/src/vizro/models/_components/ag_grid.py @@ -10,7 +10,7 @@ from pydantic import Field, PrivateAttr, validator from dash import ClientsideFunction, Input, Output, clientside_callback -from vizro.actions._actions_utils import CallbackTriggerDict, _get_component_actions, _get_parent_vizro_model +from vizro.actions._actions_utils import CallbackTriggerDict, _get_component_actions, _get_parent_model from vizro.managers import data_manager from vizro.models import Action, VizroBaseModel from vizro.models._action._actions_chain import _action_validator_factory @@ -100,7 +100,7 @@ def _filter_interaction( return data_frame # ctd_active_cell["id"] represents the underlying table id, so we need to fetch its parent Vizro Table actions. - source_table_actions = _get_component_actions(_get_parent_vizro_model(ctd_cellClicked["id"])) + source_table_actions = _get_component_actions(_get_parent_model(ctd_cellClicked["id"])) for action in source_table_actions: if action.function._function.__name__ != "filter_interaction" or target not in action.function["targets"]: @@ -119,7 +119,7 @@ def build(self): clientside_callback( ClientsideFunction(namespace="dashboard", function_name="update_ag_grid_theme"), Output(self._input_component_id, "className"), - Input("theme_selector", "checked"), + Input("theme-selector", "value"), ) return dcc.Loading( diff --git a/vizro-core/src/vizro/models/_components/button.py b/vizro-core/src/vizro/models/_components/button.py index 500c8800b..6c95617a2 100644 --- a/vizro-core/src/vizro/models/_components/button.py +++ b/vizro-core/src/vizro/models/_components/button.py @@ -1,6 +1,7 @@ from typing import Literal import dash_bootstrap_components as dbc +from dash import get_relative_path try: from pydantic.v1 import Field @@ -24,6 +25,7 @@ class Button(VizroBaseModel): type: Literal["button"] = "button" text: str = Field("Click me!", description="Text to be displayed on button.") + href: str = Field("", description="URL (relative or absolute) to navigate to.") actions: list[Action] = [] # Re-used validators @@ -31,4 +33,9 @@ class Button(VizroBaseModel): @_log_call def build(self): - return dbc.Button(id=self.id, children=self.text) + return dbc.Button( + id=self.id, + children=self.text, + href=get_relative_path(self.href) if self.href.startswith("/") else self.href, + target="_top", + ) diff --git a/vizro-core/src/vizro/models/_components/form/_form_utils.py b/vizro-core/src/vizro/models/_components/form/_form_utils.py index 18e666882..14a20a169 100644 --- a/vizro-core/src/vizro/models/_components/form/_form_utils.py +++ b/vizro-core/src/vizro/models/_components/form/_form_utils.py @@ -54,6 +54,9 @@ def validate_value(cls, value, values): [entry["value"] for entry in values["options"]] if isinstance(values["options"][0], dict) else values["options"] ) + if hasattr(value, "__iter__") and ALL_OPTION in value: + return value + if value and not is_value_contained(value, possible_values): raise ValueError("Please provide a valid value from `options`.") diff --git a/vizro-core/src/vizro/models/_components/form/_text_area.py b/vizro-core/src/vizro/models/_components/form/_text_area.py index 5ac25fdb7..bb1ea7fa1 100644 --- a/vizro-core/src/vizro/models/_components/form/_text_area.py +++ b/vizro-core/src/vizro/models/_components/form/_text_area.py @@ -4,9 +4,9 @@ from dash import html try: - from pydantic.v1 import Field + from pydantic.v1 import Field, PrivateAttr except ImportError: # pragma: no cov - from pydantic import Field + from pydantic import Field, PrivateAttr from vizro.models import Action, VizroBaseModel from vizro.models._action._actions_chain import _action_validator_factory @@ -32,6 +32,9 @@ class TextArea(VizroBaseModel): placeholder: str = Field("", description="Default text to display in input field") actions: list[Action] = [] + # Component properties for actions and interactions + _input_property: str = PrivateAttr("value") + # Re-used validators # TODO: Before making public, consider how actions should be triggered and what the default property should be # See comment thread: https://github.com/mckinsey/vizro/pull/298#discussion_r1478137654 diff --git a/vizro-core/src/vizro/models/_components/form/_user_input.py b/vizro-core/src/vizro/models/_components/form/_user_input.py index bb98a14bb..7ca821f65 100644 --- a/vizro-core/src/vizro/models/_components/form/_user_input.py +++ b/vizro-core/src/vizro/models/_components/form/_user_input.py @@ -4,9 +4,9 @@ from dash import html try: - from pydantic.v1 import Field + from pydantic.v1 import Field, PrivateAttr except ImportError: # pragma: no cov - from pydantic import Field + from pydantic import Field, PrivateAttr from vizro.models import Action, VizroBaseModel from vizro.models._action._actions_chain import _action_validator_factory @@ -32,6 +32,9 @@ class UserInput(VizroBaseModel): placeholder: str = Field("", description="Default text to display in input field") actions: list[Action] = [] + # Component properties for actions and interactions + _input_property: str = PrivateAttr("value") + # Re-used validators # TODO: Before making public, consider how actions should be triggered and what the default property should be # See comment thread: https://github.com/mckinsey/vizro/pull/298#discussion_r1478137654 diff --git a/vizro-core/src/vizro/models/_components/form/checklist.py b/vizro-core/src/vizro/models/_components/form/checklist.py index 68cb26ad1..d69e725cc 100644 --- a/vizro-core/src/vizro/models/_components/form/checklist.py +++ b/vizro-core/src/vizro/models/_components/form/checklist.py @@ -38,6 +38,8 @@ class Checklist(VizroBaseModel): title: str = Field("", description="Title to be displayed") actions: list[Action] = [] + _dynamic: bool = PrivateAttr(False) + # Component properties for actions and interactions _input_property: str = PrivateAttr("value") @@ -46,9 +48,8 @@ class Checklist(VizroBaseModel): _validate_options = root_validator(allow_reuse=True, pre=True)(validate_options_dict) _validate_value = validator("value", allow_reuse=True, always=True)(validate_value) - @_log_call - def build(self): - full_options, default_value = get_options_and_default(options=self.options, multi=True) + def __call__(self, options): + full_options, default_value = get_options_and_default(options=options, multi=True) return html.Fieldset( children=[ @@ -62,3 +63,14 @@ def build(self): ), ] ) + + def _build_dynamic_placeholder(self): + if self.value is None: + _, default_value = get_options_and_default(self.options, multi=True) + self.value = [default_value] + + return self.__call__(self.options) + + @_log_call + def build(self): + return self._build_dynamic_placeholder() if self._dynamic else self.__call__(self.options) diff --git a/vizro-core/src/vizro/models/_components/form/dropdown.py b/vizro-core/src/vizro/models/_components/form/dropdown.py index d0fa24444..aa7f89660 100755 --- a/vizro-core/src/vizro/models/_components/form/dropdown.py +++ b/vizro-core/src/vizro/models/_components/form/dropdown.py @@ -65,6 +65,10 @@ class Dropdown(VizroBaseModel): title: str = Field("", description="Title to be displayed") actions: list[Action] = [] + # Consider making the _dynamic public later. The same property could also be used for all other components. + # For example: vm.Graph could have a dynamic that is by default set on True. + _dynamic: bool = PrivateAttr(False) + # Component properties for actions and interactions _input_property: str = PrivateAttr("value") @@ -82,9 +86,8 @@ def validate_multi(cls, multi, values): raise ValueError("Please set multi=True if providing a list of default values.") return multi - @_log_call - def build(self): - full_options, default_value = get_options_and_default(options=self.options, multi=self.multi) + def __call__(self, options): + full_options, default_value = get_options_and_default(options=options, multi=self.multi) option_height = _calculate_option_height(full_options) return html.Div( @@ -95,9 +98,24 @@ def build(self): options=full_options, value=self.value if self.value is not None else default_value, multi=self.multi, - persistence=True, optionHeight=option_height, + persistence=True, persistence_type="session", ), ] ) + + def _build_dynamic_placeholder(self): + # Setting self.value is kind of Dropdown pre_build method. It sets self.value only the first time if it's None. + # We cannot create pre_build for the Dropdown because it has to be called after vm.Filter.pre_build, but nothing + # guarantees that. We can call Filter.selector.pre_build() from the Filter.pre_build() method if we decide that. + # TODO: move this to pre_build once we have better control of the ordering. + if self.value is None: + _, default_value = get_options_and_default(self.options, self.multi) + self.value = default_value + + return self.__call__(self.options) + + @_log_call + def build(self): + return self._build_dynamic_placeholder() if self._dynamic else self.__call__(self.options) diff --git a/vizro-core/src/vizro/models/_components/form/radio_items.py b/vizro-core/src/vizro/models/_components/form/radio_items.py index dfa282126..25b67beef 100644 --- a/vizro-core/src/vizro/models/_components/form/radio_items.py +++ b/vizro-core/src/vizro/models/_components/form/radio_items.py @@ -39,6 +39,8 @@ class RadioItems(VizroBaseModel): title: str = Field("", description="Title to be displayed") actions: list[Action] = [] + _dynamic: bool = PrivateAttr(False) + # Component properties for actions and interactions _input_property: str = PrivateAttr("value") @@ -47,9 +49,8 @@ class RadioItems(VizroBaseModel): _validate_options = root_validator(allow_reuse=True, pre=True)(validate_options_dict) _validate_value = validator("value", allow_reuse=True, always=True)(validate_value) - @_log_call - def build(self): - full_options, default_value = get_options_and_default(options=self.options, multi=False) + def __call__(self, options): + full_options, default_value = get_options_and_default(options=options, multi=False) return html.Fieldset( children=[ @@ -63,3 +64,14 @@ def build(self): ), ] ) + + def _build_dynamic_placeholder(self): + if self.value is None: + _, default_value = get_options_and_default(self.options, multi=False) + self.value = default_value + + return self.__call__(self.options) + + @_log_call + def build(self): + return self._build_dynamic_placeholder() if self._dynamic else self.__call__(self.options) diff --git a/vizro-core/src/vizro/models/_components/form/range_slider.py b/vizro-core/src/vizro/models/_components/form/range_slider.py index 16f0cb8c9..a96521708 100644 --- a/vizro-core/src/vizro/models/_components/form/range_slider.py +++ b/vizro-core/src/vizro/models/_components/form/range_slider.py @@ -50,6 +50,8 @@ class RangeSlider(VizroBaseModel): title: str = Field("", description="Title to be displayed.") actions: list[Action] = [] + _dynamic: bool = PrivateAttr(False) + # Component properties for actions and interactions _input_property: str = PrivateAttr("value") @@ -60,10 +62,7 @@ class RangeSlider(VizroBaseModel): _set_default_marks = validator("marks", allow_reuse=True, always=True)(set_default_marks) _set_actions = _action_validator_factory("value") - @_log_call - def build(self): - init_value = self.value or [self.min, self.max] # type: ignore[list-item] - + def __call__(self, min, max, current_value): output = [ Output(f"{self.id}_start_value", "value"), Output(f"{self.id}_end_value", "value"), @@ -86,7 +85,7 @@ def build(self): return html.Div( children=[ - dcc.Store(f"{self.id}_callback_data", data={"id": self.id, "min": self.min, "max": self.max}), + dcc.Store(f"{self.id}_callback_data", data={"id": self.id, "min": min, "max": max}), html.Div( children=[ dbc.Label(children=self.title, html_for=self.id) if self.title else None, @@ -96,10 +95,10 @@ def build(self): id=f"{self.id}_start_value", type="number", placeholder="min", - min=self.min, - max=self.max, + min=min, + max=max, step=self.step, - value=init_value[0], + value=current_value[0], persistence=True, persistence_type="session", className="slider-text-input-field", @@ -109,15 +108,15 @@ def build(self): id=f"{self.id}_end_value", type="number", placeholder="max", - min=self.min, - max=self.max, + min=min, + max=max, step=self.step, - value=init_value[1], + value=current_value[1], persistence=True, persistence_type="session", className="slider-text-input-field", ), - dcc.Store(id=f"{self.id}_input_store", storage_type="session", data=init_value), + dcc.Store(id=f"{self.id}_input_store", storage_type="session"), ], className="slider-text-input-container", ), @@ -126,14 +125,26 @@ def build(self): ), dcc.RangeSlider( id=self.id, - min=self.min, - max=self.max, + min=min, + max=max, step=self.step, marks=self.marks, - value=init_value, + value=current_value, persistence=True, persistence_type="session", className="slider-track-without-marks" if self.marks is None else "slider-track-with-marks", ), ] ) + + def _build_dynamic_placeholder(self, current_value): + return self.__call__(self.min, self.max, current_value) + + @_log_call + def build(self): + current_value = self.value or [self.min, self.max] # type: ignore[list-item] + return ( + self._build_dynamic_placeholder(current_value) + if self._dynamic + else self.__call__(self.min, self.max, current_value) + ) diff --git a/vizro-core/src/vizro/models/_components/form/slider.py b/vizro-core/src/vizro/models/_components/form/slider.py index 65b37fe9a..2ffdb9f6a 100644 --- a/vizro-core/src/vizro/models/_components/form/slider.py +++ b/vizro-core/src/vizro/models/_components/form/slider.py @@ -48,6 +48,8 @@ class Slider(VizroBaseModel): title: str = Field("", description="Title to be displayed.") actions: list[Action] = [] + _dynamic: bool = PrivateAttr(False) + # Component properties for actions and interactions _input_property: str = PrivateAttr("value") @@ -58,10 +60,7 @@ class Slider(VizroBaseModel): _set_default_marks = validator("marks", allow_reuse=True, always=True)(set_default_marks) _set_actions = _action_validator_factory("value") - @_log_call - def build(self): - init_value = self.value or self.min - + def __call__(self, min, max, current_value): output = [ Output(f"{self.id}_end_value", "value"), Output(self.id, "value"), @@ -82,7 +81,7 @@ def build(self): return html.Div( children=[ - dcc.Store(f"{self.id}_callback_data", data={"id": self.id, "min": self.min, "max": self.max}), + dcc.Store(f"{self.id}_callback_data", data={"id": self.id, "min": min, "max": max}), html.Div( children=[ dbc.Label(children=self.title, html_for=self.id) if self.title else None, @@ -92,15 +91,15 @@ def build(self): id=f"{self.id}_end_value", type="number", placeholder="max", - min=self.min, - max=self.max, + min=min, + max=max, step=self.step, - value=init_value, + value=current_value, persistence=True, persistence_type="session", className="slider-text-input-field", ), - dcc.Store(id=f"{self.id}_input_store", storage_type="session", data=init_value), + dcc.Store(id=f"{self.id}_input_store", storage_type="session"), ], className="slider-text-input-container", ), @@ -109,11 +108,11 @@ def build(self): ), dcc.Slider( id=self.id, - min=self.min, - max=self.max, + min=min, + max=max, step=self.step, marks=self.marks, - value=init_value, + value=current_value, included=False, persistence=True, persistence_type="session", @@ -121,3 +120,15 @@ def build(self): ), ] ) + + def _build_dynamic_placeholder(self, current_value): + return self.__call__(self.min, self.max, current_value) + + @_log_call + def build(self): + current_value = self.value if self.value is not None else self.min + return ( + self._build_dynamic_placeholder(current_value) + if self._dynamic + else self.__call__(self.min, self.max, current_value) + ) diff --git a/vizro-core/src/vizro/models/_components/graph.py b/vizro-core/src/vizro/models/_components/graph.py index 1250a2992..8772975c6 100644 --- a/vizro-core/src/vizro/models/_components/graph.py +++ b/vizro-core/src/vizro/models/_components/graph.py @@ -168,7 +168,7 @@ def build(self): output=[Output(self.id, "figure"), Output(self.id, "style")], inputs=[ Input(self.id, "figure"), - Input("theme_selector", "checked"), + Input("theme-selector", "value"), State("vizro_themes", "data"), ], prevent_initial_call=True, diff --git a/vizro-core/src/vizro/models/_components/table.py b/vizro-core/src/vizro/models/_components/table.py index 8ffb4ea8d..edfea4c5c 100644 --- a/vizro-core/src/vizro/models/_components/table.py +++ b/vizro-core/src/vizro/models/_components/table.py @@ -9,7 +9,7 @@ except ImportError: # pragma: no cov from pydantic import Field, PrivateAttr, validator -from vizro.actions._actions_utils import CallbackTriggerDict, _get_component_actions, _get_parent_vizro_model +from vizro.actions._actions_utils import CallbackTriggerDict, _get_component_actions, _get_parent_model from vizro.managers import data_manager from vizro.models import Action, VizroBaseModel from vizro.models._action._actions_chain import _action_validator_factory @@ -104,7 +104,7 @@ def _filter_interaction( return data_frame # ctd_active_cell["id"] represents the underlying table id, so we need to fetch its parent Vizro Table actions. - source_table_actions = _get_component_actions(_get_parent_vizro_model(ctd_active_cell["id"])) + source_table_actions = _get_component_actions(_get_parent_model(ctd_active_cell["id"])) for action in source_table_actions: if action.function._function.__name__ != "filter_interaction" or target not in action.function["targets"]: diff --git a/vizro-core/src/vizro/models/_components/tabs.py b/vizro-core/src/vizro/models/_components/tabs.py index bd19f40cf..099d2f1d9 100644 --- a/vizro-core/src/vizro/models/_components/tabs.py +++ b/vizro-core/src/vizro/models/_components/tabs.py @@ -2,8 +2,7 @@ from typing import TYPE_CHECKING, Literal -import dash_mantine_components as dmc -from dash import html +import dash_bootstrap_components as dbc try: from pydantic.v1 import validator @@ -33,21 +32,9 @@ class Tabs(VizroBaseModel): @_log_call def build(self): - tabs_list = dmc.TabsList( - [dmc.Tab(tab.title, value=tab.id, className="tab-title") for tab in self.tabs], - className="tabs-list", - ) - - tabs_panels = [ - dmc.TabsPanel(html.Div([tab.build()], className="tab-content"), value=tab.id, className="tabs-panel") - for tab in self.tabs - ] - - return dmc.Tabs( + return dbc.Tabs( id=self.id, - value=self.tabs[0].id, - children=[tabs_list, *tabs_panels], + children=[dbc.Tab(tab.build(), label=tab.title) for tab in self.tabs], persistence=True, persistence_type="session", - className="tabs", ) diff --git a/vizro-core/src/vizro/models/_controls/filter.py b/vizro-core/src/vizro/models/_controls/filter.py index 683e7f870..8a18add8e 100644 --- a/vizro-core/src/vizro/models/_controls/filter.py +++ b/vizro-core/src/vizro/models/_controls/filter.py @@ -1,9 +1,10 @@ from __future__ import annotations -from typing import Any, Literal, Union +from collections.abc import Iterable +from typing import Any, Literal, Union, cast -import numpy as np import pandas as pd +from dash import dcc from pandas.api.types import is_datetime64_any_dtype, is_numeric_dtype from vizro.managers._data_manager import DataSourceName @@ -13,10 +14,11 @@ except ImportError: # pragma: no cov from pydantic import Field, PrivateAttr, validator -from vizro._constants import FILTER_ACTION_PREFIX +from vizro._constants import ALL_OPTION, FILTER_ACTION_PREFIX from vizro.actions import _filter from vizro.managers import data_manager, model_manager -from vizro.managers._model_manager import ModelID +from vizro.managers._data_manager import _DynamicData +from vizro.managers._model_manager import FIGURE_MODELS, ModelID from vizro.models import Action, VizroBaseModel from vizro.models._components.form import ( Checklist, @@ -46,6 +48,10 @@ "categorical": SELECTORS["numerical"] + SELECTORS["temporal"], } +# TODO: Remove DYNAMIC_SELECTORS along with its validation check when support dynamic mode for the DatePicker selector. +# Tuple of filter selectors that support dynamic mode +DYNAMIC_SELECTORS = (Dropdown, Checklist, RadioItems, Slider, RangeSlider) + def _filter_between(series: pd.Series, value: Union[list[float], list[str]]) -> pd.Series: if is_datetime64_any_dtype(series): @@ -88,6 +94,12 @@ class Filter(VizroBaseModel): "If none are given then target all components on the page that use `column`.", ) selector: SelectorType = None + + _dynamic: bool = PrivateAttr(False) + + # Component properties for actions and interactions + _output_component_property: str = PrivateAttr("children") + _column_type: Literal["numerical", "categorical", "temporal"] = PrivateAttr() @validator("targets", each_item=True) @@ -96,24 +108,53 @@ def check_target_present(cls, target): raise ValueError(f"Target {target} not found in model_manager.") return target + def __call__(self, target_to_data_frame: dict[ModelID, pd.DataFrame], current_value: Any): + # Only relevant for a dynamic filter. + # Although targets are fixed at build time, the validation logic is repeated during runtime, so if a column + # is missing then it will raise an error. We could change this if we wanted. + targeted_data = self._validate_targeted_data( + {target: data_frame for target, data_frame in target_to_data_frame.items() if target in self.targets}, + eagerly_raise_column_not_found_error=True, + ) + + if (column_type := self._validate_column_type(targeted_data)) != self._column_type: + raise ValueError( + f"{self.column} has changed type from {self._column_type} to {column_type}. A filtered column cannot " + "change type while the dashboard is running." + ) + + if isinstance(self.selector, SELECTORS["categorical"]): + return self.selector(options=self._get_options(targeted_data, current_value)) + else: + _min, _max = self._get_min_max(targeted_data, current_value) + # "current_value" is propagated only to support dcc.Input and dcc.Store components in numerical selectors + # to work with a dynamic selector. This can be removed when dash persistence bug is fixed. + return self.selector(min=_min, max=_max, current_value=current_value) + @_log_call def pre_build(self): # If targets aren't explicitly provided then try to target all figures on the page. In this case we don't # want to raise an error if the column is not found in a figure's data_frame, it will just be ignored. # This is the case when bool(self.targets) is False. # Possibly in future this will change (which would be breaking change). - proposed_targets = self.targets or model_manager._get_page_model_ids_with_figure( - page_id=model_manager._get_model_page_id(model_id=ModelID(str(self.id))) - ) - # TODO NEXT: how to handle pre_build for dynamic filters? Do we still require default argument values in - # `load` to establish selector type etc.? Can we take selector values from model_manager to supply these? - # Or just don't do validation at pre_build time and wait until state is available during build time instead? - # What should the load kwargs be here? Remember they need to be {} for static data. - # Note that currently _get_unfiltered_data is only suitable for use at runtime since it requires - # ctd_parameters. That could be changed to just reuse that function. + proposed_targets = self.targets or [ + cast(ModelID, model.id) + for model in cast( + Iterable[VizroBaseModel], model_manager._get_models(FIGURE_MODELS, model_manager._get_model_page(self)) + ) + ] + # TODO: Currently dynamic data functions require a default value for every argument. Even when there is a + # dataframe parameter, the default value is used when pre-build the filter e.g. to find the targets, + # column type (and hence selector) and initial values. There are three ways to handle this: + # 1. (Current approach) - Propagate {} and use only default arguments value in the dynamic data function. + # 2. Propagate values from the model_manager and relax the limitation of requiring argument default values. + # 3. Skip the pre-build and do everything in the build method (if possible). + # Find more about the mentioned limitation at: https://github.com/mckinsey/vizro/pull/879/files#r1846609956 + # Even if the solution changes for dynamic data, static data should still use {} as the arguments here. multi_data_source_name_load_kwargs: list[tuple[DataSourceName, dict[str, Any]]] = [ (model_manager[target]["data_frame"], {}) for target in proposed_targets ] + target_to_data_frame = dict(zip(proposed_targets, data_manager._multi_load(multi_data_source_name_load_kwargs))) targeted_data = self._validate_targeted_data( target_to_data_frame, eagerly_raise_column_not_found_error=bool(self.targets) @@ -131,6 +172,23 @@ def pre_build(self): f"'{self.column}'." ) + # Check if the filter is dynamic. Dynamic filter means that the filter is updated when the page is refreshed + # which causes "options" for categorical or "min" and "max" for numerical/temporal selectors to be updated. + # The filter is dynamic iff mentioned attributes ("options"/"min"/"max") are not explicitly provided and + # filter targets at least one figure that uses dynamic data source. Note that min or max = 0 are Falsey values + # but should still count as manually set. + if isinstance(self.selector, DYNAMIC_SELECTORS) and ( + not getattr(self.selector, "options", []) + and getattr(self.selector, "min", None) is None + and getattr(self.selector, "max", None) is None + ): + for target_id in self.targets: + data_source_name = model_manager[target_id]["data_frame"] + if isinstance(data_manager[data_source_name], _DynamicData): + self._dynamic = True + self.selector._dynamic = True + break + # Set appropriate properties for the selector. if isinstance(self.selector, SELECTORS["numerical"] + SELECTORS["temporal"]): _min, _max = self._get_min_max(targeted_data) @@ -158,32 +216,33 @@ def pre_build(self): ) ] - def __call__(self, target_to_data_frame: dict[ModelID, pd.DataFrame]): - # Only relevant for a dynamic filter. - # Although targets are fixed at build time, the validation logic is repeated during runtime, so if a column - # is missing then it will raise an error. We could change this if we wanted. - # Call this from actions_utils - targeted_data = self._validate_targeted_data( - {target: data_frame for target, data_frame in target_to_data_frame.items() if target in self.targets}, - eagerly_raise_column_not_found_error=True, - ) - - if (column_type := self._validate_column_type(targeted_data)) != self._column_type: - raise ValueError( - f"{self.column} has changed type from {self._column_type} to {column_type}. A filtered column cannot " - "change type while the dashboard is running." - ) - - # TODO: when implement dynamic, will need to do something with this e.g. pass to selector.__call__. - # if isinstance(self.selector, SELECTORS["numerical"] + SELECTORS["temporal"]): - # options = self._get_options(targeted_data) - # else: - # # Categorical selector. - # _min, _max = self._get_min_max(targeted_data) - @_log_call def build(self): - return self.selector.build() + selector_build_obj = self.selector.build() + # TODO: Align the (dynamic) object's return structure with the figure's components when the Dash bug is fixed. + # This means returning an empty "html.Div(id=self.id, className=...)" as a placeholder from Filter.build(). + # Also, make selector.title visible when the filter is reloading. + if not self._dynamic: + return selector_build_obj + + # Temporarily hide the selector and numeric dcc.Input components during the filter reloading process. + # Other components, such as the title, remain visible because of the configuration: + # overlay_style={"visibility": "visible"} in dcc.Loading. + # Note: dcc.Slider and dcc.RangeSlider do not support the "style" property directly, + # so the "className" attribute is used to apply custom CSS for visibility control. + # Reference for Dash class names: https://dashcheatsheet.pythonanywhere.com/ + selector_build_obj[self.selector.id].className = "invisible" + if f"{self.selector.id}_start_value" in selector_build_obj: + selector_build_obj[f"{self.selector.id}_start_value"].className = "d-none" + if f"{self.selector.id}_end_value" in selector_build_obj: + selector_build_obj[f"{self.selector.id}_end_value"].className = "d-none" + + return dcc.Loading( + id=self.id, + children=selector_build_obj, + color="grey", + overlay_style={"visibility": "visible"}, + ) def _validate_targeted_data( self, target_to_data_frame: dict[ModelID, pd.DataFrame], eagerly_raise_column_not_found_error @@ -205,6 +264,7 @@ def _validate_targeted_data( f"Selected column {self.column} not found in any dataframe for " f"{', '.join(target_to_data_frame.keys())}." ) + # TODO: Enable empty data_frame handling if targeted_data.empty: raise ValueError( f"Selected column {self.column} does not contain anything in any dataframe for " @@ -231,17 +291,24 @@ def _validate_column_type(self, targeted_data: pd.DataFrame) -> Literal["numeric ) @staticmethod - def _get_min_max(targeted_data: pd.DataFrame) -> tuple[float, float]: + def _get_min_max(targeted_data: pd.DataFrame, current_value=None) -> tuple[float, float]: + targeted_data = pd.concat([targeted_data, pd.Series(current_value)]).stack().dropna() # noqa: PD013 + + _min = targeted_data.min(axis=None) + _max = targeted_data.max(axis=None) + # Use item() to convert to convert scalar from numpy to Python type. This isn't needed during pre_build because # pydantic will coerce the type, but it is necessary in __call__ where we don't update model field values # and instead just pass straight to the Dash component. - return targeted_data.min(axis=None).item(), targeted_data.max(axis=None).item() + # However, in some cases _min and _max are already Python types and so item() call is not required. + _min = _min if not hasattr(_min, "item") else _min.item() + _max = _max if not hasattr(_max, "item") else _max.item() + + return _min, _max @staticmethod - def _get_options(targeted_data: pd.DataFrame) -> list[Any]: - # Use tolist() to convert to convert scalar from numpy to Python type. This isn't needed during pre_build - # because pydantic will coerce the type, but it is necessary in __call__ where we don't update model field - # values and instead just pass straight to the Dash component. + def _get_options(targeted_data: pd.DataFrame, current_value=None) -> list[Any]: # The dropna() isn't strictly required here but will be in future pandas versions when the behavior of stack # changes. See https://pandas.pydata.org/docs/whatsnew/v2.1.0.html#whatsnew-210-enhancements-new-stack. - return np.unique(targeted_data.stack().dropna()).tolist() # noqa: PD013 + targeted_data = pd.concat([targeted_data, pd.Series(current_value)]).stack().dropna() # noqa: PD013 + return sorted(set(targeted_data) - {ALL_OPTION}) diff --git a/vizro-core/src/vizro/models/_controls/parameter.py b/vizro-core/src/vizro/models/_controls/parameter.py index e7d6d537c..cdc76936c 100644 --- a/vizro-core/src/vizro/models/_controls/parameter.py +++ b/vizro-core/src/vizro/models/_controls/parameter.py @@ -1,4 +1,5 @@ -from typing import Literal +from collections.abc import Iterable +from typing import Literal, cast try: from pydantic.v1 import Field, validator @@ -55,12 +56,13 @@ def check_data_frame_as_target_argument(cls, target): f"Invalid target {target}. 'data_frame' target must be supplied in the form " ".data_frame." ) + # TODO: Add validation: Make sure the target data_frame is _DynamicData. return target @validator("targets") def check_duplicate_parameter_target(cls, targets): all_targets = targets.copy() - for _, param in model_manager._items_with_type(Parameter): + for param in cast(Iterable[Parameter], model_manager._get_models(Parameter)): all_targets.extend(param.targets) duplicate_targets = {item for item in all_targets if all_targets.count(item) > 1} if duplicate_targets: diff --git a/vizro-core/src/vizro/models/_dashboard.py b/vizro-core/src/vizro/models/_dashboard.py index 4b3b0e66a..b635b8e95 100644 --- a/vizro-core/src/vizro/models/_dashboard.py +++ b/vizro-core/src/vizro/models/_dashboard.py @@ -8,7 +8,6 @@ import dash import dash_bootstrap_components as dbc -import dash_mantine_components as dmc import plotly.io as pio from dash import ( ClientsideFunction, @@ -142,7 +141,7 @@ def build(self): ClientsideFunction(namespace="dashboard", function_name="update_dashboard_theme"), # This currently doesn't do anything, but we need to define an Output such that the callback is triggered. Output("dashboard-container", "className"), - Input("theme_selector", "checked"), + Input("theme-selector", "value"), ) left_side_div_present = any([len(self.pages) > 1, self.pages[0].controls]) if left_side_div_present: @@ -197,12 +196,11 @@ def _get_page_divs(self, page: Page) -> _PageDivsType: else html.H2(id="dashboard-title", hidden=True) ) settings = html.Div( - children=dmc.Switch( - id="theme_selector", - checked=self.theme == "vizro_light", + children=dbc.Switch( + id="theme-selector", + value=self.theme == "vizro_light", persistence=True, persistence_type="session", - className="toggle-switch", ), id="settings", ) @@ -310,32 +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=dmc.Switch( - id="theme_selector", - checked=self.theme == "vizro_light", - persistence=True, - persistence_type="session", - className="toggle-switch", - ), - 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/models/_navigation/_navigation_utils.py b/vizro-core/src/vizro/models/_navigation/_navigation_utils.py index 7e8e1f15f..387cf8b9a 100644 --- a/vizro-core/src/vizro/models/_navigation/_navigation_utils.py +++ b/vizro-core/src/vizro/models/_navigation/_navigation_utils.py @@ -1,7 +1,8 @@ from __future__ import annotations import itertools -from typing import TypedDict +from collections.abc import Iterable +from typing import TypedDict, cast import dash_bootstrap_components as dbc @@ -15,8 +16,7 @@ def _validate_pages(pages): pages_as_list = list(itertools.chain(*pages.values())) if isinstance(pages, dict) else pages # Ideally we would use dashboard.pages in the model manager here, but we only register pages in # dashboard.pre_build and model manager cannot find a Dashboard at validation time. - # page[0] gives the page model ID. - registered_pages = [page[0] for page in model_manager._items_with_type(Page)] + registered_pages = [page.id for page in cast(Iterable[Page], model_manager._get_models(Page))] if not pages_as_list: raise ValueError("Ensure this value has at least 1 item.") diff --git a/vizro-core/src/vizro/models/_page.py b/vizro-core/src/vizro/models/_page.py index 6b45a409c..e137987bb 100644 --- a/vizro-core/src/vizro/models/_page.py +++ b/vizro-core/src/vizro/models/_page.py @@ -1,7 +1,7 @@ from __future__ import annotations -from collections.abc import Mapping -from typing import Any, Optional, TypedDict, Union +from collections.abc import Iterable, Mapping +from typing import Any, Optional, TypedDict, Union, cast from dash import dcc, html @@ -13,8 +13,8 @@ from vizro._constants import ON_PAGE_LOAD_ACTION_PREFIX from vizro.actions import _on_page_load from vizro.managers import model_manager -from vizro.managers._model_manager import DuplicateIDError, ModelID -from vizro.models import Action, Layout, VizroBaseModel +from vizro.managers._model_manager import FIGURE_MODELS, DuplicateIDError +from vizro.models import Action, Filter, Layout, VizroBaseModel from vizro.models._action._actions_chain import ActionsChain, Trigger from vizro.models._layout import set_layout from vizro.models._models_utils import _log_call, check_captured_callable, validate_min_length @@ -96,8 +96,16 @@ def __vizro_exclude_fields__(self) -> Optional[Union[set[str], Mapping[str, Any] @_log_call def pre_build(self): - # TODO: Remove default on page load action if possible - targets = model_manager._get_page_model_ids_with_figure(page_id=ModelID(str(self.id))) + figure_targets = [ + model.id for model in cast(Iterable[VizroBaseModel], model_manager._get_models(FIGURE_MODELS, page=self)) + ] + filter_targets = [ + filter.id + for filter in cast(Iterable[Filter], model_manager._get_models(Filter, page=self)) + if filter._dynamic + ] + targets = figure_targets + filter_targets + if targets: self.actions = [ ActionsChain( diff --git a/vizro-core/src/vizro/static/css/aggrid.css b/vizro-core/src/vizro/static/css/aggrid.css index 81d463b9b..286e28d0b 100644 --- a/vizro-core/src/vizro/static/css/aggrid.css +++ b/vizro-core/src/vizro/static/css/aggrid.css @@ -1,27 +1,27 @@ .ag-theme-quartz.ag-theme-vizro, .ag-theme-quartz-dark.ag-theme-vizro { - --ag-active-color: var(--state-overlays-selected-hover); - --ag-background-color: var(--main-container-bg-color); - --ag-odd-row-background-color: var(--main-container-bg-color); + --ag-active-color: var(--stateOverlays-selectedHover); + --ag-background-color: var(--right-side-bg); + --ag-odd-row-background-color: var(--right-side-bg); --ag-header-foreground-color: var(--text-secondary); --ag-data-color: var(--text-primary); - --ag-header-background-color: var(--main-container-bg-color); + --ag-header-background-color: var(--right-side-bg); --ag-icon-font-family: aggridquartz; - --ag-icon-size: var(--text-size-02); + --ag-icon-size: 0.875rem; --ag-row-height: 48px; --ag-header-height: 40px; --ag-borders: none; --ag-border-radius: 0; --ag-border-color: transparent; --ag-row-border-style: solid; - --ag-row-border-color: var(--border-subtle-alpha-01); + --ag-row-border-color: var(--border-subtleAlpha01); --ag-row-border-width: 1px; - --ag-selected-row-background-color: var(--state-overlays-selected); + --ag-selected-row-background-color: var(--stateOverlays-selected); --ag-checkbox-checked-color: var(--text-primary); --ag-header-column-resize-handle-display: block; --ag-header-column-resize-handle-height: 30%; --ag-header-column-resize-handle-width: 1px; - --ag-header-column-resize-handle-color: var(--border-subtle-alpha-02); + --ag-header-column-resize-handle-color: var(--border-subtleAlpha02); --ag-range-selection-border-color: transparent; --ag-input-focus-border-color: transparent; } @@ -29,7 +29,7 @@ /* Header ------- */ #dashboard-container .ag-header-row { align-items: flex-start; - border-bottom: 1px solid var(--border-subtle-alpha-02); + border-bottom: 1px solid var(--border-subtleAlpha02); display: flex; } @@ -37,12 +37,12 @@ align-items: center; display: flex; height: 40px; - padding: 0 var(--spacing-03); + padding: 0 0.75rem; } #dashboard-container .ag-header-cell-text { letter-spacing: -0.112px; - line-height: var(--spacing-04); + line-height: 1rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -55,7 +55,7 @@ /* Rows ------- */ #dashboard-container .ag-cell { - padding: 0 var(--spacing-03); + padding: 0 0.75rem; } #dashboard-container .ag-cell-focus { @@ -63,24 +63,24 @@ } #dashboard-container .ag-cell-focus:not(.ag-cell-range-selected):focus-within { - background: var(--state-overlays-selected); + background: var(--stateOverlays-selected); } /* Pop up menu ----- */ #dashboard-container .ag-menu { - background-color: var(--surfaces-bg-02); - border: 1px solid var(--border-subtle-alpha-02); + background-color: var(--surfaces-bg02); + border: 1px solid var(--border-subtleAlpha02); color: var(--text-primary); } #dashboard-container .ag-ltr .ag-filter-filter input { background-color: var(--field-enabled); - box-shadow: var(--box-shadow-elevation-0); + box-shadow: var(--elevation-0); display: flex; - font-size: var(--text-size-02); - font-weight: var(--text-weight-regular); - letter-spacing: var(--letter-spacing-body-ui-02); - line-height: var(--text-size-03); + font-size: 0.875rem; + font-weight: 400; + letter-spacing: -0.112px; + line-height: 1rem; text-overflow: ellipsis; } @@ -89,24 +89,24 @@ border: none; border-radius: 0; box-shadow: none; - font-size: var(--text-size-02); + font-size: 0.875rem; } #dashboard-container .ag-filter-select .ag-picker-field-wrapper { background-color: var(--field-enabled); - box-shadow: var(--box-shadow-elevation-0); + box-shadow: var(--elevation-0); } #dashboard-container .ag-select-list { background-color: var(--field-enabled); - box-shadow: var(--box-shadow-elevation-0); + box-shadow: var(--elevation-0); color: var(--text-primary); - font-size: var(--text-size-02); - line-height: var(--text-size-05); + font-size: 0.875rem; + line-height: 1.5rem; } #dashboard-container .ag-select-list-item.ag-active-item { - background-color: var(--state-overlays-hover); + background-color: var(--stateOverlays-hover); } #dashboard-container .ag-radio-button-input-wrapper { @@ -123,12 +123,12 @@ #dashboard-container .ag-filter-apply-panel { justify-content: flex-start; - padding: var(--spacing-02) 0 var(--spacing-03) 0; + padding: 0.5rem 0 0.75rem; } #dashboard-container .ag-filter-condition { justify-content: flex-start; - padding: var(--spacing-02) var(--spacing-01); + padding: 0.5rem 0.25rem; } /* Scroll Bar */ @@ -157,28 +157,27 @@ padding-left: 0; } -/* Buttons */ #dashboard-container .ag-standard-button { background: var(--fill-active); border: none; border-radius: 0; - box-shadow: var(--box-shadow-elevation-0); - color: var(--text-contrast-primary); - font-size: var(--text-size-02); - font-weight: var(--text-weight-semibold); + box-shadow: var(--elevation-0); + color: var(--text-primary-inverted); + font-size: 0.875rem; + font-weight: 600; height: 32px; - letter-spacing: var(--letter-spacing-body-link-02); - line-height: var(--text-size-05); - padding: var(--spacing-01) var(--spacing-03); + letter-spacing: -0.028px; + line-height: 1.5rem; + padding: 0.25rem 0.75rem; text-transform: none; } #dashboard-container .ag-standard-button:hover { background: linear-gradient( - var(--state-overlays-contrast-hover), - var(--state-overlays-contrast-hover) + var(--stateOverlays-hover-inverted), + var(--stateOverlays-hover-inverted) ), var(--fill-active); - color: var(--text-contrast-primary); + color: var(--text-primary-inverted); text-decoration-line: underline; } diff --git a/vizro-core/src/vizro/static/css/bootstrap_overwrites.css b/vizro-core/src/vizro/static/css/bootstrap_overwrites.css index 59197a251..7384ffe6d 100644 --- a/vizro-core/src/vizro/static/css/bootstrap_overwrites.css +++ b/vizro-core/src/vizro/static/css/bootstrap_overwrites.css @@ -1,6 +1,34 @@ -/* This file contains overwrites, which we want to have as defaults for vizro -but do not want to take over to `vizro-bootstrap` as these settings might not be generic enough. */ +/* This file contains overwrites, which we want to have as defaults for vizro framework +but do not want to take over to `vizro-bootstrap` as these settings might not be generic enough +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); + --right-side-bg: var(--surfaces-bg03); + --text-code-string: #95c2e7; + --text-code-keyword: #f4766e; + --text-code-meta: #c8ace1; + --text-code-type: #f69d50; + --text-code-literal: #6bb5fd; + --slider-rail-bg: var(--primary-100); + --collapse-icon-bg: var(--primary-500); +} + +[data-bs-theme="light"] { + --dropdown-label-bg: var(--primary-300); + --right-side-bg: var(--surfaces-bg01); + --text-code-string: #0a3069; + --text-code-keyword: #d12d39; + --text-code-meta: #6f42c1; + --text-code-type: #f69d50; + --text-code-literal: #005cc5; + --fill-icon-image-card: invert(64%) sepia(0%) saturate(1375%); + --slider-rail-bg: var(--primary-900); + --collapse-icon-bg: var(--primary-300); +} + +/* CARDS */ .card .nav-link { height: 100%; } @@ -9,6 +37,7 @@ but do not want to take over to `vizro-bootstrap` as these settings might not be margin-bottom: 0; } +/* ACCORDION */ .accordion-item .nav-link { padding: 0.5rem 1rem; } @@ -16,3 +45,29 @@ but do not want to take over to `vizro-bootstrap` as these settings might not be .accordion-item .nav-link.active { border-left: 2px solid var(--border-enabled); } + +/* TABS */ +.nav-tabs { + margin-bottom: 1.25rem; +} + +.tab-content { + height: calc(100% - 3.25rem); /* 3.25rem: nav-tabs margin + height */ +} + +/* The dbc component adds an additional div element to which we cannot assign a className. +To ensure the dynamic height adjustment and prevent scrolling, the height must be specified for that div as below. */ +.tab-pane, +.tab-pane > div:first-child { + height: 100%; +} + +/* Hides title of the first container given the title is already reflected in the tab title */ +.tab-content .container-title:first-of-type { + display: none; +} + +/* Remove label that automatically gets added when calling `dbc.Switch` to remove gap. */ +label[for="theme-selector"] { + display: none; +} diff --git a/vizro-core/src/vizro/static/css/container.css b/vizro-core/src/vizro/static/css/container.css deleted file mode 100644 index 6fe15633f..000000000 --- a/vizro-core/src/vizro/static/css/container.css +++ /dev/null @@ -1,5 +0,0 @@ -.page-component-container { - display: flex; - flex-direction: column; - height: 100%; -} diff --git a/vizro-core/src/vizro/static/css/datepicker.css b/vizro-core/src/vizro/static/css/datepicker.css index b2d4a4ebb..3e167d5d0 100644 --- a/vizro-core/src/vizro/static/css/datepicker.css +++ b/vizro-core/src/vizro/static/css/datepicker.css @@ -1,6 +1,6 @@ .datepicker .mantine-Input-wrapper { font-family: unset; - height: var(--spacing-08); + height: 2rem; } .datepicker .mantine-DateRangePicker-input, @@ -8,13 +8,13 @@ background-color: var(--field-enabled); border: none; border-radius: 0; - box-shadow: var(--box-shadow-elevation-0); + box-shadow: var(--elevation-0); color: var(--text-secondary); - font-size: var(--text-size-02); - height: var(--spacing-08); - line-height: var(--spacing-04); - min-height: var(--spacing-08); - padding: 0 var(--spacing-02); + font-size: 0.875rem; + height: 2rem; + line-height: 1rem; + min-height: 2rem; + padding: 0 0.5rem; } .datepicker .mantine-DateRangePicker-input:hover, @@ -27,8 +27,8 @@ background: var(--field-enabled); border: none; border-radius: 0; - box-shadow: var(--box-shadow-elevation-1); - padding: var(--spacing-04) 11px; /* 11px otherwise not aligned with controls */ + box-shadow: var(--elevation-1); + padding: 1rem 11px; /* 11px otherwise not aligned with controls */ } .datepicker .mantine-UnstyledButton-root { @@ -39,7 +39,7 @@ } .datepicker .mantine-UnstyledButton-root:hover { - background: var(--state-overlays-hover); + background: var(--stateOverlays-hover); color: var(--text-primary); } @@ -47,7 +47,7 @@ .datepicker .mantine-DatePicker-weekday { color: var(--text-secondary); font-family: unset; - padding: var(--spacing-02); + padding: 0.5rem; } .datepicker .mantine-DateRangePicker-cell, @@ -75,7 +75,7 @@ .datepicker .mantine-DateRangePicker-day:focus-visible, .datepicker .mantine-DatePicker-day:hover, .datepicker .mantine-DatePicker-day:focus-visible { - background: var(--state-overlays-hover); + background: var(--stateOverlays-hover); border: none; color: var(--text-primary); outline: none; @@ -83,7 +83,7 @@ } .datepicker .mantine-DateRangePicker-day[data-in-range] { - background: var(--state-overlays-selected); + background: var(--stateOverlays-selected); color: var(--text-primary); } @@ -93,8 +93,8 @@ .datepicker .mantine-DatePicker-day[data-selected], .datepicker .mantine-DatePicker-yearPickerControlActive, .datepicker .mantine-DatePicker-monthPickerControlActive { - background: var(--state-overlays-selected-inverse); - color: var(--text-contrast-primary); + background: var(--stateOverlays-selected-inverted); + color: var(--text-primary-inverted); text-decoration: underline; } diff --git a/vizro-core/src/vizro/static/css/dropdown.css b/vizro-core/src/vizro/static/css/dropdown.css index 25d8ef943..1b5b01cad 100644 --- a/vizro-core/src/vizro/static/css/dropdown.css +++ b/vizro-core/src/vizro/static/css/dropdown.css @@ -3,8 +3,8 @@ background-color: var(--field-enabled); border: none; border-radius: 0; - box-shadow: var(--box-shadow-elevation-0); - font-size: var(--text-size-02); + box-shadow: var(--elevation-0); + font-size: 0.875rem; } /* Dropdown menu when clicking on expand arrow */ @@ -12,20 +12,20 @@ background-color: var(--field-enabled); border: none; border-radius: 0; - box-shadow: var(--box-shadow-elevation-0); - font-size: var(--text-size-02); - line-height: var(--text-size-05); + box-shadow: var(--elevation-0); + font-size: 0.875rem; + line-height: 1.5rem; } /* Dropdown menu options */ #dashboard-container .VirtualizedSelectOption { color: var(--text-primary); - font-weight: var(--text-weight-regular); + font-weight: 400; } /* Dropdown menu hover effect */ #dashboard-container .VirtualizedSelectFocusedOption { - background-color: var(--state-overlays-hover); + background-color: var(--stateOverlays-hover); } /* Input box for existing values and user input */ @@ -43,7 +43,7 @@ /* User input */ #dashboard-container .dash-dropdown .Select-input { display: block; - height: var(--tag-height); + height: 24px; margin-left: unset; } @@ -58,12 +58,12 @@ /* Border on focus */ #dashboard-container .is-focused:not(.is-open) > .Select-control { - box-shadow: 0 0 0 2px var(--focus-focus) inset; + box-shadow: 0 0 0 2px var(--focus) inset; } /* Single-select dropdown only ------------------- */ #dashboard-container .Select--single .Select-value { - padding-left: var(--spacing-02); + padding-left: 0.5rem; } .has-value.Select--single > .Select-control .Select-value .Select-value-label, @@ -78,24 +78,23 @@ /* Tags --------------------------- */ #dashboard-container .Select--multi .Select-value { - background-color: var(--tags-bg-color); + background-color: var(--dropdown-label-bg); border: 0; border-radius: 0; - box-shadow: var(--box-shadow-elevation-0); - color: var(--tags-text-color); - font-size: var(--text-size-02); - height: var(--tag-height); + box-shadow: var(--elevation-0); + color: var(--text-secondary); + font-size: 0.875rem; height: 100%; - letter-spacing: var(--letter-spacing-body-ui-02); + letter-spacing: -0.112px; margin: 0; - padding: 0 var(--spacing-01); + padding: 0 0.25rem; vertical-align: baseline; } /* Tag: Label */ #dashboard-container .Select--multi .Select-value-label { color: var(--text-primary); - line-height: var(--text-size-05); + line-height: 1.5rem; padding: 0 4px; } diff --git a/vizro-core/src/vizro/static/css/images.css b/vizro-core/src/vizro/static/css/images.css deleted file mode 100644 index df3e4a4d0..000000000 --- a/vizro-core/src/vizro/static/css/images.css +++ /dev/null @@ -1,21 +0,0 @@ -img[src*="floating-right"] { - float: right; -} - -img[src*="floating-left"] { - float: left; -} - -img[src*="floating-center"] { - display: block; - float: none; - height: auto; - margin: auto; - max-width: 100%; -} - -img[src*="icon-top"] { - filter: var(--fill-icon-image-card); - height: 36px; - width: 36px; -} diff --git a/vizro-core/src/vizro/static/css/layout.css b/vizro-core/src/vizro/static/css/layout.css index f20e6dddd..a7c66f864 100644 --- a/vizro-core/src/vizro/static/css/layout.css +++ b/vizro-core/src/vizro/static/css/layout.css @@ -10,24 +10,24 @@ display: flex; flex: 0 0 auto; flex-direction: column; - gap: var(--spacing-06); + gap: 1.5rem; overflow: auto; padding: 24px 24px 0; width: 324px; } #left-side { - background-color: var(--surfaces-bg-02); + background-color: var(--surfaces-bg02); display: flex; flex-direction: row; height: 100%; } #right-side { - background: var(--main-container-bg-color); + background: var(--right-side-bg); display: flex; flex-direction: column; - gap: var(--spacing-05); + gap: 1.25rem; padding: 24px; width: 100%; } @@ -42,7 +42,7 @@ #page-header { align-items: center; - background-color: var(--surfaces-bg-02); + background-color: var(--surfaces-bg02); display: flex; flex-direction: row; height: 60px; @@ -53,12 +53,12 @@ } #page-header:not(:empty) { - border-bottom: 1px solid var(--border-subtle-alpha-01); + border-bottom: 1px solid var(--border-subtleAlpha01); } #page-components { overflow: auto; - padding-top: var(--spacing-01); + padding-top: 0.25rem; } .grid-layout { @@ -76,38 +76,13 @@ align-self: stretch; display: flex; flex-direction: column; - gap: var(--spacing-06); - padding-bottom: var(--spacing-06); - padding-top: var(--spacing-02); + gap: 1.5rem; + padding-bottom: 1.5rem; + padding-top: 0.5rem; } #control-panel:not(:empty) { - border-bottom: 1px solid var(--border-subtle-alpha-01); -} - -.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; + border-bottom: 1px solid var(--border-subtleAlpha01); } .dashboard_title { @@ -125,7 +100,7 @@ } #left-sidebar { - border-right: 1px solid var(--border-subtle-alpha-01); + border-right: 1px solid var(--border-subtleAlpha01); display: flex; flex-direction: column; gap: 40px; @@ -177,7 +152,7 @@ } #collapse-icon.material-symbols-outlined { - background-color: var(--tooltip-bg-color); + background-color: var(--collapse-icon-bg); border-radius: 50%; color: var(--text-disabled); cursor: pointer; @@ -185,7 +160,7 @@ } #collapse-icon.material-symbols-outlined:hover { - color: var(--text-active); + color: var(--text-primary); } .collapse-icon-div { @@ -195,6 +170,12 @@ 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/scroll_bar.css b/vizro-core/src/vizro/static/css/scroll_bar.css index 3c9dc34af..43381bfa6 100644 --- a/vizro-core/src/vizro/static/css/scroll_bar.css +++ b/vizro-core/src/vizro/static/css/scroll_bar.css @@ -9,11 +9,11 @@ } ::-webkit-scrollbar-thumb:hover { - background: var(--fill-medium-emphasis); + background: var(--fill-secondary); } #left-main::-webkit-scrollbar-thumb { - border-color: var(--surfaces-bg-02); + border-color: var(--surfaces-bg02); } .card::-webkit-scrollbar-thumb, @@ -23,11 +23,11 @@ } #page-components::-webkit-scrollbar-thumb { - border-color: var(--main-container-bg-color); + border-color: var(--right-side-bg); } .table-container::-webkit-scrollbar-thumb { - border-color: var(--main-container-bg-color); + border-color: var(--right-side-bg); } /* Can't control thumb color so hover is turned off */ diff --git a/vizro-core/src/vizro/static/css/slider.css b/vizro-core/src/vizro/static/css/slider.css index c3e936efc..db27caf43 100644 --- a/vizro-core/src/vizro/static/css/slider.css +++ b/vizro-core/src/vizro/static/css/slider.css @@ -5,16 +5,16 @@ .rc-slider-rail, .rc-slider-track { - background-color: var(--slider-background-color); + background-color: var(--slider-rail-bg); height: 2px; } .rc-slider-track { - background-color: var(--fill-on-active); + background-color: var(--fill-active); } .rc-slider-dot { - background-color: var(--slider-background-color); + background-color: var(--slider-rail-bg); border: var(--field-enabled); bottom: 0; height: 6px; @@ -22,16 +22,16 @@ } .rc-slider-dot-active { - background-color: var(--fill-on-active); + background-color: var(--fill-active); } .rc-slider-handle, .rc-slider-handle:hover, .rc-slider-handle-dragging.rc-slider-handle-dragging.rc-slider-handle-dragging, .rc-slider-handle-click-focused:focus { - background-color: var(--surfaces-bg-01); - border: solid 6px var(--fill-on-active); - border-color: var(--fill-on-active); + background-color: var(--surfaces-bg01); + border: solid 6px var(--fill-active); + border-color: var(--fill-active); border-radius: 100%; height: 16px; margin: -8px 0 0; @@ -41,7 +41,7 @@ .rc-slider-handle:hover, .rc-slider-handle-dragging.rc-slider-handle-dragging.rc-slider-handle-dragging { - border: solid 4px var(--fill-on-active); + border: solid 4px var(--fill-active); box-shadow: unset; cursor: context-menu; } @@ -51,7 +51,7 @@ } .rc-slider-handle:active { - border: solid 6px var(--fill-on-active); + border: solid 6px var(--fill-active); box-shadow: unset; cursor: grabbing; height: 16px; @@ -61,14 +61,14 @@ .rc-slider-mark-text { color: var(--text-secondary); - font-size: var(--text-size-01); - line-height: var(--text-size-02); - margin-top: var(--spacing-01); + font-size: 0.75rem; + line-height: 0.875rem; + margin-top: 0.25rem; min-width: 44px; } .rc-slider-mark-text-active { - color: var(--fill-on-active); + color: var(--fill-active); } .slider-track-with-marks, @@ -96,7 +96,7 @@ input.dash-input:invalid { display: flex; flex-direction: row; justify-content: space-between; - line-height: var(--text-size-03); + line-height: 1rem; } .slider-label-input label { @@ -107,24 +107,24 @@ input.dash-input:invalid { .slider-text-input-container { display: flex; flex-direction: row; - gap: var(--spacing-01); - margin-bottom: var(--spacing-03); + gap: 0.25rem; + margin-bottom: 0.75rem; } .slider-text-input-field { background-color: transparent; color: var(--text-secondary); - font-size: var(--text-size-02); + font-size: 0.875rem; max-width: 80px; /* required for Mozilla */ padding: 0; text-align: center; text-decoration: underline; - text-decoration-color: var(--border-subtle-alpha-01); + text-decoration-color: var(--border-subtleAlpha01); } .slider-text-input-range-separator { color: var(--text-secondary); - font-size: var(--text-size-02); + font-size: 0.875rem; } /* To remove number input spin box */ diff --git a/vizro-core/src/vizro/static/css/table.css b/vizro-core/src/vizro/static/css/table.css index a1fb896f6..e6e876496 100644 --- a/vizro-core/src/vizro/static/css/table.css +++ b/vizro-core/src/vizro/static/css/table.css @@ -11,7 +11,7 @@ .dash-spreadsheet-container .dash-spreadsheet-inner th { - background-color: var(--main-container-bg-color); + background-color: var(--right-side-bg); color: var(--text-primary); padding: 10px 0; } @@ -21,7 +21,7 @@ .dash-spreadsheet-container .dash-spreadsheet-inner td { - background-color: var(--main-container-bg-color); + background-color: var(--right-side-bg); color: var(--text-primary); } @@ -30,7 +30,7 @@ .dash-spreadsheet-container .dash-spreadsheet-inner table { - --hover: var(--main-container-bg-color); + --hover: var(--right-side-bg); width: 100%; } diff --git a/vizro-core/src/vizro/static/css/tabs.css b/vizro-core/src/vizro/static/css/tabs.css deleted file mode 100644 index 2e49d1c21..000000000 --- a/vizro-core/src/vizro/static/css/tabs.css +++ /dev/null @@ -1,59 +0,0 @@ -.tabs { - display: flex; - flex-direction: column; - gap: 20px; - height: 100%; -} - -.tabs-list { - align-items: flex-start; - align-self: stretch; - border-bottom: 1px solid var(--border-subtle-alpha-01); - display: flex; - flex-wrap: nowrap; - gap: 16px; - height: 32px; -} - -.tabs-panel { - height: calc(100% - 60px); -} - -.tab-content { - height: 100%; -} - -.tab-title { - align-items: flex-start; - border-bottom: 1px solid transparent; - color: var(--text-secondary); - display: flex; - font-size: 16px; - height: 32px; - line-height: 20px; - margin: 0; - padding: 0; -} - -.tab-title:hover { - background-color: transparent; - border-bottom: 1px solid var(--text-active); -} - -.tab-title[data-active] { - border-bottom: 1px solid var(--text-active); - color: var(--text-active); -} - -.tab-title[data-active]:hover { - background-color: transparent; - border-color: 1px solid var(--text-active); -} - -.tab-title[data-active] .mantine-Tabs-tabLabel { - color: var(--text-active); -} - -.tab-content > .page-component-container > .container-title { - display: none; -} diff --git a/vizro-core/src/vizro/static/css/toggle.css b/vizro-core/src/vizro/static/css/toggle.css deleted file mode 100644 index 5158e4c16..000000000 --- a/vizro-core/src/vizro/static/css/toggle.css +++ /dev/null @@ -1,43 +0,0 @@ -.toggle-switch { - width: 32px; -} - -#dashboard-container .mantine-Switch-track { - background-color: var(--fill-subtle); - border: 1px solid var(--border-enabled); - border-radius: 16px; - height: 16px; - min-width: 32px; - width: 32px; -} - -#dashboard-container .mantine-Switch-track:focus { - border: 2px solid var(--focus-focus); -} - -#dashboard-container .mantine-Switch-input { - margin: 0; -} - -#dashboard-container .mantine-Switch-trackLabel { - height: 16px; - margin: 0; - width: 32px; -} - -#dashboard-container .mantine-Switch-thumb { - background-color: var(--fill-medium-emphasis); - border: none; - height: 10px; - width: 10px; -} - -#dashboard-container input:checked + * > .mantine-11dx59s { - background: var(--text-contrast-primary); - left: calc(100% - 12px); -} - -#dashboard-container input:checked + * > .mantine-69c9zd { - background: var(--text-primary); - border-color: var(--border-enabled); -} diff --git a/vizro-core/src/vizro/static/css/token_names.css b/vizro-core/src/vizro/static/css/token_names.css deleted file mode 100644 index 589269228..000000000 --- a/vizro-core/src/vizro/static/css/token_names.css +++ /dev/null @@ -1,89 +0,0 @@ -:root { - --primary-dark-100: #333640; - --primary-dark-200: #2f323c; - --primary-dark-300: #2b2e39; - --primary-dark-400: #272a35; - --primary-dark-500: #232632; - --primary-dark-600: #1f222e; - --primary-dark-700: #1b1e2a; - --primary-dark-800: #181b26; - --primary-dark-900: #141721; - --fill-dark-mode-active: rgba(255, 255, 255, 0.88); - --surfaces-dark-mode-bg-01: rgba(35, 38, 50, 1); - --surfaces-dark-mode-bg-02: rgba(27, 30, 42, 1); - --surfaces-dark-mode-bg-03: rgba(20, 23, 33, 1); - --surfaces-dark-mode-bg-card: rgba(35, 38, 50, 1); - --border-dark-mode-disabled: rgba(255, 255, 255, 0.3); - --field-dark-mode-disabled: rgba(240, 241, 242, 1); - --fill-dark-mode-disabled: rgba(255, 255, 255, 0.3); - --text-dark-mode-disabled: rgba(255, 255, 255, 0.3); - --border-dark-mode-enabled: rgba(255, 255, 255, 0.6); - --field-dark-mode-enabled: rgba(43, 46, 57, 1); - --status-dark-mode-error: rgba(245, 101, 101, 1); - --border-dark-mode-hover: rgba(255, 255, 255, 0.88); - --field-dark-mode-hover: rgba(55, 58, 68, 1); - --fill-dark-mode-hover-selected: rgba(255, 255, 255, 1); - --status-dark-mode-information: rgba(0, 180, 255, 1); - --fill-dark-mode-medium-emphasis: rgba(255, 255, 255, 0.6); - --text-dark-mode-placeholder: rgba(255, 255, 255, 0.38); - --text-dark-mode-primary: rgba(255, 255, 255, 0.88); - --text-dark-mode-secondary: rgba(255, 255, 255, 0.6); - --border-dark-mode-selected: #ffffff; - --fill-dark-mode-subtle: rgba(255, 255, 255, 0.1); - --border-dark-mode-subtle-alpha-01: rgba(255, 255, 255, 0.1); - --border-dark-mode-subtle-alpha-02: rgba(255, 255, 255, 0.16); - --status-dark-mode-success: rgba(64, 216, 110, 1); - --tags-dark-mode-text-color: rgba(255, 255, 255, 0.6); - --status-dark-mode-warning: rgba(255, 193, 7, 1); - --primary-light-100: #fafafb; - --primary-light-200: #f5f6f6; - --primary-light-300: #f2f3f4; - --primary-light-400: #ebedee; - --primary-light-50: #ffffff; - --primary-light-500: #e6e8ea; - --primary-light-600: #e2e4e6; - --primary-light-700: #dddfe1; - --primary-light-800: #d8dadd; - --primary-light-900: #d3d6d9; - --fill-light-mode-active: rgba(20, 23, 33, 0.88); - --surfaces-light-mode-bg-01: rgba(255, 255, 255, 1); - --surfaces-light-mode-bg-02: rgba(245, 246, 246, 1); - --surfaces-light-mode-bg-03: rgba(230, 232, 234, 1); - --surfaces-light-mode-bg-card: rgba(245, 246, 246, 1); - --border-light-mode-disabled: rgba(20, 23, 33, 0.3); - --field-light-mode-disabled: rgba(240, 241, 242, 1); - --fill-light-mode-disabled: rgba(20, 23, 33, 0.3); - --text-light-mode-disabled: rgba(20, 23, 33, 0.3); - --border-light-mode-enabled: rgba(20, 23, 33, 0.6); - --field-light-mode-enabled: rgba(250, 250, 251, 1); - --status-light-mode-error: rgba(240, 59, 58, 1); - --border-light-mode-hover: rgba(20, 23, 33, 0.88); - --field-light-mode-hover: rgba(255, 255, 255, 1); - --fill-light-mode-hover-selected: rgba(20, 23, 33, 1); - --status-light-mode-information: rgba(0, 158, 255, 1); - --fill-light-mode-medium-emphasis: rgba(20, 23, 33, 0.6); - --text-light-mode-placeholder: rgba(20, 23, 33, 0.38); - --text-light-mode-primary: rgba(20, 23, 33, 0.88); - --text-light-mode-secondary: rgba(20, 23, 33, 0.6); - --border-light-mode-selected: #141721; - --fill-light-mode-subtle: rgba(0, 0, 0, 0.1); - --border-light-mode-subtle-alpha-01: rgba(20, 23, 33, 0.1); - --border-light-mode-subtle-alpha-02: rgba(20, 23, 33, 0.16); - --status-light-mode-success: rgba(38, 191, 86, 1); - --tags-light-mode-text-color: rgba(255, 255, 255, 0.6); - --status-light-mode-warning: rgba(241, 124, 2, 1); - --overlays-on-light-ui-high-contrast: rgba(239, 240, 248, 0.88); - --overlays-on-light-ui-medium-contrast: rgba(239, 240, 248, 0.6); - --state-overlays-dark-mode-active: rgba(255, 255, 255, 0.08); - --state-overlays-dark-mode-enable: rgba(255, 255, 255, 0); - --state-overlays-dark-mode-hover: rgba(255, 255, 255, 0.04); - --state-overlays-dark-mode-selected: rgba(255, 255, 255, 0.1); - --state-overlays-dark-mode-selected-inverse: #ffffff; - --state-overlays-dark-mode-selected-hover: rgba(255, 255, 255, 0.16); - --state-overlays-light-mode-active: rgba(20, 23, 33, 0.12); - --state-overlays-light-mode-enable: rgba(20, 23, 33, 0); - --state-overlays-light-mode-hover: rgba(20, 23, 33, 0.06); - --state-overlays-light-mode-selected: rgba(20, 23, 33, 0.08); - --state-overlays-light-mode-selected-inverse: #141721; - --state-overlays-light-mode-selected-hover: rgba(20, 23, 33, 0.24); -} diff --git a/vizro-core/src/vizro/static/css/tooltip.css b/vizro-core/src/vizro/static/css/tooltip.css deleted file mode 100644 index 478b879a3..000000000 --- a/vizro-core/src/vizro/static/css/tooltip.css +++ /dev/null @@ -1,25 +0,0 @@ -.tooltip-inner { - border-radius: 0; - box-shadow: var(--box-shadow-elevation-0); - filter: drop-shadow(0 2px 2px #141721); - font-size: var(--text-size-01); - font-weight: var(--text-weight-light); - letter-spacing: var(--letter-spacing-help-text); - line-height: var(--text-size-03); - max-width: 180px; - overflow-wrap: break-word; - padding: var(--spacing-01) var(--spacing-02); - white-space: pre-wrap; -} - -.bs-tooltip-end .tooltip-arrow::before { - right: -3px; -} - -.tooltip.show { - opacity: 1; -} - -.tooltip { - pointer-events: none; -} diff --git a/vizro-core/src/vizro/static/css/variables.css b/vizro-core/src/vizro/static/css/variables.css deleted file mode 100644 index 20ebc078c..000000000 --- a/vizro-core/src/vizro/static/css/variables.css +++ /dev/null @@ -1,167 +0,0 @@ -/* Add variables here that are the same in dark and light */ -:root { - --spacing-01: 4px; - --spacing-02: 8px; - --spacing-03: 12px; - --spacing-04: 16px; - --spacing-05: 20px; - --spacing-06: 24px; - --spacing-07: 28px; - --spacing-08: 32px; - --tag-height: 24px; - --text-size-01: 12px; - --text-size-02: 14px; - --text-size-03: 16px; - --text-size-04: 20px; - --text-size-05: 24px; - --text-size-06: 28px; - --text-size-07: 32px; - --letter-spacing-body-edit-01: -0.032px; - --letter-spacing-body-edit-02: -0.028px; - --letter-spacing-body-link-02: -0.056px; - --letter-spacing-body-ui-01: -0.128px; - --letter-spacing-body-ui-02: -0.112px; - --letter-spacing-heading-h1: -0.096px; - --letter-spacing-heading-h4: -0.016px; - --letter-spacing-help-text: 0.024px; - --text-weight-light: 300; - --text-weight-regular: 400; - --text-weight-semibold: 600; -} - -/* Ensure dark and light have same list of variables (only color variables) */ -[data-bs-theme="dark"] { - --fill-active: var(--fill-dark-mode-active); - --text-active: rgba(255, 255, 255, 1); - --slider-background-color: #373a44; - --surfaces-bg-01: var(--surfaces-dark-mode-bg-01); - --surfaces-bg-02: var(--surfaces-dark-mode-bg-02); - --surfaces-bg-03: var(--surfaces-dark-mode-bg-03); - --surfaces-bg-card: var(--surfaces-dark-mode-bg-card); - --tags-bg-color: #181b26; - --tooltip-bg-color: var(--primary-dark-500); - --main-container-bg-color: rgba(20, 23, 33, 1); - --fill-contrast-hover-selected: rgba(20, 23, 33, 1); - --text-contrast-primary: rgba(20, 23, 33, 0.88); - --border-contrast-selected: rgba(20, 23, 33, 1); - --border-disabled: var(--border-dark-mode-disabled); - --field-disabled: var(--field-dark-mode-disabled); - --text-disabled: var(--text-dark-mode-disabled); - --border-enabled: var(--border-dark-mode-enabled); - --field-enabled: var(--field-dark-mode-enabled); - --status-error: var(--status-dark-mode-error); - --focus-focus: rgba(0, 133, 255, 0.6); - --status-focus: rgba(0, 133, 255, 0.6); - --background-hover: rgba(255, 255, 255, 0.08); - --border-hover: var(--border-dark-mode-hover); - --field-hover: var(--field-dark-mode-hover); - --fill-hover-selected: var(--fill-dark-mode-hover-selected); - --status-information: var(--status-dark-mode-information); - --fill-medium-emphasis: var(--fill-dark-mode-medium-emphasis); - --fill-on-active: rgba(255, 255, 255, 1); - --state-overlays-active: var(--state-overlays-dark-mode-active); - --state-overlays-contrast-hover: var(--state-overlays-light-mode-hover); - --state-overlays-enable: var(--state-overlays-dark-mode-enable); - --state-overlays-hover: var(--state-overlays-dark-mode-hover); - --state-overlays-selected: var(--state-overlays-dark-mode-selected); - --state-overlays-selected-inverse: var( - --state-overlays-dark-mode-selected-inverse - ); - --state-overlays-selected-hover: var( - --state-overlays-dark-mode-selected-hover - ); - --text-placeholder: var(--text-dark-mode-placeholder); - --text-primary: var(--text-dark-mode-primary); - --text-secondary: var(--text-dark-mode-secondary); - --background-selected: rgba(0, 0, 0, 0.12); - --border-selected: var(--border-dark-mode-selected); - --box-shadow-elevation-0: 0px 1px 1px 0px rgba(20, 23, 33, 0.12), - 0px 0px 1px 1px rgba(20, 23, 33, 0.08); - --box-shadow-elevation-1: 0px 2px 4px -1px rgba(20, 23, 33, 0.38), - 0px 1px 2px -1px rgba(20, 23, 33, 0.88); - --box-shadow-elevation-card: 0px 2px 2px -1px rgba(20, 23, 33, 0.38), - 0px 2px 6px -1px rgba(20, 23, 33, 0.16); - --box-shadow-elevation-card-hover: 0px 2px 2px 0px rgba(20, 23, 33, 0.24), - 0px 4px 8px 1px rgba(20, 23, 33, 0.12); - --fill-subtle: var(--fill-dark-mode-subtle); - --border-subtle-alpha-01: var(--border-dark-mode-subtle-alpha-01); - --border-subtle-alpha-02: var(--border-dark-mode-subtle-alpha-02); - --status-success: var(--status-dark-mode-success); - --tags-text-color: var(--tags-dark-mode-text-color); - --status-warning: var(--status-dark-mode-warning); - --text-code-string: #95c2e7; - --text-code-keyword: #f4766e; - --text-code-meta: #c8ace1; - --text-code-type: #f69d50; - --text-code-literal: #6bb5fd; -} - -[data-bs-theme="light"] { - --fill-accordion-button: invert(64%) sepia(0%) saturate(1375%); - --fill-active: var(--fill-light-mode-active); - --text-active: rgba(20, 23, 33, 1); - --slider-background-color: #d3d6d9; - --surfaces-bg-01: var(--surfaces-light-mode-bg-01); - --surfaces-bg-02: var(--surfaces-light-mode-bg-02); - --surfaces-bg-03: var(--surfaces-light-mode-bg-03); - --surfaces-bg-card: var(--surfaces-light-mode-bg-card); - --tags-bg-color: var(--primary-light-300); - --tooltip-bg-color: var(--primary-light-100); - --main-container-bg-color: rgba(255, 255, 255, 1); - --fill-contrast-hover-selected: rgba(255, 255, 255, 1); - --text-contrast-primary: rgba(255, 255, 255, 0.88); - --border-contrast-selected: rgba(255, 255, 255, 1); - --border-disabled: var(--border-light-mode-disabled); - --field-disabled: var(--field-light-mode-disabled); - --text-disabled: var(--text-light-mode-disabled); - --border-enabled: var(--border-light-mode-enabled); - --field-enabled: var(--field-light-mode-enabled); - --status-error: var(--status-light-mode-error); - --focus-focus: rgba(0, 133, 255, 0.6); - --status-focus: rgba(0, 133, 255, 0.6); - --background-hover: var(--overlays-on-light-ui-medium-contrast); - --border-hover: var(--border-light-mode-hover); - --field-hover: var(--field-light-mode-hover); - --fill-hover-selected: var(--fill-light-mode-hover-selected); - --fill-icon-image-card: invert(64%) sepia(0%) saturate(1375%) - hue-rotate(247deg) brightness(95%) contrast(90%); - --status-information: var(--status-light-mode-information); - --fill-medium-emphasis: var(--fill-light-mode-medium-emphasis); - --fill-on-active: rgba(0, 0, 0, 1); - --state-overlays-active: var(--state-overlays-light-mode-active); - --state-overlays-contrast-hover: var(--state-overlays-dark-mode-hover); - --state-overlays-enable: var(--state-overlays-light-mode-enable); - --state-overlays-hover: var(--state-overlays-light-mode-hover); - --state-overlays-selected: var(--state-overlays-light-mode-selected); - --state-overlays-selected-inverse: var( - --state-overlays-light-mode-selected-inverse - ); - --state-overlays-selected-hover: var( - --state-overlays-light-mode-selected-hover - ); - --text-placeholder: var(--text-light-mode-placeholder); - --text-primary: var(--text-light-mode-primary); - --text-secondary: var(--text-light-mode-secondary); - --background-selected: var(--overlays-on-light-ui-high-contrast); - --border-selected: var(--border-light-mode-selected); - --box-shadow-elevation-0: 0px 1px 1px 0px rgba(20, 23, 33, 0.12), - 0px 0px 1px 1px rgba(20, 23, 33, 0.08); - --box-shadow-elevation-1: 0px 2px 4px -1px rgba(20, 23, 33, 0.12), - 0px 1px 2px -1px rgba(20, 23, 33, 0.08); - --box-shadow-elevation-card: 0px 1px 1px 0px rgba(20, 23, 33, 0.08), - 0px 0px 1px 0px rgba(20, 23, 33, 0.38); - --box-shadow-elevation-card-hover: 0px 2px 2px 0px rgba(20, 23, 33, 0.24), - 0px 4px 8px 1px rgba(20, 23, 33, 0.12); - --fill-subtle: var(--fill-light-mode-subtle); - --border-subtle-alpha-01: var(--border-light-mode-subtle-alpha-01); - --border-subtle-alpha-02: var(--border-light-mode-subtle-alpha-02); - --status-success: var(--status-light-mode-success); - --tags-text-color: var(--tags-light-mode-text-color); - --status-warning: var(--status-light-mode-warning); - --inverse-color: invert(100%); - --text-code-string: #0a3069; - --text-code-keyword: #d12d39; - --text-code-meta: #6f42c1; - --text-code-type: #f69d50; - --text-code-literal: #005cc5; -} diff --git a/vizro-core/src/vizro/static/css/vizro-bootstrap.min.css b/vizro-core/src/vizro/static/css/vizro-bootstrap.min.css index 490a9dbbf..83c26bc15 100644 --- a/vizro-core/src/vizro/static/css/vizro-bootstrap.min.css +++ b/vizro-core/src/vizro/static/css/vizro-bootstrap.min.css @@ -2,4 +2,4 @@ * Bootstrap v5.3.3 (https://getbootstrap.com/) * Copyright 2011-2024 The Bootstrap Authors * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - */:root,[data-bs-theme=light]{--bs-blue: #1a85ff;--bs-indigo: #6610f2;--bs-purple: #976fd1;--bs-pink: #d41159;--bs-red: #ff5267;--bs-orange: #ff9222;--bs-yellow: #fdc935;--bs-green: #689f38;--bs-teal: #08bdba;--bs-cyan: #00b4ff;--bs-black: #000;--bs-white: #fff;--bs-gray: #6c757d;--bs-gray-dark: #343a40;--bs-gray-100: #f8f9fa;--bs-gray-200: #e9ecef;--bs-gray-300: #dee2e6;--bs-gray-400: #ced4da;--bs-gray-500: #adb5bd;--bs-gray-600: #6c757d;--bs-gray-700: #495057;--bs-gray-800: #343a40;--bs-gray-900: #212529;--bs-primary: #1a85ff;--bs-secondary: #6c757d;--bs-success: #689f38;--bs-info: #00b4ff;--bs-warning: #fdc935;--bs-danger: #ff5267;--bs-light: #f8f9fa;--bs-dark: #212529;--bs-primary-rgb: 26, 133, 255;--bs-secondary-rgb: 108, 117, 125;--bs-success-rgb: 104, 159, 56;--bs-info-rgb: 0, 180, 255;--bs-warning-rgb: 253, 201, 53;--bs-danger-rgb: 255, 82, 103;--bs-light-rgb: 248, 249, 250;--bs-dark-rgb: 33, 37, 41;--bs-primary-text-emphasis: #0a3566;--bs-secondary-text-emphasis: #2b2f32;--bs-success-text-emphasis: #2a4016;--bs-info-text-emphasis: #004866;--bs-warning-text-emphasis: #655015;--bs-danger-text-emphasis: #662129;--bs-light-text-emphasis: #495057;--bs-dark-text-emphasis: #495057;--bs-primary-bg-subtle: #d1e7ff;--bs-secondary-bg-subtle: #e2e3e5;--bs-success-bg-subtle: #e1ecd7;--bs-info-bg-subtle: #ccf0ff;--bs-warning-bg-subtle: #fff4d7;--bs-danger-bg-subtle: #ffdce1;--bs-light-bg-subtle: #fcfcfd;--bs-dark-bg-subtle: #ced4da;--bs-primary-border-subtle: #a3ceff;--bs-secondary-border-subtle: #c4c8cb;--bs-success-border-subtle: #c3d9af;--bs-info-border-subtle: #99e1ff;--bs-warning-border-subtle: #fee9ae;--bs-danger-border-subtle: #ffbac2;--bs-light-border-subtle: #e9ecef;--bs-dark-border-subtle: #adb5bd;--bs-white-rgb: 255, 255, 255;--bs-black-rgb: 0, 0, 0;--bs-font-sans-serif: inter, sans-serif, arial, serif, "Segoe UI", roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family: var(--bs-font-sans-serif);--bs-body-font-size:0.875rem;--bs-body-font-weight: 400;--bs-body-line-height: 1.5;--bs-body-color: #212529;--bs-body-color-rgb: 33, 37, 41;--bs-body-bg: #fff;--bs-body-bg-rgb: 255, 255, 255;--bs-emphasis-color: #000;--bs-emphasis-color-rgb: 0, 0, 0;--bs-secondary-color: rgba(33, 37, 41, 0.75);--bs-secondary-color-rgb: 33, 37, 41;--bs-secondary-bg: #e9ecef;--bs-secondary-bg-rgb: 233, 236, 239;--bs-tertiary-color: rgba(33, 37, 41, 0.5);--bs-tertiary-color-rgb: 33, 37, 41;--bs-tertiary-bg: #f8f9fa;--bs-tertiary-bg-rgb: 248, 249, 250;--bs-heading-color: inherit;--bs-link-color: #1a85ff;--bs-link-color-rgb: 26, 133, 255;--bs-link-decoration: underline;--bs-link-hover-color: #156acc;--bs-link-hover-color-rgb: 21, 106, 204;--bs-code-color: #d41159;--bs-highlight-color: #212529;--bs-highlight-bg: #fff4d7;--bs-border-width: 1px;--bs-border-style: solid;--bs-border-color: #dee2e6;--bs-border-color-translucent: rgba(0, 0, 0, 0.175);--bs-border-radius: 0;--bs-border-radius-sm: 0;--bs-border-radius-lg: 0;--bs-border-radius-xl: 1rem;--bs-border-radius-xxl: 2rem;--bs-border-radius-2xl: var(--bs-border-radius-xxl);--bs-border-radius-pill: 50rem;--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);--bs-focus-ring-width: 0.25rem;--bs-focus-ring-opacity: 0.25;--bs-focus-ring-color: rgba(26, 133, 255, 0.25);--bs-form-valid-color: #689f38;--bs-form-valid-border-color: #689f38;--bs-form-invalid-color: #ff5267;--bs-form-invalid-border-color: #ff5267}[data-bs-theme=dark]{color-scheme:dark;--bs-body-color: #dee2e6;--bs-body-color-rgb: 222, 226, 230;--bs-body-bg: #212529;--bs-body-bg-rgb: 33, 37, 41;--bs-emphasis-color: #fff;--bs-emphasis-color-rgb: 255, 255, 255;--bs-secondary-color: rgba(222, 226, 230, 0.75);--bs-secondary-color-rgb: 222, 226, 230;--bs-secondary-bg: #343a40;--bs-secondary-bg-rgb: 52, 58, 64;--bs-tertiary-color: rgba(222, 226, 230, 0.5);--bs-tertiary-color-rgb: 222, 226, 230;--bs-tertiary-bg: #2b3035;--bs-tertiary-bg-rgb: 43, 48, 53;--bs-primary-text-emphasis: #76b6ff;--bs-secondary-text-emphasis: #a7acb1;--bs-success-text-emphasis: #a4c588;--bs-info-text-emphasis: #66d2ff;--bs-warning-text-emphasis: #fedf86;--bs-danger-text-emphasis: #ff97a4;--bs-light-text-emphasis: #f8f9fa;--bs-dark-text-emphasis: #dee2e6;--bs-primary-bg-subtle: #051b33;--bs-secondary-bg-subtle: #161719;--bs-success-bg-subtle: #15200b;--bs-info-bg-subtle: #002433;--bs-warning-bg-subtle: #33280b;--bs-danger-bg-subtle: #331015;--bs-light-bg-subtle: #343a40;--bs-dark-bg-subtle: #1a1d20;--bs-primary-border-subtle: #105099;--bs-secondary-border-subtle: #41464b;--bs-success-border-subtle: #3e5f22;--bs-info-border-subtle: #006c99;--bs-warning-border-subtle: #987920;--bs-danger-border-subtle: #99313e;--bs-light-border-subtle: #495057;--bs-dark-border-subtle: #343a40;--bs-heading-color: inherit;--bs-link-color: #76b6ff;--bs-link-hover-color: #91c5ff;--bs-link-color-rgb: 118, 182, 255;--bs-link-hover-color-rgb: 145, 197, 255;--bs-code-color: #e5709b;--bs-highlight-color: #dee2e6;--bs-highlight-bg: #655015;--bs-border-color: #495057;--bs-border-color-translucent: rgba(255, 255, 255, 0.15);--bs-form-valid-color: #a4c588;--bs-form-valid-border-color: #a4c588;--bs-form-invalid-color: #ff97a4;--bs-form-invalid-border-color: #ff97a4}*,*::before,*::after{box-sizing:border-box}@media(prefers-reduced-motion: no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0)}hr{margin:1rem 0;color:inherit;border:0;border-top:var(--bs-border-width) solid;opacity:.25}h6,.h6,h5,.h5,h4,.h4,h3,.h3,h2,.h2,h1,.h1{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2;color:var(--bs-heading-color)}h1,.h1{font-size:calc(1.34375rem + 1.125vw)}@media(min-width: 1200px){h1,.h1{font-size:2.1875rem}}h2,.h2{font-size:calc(1.3rem + 0.6vw)}@media(min-width: 1200px){h2,.h2{font-size:1.75rem}}h3,.h3{font-size:calc(1.278125rem + 0.3375vw)}@media(min-width: 1200px){h3,.h3{font-size:1.53125rem}}h4,.h4{font-size:calc(1.25625rem + 0.075vw)}@media(min-width: 1200px){h4,.h4{font-size:1.3125rem}}h5,.h5{font-size:1.09375rem}h6,.h6{font-size:0.875rem}p{margin-top:0;margin-bottom:.5rem}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}ol,ul,dl{margin-top:0;margin-bottom:1rem}ol ol,ul ul,ol ul,ul ol{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small,.small{font-size:0.875em}mark,.mark{padding:.1875em;color:var(--bs-highlight-color);background-color:var(--bs-highlight-bg)}sub,sup{position:relative;font-size:0.75em;line-height:0;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}a{color:rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));text-decoration:underline}a:hover{--bs-link-color-rgb: var(--bs-link-hover-color-rgb)}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}pre,code,kbd,samp{font-family:var(--bs-font-monospace);font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:0.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:0.875em;color:var(--bs-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:.1875rem .375rem;font-size:0.875em;color:var(--bs-body-bg);background-color:var(--bs-body-color)}kbd kbd{padding:0;font-size:1em}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-secondary-color);text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}thead,tbody,tfoot,tr,td,th{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}input,button,select,optgroup,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none !important}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}button:not(:disabled),[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + 0.3vw);line-height:inherit}@media(min-width: 1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-text,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none !important}.lead{font-size:1.09375rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media(min-width: 1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media(min-width: 1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media(min-width: 1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media(min-width: 1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media(min-width: 1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media(min-width: 1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:0.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.09375rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:0.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:var(--bs-body-bg);border:var(--bs-border-width) solid var(--bs-border-color);max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:0.875em;color:var(--bs-secondary-color)}.container,.container-fluid,.container-xxl,.container-xl,.container-lg,.container-md,.container-sm{--bs-gutter-x: 1.5rem;--bs-gutter-y: 0;width:100%;padding-right:calc(var(--bs-gutter-x)*.5);padding-left:calc(var(--bs-gutter-x)*.5);margin-right:auto;margin-left:auto}@media(min-width: 576px){.container-sm,.container{max-width:540px}}@media(min-width: 768px){.container-md,.container-sm,.container{max-width:720px}}@media(min-width: 992px){.container-lg,.container-md,.container-sm,.container{max-width:960px}}@media(min-width: 1200px){.container-xl,.container-lg,.container-md,.container-sm,.container{max-width:1140px}}@media(min-width: 1400px){.container-xxl,.container-xl,.container-lg,.container-md,.container-sm,.container{max-width:1320px}}:root{--bs-breakpoint-xs: 0;--bs-breakpoint-sm: 576px;--bs-breakpoint-md: 768px;--bs-breakpoint-lg: 992px;--bs-breakpoint-xl: 1200px;--bs-breakpoint-xxl: 1400px}.row{--bs-gutter-x: 1.5rem;--bs-gutter-y: 0;display:flex;flex-wrap:wrap;margin-top:calc(-1*var(--bs-gutter-y));margin-right:calc(-0.5*var(--bs-gutter-x));margin-left:calc(-0.5*var(--bs-gutter-x))}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x)*.5);padding-left:calc(var(--bs-gutter-x)*.5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.66666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x: 0}.g-0,.gy-0{--bs-gutter-y: 0}.g-1,.gx-1{--bs-gutter-x: 0.25rem}.g-1,.gy-1{--bs-gutter-y: 0.25rem}.g-2,.gx-2{--bs-gutter-x: 0.5rem}.g-2,.gy-2{--bs-gutter-y: 0.5rem}.g-3,.gx-3{--bs-gutter-x: 1rem}.g-3,.gy-3{--bs-gutter-y: 1rem}.g-4,.gx-4{--bs-gutter-x: 1.5rem}.g-4,.gy-4{--bs-gutter-y: 1.5rem}.g-5,.gx-5{--bs-gutter-x: 3rem}.g-5,.gy-5{--bs-gutter-y: 3rem}@media(min-width: 576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.66666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x: 0}.g-sm-0,.gy-sm-0{--bs-gutter-y: 0}.g-sm-1,.gx-sm-1{--bs-gutter-x: 0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y: 0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x: 0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y: 0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x: 1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y: 1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x: 1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y: 1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x: 3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y: 3rem}}@media(min-width: 768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.66666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x: 0}.g-md-0,.gy-md-0{--bs-gutter-y: 0}.g-md-1,.gx-md-1{--bs-gutter-x: 0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y: 0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x: 0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y: 0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x: 1rem}.g-md-3,.gy-md-3{--bs-gutter-y: 1rem}.g-md-4,.gx-md-4{--bs-gutter-x: 1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y: 1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x: 3rem}.g-md-5,.gy-md-5{--bs-gutter-y: 3rem}}@media(min-width: 992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.66666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x: 0}.g-lg-0,.gy-lg-0{--bs-gutter-y: 0}.g-lg-1,.gx-lg-1{--bs-gutter-x: 0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y: 0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x: 0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y: 0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x: 1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y: 1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x: 1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y: 1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x: 3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y: 3rem}}@media(min-width: 1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.66666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x: 0}.g-xl-0,.gy-xl-0{--bs-gutter-y: 0}.g-xl-1,.gx-xl-1{--bs-gutter-x: 0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y: 0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x: 0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y: 0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x: 1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y: 1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x: 1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y: 1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x: 3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y: 3rem}}@media(min-width: 1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.66666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x: 0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y: 0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x: 0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y: 0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x: 0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y: 0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x: 1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y: 1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x: 1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y: 1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x: 3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y: 3rem}}.table{--bs-table-color-type: initial;--bs-table-bg-type: initial;--bs-table-color-state: initial;--bs-table-bg-state: initial;--bs-table-color: var(--bs-emphasis-color);--bs-table-bg: var(--bs-body-bg);--bs-table-border-color: var(--bs-border-color);--bs-table-accent-bg: transparent;--bs-table-striped-color: var(--bs-emphasis-color);--bs-table-striped-bg: rgba(var(--bs-emphasis-color-rgb), 0.05);--bs-table-active-color: var(--bs-emphasis-color);--bs-table-active-bg: rgba(var(--bs-emphasis-color-rgb), 0.1);--bs-table-hover-color: var(--bs-emphasis-color);--bs-table-hover-bg: rgba(var(--bs-emphasis-color-rgb), 0.075);width:100%;margin-bottom:1rem;vertical-align:top;border-color:var(--bs-table-border-color)}.table>:not(caption)>*>*{padding:.5rem .5rem;color:var(--bs-table-color-state, var(--bs-table-color-type, var(--bs-table-color)));background-color:var(--bs-table-bg);border-bottom-width:var(--bs-border-width);box-shadow:inset 0 0 0 9999px var(--bs-table-bg-state, var(--bs-table-bg-type, var(--bs-table-accent-bg)))}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table-group-divider{border-top:calc(var(--bs-border-width)*2) solid currentcolor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:var(--bs-border-width) 0}.table-bordered>:not(caption)>*>*{border-width:0 var(--bs-border-width)}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-color-type: var(--bs-table-striped-color);--bs-table-bg-type: var(--bs-table-striped-bg)}.table-striped-columns>:not(caption)>tr>:nth-child(even){--bs-table-color-type: var(--bs-table-striped-color);--bs-table-bg-type: var(--bs-table-striped-bg)}.table-active{--bs-table-color-state: var(--bs-table-active-color);--bs-table-bg-state: var(--bs-table-active-bg)}.table-hover>tbody>tr:hover>*{--bs-table-color-state: var(--bs-table-hover-color);--bs-table-bg-state: var(--bs-table-hover-bg)}.table-primary{--bs-table-color: #000;--bs-table-bg: #d1e7ff;--bs-table-border-color: #a7b9cc;--bs-table-striped-bg: #c7dbf2;--bs-table-striped-color: #000;--bs-table-active-bg: #bcd0e6;--bs-table-active-color: #000;--bs-table-hover-bg: #c1d6ec;--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-secondary{--bs-table-color: #000;--bs-table-bg: #e2e3e5;--bs-table-border-color: #b5b6b7;--bs-table-striped-bg: #d7d8da;--bs-table-striped-color: #000;--bs-table-active-bg: #cbccce;--bs-table-active-color: #000;--bs-table-hover-bg: #d1d2d4;--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-success{--bs-table-color: #000;--bs-table-bg: #e1ecd7;--bs-table-border-color: #b4bdac;--bs-table-striped-bg: #d6e0cc;--bs-table-striped-color: #000;--bs-table-active-bg: #cbd4c2;--bs-table-active-color: #000;--bs-table-hover-bg: #d0dac7;--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-info{--bs-table-color: #000;--bs-table-bg: #ccf0ff;--bs-table-border-color: #a3c0cc;--bs-table-striped-bg: #c2e4f2;--bs-table-striped-color: #000;--bs-table-active-bg: #b8d8e6;--bs-table-active-color: #000;--bs-table-hover-bg: #bddeec;--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-warning{--bs-table-color: #000;--bs-table-bg: #fff4d7;--bs-table-border-color: #ccc3ac;--bs-table-striped-bg: #f2e8cc;--bs-table-striped-color: #000;--bs-table-active-bg: #e6dcc2;--bs-table-active-color: #000;--bs-table-hover-bg: #ece2c7;--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-danger{--bs-table-color: #000;--bs-table-bg: #ffdce1;--bs-table-border-color: #ccb0b4;--bs-table-striped-bg: #f2d1d6;--bs-table-striped-color: #000;--bs-table-active-bg: #e6c6cb;--bs-table-active-color: #000;--bs-table-hover-bg: #ecccd0;--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-light{--bs-table-color: #000;--bs-table-bg: #f8f9fa;--bs-table-border-color: #c6c7c8;--bs-table-striped-bg: #ecedee;--bs-table-striped-color: #000;--bs-table-active-bg: #dfe0e1;--bs-table-active-color: #000;--bs-table-hover-bg: #e5e6e7;--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-dark{--bs-table-color: #fff;--bs-table-bg: #212529;--bs-table-border-color: #4d5154;--bs-table-striped-bg: #2c3034;--bs-table-striped-color: #fff;--bs-table-active-bg: #373b3e;--bs-table-active-color: #fff;--bs-table-hover-bg: #323539;--bs-table-hover-color: #fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media(max-width: 575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media(max-width: 767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media(max-width: 991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media(max-width: 1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media(max-width: 1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(0.25rem + var(--bs-border-width));padding-bottom:calc(0.25rem + var(--bs-border-width));margin-bottom:0;font-size:inherit;line-height:1.6}.col-form-label-lg{padding-top:calc(0.5rem + var(--bs-border-width));padding-bottom:calc(0.5rem + var(--bs-border-width));font-size:0.875rem}.col-form-label-sm{padding-top:calc(0.25rem + var(--bs-border-width));padding-bottom:calc(0.25rem + var(--bs-border-width));font-size:0.65625rem}.form-text{margin-top:.25rem;font-size:0.875em;color:var(--bs-secondary-color)}.form-control{display:block;width:100%;padding:.25rem .5rem;font-size:0.875rem;font-weight:400;line-height:1.6;color:var(--bs-body-color);-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-body-bg);background-clip:padding-box;border:var(--bs-border-width) solid var(--bs-border-color);border-radius:0}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:var(--bs-body-color);background-color:var(--bs-body-bg);border-color:#8dc2ff;outline:0;box-shadow:0 0 0 .25rem rgba(26,133,255,.25)}.form-control::-webkit-date-and-time-value{min-width:85px;height:1.6em;margin:0}.form-control::-webkit-datetime-edit{display:block;padding:0}.form-control::-moz-placeholder{color:var(--bs-secondary-color);opacity:1}.form-control::placeholder{color:var(--bs-secondary-color);opacity:1}.form-control:disabled{background-color:var(--bs-secondary-bg);opacity:1}.form-control::file-selector-button{padding:.25rem .5rem;margin:-0.25rem -0.5rem;margin-inline-end:.5rem;color:var(--bs-body-color);background-color:var(--bs-tertiary-bg);pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:var(--bs-border-width);border-radius:0}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:var(--bs-secondary-bg)}.form-control-plaintext{display:block;width:100%;padding:.25rem 0;margin-bottom:0;line-height:1.6;color:var(--bs-body-color);background-color:rgba(0,0,0,0);border:solid rgba(0,0,0,0);border-width:var(--bs-border-width) 0}.form-control-plaintext:focus{outline:0}.form-control-plaintext.form-control-sm,.form-control-plaintext.form-control-lg{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.6em + 0.5rem + calc(var(--bs-border-width) * 2));padding:.25rem .5rem;font-size:0.65625rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-0.25rem -0.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.6em + 1rem + calc(var(--bs-border-width) * 2));padding:.5rem 1rem;font-size:0.875rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-0.5rem -1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.6em + 0.5rem + calc(var(--bs-border-width) * 2))}textarea.form-control-sm{min-height:calc(1.6em + 0.5rem + calc(var(--bs-border-width) * 2))}textarea.form-control-lg{min-height:calc(1.6em + 1rem + calc(var(--bs-border-width) * 2))}.form-control-color{width:3rem;height:calc(1.6em + 0.5rem + calc(var(--bs-border-width) * 2));padding:.25rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{border:0 !important}.form-control-color::-webkit-color-swatch{border:0 !important}.form-control-color.form-control-sm{height:calc(1.6em + 0.5rem + calc(var(--bs-border-width) * 2))}.form-control-color.form-control-lg{height:calc(1.6em + 1rem + calc(var(--bs-border-width) * 2))}.form-select{--bs-form-select-bg-img: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");display:block;width:100%;padding:.25rem 1.5rem .25rem .5rem;font-size:0.875rem;font-weight:400;line-height:1.6;color:var(--bs-body-color);-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-body-bg);background-image:var(--bs-form-select-bg-img),var(--bs-form-select-bg-icon, none);background-repeat:no-repeat;background-position:right .5rem center;background-size:16px 12px;border:var(--bs-border-width) solid var(--bs-border-color);border-radius:0}.form-select:focus{border-color:#8dc2ff;outline:0;box-shadow:0 0 0 .25rem rgba(26,133,255,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.5rem;background-image:none}.form-select:disabled{background-color:var(--bs-secondary-bg)}.form-select:-moz-focusring{color:rgba(0,0,0,0);text-shadow:0 0 0 var(--bs-body-color)}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:0.65625rem}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:0.875rem}[data-bs-theme=dark] .form-select{--bs-form-select-bg-img: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23dee2e6' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e")}.form-check{display:block;min-height:1.3125rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-reverse{padding-right:1.5em;padding-left:0;text-align:right}.form-check-reverse .form-check-input{float:right;margin-right:-1.5em;margin-left:0}.form-check-input{--bs-form-check-bg: var(--bs-body-bg);flex-shrink:0;width:1em;height:1em;margin-top:.25em;vertical-align:top;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-form-check-bg);background-image:var(--bs-form-check-bg-image);background-repeat:no-repeat;background-position:center;background-size:contain;border:var(--bs-border-width) solid var(--bs-border-color);-webkit-print-color-adjust:exact;print-color-adjust:exact}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#8dc2ff;outline:0;box-shadow:0 0 0 .25rem rgba(26,133,255,.25)}.form-check-input:checked{background-color:#1a85ff;border-color:#1a85ff}.form-check-input:checked[type=checkbox]{--bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{--bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#1a85ff;border-color:#1a85ff;--bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input[disabled]~.form-check-label,.form-check-input:disabled~.form-check-label{cursor:default;opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");width:2em;margin-left:-2.5em;background-image:var(--bs-form-switch-bg);background-position:left center;border-radius:0}.form-switch .form-check-input:focus{--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%238dc2ff'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-switch.form-check-reverse{padding-right:2.5em;padding-left:0}.form-switch.form-check-reverse .form-check-input{margin-right:-2.5em;margin-left:0}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0, 0, 0, 0);pointer-events:none}.btn-check[disabled]+.btn,.btn-check:disabled+.btn{pointer-events:none;filter:none;opacity:1}[data-bs-theme=dark] .form-switch .form-check-input:not(:checked):not(:focus){--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.25%29'/%3e%3c/svg%3e")}.form-range{width:100%;height:1.5rem;padding:0;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:rgba(0,0,0,0)}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(26,133,255,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(26,133,255,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-0.25rem;-webkit-appearance:none;appearance:none;background-color:#1a85ff;border:0}.form-range::-webkit-slider-thumb:active{background-color:#badaff}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:rgba(0,0,0,0);cursor:pointer;background-color:var(--bs-secondary-bg);border-color:rgba(0,0,0,0)}.form-range::-moz-range-thumb{width:1rem;height:1rem;-moz-appearance:none;appearance:none;background-color:#1a85ff;border:0}.form-range::-moz-range-thumb:active{background-color:#badaff}.form-range::-moz-range-track{width:100%;height:.5rem;color:rgba(0,0,0,0);cursor:pointer;background-color:var(--bs-secondary-bg);border-color:rgba(0,0,0,0)}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:var(--bs-secondary-color)}.form-range:disabled::-moz-range-thumb{background-color:var(--bs-secondary-color)}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-control-plaintext,.form-floating>.form-select{height:calc(3.5rem + calc(var(--bs-border-width) * 2));min-height:calc(3.5rem + calc(var(--bs-border-width) * 2));line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;z-index:2;height:100%;padding:1rem .5rem;overflow:hidden;text-align:start;text-overflow:ellipsis;white-space:nowrap;pointer-events:none;border:var(--bs-border-width) solid rgba(0,0,0,0);transform-origin:0 0}.form-floating>.form-control,.form-floating>.form-control-plaintext{padding:1rem .5rem}.form-floating>.form-control::-moz-placeholder, .form-floating>.form-control-plaintext::-moz-placeholder{color:rgba(0,0,0,0)}.form-floating>.form-control::placeholder,.form-floating>.form-control-plaintext::placeholder{color:rgba(0,0,0,0)}.form-floating>.form-control:not(:-moz-placeholder-shown), .form-floating>.form-control-plaintext:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown),.form-floating>.form-control-plaintext:focus,.form-floating>.form-control-plaintext:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:-webkit-autofill,.form-floating>.form-control-plaintext:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{color:rgba(var(--bs-body-color-rgb), 0.65);transform:scale(0.85) translateY(-0.5rem) translateX(0.15rem)}.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-control-plaintext~label,.form-floating>.form-select~label{color:rgba(var(--bs-body-color-rgb), 0.65);transform:scale(0.85) translateY(-0.5rem) translateX(0.15rem)}.form-floating>.form-control:not(:-moz-placeholder-shown)~label::after{position:absolute;inset:1rem .25rem;z-index:-1;height:1.5em;content:"";background-color:var(--bs-body-bg)}.form-floating>.form-control:focus~label::after,.form-floating>.form-control:not(:placeholder-shown)~label::after,.form-floating>.form-control-plaintext~label::after,.form-floating>.form-select~label::after{position:absolute;inset:1rem .25rem;z-index:-1;height:1.5em;content:"";background-color:var(--bs-body-bg)}.form-floating>.form-control:-webkit-autofill~label{color:rgba(var(--bs-body-color-rgb), 0.65);transform:scale(0.85) translateY(-0.5rem) translateX(0.15rem)}.form-floating>.form-control-plaintext~label{border-width:var(--bs-border-width) 0}.form-floating>:disabled~label,.form-floating>.form-control:disabled~label{color:#6c757d}.form-floating>:disabled~label::after,.form-floating>.form-control:disabled~label::after{background-color:var(--bs-secondary-bg)}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-select,.input-group>.form-floating{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-select:focus,.input-group>.form-floating:focus-within{z-index:5}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:5}.input-group-text{display:flex;align-items:center;padding:.25rem .5rem;font-size:0.875rem;font-weight:400;line-height:1.6;color:var(--bs-body-color);text-align:center;white-space:nowrap;background-color:var(--bs-tertiary-bg);border:var(--bs-border-width) solid var(--bs-border-color)}.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text,.input-group-lg>.btn{padding:.5rem 1rem;font-size:0.875rem}.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text,.input-group-sm>.btn{padding:.25rem .5rem;font-size:0.65625rem}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:2rem}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:calc(var(--bs-border-width)*-1)}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:0.875em;color:var(--bs-form-valid-color)}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:0.65625rem;color:#fff;background-color:var(--bs-success)}.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip,.is-valid~.valid-feedback,.is-valid~.valid-tooltip{display:block}.was-validated .form-control:valid,.form-control.is-valid{border-color:var(--bs-form-valid-border-color);padding-right:calc(1.6em + 0.5rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23689f38' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(0.4em + 0.125rem) center;background-size:calc(0.8em + 0.25rem) calc(0.8em + 0.25rem)}.was-validated .form-control:valid:focus,.form-control.is-valid:focus{border-color:var(--bs-form-valid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb), 0.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.6em + 0.5rem);background-position:top calc(0.4em + 0.125rem) right calc(0.4em + 0.125rem)}.was-validated .form-select:valid,.form-select.is-valid{border-color:var(--bs-form-valid-border-color)}.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"],.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"]{--bs-form-select-bg-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23689f38' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");padding-right:2.75rem;background-position:right .5rem center,center right 1.5rem;background-size:16px 12px,calc(0.8em + 0.25rem) calc(0.8em + 0.25rem)}.was-validated .form-select:valid:focus,.form-select.is-valid:focus{border-color:var(--bs-form-valid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb), 0.25)}.was-validated .form-control-color:valid,.form-control-color.is-valid{width:calc(3rem + calc(1.6em + 0.5rem))}.was-validated .form-check-input:valid,.form-check-input.is-valid{border-color:var(--bs-form-valid-border-color)}.was-validated .form-check-input:valid:checked,.form-check-input.is-valid:checked{background-color:var(--bs-form-valid-color)}.was-validated .form-check-input:valid:focus,.form-check-input.is-valid:focus{box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb), 0.25)}.was-validated .form-check-input:valid~.form-check-label,.form-check-input.is-valid~.form-check-label{color:var(--bs-form-valid-color)}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.was-validated .input-group>.form-control:not(:focus):valid,.input-group>.form-control:not(:focus).is-valid,.was-validated .input-group>.form-select:not(:focus):valid,.input-group>.form-select:not(:focus).is-valid,.was-validated .input-group>.form-floating:not(:focus-within):valid,.input-group>.form-floating:not(:focus-within).is-valid{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:0.875em;color:var(--bs-form-invalid-color)}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:0.65625rem;color:#fff;background-color:var(--bs-danger)}.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip,.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip{display:block}.was-validated .form-control:invalid,.form-control.is-invalid{border-color:var(--bs-form-invalid-border-color);padding-right:calc(1.6em + 0.5rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23ff5267'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23ff5267' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(0.4em + 0.125rem) center;background-size:calc(0.8em + 0.25rem) calc(0.8em + 0.25rem)}.was-validated .form-control:invalid:focus,.form-control.is-invalid:focus{border-color:var(--bs-form-invalid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb), 0.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.6em + 0.5rem);background-position:top calc(0.4em + 0.125rem) right calc(0.4em + 0.125rem)}.was-validated .form-select:invalid,.form-select.is-invalid{border-color:var(--bs-form-invalid-border-color)}.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"],.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"]{--bs-form-select-bg-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23ff5267'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23ff5267' stroke='none'/%3e%3c/svg%3e");padding-right:2.75rem;background-position:right .5rem center,center right 1.5rem;background-size:16px 12px,calc(0.8em + 0.25rem) calc(0.8em + 0.25rem)}.was-validated .form-select:invalid:focus,.form-select.is-invalid:focus{border-color:var(--bs-form-invalid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb), 0.25)}.was-validated .form-control-color:invalid,.form-control-color.is-invalid{width:calc(3rem + calc(1.6em + 0.5rem))}.was-validated .form-check-input:invalid,.form-check-input.is-invalid{border-color:var(--bs-form-invalid-border-color)}.was-validated .form-check-input:invalid:checked,.form-check-input.is-invalid:checked{background-color:var(--bs-form-invalid-color)}.was-validated .form-check-input:invalid:focus,.form-check-input.is-invalid:focus{box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb), 0.25)}.was-validated .form-check-input:invalid~.form-check-label,.form-check-input.is-invalid~.form-check-label{color:var(--bs-form-invalid-color)}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.was-validated .input-group>.form-control:not(:focus):invalid,.input-group>.form-control:not(:focus).is-invalid,.was-validated .input-group>.form-select:not(:focus):invalid,.input-group>.form-select:not(:focus).is-invalid,.was-validated .input-group>.form-floating:not(:focus-within):invalid,.input-group>.form-floating:not(:focus-within).is-invalid{z-index:4}.btn{--bs-btn-padding-x: 0.5rem;--bs-btn-padding-y: 0.25rem;--bs-btn-font-family: ;--bs-btn-font-size:0.875rem;--bs-btn-font-weight: 400;--bs-btn-line-height: 1.6;--bs-btn-color: var(--bs-body-color);--bs-btn-bg: transparent;--bs-btn-border-width: var(--bs-border-width);--bs-btn-border-color: transparent;--bs-btn-border-radius: var(--bs-border-radius);--bs-btn-hover-border-color: transparent;--bs-btn-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);--bs-btn-disabled-opacity: 1;--bs-btn-focus-box-shadow: 0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), .5);display:inline-block;padding:var(--bs-btn-padding-y) var(--bs-btn-padding-x);font-family:var(--bs-btn-font-family);font-size:var(--bs-btn-font-size);font-weight:var(--bs-btn-font-weight);line-height:var(--bs-btn-line-height);color:var(--bs-btn-color);text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;border:var(--bs-btn-border-width) solid var(--bs-btn-border-color);background-color:var(--bs-btn-bg)}.btn:hover{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color)}.btn-check+.btn:hover{color:var(--bs-btn-color);background-color:var(--bs-btn-bg);border-color:var(--bs-btn-border-color)}.btn:focus-visible{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:focus-visible+.btn{border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked+.btn,:not(.btn-check)+.btn:active,.btn:first-child:active,.btn.active,.btn.show{color:var(--bs-btn-active-color);background-color:var(--bs-btn-active-bg);border-color:var(--bs-btn-active-border-color)}.btn-check:checked+.btn:focus-visible,:not(.btn-check)+.btn:active:focus-visible,.btn:first-child:active:focus-visible,.btn.active:focus-visible,.btn.show:focus-visible{box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked:focus-visible+.btn{box-shadow:var(--bs-btn-focus-box-shadow)}.btn:disabled,.btn.disabled,fieldset:disabled .btn{color:var(--bs-btn-disabled-color);pointer-events:none;background-color:var(--bs-btn-disabled-bg);border-color:var(--bs-btn-disabled-border-color);opacity:var(--bs-btn-disabled-opacity)}.btn-primary{--bs-btn-color: #000;--bs-btn-bg: #1a85ff;--bs-btn-border-color: #1a85ff;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #3c97ff;--bs-btn-hover-border-color: #3191ff;--bs-btn-focus-shadow-rgb: 22, 113, 217;--bs-btn-active-color: #000;--bs-btn-active-bg: #489dff;--bs-btn-active-border-color: #3191ff;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #000;--bs-btn-disabled-bg: #1a85ff;--bs-btn-disabled-border-color: #1a85ff}.btn-secondary{--bs-btn-color: #fff;--bs-btn-bg: #6c757d;--bs-btn-border-color: #6c757d;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #5c636a;--bs-btn-hover-border-color: #565e64;--bs-btn-focus-shadow-rgb: 130, 138, 145;--bs-btn-active-color: #fff;--bs-btn-active-bg: #565e64;--bs-btn-active-border-color: #51585e;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #6c757d;--bs-btn-disabled-border-color: #6c757d}.btn-success{--bs-btn-color: #000;--bs-btn-bg: #689f38;--bs-btn-border-color: #689f38;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #7fad56;--bs-btn-hover-border-color: #77a94c;--bs-btn-focus-shadow-rgb: 88, 135, 48;--bs-btn-active-color: #000;--bs-btn-active-bg: #86b260;--bs-btn-active-border-color: #77a94c;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #000;--bs-btn-disabled-bg: #689f38;--bs-btn-disabled-border-color: #689f38}.btn-info{--bs-btn-color: #000;--bs-btn-bg: #00b4ff;--bs-btn-border-color: #00b4ff;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #26bfff;--bs-btn-hover-border-color: #1abcff;--bs-btn-focus-shadow-rgb: 0, 153, 217;--bs-btn-active-color: #000;--bs-btn-active-bg: #33c3ff;--bs-btn-active-border-color: #1abcff;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #000;--bs-btn-disabled-bg: #00b4ff;--bs-btn-disabled-border-color: #00b4ff}.btn-warning{--bs-btn-color: #000;--bs-btn-bg: #fdc935;--bs-btn-border-color: #fdc935;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #fdd153;--bs-btn-hover-border-color: #fdce49;--bs-btn-focus-shadow-rgb: 215, 171, 45;--bs-btn-active-color: #000;--bs-btn-active-bg: #fdd45d;--bs-btn-active-border-color: #fdce49;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #000;--bs-btn-disabled-bg: #fdc935;--bs-btn-disabled-border-color: #fdc935}.btn-danger{--bs-btn-color: #000;--bs-btn-bg: #ff5267;--bs-btn-border-color: #ff5267;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #ff6c7e;--bs-btn-hover-border-color: #ff6376;--bs-btn-focus-shadow-rgb: 217, 70, 88;--bs-btn-active-color: #000;--bs-btn-active-bg: #ff7585;--bs-btn-active-border-color: #ff6376;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #000;--bs-btn-disabled-bg: #ff5267;--bs-btn-disabled-border-color: #ff5267}.btn-light{--bs-btn-color: #000;--bs-btn-bg: #f8f9fa;--bs-btn-border-color: #f8f9fa;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #d3d4d5;--bs-btn-hover-border-color: #c6c7c8;--bs-btn-focus-shadow-rgb: 211, 212, 213;--bs-btn-active-color: #000;--bs-btn-active-bg: #c6c7c8;--bs-btn-active-border-color: #babbbc;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #000;--bs-btn-disabled-bg: #f8f9fa;--bs-btn-disabled-border-color: #f8f9fa}.btn-dark{--bs-btn-color: #fff;--bs-btn-bg: #212529;--bs-btn-border-color: #212529;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #424649;--bs-btn-hover-border-color: #373b3e;--bs-btn-focus-shadow-rgb: 66, 70, 73;--bs-btn-active-color: #fff;--bs-btn-active-bg: #4d5154;--bs-btn-active-border-color: #373b3e;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #212529;--bs-btn-disabled-border-color: #212529}.btn-outline-primary{--bs-btn-color: #1a85ff;--bs-btn-border-color: #1a85ff;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #1a85ff;--bs-btn-hover-border-color: #1a85ff;--bs-btn-focus-shadow-rgb: 26, 133, 255;--bs-btn-active-color: #000;--bs-btn-active-bg: #1a85ff;--bs-btn-active-border-color: #1a85ff;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #1a85ff;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #1a85ff;--bs-gradient: none}.btn-outline-secondary{--bs-btn-color: #6c757d;--bs-btn-border-color: #6c757d;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #6c757d;--bs-btn-hover-border-color: #6c757d;--bs-btn-focus-shadow-rgb: 108, 117, 125;--bs-btn-active-color: #fff;--bs-btn-active-bg: #6c757d;--bs-btn-active-border-color: #6c757d;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #6c757d;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #6c757d;--bs-gradient: none}.btn-outline-success{--bs-btn-color: #689f38;--bs-btn-border-color: #689f38;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #689f38;--bs-btn-hover-border-color: #689f38;--bs-btn-focus-shadow-rgb: 104, 159, 56;--bs-btn-active-color: #000;--bs-btn-active-bg: #689f38;--bs-btn-active-border-color: #689f38;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #689f38;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #689f38;--bs-gradient: none}.btn-outline-info{--bs-btn-color: #00b4ff;--bs-btn-border-color: #00b4ff;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #00b4ff;--bs-btn-hover-border-color: #00b4ff;--bs-btn-focus-shadow-rgb: 0, 180, 255;--bs-btn-active-color: #000;--bs-btn-active-bg: #00b4ff;--bs-btn-active-border-color: #00b4ff;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #00b4ff;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #00b4ff;--bs-gradient: none}.btn-outline-warning{--bs-btn-color: #fdc935;--bs-btn-border-color: #fdc935;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #fdc935;--bs-btn-hover-border-color: #fdc935;--bs-btn-focus-shadow-rgb: 253, 201, 53;--bs-btn-active-color: #000;--bs-btn-active-bg: #fdc935;--bs-btn-active-border-color: #fdc935;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #fdc935;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #fdc935;--bs-gradient: none}.btn-outline-danger{--bs-btn-color: #ff5267;--bs-btn-border-color: #ff5267;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #ff5267;--bs-btn-hover-border-color: #ff5267;--bs-btn-focus-shadow-rgb: 255, 82, 103;--bs-btn-active-color: #000;--bs-btn-active-bg: #ff5267;--bs-btn-active-border-color: #ff5267;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #ff5267;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #ff5267;--bs-gradient: none}.btn-outline-light{--bs-btn-color: #f8f9fa;--bs-btn-border-color: #f8f9fa;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #f8f9fa;--bs-btn-hover-border-color: #f8f9fa;--bs-btn-focus-shadow-rgb: 248, 249, 250;--bs-btn-active-color: #000;--bs-btn-active-bg: #f8f9fa;--bs-btn-active-border-color: #f8f9fa;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #f8f9fa;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #f8f9fa;--bs-gradient: none}.btn-outline-dark{--bs-btn-color: #212529;--bs-btn-border-color: #212529;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #212529;--bs-btn-hover-border-color: #212529;--bs-btn-focus-shadow-rgb: 33, 37, 41;--bs-btn-active-color: #fff;--bs-btn-active-bg: #212529;--bs-btn-active-border-color: #212529;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #212529;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #212529;--bs-gradient: none}.btn-link{--bs-btn-font-weight: 400;--bs-btn-color: var(--bs-link-color);--bs-btn-bg: transparent;--bs-btn-border-color: transparent;--bs-btn-hover-color: var(--bs-link-hover-color);--bs-btn-hover-border-color: transparent;--bs-btn-active-color: var(--bs-link-hover-color);--bs-btn-active-border-color: transparent;--bs-btn-disabled-color: #6c757d;--bs-btn-disabled-border-color: transparent;--bs-btn-box-shadow: 0 0 0 #000;--bs-btn-focus-shadow-rgb: 22, 113, 217;text-decoration:underline}.btn-link:focus-visible{color:var(--bs-btn-color)}.btn-link:hover{color:var(--bs-btn-hover-color)}.btn-lg,.btn-group-lg>.btn{--bs-btn-padding-y: 0.5rem;--bs-btn-padding-x: 1rem;--bs-btn-font-size:0.875rem;--bs-btn-border-radius: var(--bs-border-radius-lg)}.btn-sm,.btn-group-sm>.btn{--bs-btn-padding-y: 0.25rem;--bs-btn-padding-x: 0.5rem;--bs-btn-font-size:0.65625rem;--bs-btn-border-radius: var(--bs-border-radius-sm)}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden}.collapsing.collapse-horizontal{width:0;height:auto}.dropup,.dropend,.dropdown,.dropstart,.dropup-center,.dropdown-center{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid rgba(0,0,0,0);border-bottom:0;border-left:.3em solid rgba(0,0,0,0)}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{--bs-dropdown-zindex: 1000;--bs-dropdown-min-width: 10rem;--bs-dropdown-padding-x: 0;--bs-dropdown-padding-y: 0.5rem;--bs-dropdown-spacer: 0.125rem;--bs-dropdown-font-size:0.875rem;--bs-dropdown-color: var(--bs-body-color);--bs-dropdown-bg: var(--bs-body-bg);--bs-dropdown-border-color: var(--bs-border-color-translucent);--bs-dropdown-border-radius: var(--bs-border-radius);--bs-dropdown-border-width: var(--bs-border-width);--bs-dropdown-inner-border-radius: calc(var(--bs-border-radius) - var(--bs-border-width));--bs-dropdown-divider-bg: var(--bs-border-color-translucent);--bs-dropdown-divider-margin-y: 0.5rem;--bs-dropdown-box-shadow: var(--bs-box-shadow);--bs-dropdown-link-color: var(--bs-body-color);--bs-dropdown-link-hover-color: var(--bs-body-color);--bs-dropdown-link-hover-bg: var(--bs-tertiary-bg);--bs-dropdown-link-active-color: #fff;--bs-dropdown-link-active-bg: #1a85ff;--bs-dropdown-link-disabled-color: var(--bs-tertiary-color);--bs-dropdown-item-padding-x: 1rem;--bs-dropdown-item-padding-y: 0.25rem;--bs-dropdown-header-color: #6c757d;--bs-dropdown-header-padding-x: 1rem;--bs-dropdown-header-padding-y: 0.5rem;position:absolute;z-index:var(--bs-dropdown-zindex);display:none;min-width:var(--bs-dropdown-min-width);padding:var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x);margin:0;font-size:var(--bs-dropdown-font-size);color:var(--bs-dropdown-color);text-align:left;list-style:none;background-color:var(--bs-dropdown-bg);background-clip:padding-box;border:var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color)}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:var(--bs-dropdown-spacer)}.dropdown-menu-start{--bs-position: start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position: end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media(min-width: 576px){.dropdown-menu-sm-start{--bs-position: start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position: end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media(min-width: 768px){.dropdown-menu-md-start{--bs-position: start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position: end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media(min-width: 992px){.dropdown-menu-lg-start{--bs-position: start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position: end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media(min-width: 1200px){.dropdown-menu-xl-start{--bs-position: start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position: end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media(min-width: 1400px){.dropdown-menu-xxl-start{--bs-position: start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position: end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:var(--bs-dropdown-spacer)}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid rgba(0,0,0,0);border-bottom:.3em solid;border-left:.3em solid rgba(0,0,0,0)}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:var(--bs-dropdown-spacer)}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid rgba(0,0,0,0);border-right:0;border-bottom:.3em solid rgba(0,0,0,0);border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:var(--bs-dropdown-spacer)}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid rgba(0,0,0,0);border-right:.3em solid;border-bottom:.3em solid rgba(0,0,0,0)}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:var(--bs-dropdown-divider-margin-y) 0;overflow:hidden;border-top:1px solid var(--bs-dropdown-divider-bg);opacity:1}.dropdown-item{display:block;width:100%;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);clear:both;font-weight:400;color:var(--bs-dropdown-link-color);text-align:inherit;text-decoration:none;white-space:nowrap;background-color:rgba(0,0,0,0);border:0}.dropdown-item:hover,.dropdown-item:focus{color:var(--bs-dropdown-link-hover-color);background-color:var(--bs-dropdown-link-hover-bg)}.dropdown-item.active,.dropdown-item:active{color:var(--bs-dropdown-link-active-color);text-decoration:none;background-color:var(--bs-dropdown-link-active-bg)}.dropdown-item.disabled,.dropdown-item:disabled{color:var(--bs-dropdown-link-disabled-color);pointer-events:none;background-color:rgba(0,0,0,0)}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:var(--bs-dropdown-header-padding-y) var(--bs-dropdown-header-padding-x);margin-bottom:0;font-size:0.65625rem;color:var(--bs-dropdown-header-color);white-space:nowrap}.dropdown-item-text{display:block;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);color:var(--bs-dropdown-link-color)}.dropdown-menu-dark{--bs-dropdown-color: #dee2e6;--bs-dropdown-bg: #343a40;--bs-dropdown-border-color: var(--bs-border-color-translucent);--bs-dropdown-box-shadow: ;--bs-dropdown-link-color: #dee2e6;--bs-dropdown-link-hover-color: #fff;--bs-dropdown-divider-bg: var(--bs-border-color-translucent);--bs-dropdown-link-hover-bg: rgba(255, 255, 255, 0.15);--bs-dropdown-link-active-color: #fff;--bs-dropdown-link-active-bg: #1a85ff;--bs-dropdown-link-disabled-color: #adb5bd;--bs-dropdown-header-color: #adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;flex:1 1 auto}.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn:hover,.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn:hover,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn.active{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>:not(.btn-check:first-child)+.btn,.btn-group>.btn-group:not(:first-child){margin-left:calc(var(--bs-border-width)*-1)}.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-sm+.dropdown-toggle-split,.btn-group-sm>.btn+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-lg+.dropdown-toggle-split,.btn-group-lg>.btn+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn:not(:first-child),.btn-group-vertical>.btn-group:not(:first-child){margin-top:calc(var(--bs-border-width)*-1)}.nav{--bs-nav-link-padding-x: 0.5rem;--bs-nav-link-padding-y: 0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color: var(--bs-link-color);--bs-nav-link-hover-color: var(--bs-link-hover-color);--bs-nav-link-disabled-color: var(--bs-secondary-color);display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);font-size:var(--bs-nav-link-font-size);font-weight:var(--bs-nav-link-font-weight);color:var(--bs-nav-link-color);text-decoration:none;background:none;border:0}.nav-link:hover,.nav-link:focus{color:var(--bs-nav-link-hover-color)}.nav-link:focus-visible{outline:0;box-shadow:0 0 0 .25rem rgba(26,133,255,.25)}.nav-link.disabled,.nav-link:disabled{color:var(--bs-nav-link-disabled-color);pointer-events:none;cursor:default}.nav-tabs{--bs-nav-tabs-border-width: var(--bs-border-width);--bs-nav-tabs-border-color: var(--bs-border-color);--bs-nav-tabs-border-radius: var(--bs-border-radius);--bs-nav-tabs-link-hover-border-color: var(--bs-secondary-bg) var(--bs-secondary-bg) var(--bs-border-color);--bs-nav-tabs-link-active-color: var(--bs-emphasis-color);--bs-nav-tabs-link-active-bg: var(--bs-body-bg);--bs-nav-tabs-link-active-border-color: var(--bs-border-color) var(--bs-border-color) var(--bs-body-bg);border-bottom:var(--bs-nav-tabs-border-width) solid var(--bs-nav-tabs-border-color)}.nav-tabs .nav-link{margin-bottom:calc(-1*var(--bs-nav-tabs-border-width));border:var(--bs-nav-tabs-border-width) solid rgba(0,0,0,0)}.nav-tabs .nav-link:hover,.nav-tabs .nav-link:focus{isolation:isolate;border-color:var(--bs-nav-tabs-link-hover-border-color)}.nav-tabs .nav-link.active,.nav-tabs .nav-item.show .nav-link{color:var(--bs-nav-tabs-link-active-color);background-color:var(--bs-nav-tabs-link-active-bg);border-color:var(--bs-nav-tabs-link-active-border-color)}.nav-tabs .dropdown-menu{margin-top:calc(-1*var(--bs-nav-tabs-border-width))}.nav-pills{--bs-nav-pills-border-radius: var(--bs-border-radius);--bs-nav-pills-link-active-color: #fff;--bs-nav-pills-link-active-bg: #1a85ff}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:var(--bs-nav-pills-link-active-color);background-color:var(--bs-nav-pills-link-active-bg)}.nav-underline{--bs-nav-underline-gap: 1rem;--bs-nav-underline-border-width: 0.125rem;--bs-nav-underline-link-active-color: var(--bs-emphasis-color);gap:var(--bs-nav-underline-gap)}.nav-underline .nav-link{padding-right:0;padding-left:0;border-bottom:var(--bs-nav-underline-border-width) solid rgba(0,0,0,0)}.nav-underline .nav-link:hover,.nav-underline .nav-link:focus{border-bottom-color:currentcolor}.nav-underline .nav-link.active,.nav-underline .show>.nav-link{font-weight:700;color:var(--bs-nav-underline-link-active-color);border-bottom-color:currentcolor}.nav-fill>.nav-link,.nav-fill .nav-item{flex:1 1 auto;text-align:center}.nav-justified>.nav-link,.nav-justified .nav-item{flex-basis:0;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{--bs-navbar-padding-x: 0;--bs-navbar-padding-y: 0.5rem;--bs-navbar-color: rgba(var(--bs-emphasis-color-rgb), 0.65);--bs-navbar-hover-color: rgba(var(--bs-emphasis-color-rgb), 0.8);--bs-navbar-disabled-color: rgba(var(--bs-emphasis-color-rgb), 0.3);--bs-navbar-active-color: rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-padding-y: 0.5rem;--bs-navbar-brand-margin-end: 1rem;--bs-navbar-brand-font-size: 0.875rem;--bs-navbar-brand-color: rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-hover-color: rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-nav-link-padding-x: 0.5rem;--bs-navbar-toggler-padding-y: 0.25rem;--bs-navbar-toggler-padding-x: 0.75rem;--bs-navbar-toggler-font-size: 0.875rem;--bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%2833, 37, 41, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");--bs-navbar-toggler-border-color: rgba(var(--bs-emphasis-color-rgb), 0.15);--bs-navbar-toggler-border-radius: var(--bs-border-radius);--bs-navbar-toggler-focus-width: 0.25rem;--bs-navbar-toggler-transition: box-shadow 0.15s ease-in-out;position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:var(--bs-navbar-padding-y) var(--bs-navbar-padding-x)}.navbar>.container,.navbar>.container-fluid,.navbar>.container-sm,.navbar>.container-md,.navbar>.container-lg,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:var(--bs-navbar-brand-padding-y);padding-bottom:var(--bs-navbar-brand-padding-y);margin-right:var(--bs-navbar-brand-margin-end);font-size:var(--bs-navbar-brand-font-size);color:var(--bs-navbar-brand-color);text-decoration:none;white-space:nowrap}.navbar-brand:hover,.navbar-brand:focus{color:var(--bs-navbar-brand-hover-color)}.navbar-nav{--bs-nav-link-padding-x: 0;--bs-nav-link-padding-y: 0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color: var(--bs-navbar-color);--bs-nav-link-hover-color: var(--bs-navbar-hover-color);--bs-nav-link-disabled-color: var(--bs-navbar-disabled-color);display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link.active,.navbar-nav .nav-link.show{color:var(--bs-navbar-active-color)}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-navbar-color)}.navbar-text a,.navbar-text a:hover,.navbar-text a:focus{color:var(--bs-navbar-active-color)}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:var(--bs-navbar-toggler-padding-y) var(--bs-navbar-toggler-padding-x);font-size:var(--bs-navbar-toggler-font-size);line-height:1;color:var(--bs-navbar-color);background-color:rgba(0,0,0,0);border:var(--bs-border-width) solid var(--bs-navbar-toggler-border-color)}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 var(--bs-navbar-toggler-focus-width)}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-image:var(--bs-navbar-toggler-icon-bg);background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height, 75vh);overflow-y:auto}@media(min-width: 576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex !important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto !important;height:auto !important;visibility:visible !important;background-color:rgba(0,0,0,0) !important;border:0 !important;transform:none !important}.navbar-expand-sm .offcanvas .offcanvas-header{display:none}.navbar-expand-sm .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media(min-width: 768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex !important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto !important;height:auto !important;visibility:visible !important;background-color:rgba(0,0,0,0) !important;border:0 !important;transform:none !important}.navbar-expand-md .offcanvas .offcanvas-header{display:none}.navbar-expand-md .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media(min-width: 992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex !important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto !important;height:auto !important;visibility:visible !important;background-color:rgba(0,0,0,0) !important;border:0 !important;transform:none !important}.navbar-expand-lg .offcanvas .offcanvas-header{display:none}.navbar-expand-lg .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media(min-width: 1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex !important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto !important;height:auto !important;visibility:visible !important;background-color:rgba(0,0,0,0) !important;border:0 !important;transform:none !important}.navbar-expand-xl .offcanvas .offcanvas-header{display:none}.navbar-expand-xl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media(min-width: 1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex !important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto !important;height:auto !important;visibility:visible !important;background-color:rgba(0,0,0,0) !important;border:0 !important;transform:none !important}.navbar-expand-xxl .offcanvas .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex !important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto !important;height:auto !important;visibility:visible !important;background-color:rgba(0,0,0,0) !important;border:0 !important;transform:none !important}.navbar-expand .offcanvas .offcanvas-header{display:none}.navbar-expand .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-dark,.navbar[data-bs-theme=dark]{--bs-navbar-color: rgba(255, 255, 255, 0.55);--bs-navbar-hover-color: rgba(255, 255, 255, 0.75);--bs-navbar-disabled-color: rgba(255, 255, 255, 0.25);--bs-navbar-active-color: #fff;--bs-navbar-brand-color: #fff;--bs-navbar-brand-hover-color: #fff;--bs-navbar-toggler-border-color: rgba(255, 255, 255, 0.1);--bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}[data-bs-theme=dark] .navbar-toggler-icon{--bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.card{--bs-card-spacer-y: 1rem;--bs-card-spacer-x: 1rem;--bs-card-title-spacer-y: 0.5rem;--bs-card-title-color: ;--bs-card-subtitle-color: ;--bs-card-border-width: var(--bs-border-width);--bs-card-border-color: var(--bs-border-color-translucent);--bs-card-border-radius: var(--bs-border-radius);--bs-card-box-shadow: ;--bs-card-inner-border-radius: calc(var(--bs-border-radius) - (var(--bs-border-width)));--bs-card-cap-padding-y: 0.5rem;--bs-card-cap-padding-x: 1rem;--bs-card-cap-bg: rgba(var(--bs-body-color-rgb), 0.03);--bs-card-cap-color: ;--bs-card-height: ;--bs-card-color: ;--bs-card-bg: var(--bs-body-bg);--bs-card-img-overlay-padding: 1rem;--bs-card-group-margin: 0.75rem;position:relative;display:flex;flex-direction:column;min-width:0;height:var(--bs-card-height);color:var(--bs-body-color);word-wrap:break-word;background-color:var(--bs-card-bg);background-clip:border-box;border:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0}.card>.list-group:last-child{border-bottom-width:0}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:var(--bs-card-spacer-y) var(--bs-card-spacer-x);color:var(--bs-card-color)}.card-title{margin-bottom:var(--bs-card-title-spacer-y);color:var(--bs-card-title-color)}.card-subtitle{margin-top:calc(-0.5*var(--bs-card-title-spacer-y));margin-bottom:0;color:var(--bs-card-subtitle-color)}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:var(--bs-card-spacer-x)}.card-header{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);margin-bottom:0;color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-bottom:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-footer{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-top:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-header-tabs{margin-right:calc(-0.5*var(--bs-card-cap-padding-x));margin-bottom:calc(-1*var(--bs-card-cap-padding-y));margin-left:calc(-0.5*var(--bs-card-cap-padding-x));border-bottom:0}.card-header-tabs .nav-link.active{background-color:var(--bs-card-bg);border-bottom-color:var(--bs-card-bg)}.card-header-pills{margin-right:calc(-0.5*var(--bs-card-cap-padding-x));margin-left:calc(-0.5*var(--bs-card-cap-padding-x))}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:var(--bs-card-img-overlay-padding)}.card-img,.card-img-top,.card-img-bottom{width:100%}.card-group>.card{margin-bottom:var(--bs-card-group-margin)}@media(min-width: 576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}}.accordion{--bs-accordion-color: var(--bs-body-color);--bs-accordion-bg: transparent;--bs-accordion-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, border-radius 0.15s ease;--bs-accordion-border-color: var(--bs-border-color);--bs-accordion-border-width: 0;--bs-accordion-border-radius: var(--bs-border-radius);--bs-accordion-inner-border-radius: calc(var(--bs-border-radius) - 0);--bs-accordion-btn-padding-x: 0;--bs-accordion-btn-padding-y: 1rem;--bs-accordion-btn-color: var(--bs-body-color);--bs-accordion-btn-bg: var(--bs-accordion-bg);--bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23212529' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M2 5L8 11L14 5'/%3e%3c/svg%3e");--bs-accordion-btn-icon-width: 0.75rem;--bs-accordion-btn-icon-transform: rotate(-180deg);--bs-accordion-btn-icon-transition: transform 0.2s ease-in-out;--bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%230a3566' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M2 5L8 11L14 5'/%3e%3c/svg%3e");--bs-accordion-btn-focus-box-shadow: 0 0 0 0.25rem rgba(26, 133, 255, 0.25);--bs-accordion-body-padding-x: 0;--bs-accordion-body-padding-y: 0;--bs-accordion-active-color: var(--bs-primary-text-emphasis);--bs-accordion-active-bg: var(--bs-primary-bg-subtle)}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:var(--bs-accordion-btn-padding-y) var(--bs-accordion-btn-padding-x);font-size:0.875rem;color:var(--bs-accordion-btn-color);text-align:left;background-color:var(--bs-accordion-btn-bg);border:0;overflow-anchor:none}.accordion-button:not(.collapsed){color:var(--bs-accordion-active-color);background-color:var(--bs-accordion-active-bg);box-shadow:inset 0 calc(-1*var(--bs-accordion-border-width)) 0 var(--bs-accordion-border-color)}.accordion-button:not(.collapsed)::after{background-image:var(--bs-accordion-btn-active-icon);transform:var(--bs-accordion-btn-icon-transform)}.accordion-button::after{flex-shrink:0;width:var(--bs-accordion-btn-icon-width);height:var(--bs-accordion-btn-icon-width);margin-left:auto;content:"";background-image:var(--bs-accordion-btn-icon);background-repeat:no-repeat;background-size:var(--bs-accordion-btn-icon-width)}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;outline:0;box-shadow:var(--bs-accordion-btn-focus-box-shadow)}.accordion-header{margin-bottom:0}.accordion-item{color:var(--bs-accordion-color);background-color:var(--bs-accordion-bg);border:var(--bs-accordion-border-width) solid var(--bs-accordion-border-color)}.accordion-item:not(:first-of-type){border-top:0}.accordion-body{padding:var(--bs-accordion-body-padding-y) var(--bs-accordion-body-padding-x)}.accordion-flush>.accordion-item{border-right:0;border-left:0}.accordion-flush>.accordion-item:first-child{border-top:0}.accordion-flush>.accordion-item:last-child{border-bottom:0}[data-bs-theme=dark] .accordion-button::after{--bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%2376b6ff'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");--bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%2376b6ff'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.breadcrumb{--bs-breadcrumb-padding-x: 0;--bs-breadcrumb-padding-y: 0;--bs-breadcrumb-margin-bottom: 1rem;--bs-breadcrumb-bg: ;--bs-breadcrumb-border-radius: ;--bs-breadcrumb-divider-color: var(--bs-secondary-color);--bs-breadcrumb-item-padding-x: 0.5rem;--bs-breadcrumb-item-active-color: var(--bs-secondary-color);display:flex;flex-wrap:wrap;padding:var(--bs-breadcrumb-padding-y) var(--bs-breadcrumb-padding-x);margin-bottom:var(--bs-breadcrumb-margin-bottom);font-size:var(--bs-breadcrumb-font-size);list-style:none;background-color:var(--bs-breadcrumb-bg)}.breadcrumb-item+.breadcrumb-item{padding-left:var(--bs-breadcrumb-item-padding-x)}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:var(--bs-breadcrumb-item-padding-x);color:var(--bs-breadcrumb-divider-color);content:var(--bs-breadcrumb-divider, "/") /* rtl: var(--bs-breadcrumb-divider, "/") */}.breadcrumb-item.active{color:var(--bs-breadcrumb-item-active-color)}.pagination{--bs-pagination-padding-x: 0.75rem;--bs-pagination-padding-y: 0.375rem;--bs-pagination-font-size:0.875rem;--bs-pagination-color: var(--bs-link-color);--bs-pagination-bg: var(--bs-body-bg);--bs-pagination-border-width: var(--bs-border-width);--bs-pagination-border-color: var(--bs-border-color);--bs-pagination-border-radius: var(--bs-border-radius);--bs-pagination-hover-color: var(--bs-link-hover-color);--bs-pagination-hover-bg: var(--bs-tertiary-bg);--bs-pagination-hover-border-color: var(--bs-border-color);--bs-pagination-focus-color: var(--bs-link-hover-color);--bs-pagination-focus-bg: var(--bs-secondary-bg);--bs-pagination-focus-box-shadow: 0 0 0 0.25rem rgba(26, 133, 255, 0.25);--bs-pagination-active-color: #fff;--bs-pagination-active-bg: #1a85ff;--bs-pagination-active-border-color: #1a85ff;--bs-pagination-disabled-color: var(--bs-secondary-color);--bs-pagination-disabled-bg: var(--bs-secondary-bg);--bs-pagination-disabled-border-color: var(--bs-border-color);display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;padding:var(--bs-pagination-padding-y) var(--bs-pagination-padding-x);font-size:var(--bs-pagination-font-size);color:var(--bs-pagination-color);text-decoration:none;background-color:var(--bs-pagination-bg);border:var(--bs-pagination-border-width) solid var(--bs-pagination-border-color)}.page-link:hover{z-index:2;color:var(--bs-pagination-hover-color);background-color:var(--bs-pagination-hover-bg);border-color:var(--bs-pagination-hover-border-color)}.page-link:focus{z-index:3;color:var(--bs-pagination-focus-color);background-color:var(--bs-pagination-focus-bg);outline:0;box-shadow:var(--bs-pagination-focus-box-shadow)}.page-link.active,.active>.page-link{z-index:3;color:var(--bs-pagination-active-color);background-color:var(--bs-pagination-active-bg);border-color:var(--bs-pagination-active-border-color)}.page-link.disabled,.disabled>.page-link{color:var(--bs-pagination-disabled-color);pointer-events:none;background-color:var(--bs-pagination-disabled-bg);border-color:var(--bs-pagination-disabled-border-color)}.page-item:not(:first-child) .page-link{margin-left:calc(var(--bs-border-width)*-1)}.pagination-lg{--bs-pagination-padding-x: 1.5rem;--bs-pagination-padding-y: 0.75rem;--bs-pagination-font-size:0.875rem;--bs-pagination-border-radius: var(--bs-border-radius-lg)}.pagination-sm{--bs-pagination-padding-x: 0.5rem;--bs-pagination-padding-y: 0.25rem;--bs-pagination-font-size:0.65625rem;--bs-pagination-border-radius: var(--bs-border-radius-sm)}.badge{--bs-badge-padding-x: 0.65em;--bs-badge-padding-y: 0.35em;--bs-badge-font-size:0.75em;--bs-badge-font-weight: 700;--bs-badge-color: #fff;--bs-badge-border-radius: var(--bs-border-radius);display:inline-block;padding:var(--bs-badge-padding-y) var(--bs-badge-padding-x);font-size:var(--bs-badge-font-size);font-weight:var(--bs-badge-font-weight);line-height:1;color:var(--bs-badge-color);text-align:center;white-space:nowrap;vertical-align:baseline}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{--bs-alert-bg: transparent;--bs-alert-padding-x: 1rem;--bs-alert-padding-y: 1rem;--bs-alert-margin-bottom: 1rem;--bs-alert-color: inherit;--bs-alert-border-color: transparent;--bs-alert-border: var(--bs-border-width) solid var(--bs-alert-border-color);--bs-alert-border-radius: var(--bs-border-radius);--bs-alert-link-color: inherit;position:relative;padding:var(--bs-alert-padding-y) var(--bs-alert-padding-x);margin-bottom:var(--bs-alert-margin-bottom);color:var(--bs-alert-color);background-color:var(--bs-alert-bg);border:var(--bs-alert-border)}.alert-heading{color:inherit}.alert-link{font-weight:700;color:var(--bs-alert-link-color)}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{--bs-alert-color: var(--bs-primary-text-emphasis);--bs-alert-bg: var(--bs-primary-bg-subtle);--bs-alert-border-color: var(--bs-primary-border-subtle);--bs-alert-link-color: var(--bs-primary-text-emphasis)}.alert-secondary{--bs-alert-color: var(--bs-secondary-text-emphasis);--bs-alert-bg: var(--bs-secondary-bg-subtle);--bs-alert-border-color: var(--bs-secondary-border-subtle);--bs-alert-link-color: var(--bs-secondary-text-emphasis)}.alert-success{--bs-alert-color: var(--bs-success-text-emphasis);--bs-alert-bg: var(--bs-success-bg-subtle);--bs-alert-border-color: var(--bs-success-border-subtle);--bs-alert-link-color: var(--bs-success-text-emphasis)}.alert-info{--bs-alert-color: var(--bs-info-text-emphasis);--bs-alert-bg: var(--bs-info-bg-subtle);--bs-alert-border-color: var(--bs-info-border-subtle);--bs-alert-link-color: var(--bs-info-text-emphasis)}.alert-warning{--bs-alert-color: var(--bs-warning-text-emphasis);--bs-alert-bg: var(--bs-warning-bg-subtle);--bs-alert-border-color: var(--bs-warning-border-subtle);--bs-alert-link-color: var(--bs-warning-text-emphasis)}.alert-danger{--bs-alert-color: var(--bs-danger-text-emphasis);--bs-alert-bg: var(--bs-danger-bg-subtle);--bs-alert-border-color: var(--bs-danger-border-subtle);--bs-alert-link-color: var(--bs-danger-text-emphasis)}.alert-light{--bs-alert-color: var(--bs-light-text-emphasis);--bs-alert-bg: var(--bs-light-bg-subtle);--bs-alert-border-color: var(--bs-light-border-subtle);--bs-alert-link-color: var(--bs-light-text-emphasis)}.alert-dark{--bs-alert-color: var(--bs-dark-text-emphasis);--bs-alert-bg: var(--bs-dark-bg-subtle);--bs-alert-border-color: var(--bs-dark-border-subtle);--bs-alert-link-color: var(--bs-dark-text-emphasis)}.progress,.progress-stacked{--bs-progress-height: 1rem;--bs-progress-font-size:0.65625rem;--bs-progress-bg: var(--bs-secondary-bg);--bs-progress-border-radius: var(--bs-border-radius);--bs-progress-box-shadow: var(--bs-box-shadow-inset);--bs-progress-bar-color: #fff;--bs-progress-bar-bg: #1a85ff;--bs-progress-bar-transition: width 0.6s ease;display:flex;height:var(--bs-progress-height);overflow:hidden;font-size:var(--bs-progress-font-size);background-color:var(--bs-progress-bg)}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:var(--bs-progress-bar-color);text-align:center;white-space:nowrap;background-color:var(--bs-progress-bar-bg)}.progress-bar-striped{background-image:linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-size:var(--bs-progress-height) var(--bs-progress-height)}.progress-stacked>.progress{overflow:visible}.progress-stacked>.progress>.progress-bar{width:100%}.list-group{--bs-list-group-color: var(--bs-body-color);--bs-list-group-bg: var(--bs-body-bg);--bs-list-group-border-color: var(--bs-border-color);--bs-list-group-border-width: var(--bs-border-width);--bs-list-group-border-radius: var(--bs-border-radius);--bs-list-group-item-padding-x: 1rem;--bs-list-group-item-padding-y: 0.5rem;--bs-list-group-action-color: var(--bs-secondary-color);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-tertiary-bg);--bs-list-group-action-active-color: var(--bs-body-color);--bs-list-group-action-active-bg: var(--bs-secondary-bg);--bs-list-group-disabled-color: var(--bs-secondary-color);--bs-list-group-disabled-bg: var(--bs-body-bg);--bs-list-group-active-color: #fff;--bs-list-group-active-bg: #1a85ff;--bs-list-group-active-border-color: #1a85ff;display:flex;flex-direction:column;padding-left:0;margin-bottom:0}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>.list-group-item::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:var(--bs-list-group-action-color);text-align:inherit}.list-group-item-action:hover,.list-group-item-action:focus{z-index:1;color:var(--bs-list-group-action-hover-color);text-decoration:none;background-color:var(--bs-list-group-action-hover-bg)}.list-group-item-action:active{color:var(--bs-list-group-action-active-color);background-color:var(--bs-list-group-action-active-bg)}.list-group-item{position:relative;display:block;padding:var(--bs-list-group-item-padding-y) var(--bs-list-group-item-padding-x);color:var(--bs-list-group-color);text-decoration:none;background-color:var(--bs-list-group-bg);border:var(--bs-list-group-border-width) solid var(--bs-list-group-border-color)}.list-group-item.disabled,.list-group-item:disabled{color:var(--bs-list-group-disabled-color);pointer-events:none;background-color:var(--bs-list-group-disabled-bg)}.list-group-item.active{z-index:2;color:var(--bs-list-group-active-color);background-color:var(--bs-list-group-active-bg);border-color:var(--bs-list-group-active-border-color)}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:calc(-1*var(--bs-list-group-border-width));border-top-width:var(--bs-list-group-border-width)}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:calc(-1*var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}@media(min-width: 576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:calc(-1*var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media(min-width: 768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:calc(-1*var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media(min-width: 992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:calc(-1*var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media(min-width: 1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:calc(-1*var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media(min-width: 1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:calc(-1*var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}.list-group-flush>.list-group-item{border-width:0 0 var(--bs-list-group-border-width)}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{--bs-list-group-color: var(--bs-primary-text-emphasis);--bs-list-group-bg: var(--bs-primary-bg-subtle);--bs-list-group-border-color: var(--bs-primary-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-primary-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-primary-border-subtle);--bs-list-group-active-color: var(--bs-primary-bg-subtle);--bs-list-group-active-bg: var(--bs-primary-text-emphasis);--bs-list-group-active-border-color: var(--bs-primary-text-emphasis)}.list-group-item-secondary{--bs-list-group-color: var(--bs-secondary-text-emphasis);--bs-list-group-bg: var(--bs-secondary-bg-subtle);--bs-list-group-border-color: var(--bs-secondary-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-secondary-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-secondary-border-subtle);--bs-list-group-active-color: var(--bs-secondary-bg-subtle);--bs-list-group-active-bg: var(--bs-secondary-text-emphasis);--bs-list-group-active-border-color: var(--bs-secondary-text-emphasis)}.list-group-item-success{--bs-list-group-color: var(--bs-success-text-emphasis);--bs-list-group-bg: var(--bs-success-bg-subtle);--bs-list-group-border-color: var(--bs-success-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-success-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-success-border-subtle);--bs-list-group-active-color: var(--bs-success-bg-subtle);--bs-list-group-active-bg: var(--bs-success-text-emphasis);--bs-list-group-active-border-color: var(--bs-success-text-emphasis)}.list-group-item-info{--bs-list-group-color: var(--bs-info-text-emphasis);--bs-list-group-bg: var(--bs-info-bg-subtle);--bs-list-group-border-color: var(--bs-info-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-info-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-info-border-subtle);--bs-list-group-active-color: var(--bs-info-bg-subtle);--bs-list-group-active-bg: var(--bs-info-text-emphasis);--bs-list-group-active-border-color: var(--bs-info-text-emphasis)}.list-group-item-warning{--bs-list-group-color: var(--bs-warning-text-emphasis);--bs-list-group-bg: var(--bs-warning-bg-subtle);--bs-list-group-border-color: var(--bs-warning-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-warning-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-warning-border-subtle);--bs-list-group-active-color: var(--bs-warning-bg-subtle);--bs-list-group-active-bg: var(--bs-warning-text-emphasis);--bs-list-group-active-border-color: var(--bs-warning-text-emphasis)}.list-group-item-danger{--bs-list-group-color: var(--bs-danger-text-emphasis);--bs-list-group-bg: var(--bs-danger-bg-subtle);--bs-list-group-border-color: var(--bs-danger-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-danger-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-danger-border-subtle);--bs-list-group-active-color: var(--bs-danger-bg-subtle);--bs-list-group-active-bg: var(--bs-danger-text-emphasis);--bs-list-group-active-border-color: var(--bs-danger-text-emphasis)}.list-group-item-light{--bs-list-group-color: var(--bs-light-text-emphasis);--bs-list-group-bg: var(--bs-light-bg-subtle);--bs-list-group-border-color: var(--bs-light-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-light-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-light-border-subtle);--bs-list-group-active-color: var(--bs-light-bg-subtle);--bs-list-group-active-bg: var(--bs-light-text-emphasis);--bs-list-group-active-border-color: var(--bs-light-text-emphasis)}.list-group-item-dark{--bs-list-group-color: var(--bs-dark-text-emphasis);--bs-list-group-bg: var(--bs-dark-bg-subtle);--bs-list-group-border-color: var(--bs-dark-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-dark-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-dark-border-subtle);--bs-list-group-active-color: var(--bs-dark-bg-subtle);--bs-list-group-active-bg: var(--bs-dark-text-emphasis);--bs-list-group-active-border-color: var(--bs-dark-text-emphasis)}.btn-close{--bs-btn-close-color: #000;--bs-btn-close-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3e%3c/svg%3e");--bs-btn-close-opacity: 0.5;--bs-btn-close-hover-opacity: 0.75;--bs-btn-close-focus-shadow: 0 0 0 0.25rem rgba(26, 133, 255, 0.25);--bs-btn-close-focus-opacity: 1;--bs-btn-close-disabled-opacity: 0.25;--bs-btn-close-white-filter: invert(1) grayscale(100%) brightness(200%);box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:var(--bs-btn-close-color);background:rgba(0,0,0,0) var(--bs-btn-close-bg) center/1em auto no-repeat;border:0;opacity:var(--bs-btn-close-opacity)}.btn-close:hover{color:var(--bs-btn-close-color);text-decoration:none;opacity:var(--bs-btn-close-hover-opacity)}.btn-close:focus{outline:0;box-shadow:var(--bs-btn-close-focus-shadow);opacity:var(--bs-btn-close-focus-opacity)}.btn-close:disabled,.btn-close.disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:var(--bs-btn-close-disabled-opacity)}.btn-close-white{filter:var(--bs-btn-close-white-filter)}[data-bs-theme=dark] .btn-close{filter:var(--bs-btn-close-white-filter)}.toast{--bs-toast-zindex: 1090;--bs-toast-padding-x: 0.75rem;--bs-toast-padding-y: 0.5rem;--bs-toast-spacing: 1.5rem;--bs-toast-max-width: 350px;--bs-toast-font-size:0.875rem;--bs-toast-color: ;--bs-toast-bg: rgba(var(--bs-body-bg-rgb), 0.85);--bs-toast-border-width: var(--bs-border-width);--bs-toast-border-color: var(--bs-border-color-translucent);--bs-toast-border-radius: var(--bs-border-radius);--bs-toast-box-shadow: var(--bs-box-shadow);--bs-toast-header-color: var(--bs-secondary-color);--bs-toast-header-bg: rgba(var(--bs-body-bg-rgb), 0.85);--bs-toast-header-border-color: var(--bs-border-color-translucent);width:var(--bs-toast-max-width);max-width:100%;font-size:var(--bs-toast-font-size);color:var(--bs-toast-color);pointer-events:auto;background-color:var(--bs-toast-bg);background-clip:padding-box;border:var(--bs-toast-border-width) solid var(--bs-toast-border-color);box-shadow:var(--bs-toast-box-shadow)}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{--bs-toast-zindex: 1090;position:absolute;z-index:var(--bs-toast-zindex);width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:var(--bs-toast-spacing)}.toast-header{display:flex;align-items:center;padding:var(--bs-toast-padding-y) var(--bs-toast-padding-x);color:var(--bs-toast-header-color);background-color:var(--bs-toast-header-bg);background-clip:padding-box;border-bottom:var(--bs-toast-border-width) solid var(--bs-toast-header-border-color)}.toast-header .btn-close{margin-right:calc(-0.5*var(--bs-toast-padding-x));margin-left:var(--bs-toast-padding-x)}.toast-body{padding:var(--bs-toast-padding-x);word-wrap:break-word}.modal{--bs-modal-zindex: 1055;--bs-modal-width: 500px;--bs-modal-padding: 1rem;--bs-modal-margin: 0.5rem;--bs-modal-color: ;--bs-modal-bg: var(--bs-body-bg);--bs-modal-border-color: var(--bs-border-color-translucent);--bs-modal-border-width: var(--bs-border-width);--bs-modal-border-radius: var(--bs-border-radius-lg);--bs-modal-box-shadow: var(--bs-box-shadow-sm);--bs-modal-inner-border-radius: calc(var(--bs-border-radius-lg) - (var(--bs-border-width)));--bs-modal-header-padding-x: 1rem;--bs-modal-header-padding-y: 1rem;--bs-modal-header-padding: 1rem 1rem;--bs-modal-header-border-color: var(--bs-border-color);--bs-modal-header-border-width: var(--bs-border-width);--bs-modal-title-line-height: 1.5;--bs-modal-footer-gap: 0.5rem;--bs-modal-footer-bg: ;--bs-modal-footer-border-color: var(--bs-border-color);--bs-modal-footer-border-width: var(--bs-border-width);position:fixed;top:0;left:0;z-index:var(--bs-modal-zindex);display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:var(--bs-modal-margin);pointer-events:none}.modal.fade .modal-dialog{transform:translate(0, -50px)}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - var(--bs-modal-margin)*2)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - var(--bs-modal-margin)*2)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;color:var(--bs-modal-color);pointer-events:auto;background-color:var(--bs-modal-bg);background-clip:padding-box;border:var(--bs-modal-border-width) solid var(--bs-modal-border-color);outline:0}.modal-backdrop{--bs-backdrop-zindex: 1050;--bs-backdrop-bg: #000;--bs-backdrop-opacity: 0.5;position:fixed;top:0;left:0;z-index:var(--bs-backdrop-zindex);width:100vw;height:100vh;background-color:var(--bs-backdrop-bg)}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:var(--bs-backdrop-opacity)}.modal-header{display:flex;flex-shrink:0;align-items:center;padding:var(--bs-modal-header-padding);border-bottom:var(--bs-modal-header-border-width) solid var(--bs-modal-header-border-color)}.modal-header .btn-close{padding:calc(var(--bs-modal-header-padding-y)*.5) calc(var(--bs-modal-header-padding-x)*.5);margin:calc(-0.5*var(--bs-modal-header-padding-y)) calc(-0.5*var(--bs-modal-header-padding-x)) calc(-0.5*var(--bs-modal-header-padding-y)) auto}.modal-title{margin-bottom:0;line-height:var(--bs-modal-title-line-height)}.modal-body{position:relative;flex:1 1 auto;padding:var(--bs-modal-padding)}.modal-footer{display:flex;flex-shrink:0;flex-wrap:wrap;align-items:center;justify-content:flex-end;padding:calc(var(--bs-modal-padding) - var(--bs-modal-footer-gap)*.5);background-color:var(--bs-modal-footer-bg);border-top:var(--bs-modal-footer-border-width) solid var(--bs-modal-footer-border-color)}.modal-footer>*{margin:calc(var(--bs-modal-footer-gap)*.5)}@media(min-width: 576px){.modal{--bs-modal-margin: 1.75rem;--bs-modal-box-shadow: var(--bs-box-shadow)}.modal-dialog{max-width:var(--bs-modal-width);margin-right:auto;margin-left:auto}.modal-sm{--bs-modal-width: 300px}}@media(min-width: 992px){.modal-lg,.modal-xl{--bs-modal-width: 800px}}@media(min-width: 1200px){.modal-xl{--bs-modal-width: 1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0}.modal-fullscreen .modal-body{overflow-y:auto}@media(max-width: 575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}}@media(max-width: 767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}}@media(max-width: 991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}}@media(max-width: 1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}}@media(max-width: 1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}}.tooltip{--bs-tooltip-zindex: 1080;--bs-tooltip-max-width: 200px;--bs-tooltip-padding-x: 0.5rem;--bs-tooltip-padding-y: 0.25rem;--bs-tooltip-margin: ;--bs-tooltip-font-size:0.65625rem;--bs-tooltip-color: var(--bs-body-bg);--bs-tooltip-bg: var(--bs-emphasis-color);--bs-tooltip-border-radius: var(--bs-border-radius);--bs-tooltip-opacity: 0.9;--bs-tooltip-arrow-width: 0.8rem;--bs-tooltip-arrow-height: 0.4rem;z-index:var(--bs-tooltip-zindex);display:block;margin:var(--bs-tooltip-margin);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-tooltip-font-size);word-wrap:break-word;opacity:0}.tooltip.show{opacity:var(--bs-tooltip-opacity)}.tooltip .tooltip-arrow{display:block;width:var(--bs-tooltip-arrow-width);height:var(--bs-tooltip-arrow-height)}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:rgba(0,0,0,0);border-style:solid}.bs-tooltip-top .tooltip-arrow,.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow{bottom:calc(-1*var(--bs-tooltip-arrow-height))}.bs-tooltip-top .tooltip-arrow::before,.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before{top:-1px;border-width:var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width)*.5) 0;border-top-color:var(--bs-tooltip-bg)}.bs-tooltip-end .tooltip-arrow,.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow{left:calc(-1*var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-end .tooltip-arrow::before,.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before{right:-1px;border-width:calc(var(--bs-tooltip-arrow-width)*.5) var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width)*.5) 0;border-right-color:var(--bs-tooltip-bg)}.bs-tooltip-bottom .tooltip-arrow,.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow{top:calc(-1*var(--bs-tooltip-arrow-height))}.bs-tooltip-bottom .tooltip-arrow::before,.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before{bottom:-1px;border-width:0 calc(var(--bs-tooltip-arrow-width)*.5) var(--bs-tooltip-arrow-height);border-bottom-color:var(--bs-tooltip-bg)}.bs-tooltip-start .tooltip-arrow,.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow{right:calc(-1*var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-start .tooltip-arrow::before,.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before{left:-1px;border-width:calc(var(--bs-tooltip-arrow-width)*.5) 0 calc(var(--bs-tooltip-arrow-width)*.5) var(--bs-tooltip-arrow-height);border-left-color:var(--bs-tooltip-bg)}.tooltip-inner{max-width:var(--bs-tooltip-max-width);padding:var(--bs-tooltip-padding-y) var(--bs-tooltip-padding-x);color:var(--bs-tooltip-color);text-align:center;background-color:var(--bs-tooltip-bg)}.popover{--bs-popover-zindex: 1070;--bs-popover-max-width: 276px;--bs-popover-font-size:0.65625rem;--bs-popover-bg: var(--bs-body-bg);--bs-popover-border-width: var(--bs-border-width);--bs-popover-border-color: var(--bs-border-color-translucent);--bs-popover-border-radius: var(--bs-border-radius-lg);--bs-popover-inner-border-radius: calc(var(--bs-border-radius-lg) - var(--bs-border-width));--bs-popover-box-shadow: var(--bs-box-shadow);--bs-popover-header-padding-x: 1rem;--bs-popover-header-padding-y: 0.5rem;--bs-popover-header-font-size:0.875rem;--bs-popover-header-color: inherit;--bs-popover-header-bg: var(--bs-secondary-bg);--bs-popover-body-padding-x: 1rem;--bs-popover-body-padding-y: 1rem;--bs-popover-body-color: var(--bs-body-color);--bs-popover-arrow-width: 1rem;--bs-popover-arrow-height: 0.5rem;--bs-popover-arrow-border: var(--bs-popover-border-color);z-index:var(--bs-popover-zindex);display:block;max-width:var(--bs-popover-max-width);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-popover-font-size);word-wrap:break-word;background-color:var(--bs-popover-bg);background-clip:padding-box;border:var(--bs-popover-border-width) solid var(--bs-popover-border-color)}.popover .popover-arrow{display:block;width:var(--bs-popover-arrow-width);height:var(--bs-popover-arrow-height)}.popover .popover-arrow::before,.popover .popover-arrow::after{position:absolute;display:block;content:"";border-color:rgba(0,0,0,0);border-style:solid;border-width:0}.bs-popover-top>.popover-arrow,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow{bottom:calc(-1*(var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-top>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after{border-width:var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width)*.5) 0}.bs-popover-top>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before{bottom:0;border-top-color:var(--bs-popover-arrow-border)}.bs-popover-top>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after{bottom:var(--bs-popover-border-width);border-top-color:var(--bs-popover-bg)}.bs-popover-end>.popover-arrow,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow{left:calc(-1*(var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-end>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after{border-width:calc(var(--bs-popover-arrow-width)*.5) var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width)*.5) 0}.bs-popover-end>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before{left:0;border-right-color:var(--bs-popover-arrow-border)}.bs-popover-end>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after{left:var(--bs-popover-border-width);border-right-color:var(--bs-popover-bg)}.bs-popover-bottom>.popover-arrow,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow{top:calc(-1*(var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-bottom>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after{border-width:0 calc(var(--bs-popover-arrow-width)*.5) var(--bs-popover-arrow-height)}.bs-popover-bottom>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before{top:0;border-bottom-color:var(--bs-popover-arrow-border)}.bs-popover-bottom>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after{top:var(--bs-popover-border-width);border-bottom-color:var(--bs-popover-bg)}.bs-popover-bottom .popover-header::before,.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before{position:absolute;top:0;left:50%;display:block;width:var(--bs-popover-arrow-width);margin-left:calc(-0.5*var(--bs-popover-arrow-width));content:"";border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-header-bg)}.bs-popover-start>.popover-arrow,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow{right:calc(-1*(var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-start>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after{border-width:calc(var(--bs-popover-arrow-width)*.5) 0 calc(var(--bs-popover-arrow-width)*.5) var(--bs-popover-arrow-height)}.bs-popover-start>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before{right:0;border-left-color:var(--bs-popover-arrow-border)}.bs-popover-start>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after{right:var(--bs-popover-border-width);border-left-color:var(--bs-popover-bg)}.popover-header{padding:var(--bs-popover-header-padding-y) var(--bs-popover-header-padding-x);margin-bottom:0;font-size:var(--bs-popover-header-font-size);color:var(--bs-popover-header-color);background-color:var(--bs-popover-header-bg);border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-border-color)}.popover-header:empty{display:none}.popover-body{padding:var(--bs-popover-body-padding-y) var(--bs-popover-body-padding-x);color:var(--bs-popover-body-color)}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;backface-visibility:hidden}.carousel-item.active,.carousel-item-next,.carousel-item-prev{display:block}.carousel-item-next:not(.carousel-item-start),.active.carousel-item-end{transform:translateX(100%)}.carousel-item-prev:not(.carousel-item-end),.active.carousel-item-start{transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item.active,.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end{z-index:1;opacity:1}.carousel-fade .active.carousel-item-start,.carousel-fade .active.carousel-item-end{z-index:0;opacity:0}.carousel-control-prev,.carousel-control-next{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:none;border:0;opacity:.5}.carousel-control-prev:hover,.carousel-control-prev:focus,.carousel-control-next:hover,.carousel-control-next:focus{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-prev-icon,.carousel-control-next-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e") /*rtl:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")*/}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e") /*rtl:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")*/}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid rgba(0,0,0,0);border-bottom:10px solid rgba(0,0,0,0);opacity:.5}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-prev-icon,.carousel-dark .carousel-control-next-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}[data-bs-theme=dark] .carousel .carousel-control-prev-icon,[data-bs-theme=dark] .carousel .carousel-control-next-icon,[data-bs-theme=dark].carousel .carousel-control-prev-icon,[data-bs-theme=dark].carousel .carousel-control-next-icon{filter:invert(1) grayscale(100)}[data-bs-theme=dark] .carousel .carousel-indicators [data-bs-target],[data-bs-theme=dark].carousel .carousel-indicators [data-bs-target]{background-color:#000}[data-bs-theme=dark] .carousel .carousel-caption,[data-bs-theme=dark].carousel .carousel-caption{color:#000}.spinner-grow,.spinner-border{display:inline-block;width:var(--bs-spinner-width);height:var(--bs-spinner-height);vertical-align:var(--bs-spinner-vertical-align);border-radius:50%;animation:var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name)}@keyframes spinner-border{to{transform:rotate(360deg) /* rtl:ignore */}}.spinner-border{--bs-spinner-width: 2rem;--bs-spinner-height: 2rem;--bs-spinner-vertical-align: -0.125em;--bs-spinner-border-width: 0.25em;--bs-spinner-animation-speed: 0.75s;--bs-spinner-animation-name: spinner-border;border:var(--bs-spinner-border-width) solid currentcolor;border-right-color:rgba(0,0,0,0)}.spinner-border-sm{--bs-spinner-width: 1rem;--bs-spinner-height: 1rem;--bs-spinner-border-width: 0.2em}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{--bs-spinner-width: 2rem;--bs-spinner-height: 2rem;--bs-spinner-vertical-align: -0.125em;--bs-spinner-animation-speed: 0.75s;--bs-spinner-animation-name: spinner-grow;background-color:currentcolor;opacity:0}.spinner-grow-sm{--bs-spinner-width: 1rem;--bs-spinner-height: 1rem}@media(prefers-reduced-motion: reduce){.spinner-border,.spinner-grow{--bs-spinner-animation-speed: 1.5s}}.offcanvas,.offcanvas-xxl,.offcanvas-xl,.offcanvas-lg,.offcanvas-md,.offcanvas-sm{--bs-offcanvas-zindex: 1045;--bs-offcanvas-width: 400px;--bs-offcanvas-height: 30vh;--bs-offcanvas-padding-x: 1rem;--bs-offcanvas-padding-y: 1rem;--bs-offcanvas-color: var(--bs-body-color);--bs-offcanvas-bg: var(--bs-body-bg);--bs-offcanvas-border-width: var(--bs-border-width);--bs-offcanvas-border-color: var(--bs-border-color-translucent);--bs-offcanvas-box-shadow: var(--bs-box-shadow-sm);--bs-offcanvas-transition: transform 0.3s ease-in-out;--bs-offcanvas-title-line-height: 1.5}@media(max-width: 575.98px){.offcanvas-sm{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0}.offcanvas-sm.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-sm.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-sm.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-sm.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-sm.showing,.offcanvas-sm.show:not(.hiding){transform:none}.offcanvas-sm.showing,.offcanvas-sm.hiding,.offcanvas-sm.show{visibility:visible}}@media(min-width: 576px){.offcanvas-sm{--bs-offcanvas-height: auto;--bs-offcanvas-border-width: 0;background-color:rgba(0,0,0,0) !important}.offcanvas-sm .offcanvas-header{display:none}.offcanvas-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:rgba(0,0,0,0) !important}}@media(max-width: 767.98px){.offcanvas-md{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0}.offcanvas-md.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-md.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-md.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-md.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-md.showing,.offcanvas-md.show:not(.hiding){transform:none}.offcanvas-md.showing,.offcanvas-md.hiding,.offcanvas-md.show{visibility:visible}}@media(min-width: 768px){.offcanvas-md{--bs-offcanvas-height: auto;--bs-offcanvas-border-width: 0;background-color:rgba(0,0,0,0) !important}.offcanvas-md .offcanvas-header{display:none}.offcanvas-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:rgba(0,0,0,0) !important}}@media(max-width: 991.98px){.offcanvas-lg{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0}.offcanvas-lg.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-lg.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-lg.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-lg.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-lg.showing,.offcanvas-lg.show:not(.hiding){transform:none}.offcanvas-lg.showing,.offcanvas-lg.hiding,.offcanvas-lg.show{visibility:visible}}@media(min-width: 992px){.offcanvas-lg{--bs-offcanvas-height: auto;--bs-offcanvas-border-width: 0;background-color:rgba(0,0,0,0) !important}.offcanvas-lg .offcanvas-header{display:none}.offcanvas-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:rgba(0,0,0,0) !important}}@media(max-width: 1199.98px){.offcanvas-xl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0}.offcanvas-xl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xl.showing,.offcanvas-xl.show:not(.hiding){transform:none}.offcanvas-xl.showing,.offcanvas-xl.hiding,.offcanvas-xl.show{visibility:visible}}@media(min-width: 1200px){.offcanvas-xl{--bs-offcanvas-height: auto;--bs-offcanvas-border-width: 0;background-color:rgba(0,0,0,0) !important}.offcanvas-xl .offcanvas-header{display:none}.offcanvas-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:rgba(0,0,0,0) !important}}@media(max-width: 1399.98px){.offcanvas-xxl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0}.offcanvas-xxl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xxl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xxl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xxl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xxl.showing,.offcanvas-xxl.show:not(.hiding){transform:none}.offcanvas-xxl.showing,.offcanvas-xxl.hiding,.offcanvas-xxl.show{visibility:visible}}@media(min-width: 1400px){.offcanvas-xxl{--bs-offcanvas-height: auto;--bs-offcanvas-border-width: 0;background-color:rgba(0,0,0,0) !important}.offcanvas-xxl .offcanvas-header{display:none}.offcanvas-xxl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:rgba(0,0,0,0) !important}}.offcanvas{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0}.offcanvas.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas.showing,.offcanvas.show:not(.hiding){transform:none}.offcanvas.showing,.offcanvas.hiding,.offcanvas.show{visibility:visible}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x)}.offcanvas-header .btn-close{padding:calc(var(--bs-offcanvas-padding-y)*.5) calc(var(--bs-offcanvas-padding-x)*.5);margin:calc(-0.5*var(--bs-offcanvas-padding-y)) calc(-0.5*var(--bs-offcanvas-padding-x)) calc(-0.5*var(--bs-offcanvas-padding-y)) auto}.offcanvas-title{margin-bottom:0;line-height:var(--bs-offcanvas-title-line-height)}.offcanvas-body{flex-grow:1;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x);overflow-y:auto}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentcolor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{animation:placeholder-glow 2s ease-in-out infinite}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg, #000 55%, rgba(0, 0, 0, 0.8) 75%, #000 95%);mask-image:linear-gradient(130deg, #000 55%, rgba(0, 0, 0, 0.8) 75%, #000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;animation:placeholder-wave 2s linear infinite}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.text-bg-primary{color:#000 !important;background-color:RGBA(var(--bs-primary-rgb), var(--bs-bg-opacity, 1)) !important}.text-bg-secondary{color:#fff !important;background-color:RGBA(var(--bs-secondary-rgb), var(--bs-bg-opacity, 1)) !important}.text-bg-success{color:#000 !important;background-color:RGBA(var(--bs-success-rgb), var(--bs-bg-opacity, 1)) !important}.text-bg-info{color:#000 !important;background-color:RGBA(var(--bs-info-rgb), var(--bs-bg-opacity, 1)) !important}.text-bg-warning{color:#000 !important;background-color:RGBA(var(--bs-warning-rgb), var(--bs-bg-opacity, 1)) !important}.text-bg-danger{color:#000 !important;background-color:RGBA(var(--bs-danger-rgb), var(--bs-bg-opacity, 1)) !important}.text-bg-light{color:#000 !important;background-color:RGBA(var(--bs-light-rgb), var(--bs-bg-opacity, 1)) !important}.text-bg-dark{color:#fff !important;background-color:RGBA(var(--bs-dark-rgb), var(--bs-bg-opacity, 1)) !important}.link-primary{color:RGBA(var(--bs-primary-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-primary-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-primary:hover,.link-primary:focus{color:RGBA(72, 157, 255, var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(72, 157, 255, var(--bs-link-underline-opacity, 1)) !important}.link-secondary{color:RGBA(var(--bs-secondary-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-secondary-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-secondary:hover,.link-secondary:focus{color:RGBA(86, 94, 100, var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(86, 94, 100, var(--bs-link-underline-opacity, 1)) !important}.link-success{color:RGBA(var(--bs-success-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-success-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-success:hover,.link-success:focus{color:RGBA(134, 178, 96, var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(134, 178, 96, var(--bs-link-underline-opacity, 1)) !important}.link-info{color:RGBA(var(--bs-info-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-info-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-info:hover,.link-info:focus{color:RGBA(51, 195, 255, var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(51, 195, 255, var(--bs-link-underline-opacity, 1)) !important}.link-warning{color:RGBA(var(--bs-warning-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-warning-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-warning:hover,.link-warning:focus{color:RGBA(253, 212, 93, var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(253, 212, 93, var(--bs-link-underline-opacity, 1)) !important}.link-danger{color:RGBA(var(--bs-danger-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-danger-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-danger:hover,.link-danger:focus{color:RGBA(255, 117, 133, var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(255, 117, 133, var(--bs-link-underline-opacity, 1)) !important}.link-light{color:RGBA(var(--bs-light-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-light-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-light:hover,.link-light:focus{color:RGBA(249, 250, 251, var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(249, 250, 251, var(--bs-link-underline-opacity, 1)) !important}.link-dark{color:RGBA(var(--bs-dark-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-dark-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-dark:hover,.link-dark:focus{color:RGBA(26, 30, 33, var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(26, 30, 33, var(--bs-link-underline-opacity, 1)) !important}.link-body-emphasis{color:RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-body-emphasis:hover,.link-body-emphasis:focus{color:RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-opacity, 0.75)) !important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-underline-opacity, 0.75)) !important}.focus-ring:focus{outline:0;box-shadow:var(--bs-focus-ring-x, 0) var(--bs-focus-ring-y, 0) var(--bs-focus-ring-blur, 0) var(--bs-focus-ring-width) var(--bs-focus-ring-color)}.icon-link{display:inline-flex;gap:.375rem;align-items:center;text-decoration-color:rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 0.5));text-underline-offset:.25em;backface-visibility:hidden}.icon-link>.bi{flex-shrink:0;width:1em;height:1em;fill:currentcolor}.icon-link-hover:hover>.bi,.icon-link-hover:focus-visible>.bi{transform:var(--bs-icon-link-transform, translate3d(0.25em, 0, 0))}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio: 100%}.ratio-4x3{--bs-aspect-ratio: 75%}.ratio-16x9{--bs-aspect-ratio: 56.25%}.ratio-21x9{--bs-aspect-ratio: 42.8571428571%}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:sticky;top:0;z-index:1020}.sticky-bottom{position:sticky;bottom:0;z-index:1020}@media(min-width: 576px){.sticky-sm-top{position:sticky;top:0;z-index:1020}.sticky-sm-bottom{position:sticky;bottom:0;z-index:1020}}@media(min-width: 768px){.sticky-md-top{position:sticky;top:0;z-index:1020}.sticky-md-bottom{position:sticky;bottom:0;z-index:1020}}@media(min-width: 992px){.sticky-lg-top{position:sticky;top:0;z-index:1020}.sticky-lg-bottom{position:sticky;bottom:0;z-index:1020}}@media(min-width: 1200px){.sticky-xl-top{position:sticky;top:0;z-index:1020}.sticky-xl-bottom{position:sticky;bottom:0;z-index:1020}}@media(min-width: 1400px){.sticky-xxl-top{position:sticky;top:0;z-index:1020}.sticky-xxl-bottom{position:sticky;bottom:0;z-index:1020}}.hstack{display:flex;flex-direction:row;align-items:center;align-self:stretch}.vstack{display:flex;flex:1 1 auto;flex-direction:column;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){width:1px !important;height:1px !important;padding:0 !important;margin:-1px !important;overflow:hidden !important;clip:rect(0, 0, 0, 0) !important;white-space:nowrap !important;border:0 !important}.visually-hidden:not(caption),.visually-hidden-focusable:not(:focus):not(:focus-within):not(caption){position:absolute !important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;width:var(--bs-border-width);min-height:1em;background-color:currentcolor;opacity:.25}.align-baseline{vertical-align:baseline !important}.align-top{vertical-align:top !important}.align-middle{vertical-align:middle !important}.align-bottom{vertical-align:bottom !important}.align-text-bottom{vertical-align:text-bottom !important}.align-text-top{vertical-align:text-top !important}.float-start{float:left !important}.float-end{float:right !important}.float-none{float:none !important}.object-fit-contain{-o-object-fit:contain !important;object-fit:contain !important}.object-fit-cover{-o-object-fit:cover !important;object-fit:cover !important}.object-fit-fill{-o-object-fit:fill !important;object-fit:fill !important}.object-fit-scale{-o-object-fit:scale-down !important;object-fit:scale-down !important}.object-fit-none{-o-object-fit:none !important;object-fit:none !important}.opacity-0{opacity:0 !important}.opacity-25{opacity:.25 !important}.opacity-50{opacity:.5 !important}.opacity-75{opacity:.75 !important}.opacity-100{opacity:1 !important}.overflow-auto{overflow:auto !important}.overflow-hidden{overflow:hidden !important}.overflow-visible{overflow:visible !important}.overflow-scroll{overflow:scroll !important}.overflow-x-auto{overflow-x:auto !important}.overflow-x-hidden{overflow-x:hidden !important}.overflow-x-visible{overflow-x:visible !important}.overflow-x-scroll{overflow-x:scroll !important}.overflow-y-auto{overflow-y:auto !important}.overflow-y-hidden{overflow-y:hidden !important}.overflow-y-visible{overflow-y:visible !important}.overflow-y-scroll{overflow-y:scroll !important}.d-inline{display:inline !important}.d-inline-block{display:inline-block !important}.d-block{display:block !important}.d-grid{display:grid !important}.d-inline-grid{display:inline-grid !important}.d-table{display:table !important}.d-table-row{display:table-row !important}.d-table-cell{display:table-cell !important}.d-flex{display:flex !important}.d-inline-flex{display:inline-flex !important}.d-none{display:none !important}.shadow{box-shadow:var(--bs-box-shadow) !important}.shadow-sm{box-shadow:var(--bs-box-shadow-sm) !important}.shadow-lg{box-shadow:var(--bs-box-shadow-lg) !important}.shadow-none{box-shadow:none !important}.focus-ring-primary{--bs-focus-ring-color: rgba(var(--bs-primary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-secondary{--bs-focus-ring-color: rgba(var(--bs-secondary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-success{--bs-focus-ring-color: rgba(var(--bs-success-rgb), var(--bs-focus-ring-opacity))}.focus-ring-info{--bs-focus-ring-color: rgba(var(--bs-info-rgb), var(--bs-focus-ring-opacity))}.focus-ring-warning{--bs-focus-ring-color: rgba(var(--bs-warning-rgb), var(--bs-focus-ring-opacity))}.focus-ring-danger{--bs-focus-ring-color: rgba(var(--bs-danger-rgb), var(--bs-focus-ring-opacity))}.focus-ring-light{--bs-focus-ring-color: rgba(var(--bs-light-rgb), var(--bs-focus-ring-opacity))}.focus-ring-dark{--bs-focus-ring-color: rgba(var(--bs-dark-rgb), var(--bs-focus-ring-opacity))}.position-static{position:static !important}.position-relative{position:relative !important}.position-absolute{position:absolute !important}.position-fixed{position:fixed !important}.position-sticky{position:sticky !important}.top-0{top:0 !important}.top-50{top:50% !important}.top-100{top:100% !important}.bottom-0{bottom:0 !important}.bottom-50{bottom:50% !important}.bottom-100{bottom:100% !important}.start-0{left:0 !important}.start-50{left:50% !important}.start-100{left:100% !important}.end-0{right:0 !important}.end-50{right:50% !important}.end-100{right:100% !important}.translate-middle{transform:translate(-50%, -50%) !important}.translate-middle-x{transform:translateX(-50%) !important}.translate-middle-y{transform:translateY(-50%) !important}.border{border:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important}.border-0{border:0 !important}.border-top{border-top:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important}.border-top-0{border-top:0 !important}.border-end{border-right:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important}.border-end-0{border-right:0 !important}.border-bottom{border-bottom:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important}.border-bottom-0{border-bottom:0 !important}.border-start{border-left:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important}.border-start-0{border-left:0 !important}.border-primary{--bs-border-opacity: 1;border-color:rgba(var(--bs-primary-rgb), var(--bs-border-opacity)) !important}.border-secondary{--bs-border-opacity: 1;border-color:rgba(var(--bs-secondary-rgb), var(--bs-border-opacity)) !important}.border-success{--bs-border-opacity: 1;border-color:rgba(var(--bs-success-rgb), var(--bs-border-opacity)) !important}.border-info{--bs-border-opacity: 1;border-color:rgba(var(--bs-info-rgb), var(--bs-border-opacity)) !important}.border-warning{--bs-border-opacity: 1;border-color:rgba(var(--bs-warning-rgb), var(--bs-border-opacity)) !important}.border-danger{--bs-border-opacity: 1;border-color:rgba(var(--bs-danger-rgb), var(--bs-border-opacity)) !important}.border-light{--bs-border-opacity: 1;border-color:rgba(var(--bs-light-rgb), var(--bs-border-opacity)) !important}.border-dark{--bs-border-opacity: 1;border-color:rgba(var(--bs-dark-rgb), var(--bs-border-opacity)) !important}.border-black{--bs-border-opacity: 1;border-color:rgba(var(--bs-black-rgb), var(--bs-border-opacity)) !important}.border-white{--bs-border-opacity: 1;border-color:rgba(var(--bs-white-rgb), var(--bs-border-opacity)) !important}.border-primary-subtle{border-color:var(--bs-primary-border-subtle) !important}.border-secondary-subtle{border-color:var(--bs-secondary-border-subtle) !important}.border-success-subtle{border-color:var(--bs-success-border-subtle) !important}.border-info-subtle{border-color:var(--bs-info-border-subtle) !important}.border-warning-subtle{border-color:var(--bs-warning-border-subtle) !important}.border-danger-subtle{border-color:var(--bs-danger-border-subtle) !important}.border-light-subtle{border-color:var(--bs-light-border-subtle) !important}.border-dark-subtle{border-color:var(--bs-dark-border-subtle) !important}.border-1{border-width:1px !important}.border-2{border-width:2px !important}.border-3{border-width:3px !important}.border-4{border-width:4px !important}.border-5{border-width:5px !important}.border-opacity-10{--bs-border-opacity: 0.1}.border-opacity-25{--bs-border-opacity: 0.25}.border-opacity-50{--bs-border-opacity: 0.5}.border-opacity-75{--bs-border-opacity: 0.75}.border-opacity-100{--bs-border-opacity: 1}.w-25{width:25% !important}.w-50{width:50% !important}.w-75{width:75% !important}.w-100{width:100% !important}.w-auto{width:auto !important}.mw-100{max-width:100% !important}.vw-100{width:100vw !important}.min-vw-100{min-width:100vw !important}.h-25{height:25% !important}.h-50{height:50% !important}.h-75{height:75% !important}.h-100{height:100% !important}.h-auto{height:auto !important}.mh-100{max-height:100% !important}.vh-100{height:100vh !important}.min-vh-100{min-height:100vh !important}.flex-fill{flex:1 1 auto !important}.flex-row{flex-direction:row !important}.flex-column{flex-direction:column !important}.flex-row-reverse{flex-direction:row-reverse !important}.flex-column-reverse{flex-direction:column-reverse !important}.flex-grow-0{flex-grow:0 !important}.flex-grow-1{flex-grow:1 !important}.flex-shrink-0{flex-shrink:0 !important}.flex-shrink-1{flex-shrink:1 !important}.flex-wrap{flex-wrap:wrap !important}.flex-nowrap{flex-wrap:nowrap !important}.flex-wrap-reverse{flex-wrap:wrap-reverse !important}.justify-content-start{justify-content:flex-start !important}.justify-content-end{justify-content:flex-end !important}.justify-content-center{justify-content:center !important}.justify-content-between{justify-content:space-between !important}.justify-content-around{justify-content:space-around !important}.justify-content-evenly{justify-content:space-evenly !important}.align-items-start{align-items:flex-start !important}.align-items-end{align-items:flex-end !important}.align-items-center{align-items:center !important}.align-items-baseline{align-items:baseline !important}.align-items-stretch{align-items:stretch !important}.align-content-start{align-content:flex-start !important}.align-content-end{align-content:flex-end !important}.align-content-center{align-content:center !important}.align-content-between{align-content:space-between !important}.align-content-around{align-content:space-around !important}.align-content-stretch{align-content:stretch !important}.align-self-auto{align-self:auto !important}.align-self-start{align-self:flex-start !important}.align-self-end{align-self:flex-end !important}.align-self-center{align-self:center !important}.align-self-baseline{align-self:baseline !important}.align-self-stretch{align-self:stretch !important}.order-first{order:-1 !important}.order-0{order:0 !important}.order-1{order:1 !important}.order-2{order:2 !important}.order-3{order:3 !important}.order-4{order:4 !important}.order-5{order:5 !important}.order-last{order:6 !important}.m-0{margin:0 !important}.m-1{margin:.25rem !important}.m-2{margin:.5rem !important}.m-3{margin:1rem !important}.m-4{margin:1.5rem !important}.m-5{margin:3rem !important}.m-auto{margin:auto !important}.mx-0{margin-right:0 !important;margin-left:0 !important}.mx-1{margin-right:.25rem !important;margin-left:.25rem !important}.mx-2{margin-right:.5rem !important;margin-left:.5rem !important}.mx-3{margin-right:1rem !important;margin-left:1rem !important}.mx-4{margin-right:1.5rem !important;margin-left:1.5rem !important}.mx-5{margin-right:3rem !important;margin-left:3rem !important}.mx-auto{margin-right:auto !important;margin-left:auto !important}.my-0{margin-top:0 !important;margin-bottom:0 !important}.my-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.my-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.my-3{margin-top:1rem !important;margin-bottom:1rem !important}.my-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.my-5{margin-top:3rem !important;margin-bottom:3rem !important}.my-auto{margin-top:auto !important;margin-bottom:auto !important}.mt-0{margin-top:0 !important}.mt-1{margin-top:.25rem !important}.mt-2{margin-top:.5rem !important}.mt-3{margin-top:1rem !important}.mt-4{margin-top:1.5rem !important}.mt-5{margin-top:3rem !important}.mt-auto{margin-top:auto !important}.me-0{margin-right:0 !important}.me-1{margin-right:.25rem !important}.me-2{margin-right:.5rem !important}.me-3{margin-right:1rem !important}.me-4{margin-right:1.5rem !important}.me-5{margin-right:3rem !important}.me-auto{margin-right:auto !important}.mb-0{margin-bottom:0 !important}.mb-1{margin-bottom:.25rem !important}.mb-2{margin-bottom:.5rem !important}.mb-3{margin-bottom:1rem !important}.mb-4{margin-bottom:1.5rem !important}.mb-5{margin-bottom:3rem !important}.mb-auto{margin-bottom:auto !important}.ms-0{margin-left:0 !important}.ms-1{margin-left:.25rem !important}.ms-2{margin-left:.5rem !important}.ms-3{margin-left:1rem !important}.ms-4{margin-left:1.5rem !important}.ms-5{margin-left:3rem !important}.ms-auto{margin-left:auto !important}.p-0{padding:0 !important}.p-1{padding:.25rem !important}.p-2{padding:.5rem !important}.p-3{padding:1rem !important}.p-4{padding:1.5rem !important}.p-5{padding:3rem !important}.px-0{padding-right:0 !important;padding-left:0 !important}.px-1{padding-right:.25rem !important;padding-left:.25rem !important}.px-2{padding-right:.5rem !important;padding-left:.5rem !important}.px-3{padding-right:1rem !important;padding-left:1rem !important}.px-4{padding-right:1.5rem !important;padding-left:1.5rem !important}.px-5{padding-right:3rem !important;padding-left:3rem !important}.py-0{padding-top:0 !important;padding-bottom:0 !important}.py-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.py-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.py-3{padding-top:1rem !important;padding-bottom:1rem !important}.py-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.py-5{padding-top:3rem !important;padding-bottom:3rem !important}.pt-0{padding-top:0 !important}.pt-1{padding-top:.25rem !important}.pt-2{padding-top:.5rem !important}.pt-3{padding-top:1rem !important}.pt-4{padding-top:1.5rem !important}.pt-5{padding-top:3rem !important}.pe-0{padding-right:0 !important}.pe-1{padding-right:.25rem !important}.pe-2{padding-right:.5rem !important}.pe-3{padding-right:1rem !important}.pe-4{padding-right:1.5rem !important}.pe-5{padding-right:3rem !important}.pb-0{padding-bottom:0 !important}.pb-1{padding-bottom:.25rem !important}.pb-2{padding-bottom:.5rem !important}.pb-3{padding-bottom:1rem !important}.pb-4{padding-bottom:1.5rem !important}.pb-5{padding-bottom:3rem !important}.ps-0{padding-left:0 !important}.ps-1{padding-left:.25rem !important}.ps-2{padding-left:.5rem !important}.ps-3{padding-left:1rem !important}.ps-4{padding-left:1.5rem !important}.ps-5{padding-left:3rem !important}.gap-0{gap:0 !important}.gap-1{gap:.25rem !important}.gap-2{gap:.5rem !important}.gap-3{gap:1rem !important}.gap-4{gap:1.5rem !important}.gap-5{gap:3rem !important}.row-gap-0{row-gap:0 !important}.row-gap-1{row-gap:.25rem !important}.row-gap-2{row-gap:.5rem !important}.row-gap-3{row-gap:1rem !important}.row-gap-4{row-gap:1.5rem !important}.row-gap-5{row-gap:3rem !important}.column-gap-0{-moz-column-gap:0 !important;column-gap:0 !important}.column-gap-1{-moz-column-gap:.25rem !important;column-gap:.25rem !important}.column-gap-2{-moz-column-gap:.5rem !important;column-gap:.5rem !important}.column-gap-3{-moz-column-gap:1rem !important;column-gap:1rem !important}.column-gap-4{-moz-column-gap:1.5rem !important;column-gap:1.5rem !important}.column-gap-5{-moz-column-gap:3rem !important;column-gap:3rem !important}.font-monospace{font-family:var(--bs-font-monospace) !important}.fs-1{font-size:calc(1.34375rem + 1.125vw) !important}.fs-2{font-size:calc(1.3rem + 0.6vw) !important}.fs-3{font-size:calc(1.278125rem + 0.3375vw) !important}.fs-4{font-size:calc(1.25625rem + 0.075vw) !important}.fs-5{font-size:1.09375rem !important}.fs-6{font-size:0.875rem !important}.fst-italic{font-style:italic !important}.fst-normal{font-style:normal !important}.fw-lighter{font-weight:lighter !important}.fw-light{font-weight:300 !important}.fw-normal{font-weight:400 !important}.fw-medium{font-weight:500 !important}.fw-semibold{font-weight:600 !important}.fw-bold{font-weight:700 !important}.fw-bolder{font-weight:bolder !important}.lh-1{line-height:1 !important}.lh-sm{line-height:1.25 !important}.lh-base{line-height:1.5 !important}.lh-lg{line-height:2 !important}.text-start{text-align:left !important}.text-end{text-align:right !important}.text-center{text-align:center !important}.text-decoration-none{text-decoration:none !important}.text-decoration-underline{text-decoration:underline !important}.text-decoration-line-through{text-decoration:line-through !important}.text-lowercase{text-transform:lowercase !important}.text-uppercase{text-transform:uppercase !important}.text-capitalize{text-transform:capitalize !important}.text-wrap{white-space:normal !important}.text-nowrap{white-space:nowrap !important}.text-break{word-wrap:break-word !important;word-break:break-word !important}.text-primary{--bs-text-opacity: 1;color:rgba(var(--bs-primary-rgb), var(--bs-text-opacity)) !important}.text-secondary{--bs-text-opacity: 1;color:rgba(var(--bs-secondary-rgb), var(--bs-text-opacity)) !important}.text-success{--bs-text-opacity: 1;color:rgba(var(--bs-success-rgb), var(--bs-text-opacity)) !important}.text-info{--bs-text-opacity: 1;color:rgba(var(--bs-info-rgb), var(--bs-text-opacity)) !important}.text-warning{--bs-text-opacity: 1;color:rgba(var(--bs-warning-rgb), var(--bs-text-opacity)) !important}.text-danger{--bs-text-opacity: 1;color:rgba(var(--bs-danger-rgb), var(--bs-text-opacity)) !important}.text-light{--bs-text-opacity: 1;color:rgba(var(--bs-light-rgb), var(--bs-text-opacity)) !important}.text-dark{--bs-text-opacity: 1;color:rgba(var(--bs-dark-rgb), var(--bs-text-opacity)) !important}.text-black{--bs-text-opacity: 1;color:rgba(var(--bs-black-rgb), var(--bs-text-opacity)) !important}.text-white{--bs-text-opacity: 1;color:rgba(var(--bs-white-rgb), var(--bs-text-opacity)) !important}.text-body{--bs-text-opacity: 1;color:rgba(var(--bs-body-color-rgb), var(--bs-text-opacity)) !important}.text-muted{--bs-text-opacity: 1;color:var(--bs-secondary-color) !important}.text-black-50{--bs-text-opacity: 1;color:rgba(0,0,0,.5) !important}.text-white-50{--bs-text-opacity: 1;color:rgba(255,255,255,.5) !important}.text-body-secondary{--bs-text-opacity: 1;color:var(--bs-secondary-color) !important}.text-body-tertiary{--bs-text-opacity: 1;color:var(--bs-tertiary-color) !important}.text-body-emphasis{--bs-text-opacity: 1;color:var(--bs-emphasis-color) !important}.text-reset{--bs-text-opacity: 1;color:inherit !important}.text-opacity-25{--bs-text-opacity: 0.25}.text-opacity-50{--bs-text-opacity: 0.5}.text-opacity-75{--bs-text-opacity: 0.75}.text-opacity-100{--bs-text-opacity: 1}.text-primary-emphasis{color:var(--bs-primary-text-emphasis) !important}.text-secondary-emphasis{color:var(--bs-secondary-text-emphasis) !important}.text-success-emphasis{color:var(--bs-success-text-emphasis) !important}.text-info-emphasis{color:var(--bs-info-text-emphasis) !important}.text-warning-emphasis{color:var(--bs-warning-text-emphasis) !important}.text-danger-emphasis{color:var(--bs-danger-text-emphasis) !important}.text-light-emphasis{color:var(--bs-light-text-emphasis) !important}.text-dark-emphasis{color:var(--bs-dark-text-emphasis) !important}.link-opacity-10{--bs-link-opacity: 0.1}.link-opacity-10-hover:hover{--bs-link-opacity: 0.1}.link-opacity-25{--bs-link-opacity: 0.25}.link-opacity-25-hover:hover{--bs-link-opacity: 0.25}.link-opacity-50{--bs-link-opacity: 0.5}.link-opacity-50-hover:hover{--bs-link-opacity: 0.5}.link-opacity-75{--bs-link-opacity: 0.75}.link-opacity-75-hover:hover{--bs-link-opacity: 0.75}.link-opacity-100{--bs-link-opacity: 1}.link-opacity-100-hover:hover{--bs-link-opacity: 1}.link-offset-1{text-underline-offset:.125em !important}.link-offset-1-hover:hover{text-underline-offset:.125em !important}.link-offset-2{text-underline-offset:.25em !important}.link-offset-2-hover:hover{text-underline-offset:.25em !important}.link-offset-3{text-underline-offset:.375em !important}.link-offset-3-hover:hover{text-underline-offset:.375em !important}.link-underline-primary{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-primary-rgb), var(--bs-link-underline-opacity)) !important}.link-underline-secondary{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-secondary-rgb), var(--bs-link-underline-opacity)) !important}.link-underline-success{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-success-rgb), var(--bs-link-underline-opacity)) !important}.link-underline-info{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-info-rgb), var(--bs-link-underline-opacity)) !important}.link-underline-warning{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-warning-rgb), var(--bs-link-underline-opacity)) !important}.link-underline-danger{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-danger-rgb), var(--bs-link-underline-opacity)) !important}.link-underline-light{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-light-rgb), var(--bs-link-underline-opacity)) !important}.link-underline-dark{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-dark-rgb), var(--bs-link-underline-opacity)) !important}.link-underline{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-link-color-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-underline-opacity-0{--bs-link-underline-opacity: 0}.link-underline-opacity-0-hover:hover{--bs-link-underline-opacity: 0}.link-underline-opacity-10{--bs-link-underline-opacity: 0.1}.link-underline-opacity-10-hover:hover{--bs-link-underline-opacity: 0.1}.link-underline-opacity-25{--bs-link-underline-opacity: 0.25}.link-underline-opacity-25-hover:hover{--bs-link-underline-opacity: 0.25}.link-underline-opacity-50{--bs-link-underline-opacity: 0.5}.link-underline-opacity-50-hover:hover{--bs-link-underline-opacity: 0.5}.link-underline-opacity-75{--bs-link-underline-opacity: 0.75}.link-underline-opacity-75-hover:hover{--bs-link-underline-opacity: 0.75}.link-underline-opacity-100{--bs-link-underline-opacity: 1}.link-underline-opacity-100-hover:hover{--bs-link-underline-opacity: 1}.bg-primary{--bs-bg-opacity: 1;background-color:rgba(var(--bs-primary-rgb), var(--bs-bg-opacity)) !important}.bg-secondary{--bs-bg-opacity: 1;background-color:rgba(var(--bs-secondary-rgb), var(--bs-bg-opacity)) !important}.bg-success{--bs-bg-opacity: 1;background-color:rgba(var(--bs-success-rgb), var(--bs-bg-opacity)) !important}.bg-info{--bs-bg-opacity: 1;background-color:rgba(var(--bs-info-rgb), var(--bs-bg-opacity)) !important}.bg-warning{--bs-bg-opacity: 1;background-color:rgba(var(--bs-warning-rgb), var(--bs-bg-opacity)) !important}.bg-danger{--bs-bg-opacity: 1;background-color:rgba(var(--bs-danger-rgb), var(--bs-bg-opacity)) !important}.bg-light{--bs-bg-opacity: 1;background-color:rgba(var(--bs-light-rgb), var(--bs-bg-opacity)) !important}.bg-dark{--bs-bg-opacity: 1;background-color:rgba(var(--bs-dark-rgb), var(--bs-bg-opacity)) !important}.bg-black{--bs-bg-opacity: 1;background-color:rgba(var(--bs-black-rgb), var(--bs-bg-opacity)) !important}.bg-white{--bs-bg-opacity: 1;background-color:rgba(var(--bs-white-rgb), var(--bs-bg-opacity)) !important}.bg-body{--bs-bg-opacity: 1;background-color:rgba(var(--bs-body-bg-rgb), var(--bs-bg-opacity)) !important}.bg-transparent{--bs-bg-opacity: 1;background-color:rgba(0,0,0,0) !important}.bg-body-secondary{--bs-bg-opacity: 1;background-color:rgba(var(--bs-secondary-bg-rgb), var(--bs-bg-opacity)) !important}.bg-body-tertiary{--bs-bg-opacity: 1;background-color:rgba(var(--bs-tertiary-bg-rgb), var(--bs-bg-opacity)) !important}.bg-opacity-10{--bs-bg-opacity: 0.1}.bg-opacity-25{--bs-bg-opacity: 0.25}.bg-opacity-50{--bs-bg-opacity: 0.5}.bg-opacity-75{--bs-bg-opacity: 0.75}.bg-opacity-100{--bs-bg-opacity: 1}.bg-primary-subtle{background-color:var(--bs-primary-bg-subtle) !important}.bg-secondary-subtle{background-color:var(--bs-secondary-bg-subtle) !important}.bg-success-subtle{background-color:var(--bs-success-bg-subtle) !important}.bg-info-subtle{background-color:var(--bs-info-bg-subtle) !important}.bg-warning-subtle{background-color:var(--bs-warning-bg-subtle) !important}.bg-danger-subtle{background-color:var(--bs-danger-bg-subtle) !important}.bg-light-subtle{background-color:var(--bs-light-bg-subtle) !important}.bg-dark-subtle{background-color:var(--bs-dark-bg-subtle) !important}.bg-gradient{background-image:var(--bs-gradient) !important}.user-select-all{-webkit-user-select:all !important;-moz-user-select:all !important;user-select:all !important}.user-select-auto{-webkit-user-select:auto !important;-moz-user-select:auto !important;user-select:auto !important}.user-select-none{-webkit-user-select:none !important;-moz-user-select:none !important;user-select:none !important}.pe-none{pointer-events:none !important}.pe-auto{pointer-events:auto !important}.rounded{border-radius:var(--bs-border-radius) !important}.rounded-0{border-radius:0 !important}.rounded-1{border-radius:var(--bs-border-radius-sm) !important}.rounded-2{border-radius:var(--bs-border-radius) !important}.rounded-3{border-radius:var(--bs-border-radius-lg) !important}.rounded-4{border-radius:var(--bs-border-radius-xl) !important}.rounded-5{border-radius:var(--bs-border-radius-xxl) !important}.rounded-circle{border-radius:50% !important}.rounded-pill{border-radius:var(--bs-border-radius-pill) !important}.rounded-top{border-top-left-radius:var(--bs-border-radius) !important;border-top-right-radius:var(--bs-border-radius) !important}.rounded-top-0{border-top-left-radius:0 !important;border-top-right-radius:0 !important}.rounded-top-1{border-top-left-radius:var(--bs-border-radius-sm) !important;border-top-right-radius:var(--bs-border-radius-sm) !important}.rounded-top-2{border-top-left-radius:var(--bs-border-radius) !important;border-top-right-radius:var(--bs-border-radius) !important}.rounded-top-3{border-top-left-radius:var(--bs-border-radius-lg) !important;border-top-right-radius:var(--bs-border-radius-lg) !important}.rounded-top-4{border-top-left-radius:var(--bs-border-radius-xl) !important;border-top-right-radius:var(--bs-border-radius-xl) !important}.rounded-top-5{border-top-left-radius:var(--bs-border-radius-xxl) !important;border-top-right-radius:var(--bs-border-radius-xxl) !important}.rounded-top-circle{border-top-left-radius:50% !important;border-top-right-radius:50% !important}.rounded-top-pill{border-top-left-radius:var(--bs-border-radius-pill) !important;border-top-right-radius:var(--bs-border-radius-pill) !important}.rounded-end{border-top-right-radius:var(--bs-border-radius) !important;border-bottom-right-radius:var(--bs-border-radius) !important}.rounded-end-0{border-top-right-radius:0 !important;border-bottom-right-radius:0 !important}.rounded-end-1{border-top-right-radius:var(--bs-border-radius-sm) !important;border-bottom-right-radius:var(--bs-border-radius-sm) !important}.rounded-end-2{border-top-right-radius:var(--bs-border-radius) !important;border-bottom-right-radius:var(--bs-border-radius) !important}.rounded-end-3{border-top-right-radius:var(--bs-border-radius-lg) !important;border-bottom-right-radius:var(--bs-border-radius-lg) !important}.rounded-end-4{border-top-right-radius:var(--bs-border-radius-xl) !important;border-bottom-right-radius:var(--bs-border-radius-xl) !important}.rounded-end-5{border-top-right-radius:var(--bs-border-radius-xxl) !important;border-bottom-right-radius:var(--bs-border-radius-xxl) !important}.rounded-end-circle{border-top-right-radius:50% !important;border-bottom-right-radius:50% !important}.rounded-end-pill{border-top-right-radius:var(--bs-border-radius-pill) !important;border-bottom-right-radius:var(--bs-border-radius-pill) !important}.rounded-bottom{border-bottom-right-radius:var(--bs-border-radius) !important;border-bottom-left-radius:var(--bs-border-radius) !important}.rounded-bottom-0{border-bottom-right-radius:0 !important;border-bottom-left-radius:0 !important}.rounded-bottom-1{border-bottom-right-radius:var(--bs-border-radius-sm) !important;border-bottom-left-radius:var(--bs-border-radius-sm) !important}.rounded-bottom-2{border-bottom-right-radius:var(--bs-border-radius) !important;border-bottom-left-radius:var(--bs-border-radius) !important}.rounded-bottom-3{border-bottom-right-radius:var(--bs-border-radius-lg) !important;border-bottom-left-radius:var(--bs-border-radius-lg) !important}.rounded-bottom-4{border-bottom-right-radius:var(--bs-border-radius-xl) !important;border-bottom-left-radius:var(--bs-border-radius-xl) !important}.rounded-bottom-5{border-bottom-right-radius:var(--bs-border-radius-xxl) !important;border-bottom-left-radius:var(--bs-border-radius-xxl) !important}.rounded-bottom-circle{border-bottom-right-radius:50% !important;border-bottom-left-radius:50% !important}.rounded-bottom-pill{border-bottom-right-radius:var(--bs-border-radius-pill) !important;border-bottom-left-radius:var(--bs-border-radius-pill) !important}.rounded-start{border-bottom-left-radius:var(--bs-border-radius) !important;border-top-left-radius:var(--bs-border-radius) !important}.rounded-start-0{border-bottom-left-radius:0 !important;border-top-left-radius:0 !important}.rounded-start-1{border-bottom-left-radius:var(--bs-border-radius-sm) !important;border-top-left-radius:var(--bs-border-radius-sm) !important}.rounded-start-2{border-bottom-left-radius:var(--bs-border-radius) !important;border-top-left-radius:var(--bs-border-radius) !important}.rounded-start-3{border-bottom-left-radius:var(--bs-border-radius-lg) !important;border-top-left-radius:var(--bs-border-radius-lg) !important}.rounded-start-4{border-bottom-left-radius:var(--bs-border-radius-xl) !important;border-top-left-radius:var(--bs-border-radius-xl) !important}.rounded-start-5{border-bottom-left-radius:var(--bs-border-radius-xxl) !important;border-top-left-radius:var(--bs-border-radius-xxl) !important}.rounded-start-circle{border-bottom-left-radius:50% !important;border-top-left-radius:50% !important}.rounded-start-pill{border-bottom-left-radius:var(--bs-border-radius-pill) !important;border-top-left-radius:var(--bs-border-radius-pill) !important}.visible{visibility:visible !important}.invisible{visibility:hidden !important}.z-n1{z-index:-1 !important}.z-0{z-index:0 !important}.z-1{z-index:1 !important}.z-2{z-index:2 !important}.z-3{z-index:3 !important}@media(min-width: 576px){.float-sm-start{float:left !important}.float-sm-end{float:right !important}.float-sm-none{float:none !important}.object-fit-sm-contain{-o-object-fit:contain !important;object-fit:contain !important}.object-fit-sm-cover{-o-object-fit:cover !important;object-fit:cover !important}.object-fit-sm-fill{-o-object-fit:fill !important;object-fit:fill !important}.object-fit-sm-scale{-o-object-fit:scale-down !important;object-fit:scale-down !important}.object-fit-sm-none{-o-object-fit:none !important;object-fit:none !important}.d-sm-inline{display:inline !important}.d-sm-inline-block{display:inline-block !important}.d-sm-block{display:block !important}.d-sm-grid{display:grid !important}.d-sm-inline-grid{display:inline-grid !important}.d-sm-table{display:table !important}.d-sm-table-row{display:table-row !important}.d-sm-table-cell{display:table-cell !important}.d-sm-flex{display:flex !important}.d-sm-inline-flex{display:inline-flex !important}.d-sm-none{display:none !important}.flex-sm-fill{flex:1 1 auto !important}.flex-sm-row{flex-direction:row !important}.flex-sm-column{flex-direction:column !important}.flex-sm-row-reverse{flex-direction:row-reverse !important}.flex-sm-column-reverse{flex-direction:column-reverse !important}.flex-sm-grow-0{flex-grow:0 !important}.flex-sm-grow-1{flex-grow:1 !important}.flex-sm-shrink-0{flex-shrink:0 !important}.flex-sm-shrink-1{flex-shrink:1 !important}.flex-sm-wrap{flex-wrap:wrap !important}.flex-sm-nowrap{flex-wrap:nowrap !important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse !important}.justify-content-sm-start{justify-content:flex-start !important}.justify-content-sm-end{justify-content:flex-end !important}.justify-content-sm-center{justify-content:center !important}.justify-content-sm-between{justify-content:space-between !important}.justify-content-sm-around{justify-content:space-around !important}.justify-content-sm-evenly{justify-content:space-evenly !important}.align-items-sm-start{align-items:flex-start !important}.align-items-sm-end{align-items:flex-end !important}.align-items-sm-center{align-items:center !important}.align-items-sm-baseline{align-items:baseline !important}.align-items-sm-stretch{align-items:stretch !important}.align-content-sm-start{align-content:flex-start !important}.align-content-sm-end{align-content:flex-end !important}.align-content-sm-center{align-content:center !important}.align-content-sm-between{align-content:space-between !important}.align-content-sm-around{align-content:space-around !important}.align-content-sm-stretch{align-content:stretch !important}.align-self-sm-auto{align-self:auto !important}.align-self-sm-start{align-self:flex-start !important}.align-self-sm-end{align-self:flex-end !important}.align-self-sm-center{align-self:center !important}.align-self-sm-baseline{align-self:baseline !important}.align-self-sm-stretch{align-self:stretch !important}.order-sm-first{order:-1 !important}.order-sm-0{order:0 !important}.order-sm-1{order:1 !important}.order-sm-2{order:2 !important}.order-sm-3{order:3 !important}.order-sm-4{order:4 !important}.order-sm-5{order:5 !important}.order-sm-last{order:6 !important}.m-sm-0{margin:0 !important}.m-sm-1{margin:.25rem !important}.m-sm-2{margin:.5rem !important}.m-sm-3{margin:1rem !important}.m-sm-4{margin:1.5rem !important}.m-sm-5{margin:3rem !important}.m-sm-auto{margin:auto !important}.mx-sm-0{margin-right:0 !important;margin-left:0 !important}.mx-sm-1{margin-right:.25rem !important;margin-left:.25rem !important}.mx-sm-2{margin-right:.5rem !important;margin-left:.5rem !important}.mx-sm-3{margin-right:1rem !important;margin-left:1rem !important}.mx-sm-4{margin-right:1.5rem !important;margin-left:1.5rem !important}.mx-sm-5{margin-right:3rem !important;margin-left:3rem !important}.mx-sm-auto{margin-right:auto !important;margin-left:auto !important}.my-sm-0{margin-top:0 !important;margin-bottom:0 !important}.my-sm-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.my-sm-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.my-sm-3{margin-top:1rem !important;margin-bottom:1rem !important}.my-sm-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.my-sm-5{margin-top:3rem !important;margin-bottom:3rem !important}.my-sm-auto{margin-top:auto !important;margin-bottom:auto !important}.mt-sm-0{margin-top:0 !important}.mt-sm-1{margin-top:.25rem !important}.mt-sm-2{margin-top:.5rem !important}.mt-sm-3{margin-top:1rem !important}.mt-sm-4{margin-top:1.5rem !important}.mt-sm-5{margin-top:3rem !important}.mt-sm-auto{margin-top:auto !important}.me-sm-0{margin-right:0 !important}.me-sm-1{margin-right:.25rem !important}.me-sm-2{margin-right:.5rem !important}.me-sm-3{margin-right:1rem !important}.me-sm-4{margin-right:1.5rem !important}.me-sm-5{margin-right:3rem !important}.me-sm-auto{margin-right:auto !important}.mb-sm-0{margin-bottom:0 !important}.mb-sm-1{margin-bottom:.25rem !important}.mb-sm-2{margin-bottom:.5rem !important}.mb-sm-3{margin-bottom:1rem !important}.mb-sm-4{margin-bottom:1.5rem !important}.mb-sm-5{margin-bottom:3rem !important}.mb-sm-auto{margin-bottom:auto !important}.ms-sm-0{margin-left:0 !important}.ms-sm-1{margin-left:.25rem !important}.ms-sm-2{margin-left:.5rem !important}.ms-sm-3{margin-left:1rem !important}.ms-sm-4{margin-left:1.5rem !important}.ms-sm-5{margin-left:3rem !important}.ms-sm-auto{margin-left:auto !important}.p-sm-0{padding:0 !important}.p-sm-1{padding:.25rem !important}.p-sm-2{padding:.5rem !important}.p-sm-3{padding:1rem !important}.p-sm-4{padding:1.5rem !important}.p-sm-5{padding:3rem !important}.px-sm-0{padding-right:0 !important;padding-left:0 !important}.px-sm-1{padding-right:.25rem !important;padding-left:.25rem !important}.px-sm-2{padding-right:.5rem !important;padding-left:.5rem !important}.px-sm-3{padding-right:1rem !important;padding-left:1rem !important}.px-sm-4{padding-right:1.5rem !important;padding-left:1.5rem !important}.px-sm-5{padding-right:3rem !important;padding-left:3rem !important}.py-sm-0{padding-top:0 !important;padding-bottom:0 !important}.py-sm-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.py-sm-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.py-sm-3{padding-top:1rem !important;padding-bottom:1rem !important}.py-sm-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.py-sm-5{padding-top:3rem !important;padding-bottom:3rem !important}.pt-sm-0{padding-top:0 !important}.pt-sm-1{padding-top:.25rem !important}.pt-sm-2{padding-top:.5rem !important}.pt-sm-3{padding-top:1rem !important}.pt-sm-4{padding-top:1.5rem !important}.pt-sm-5{padding-top:3rem !important}.pe-sm-0{padding-right:0 !important}.pe-sm-1{padding-right:.25rem !important}.pe-sm-2{padding-right:.5rem !important}.pe-sm-3{padding-right:1rem !important}.pe-sm-4{padding-right:1.5rem !important}.pe-sm-5{padding-right:3rem !important}.pb-sm-0{padding-bottom:0 !important}.pb-sm-1{padding-bottom:.25rem !important}.pb-sm-2{padding-bottom:.5rem !important}.pb-sm-3{padding-bottom:1rem !important}.pb-sm-4{padding-bottom:1.5rem !important}.pb-sm-5{padding-bottom:3rem !important}.ps-sm-0{padding-left:0 !important}.ps-sm-1{padding-left:.25rem !important}.ps-sm-2{padding-left:.5rem !important}.ps-sm-3{padding-left:1rem !important}.ps-sm-4{padding-left:1.5rem !important}.ps-sm-5{padding-left:3rem !important}.gap-sm-0{gap:0 !important}.gap-sm-1{gap:.25rem !important}.gap-sm-2{gap:.5rem !important}.gap-sm-3{gap:1rem !important}.gap-sm-4{gap:1.5rem !important}.gap-sm-5{gap:3rem !important}.row-gap-sm-0{row-gap:0 !important}.row-gap-sm-1{row-gap:.25rem !important}.row-gap-sm-2{row-gap:.5rem !important}.row-gap-sm-3{row-gap:1rem !important}.row-gap-sm-4{row-gap:1.5rem !important}.row-gap-sm-5{row-gap:3rem !important}.column-gap-sm-0{-moz-column-gap:0 !important;column-gap:0 !important}.column-gap-sm-1{-moz-column-gap:.25rem !important;column-gap:.25rem !important}.column-gap-sm-2{-moz-column-gap:.5rem !important;column-gap:.5rem !important}.column-gap-sm-3{-moz-column-gap:1rem !important;column-gap:1rem !important}.column-gap-sm-4{-moz-column-gap:1.5rem !important;column-gap:1.5rem !important}.column-gap-sm-5{-moz-column-gap:3rem !important;column-gap:3rem !important}.text-sm-start{text-align:left !important}.text-sm-end{text-align:right !important}.text-sm-center{text-align:center !important}}@media(min-width: 768px){.float-md-start{float:left !important}.float-md-end{float:right !important}.float-md-none{float:none !important}.object-fit-md-contain{-o-object-fit:contain !important;object-fit:contain !important}.object-fit-md-cover{-o-object-fit:cover !important;object-fit:cover !important}.object-fit-md-fill{-o-object-fit:fill !important;object-fit:fill !important}.object-fit-md-scale{-o-object-fit:scale-down !important;object-fit:scale-down !important}.object-fit-md-none{-o-object-fit:none !important;object-fit:none !important}.d-md-inline{display:inline !important}.d-md-inline-block{display:inline-block !important}.d-md-block{display:block !important}.d-md-grid{display:grid !important}.d-md-inline-grid{display:inline-grid !important}.d-md-table{display:table !important}.d-md-table-row{display:table-row !important}.d-md-table-cell{display:table-cell !important}.d-md-flex{display:flex !important}.d-md-inline-flex{display:inline-flex !important}.d-md-none{display:none !important}.flex-md-fill{flex:1 1 auto !important}.flex-md-row{flex-direction:row !important}.flex-md-column{flex-direction:column !important}.flex-md-row-reverse{flex-direction:row-reverse !important}.flex-md-column-reverse{flex-direction:column-reverse !important}.flex-md-grow-0{flex-grow:0 !important}.flex-md-grow-1{flex-grow:1 !important}.flex-md-shrink-0{flex-shrink:0 !important}.flex-md-shrink-1{flex-shrink:1 !important}.flex-md-wrap{flex-wrap:wrap !important}.flex-md-nowrap{flex-wrap:nowrap !important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse !important}.justify-content-md-start{justify-content:flex-start !important}.justify-content-md-end{justify-content:flex-end !important}.justify-content-md-center{justify-content:center !important}.justify-content-md-between{justify-content:space-between !important}.justify-content-md-around{justify-content:space-around !important}.justify-content-md-evenly{justify-content:space-evenly !important}.align-items-md-start{align-items:flex-start !important}.align-items-md-end{align-items:flex-end !important}.align-items-md-center{align-items:center !important}.align-items-md-baseline{align-items:baseline !important}.align-items-md-stretch{align-items:stretch !important}.align-content-md-start{align-content:flex-start !important}.align-content-md-end{align-content:flex-end !important}.align-content-md-center{align-content:center !important}.align-content-md-between{align-content:space-between !important}.align-content-md-around{align-content:space-around !important}.align-content-md-stretch{align-content:stretch !important}.align-self-md-auto{align-self:auto !important}.align-self-md-start{align-self:flex-start !important}.align-self-md-end{align-self:flex-end !important}.align-self-md-center{align-self:center !important}.align-self-md-baseline{align-self:baseline !important}.align-self-md-stretch{align-self:stretch !important}.order-md-first{order:-1 !important}.order-md-0{order:0 !important}.order-md-1{order:1 !important}.order-md-2{order:2 !important}.order-md-3{order:3 !important}.order-md-4{order:4 !important}.order-md-5{order:5 !important}.order-md-last{order:6 !important}.m-md-0{margin:0 !important}.m-md-1{margin:.25rem !important}.m-md-2{margin:.5rem !important}.m-md-3{margin:1rem !important}.m-md-4{margin:1.5rem !important}.m-md-5{margin:3rem !important}.m-md-auto{margin:auto !important}.mx-md-0{margin-right:0 !important;margin-left:0 !important}.mx-md-1{margin-right:.25rem !important;margin-left:.25rem !important}.mx-md-2{margin-right:.5rem !important;margin-left:.5rem !important}.mx-md-3{margin-right:1rem !important;margin-left:1rem !important}.mx-md-4{margin-right:1.5rem !important;margin-left:1.5rem !important}.mx-md-5{margin-right:3rem !important;margin-left:3rem !important}.mx-md-auto{margin-right:auto !important;margin-left:auto !important}.my-md-0{margin-top:0 !important;margin-bottom:0 !important}.my-md-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.my-md-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.my-md-3{margin-top:1rem !important;margin-bottom:1rem !important}.my-md-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.my-md-5{margin-top:3rem !important;margin-bottom:3rem !important}.my-md-auto{margin-top:auto !important;margin-bottom:auto !important}.mt-md-0{margin-top:0 !important}.mt-md-1{margin-top:.25rem !important}.mt-md-2{margin-top:.5rem !important}.mt-md-3{margin-top:1rem !important}.mt-md-4{margin-top:1.5rem !important}.mt-md-5{margin-top:3rem !important}.mt-md-auto{margin-top:auto !important}.me-md-0{margin-right:0 !important}.me-md-1{margin-right:.25rem !important}.me-md-2{margin-right:.5rem !important}.me-md-3{margin-right:1rem !important}.me-md-4{margin-right:1.5rem !important}.me-md-5{margin-right:3rem !important}.me-md-auto{margin-right:auto !important}.mb-md-0{margin-bottom:0 !important}.mb-md-1{margin-bottom:.25rem !important}.mb-md-2{margin-bottom:.5rem !important}.mb-md-3{margin-bottom:1rem !important}.mb-md-4{margin-bottom:1.5rem !important}.mb-md-5{margin-bottom:3rem !important}.mb-md-auto{margin-bottom:auto !important}.ms-md-0{margin-left:0 !important}.ms-md-1{margin-left:.25rem !important}.ms-md-2{margin-left:.5rem !important}.ms-md-3{margin-left:1rem !important}.ms-md-4{margin-left:1.5rem !important}.ms-md-5{margin-left:3rem !important}.ms-md-auto{margin-left:auto !important}.p-md-0{padding:0 !important}.p-md-1{padding:.25rem !important}.p-md-2{padding:.5rem !important}.p-md-3{padding:1rem !important}.p-md-4{padding:1.5rem !important}.p-md-5{padding:3rem !important}.px-md-0{padding-right:0 !important;padding-left:0 !important}.px-md-1{padding-right:.25rem !important;padding-left:.25rem !important}.px-md-2{padding-right:.5rem !important;padding-left:.5rem !important}.px-md-3{padding-right:1rem !important;padding-left:1rem !important}.px-md-4{padding-right:1.5rem !important;padding-left:1.5rem !important}.px-md-5{padding-right:3rem !important;padding-left:3rem !important}.py-md-0{padding-top:0 !important;padding-bottom:0 !important}.py-md-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.py-md-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.py-md-3{padding-top:1rem !important;padding-bottom:1rem !important}.py-md-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.py-md-5{padding-top:3rem !important;padding-bottom:3rem !important}.pt-md-0{padding-top:0 !important}.pt-md-1{padding-top:.25rem !important}.pt-md-2{padding-top:.5rem !important}.pt-md-3{padding-top:1rem !important}.pt-md-4{padding-top:1.5rem !important}.pt-md-5{padding-top:3rem !important}.pe-md-0{padding-right:0 !important}.pe-md-1{padding-right:.25rem !important}.pe-md-2{padding-right:.5rem !important}.pe-md-3{padding-right:1rem !important}.pe-md-4{padding-right:1.5rem !important}.pe-md-5{padding-right:3rem !important}.pb-md-0{padding-bottom:0 !important}.pb-md-1{padding-bottom:.25rem !important}.pb-md-2{padding-bottom:.5rem !important}.pb-md-3{padding-bottom:1rem !important}.pb-md-4{padding-bottom:1.5rem !important}.pb-md-5{padding-bottom:3rem !important}.ps-md-0{padding-left:0 !important}.ps-md-1{padding-left:.25rem !important}.ps-md-2{padding-left:.5rem !important}.ps-md-3{padding-left:1rem !important}.ps-md-4{padding-left:1.5rem !important}.ps-md-5{padding-left:3rem !important}.gap-md-0{gap:0 !important}.gap-md-1{gap:.25rem !important}.gap-md-2{gap:.5rem !important}.gap-md-3{gap:1rem !important}.gap-md-4{gap:1.5rem !important}.gap-md-5{gap:3rem !important}.row-gap-md-0{row-gap:0 !important}.row-gap-md-1{row-gap:.25rem !important}.row-gap-md-2{row-gap:.5rem !important}.row-gap-md-3{row-gap:1rem !important}.row-gap-md-4{row-gap:1.5rem !important}.row-gap-md-5{row-gap:3rem !important}.column-gap-md-0{-moz-column-gap:0 !important;column-gap:0 !important}.column-gap-md-1{-moz-column-gap:.25rem !important;column-gap:.25rem !important}.column-gap-md-2{-moz-column-gap:.5rem !important;column-gap:.5rem !important}.column-gap-md-3{-moz-column-gap:1rem !important;column-gap:1rem !important}.column-gap-md-4{-moz-column-gap:1.5rem !important;column-gap:1.5rem !important}.column-gap-md-5{-moz-column-gap:3rem !important;column-gap:3rem !important}.text-md-start{text-align:left !important}.text-md-end{text-align:right !important}.text-md-center{text-align:center !important}}@media(min-width: 992px){.float-lg-start{float:left !important}.float-lg-end{float:right !important}.float-lg-none{float:none !important}.object-fit-lg-contain{-o-object-fit:contain !important;object-fit:contain !important}.object-fit-lg-cover{-o-object-fit:cover !important;object-fit:cover !important}.object-fit-lg-fill{-o-object-fit:fill !important;object-fit:fill !important}.object-fit-lg-scale{-o-object-fit:scale-down !important;object-fit:scale-down !important}.object-fit-lg-none{-o-object-fit:none !important;object-fit:none !important}.d-lg-inline{display:inline !important}.d-lg-inline-block{display:inline-block !important}.d-lg-block{display:block !important}.d-lg-grid{display:grid !important}.d-lg-inline-grid{display:inline-grid !important}.d-lg-table{display:table !important}.d-lg-table-row{display:table-row !important}.d-lg-table-cell{display:table-cell !important}.d-lg-flex{display:flex !important}.d-lg-inline-flex{display:inline-flex !important}.d-lg-none{display:none !important}.flex-lg-fill{flex:1 1 auto !important}.flex-lg-row{flex-direction:row !important}.flex-lg-column{flex-direction:column !important}.flex-lg-row-reverse{flex-direction:row-reverse !important}.flex-lg-column-reverse{flex-direction:column-reverse !important}.flex-lg-grow-0{flex-grow:0 !important}.flex-lg-grow-1{flex-grow:1 !important}.flex-lg-shrink-0{flex-shrink:0 !important}.flex-lg-shrink-1{flex-shrink:1 !important}.flex-lg-wrap{flex-wrap:wrap !important}.flex-lg-nowrap{flex-wrap:nowrap !important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse !important}.justify-content-lg-start{justify-content:flex-start !important}.justify-content-lg-end{justify-content:flex-end !important}.justify-content-lg-center{justify-content:center !important}.justify-content-lg-between{justify-content:space-between !important}.justify-content-lg-around{justify-content:space-around !important}.justify-content-lg-evenly{justify-content:space-evenly !important}.align-items-lg-start{align-items:flex-start !important}.align-items-lg-end{align-items:flex-end !important}.align-items-lg-center{align-items:center !important}.align-items-lg-baseline{align-items:baseline !important}.align-items-lg-stretch{align-items:stretch !important}.align-content-lg-start{align-content:flex-start !important}.align-content-lg-end{align-content:flex-end !important}.align-content-lg-center{align-content:center !important}.align-content-lg-between{align-content:space-between !important}.align-content-lg-around{align-content:space-around !important}.align-content-lg-stretch{align-content:stretch !important}.align-self-lg-auto{align-self:auto !important}.align-self-lg-start{align-self:flex-start !important}.align-self-lg-end{align-self:flex-end !important}.align-self-lg-center{align-self:center !important}.align-self-lg-baseline{align-self:baseline !important}.align-self-lg-stretch{align-self:stretch !important}.order-lg-first{order:-1 !important}.order-lg-0{order:0 !important}.order-lg-1{order:1 !important}.order-lg-2{order:2 !important}.order-lg-3{order:3 !important}.order-lg-4{order:4 !important}.order-lg-5{order:5 !important}.order-lg-last{order:6 !important}.m-lg-0{margin:0 !important}.m-lg-1{margin:.25rem !important}.m-lg-2{margin:.5rem !important}.m-lg-3{margin:1rem !important}.m-lg-4{margin:1.5rem !important}.m-lg-5{margin:3rem !important}.m-lg-auto{margin:auto !important}.mx-lg-0{margin-right:0 !important;margin-left:0 !important}.mx-lg-1{margin-right:.25rem !important;margin-left:.25rem !important}.mx-lg-2{margin-right:.5rem !important;margin-left:.5rem !important}.mx-lg-3{margin-right:1rem !important;margin-left:1rem !important}.mx-lg-4{margin-right:1.5rem !important;margin-left:1.5rem !important}.mx-lg-5{margin-right:3rem !important;margin-left:3rem !important}.mx-lg-auto{margin-right:auto !important;margin-left:auto !important}.my-lg-0{margin-top:0 !important;margin-bottom:0 !important}.my-lg-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.my-lg-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.my-lg-3{margin-top:1rem !important;margin-bottom:1rem !important}.my-lg-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.my-lg-5{margin-top:3rem !important;margin-bottom:3rem !important}.my-lg-auto{margin-top:auto !important;margin-bottom:auto !important}.mt-lg-0{margin-top:0 !important}.mt-lg-1{margin-top:.25rem !important}.mt-lg-2{margin-top:.5rem !important}.mt-lg-3{margin-top:1rem !important}.mt-lg-4{margin-top:1.5rem !important}.mt-lg-5{margin-top:3rem !important}.mt-lg-auto{margin-top:auto !important}.me-lg-0{margin-right:0 !important}.me-lg-1{margin-right:.25rem !important}.me-lg-2{margin-right:.5rem !important}.me-lg-3{margin-right:1rem !important}.me-lg-4{margin-right:1.5rem !important}.me-lg-5{margin-right:3rem !important}.me-lg-auto{margin-right:auto !important}.mb-lg-0{margin-bottom:0 !important}.mb-lg-1{margin-bottom:.25rem !important}.mb-lg-2{margin-bottom:.5rem !important}.mb-lg-3{margin-bottom:1rem !important}.mb-lg-4{margin-bottom:1.5rem !important}.mb-lg-5{margin-bottom:3rem !important}.mb-lg-auto{margin-bottom:auto !important}.ms-lg-0{margin-left:0 !important}.ms-lg-1{margin-left:.25rem !important}.ms-lg-2{margin-left:.5rem !important}.ms-lg-3{margin-left:1rem !important}.ms-lg-4{margin-left:1.5rem !important}.ms-lg-5{margin-left:3rem !important}.ms-lg-auto{margin-left:auto !important}.p-lg-0{padding:0 !important}.p-lg-1{padding:.25rem !important}.p-lg-2{padding:.5rem !important}.p-lg-3{padding:1rem !important}.p-lg-4{padding:1.5rem !important}.p-lg-5{padding:3rem !important}.px-lg-0{padding-right:0 !important;padding-left:0 !important}.px-lg-1{padding-right:.25rem !important;padding-left:.25rem !important}.px-lg-2{padding-right:.5rem !important;padding-left:.5rem !important}.px-lg-3{padding-right:1rem !important;padding-left:1rem !important}.px-lg-4{padding-right:1.5rem !important;padding-left:1.5rem !important}.px-lg-5{padding-right:3rem !important;padding-left:3rem !important}.py-lg-0{padding-top:0 !important;padding-bottom:0 !important}.py-lg-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.py-lg-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.py-lg-3{padding-top:1rem !important;padding-bottom:1rem !important}.py-lg-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.py-lg-5{padding-top:3rem !important;padding-bottom:3rem !important}.pt-lg-0{padding-top:0 !important}.pt-lg-1{padding-top:.25rem !important}.pt-lg-2{padding-top:.5rem !important}.pt-lg-3{padding-top:1rem !important}.pt-lg-4{padding-top:1.5rem !important}.pt-lg-5{padding-top:3rem !important}.pe-lg-0{padding-right:0 !important}.pe-lg-1{padding-right:.25rem !important}.pe-lg-2{padding-right:.5rem !important}.pe-lg-3{padding-right:1rem !important}.pe-lg-4{padding-right:1.5rem !important}.pe-lg-5{padding-right:3rem !important}.pb-lg-0{padding-bottom:0 !important}.pb-lg-1{padding-bottom:.25rem !important}.pb-lg-2{padding-bottom:.5rem !important}.pb-lg-3{padding-bottom:1rem !important}.pb-lg-4{padding-bottom:1.5rem !important}.pb-lg-5{padding-bottom:3rem !important}.ps-lg-0{padding-left:0 !important}.ps-lg-1{padding-left:.25rem !important}.ps-lg-2{padding-left:.5rem !important}.ps-lg-3{padding-left:1rem !important}.ps-lg-4{padding-left:1.5rem !important}.ps-lg-5{padding-left:3rem !important}.gap-lg-0{gap:0 !important}.gap-lg-1{gap:.25rem !important}.gap-lg-2{gap:.5rem !important}.gap-lg-3{gap:1rem !important}.gap-lg-4{gap:1.5rem !important}.gap-lg-5{gap:3rem !important}.row-gap-lg-0{row-gap:0 !important}.row-gap-lg-1{row-gap:.25rem !important}.row-gap-lg-2{row-gap:.5rem !important}.row-gap-lg-3{row-gap:1rem !important}.row-gap-lg-4{row-gap:1.5rem !important}.row-gap-lg-5{row-gap:3rem !important}.column-gap-lg-0{-moz-column-gap:0 !important;column-gap:0 !important}.column-gap-lg-1{-moz-column-gap:.25rem !important;column-gap:.25rem !important}.column-gap-lg-2{-moz-column-gap:.5rem !important;column-gap:.5rem !important}.column-gap-lg-3{-moz-column-gap:1rem !important;column-gap:1rem !important}.column-gap-lg-4{-moz-column-gap:1.5rem !important;column-gap:1.5rem !important}.column-gap-lg-5{-moz-column-gap:3rem !important;column-gap:3rem !important}.text-lg-start{text-align:left !important}.text-lg-end{text-align:right !important}.text-lg-center{text-align:center !important}}@media(min-width: 1200px){.float-xl-start{float:left !important}.float-xl-end{float:right !important}.float-xl-none{float:none !important}.object-fit-xl-contain{-o-object-fit:contain !important;object-fit:contain !important}.object-fit-xl-cover{-o-object-fit:cover !important;object-fit:cover !important}.object-fit-xl-fill{-o-object-fit:fill !important;object-fit:fill !important}.object-fit-xl-scale{-o-object-fit:scale-down !important;object-fit:scale-down !important}.object-fit-xl-none{-o-object-fit:none !important;object-fit:none !important}.d-xl-inline{display:inline !important}.d-xl-inline-block{display:inline-block !important}.d-xl-block{display:block !important}.d-xl-grid{display:grid !important}.d-xl-inline-grid{display:inline-grid !important}.d-xl-table{display:table !important}.d-xl-table-row{display:table-row !important}.d-xl-table-cell{display:table-cell !important}.d-xl-flex{display:flex !important}.d-xl-inline-flex{display:inline-flex !important}.d-xl-none{display:none !important}.flex-xl-fill{flex:1 1 auto !important}.flex-xl-row{flex-direction:row !important}.flex-xl-column{flex-direction:column !important}.flex-xl-row-reverse{flex-direction:row-reverse !important}.flex-xl-column-reverse{flex-direction:column-reverse !important}.flex-xl-grow-0{flex-grow:0 !important}.flex-xl-grow-1{flex-grow:1 !important}.flex-xl-shrink-0{flex-shrink:0 !important}.flex-xl-shrink-1{flex-shrink:1 !important}.flex-xl-wrap{flex-wrap:wrap !important}.flex-xl-nowrap{flex-wrap:nowrap !important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse !important}.justify-content-xl-start{justify-content:flex-start !important}.justify-content-xl-end{justify-content:flex-end !important}.justify-content-xl-center{justify-content:center !important}.justify-content-xl-between{justify-content:space-between !important}.justify-content-xl-around{justify-content:space-around !important}.justify-content-xl-evenly{justify-content:space-evenly !important}.align-items-xl-start{align-items:flex-start !important}.align-items-xl-end{align-items:flex-end !important}.align-items-xl-center{align-items:center !important}.align-items-xl-baseline{align-items:baseline !important}.align-items-xl-stretch{align-items:stretch !important}.align-content-xl-start{align-content:flex-start !important}.align-content-xl-end{align-content:flex-end !important}.align-content-xl-center{align-content:center !important}.align-content-xl-between{align-content:space-between !important}.align-content-xl-around{align-content:space-around !important}.align-content-xl-stretch{align-content:stretch !important}.align-self-xl-auto{align-self:auto !important}.align-self-xl-start{align-self:flex-start !important}.align-self-xl-end{align-self:flex-end !important}.align-self-xl-center{align-self:center !important}.align-self-xl-baseline{align-self:baseline !important}.align-self-xl-stretch{align-self:stretch !important}.order-xl-first{order:-1 !important}.order-xl-0{order:0 !important}.order-xl-1{order:1 !important}.order-xl-2{order:2 !important}.order-xl-3{order:3 !important}.order-xl-4{order:4 !important}.order-xl-5{order:5 !important}.order-xl-last{order:6 !important}.m-xl-0{margin:0 !important}.m-xl-1{margin:.25rem !important}.m-xl-2{margin:.5rem !important}.m-xl-3{margin:1rem !important}.m-xl-4{margin:1.5rem !important}.m-xl-5{margin:3rem !important}.m-xl-auto{margin:auto !important}.mx-xl-0{margin-right:0 !important;margin-left:0 !important}.mx-xl-1{margin-right:.25rem !important;margin-left:.25rem !important}.mx-xl-2{margin-right:.5rem !important;margin-left:.5rem !important}.mx-xl-3{margin-right:1rem !important;margin-left:1rem !important}.mx-xl-4{margin-right:1.5rem !important;margin-left:1.5rem !important}.mx-xl-5{margin-right:3rem !important;margin-left:3rem !important}.mx-xl-auto{margin-right:auto !important;margin-left:auto !important}.my-xl-0{margin-top:0 !important;margin-bottom:0 !important}.my-xl-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.my-xl-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.my-xl-3{margin-top:1rem !important;margin-bottom:1rem !important}.my-xl-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.my-xl-5{margin-top:3rem !important;margin-bottom:3rem !important}.my-xl-auto{margin-top:auto !important;margin-bottom:auto !important}.mt-xl-0{margin-top:0 !important}.mt-xl-1{margin-top:.25rem !important}.mt-xl-2{margin-top:.5rem !important}.mt-xl-3{margin-top:1rem !important}.mt-xl-4{margin-top:1.5rem !important}.mt-xl-5{margin-top:3rem !important}.mt-xl-auto{margin-top:auto !important}.me-xl-0{margin-right:0 !important}.me-xl-1{margin-right:.25rem !important}.me-xl-2{margin-right:.5rem !important}.me-xl-3{margin-right:1rem !important}.me-xl-4{margin-right:1.5rem !important}.me-xl-5{margin-right:3rem !important}.me-xl-auto{margin-right:auto !important}.mb-xl-0{margin-bottom:0 !important}.mb-xl-1{margin-bottom:.25rem !important}.mb-xl-2{margin-bottom:.5rem !important}.mb-xl-3{margin-bottom:1rem !important}.mb-xl-4{margin-bottom:1.5rem !important}.mb-xl-5{margin-bottom:3rem !important}.mb-xl-auto{margin-bottom:auto !important}.ms-xl-0{margin-left:0 !important}.ms-xl-1{margin-left:.25rem !important}.ms-xl-2{margin-left:.5rem !important}.ms-xl-3{margin-left:1rem !important}.ms-xl-4{margin-left:1.5rem !important}.ms-xl-5{margin-left:3rem !important}.ms-xl-auto{margin-left:auto !important}.p-xl-0{padding:0 !important}.p-xl-1{padding:.25rem !important}.p-xl-2{padding:.5rem !important}.p-xl-3{padding:1rem !important}.p-xl-4{padding:1.5rem !important}.p-xl-5{padding:3rem !important}.px-xl-0{padding-right:0 !important;padding-left:0 !important}.px-xl-1{padding-right:.25rem !important;padding-left:.25rem !important}.px-xl-2{padding-right:.5rem !important;padding-left:.5rem !important}.px-xl-3{padding-right:1rem !important;padding-left:1rem !important}.px-xl-4{padding-right:1.5rem !important;padding-left:1.5rem !important}.px-xl-5{padding-right:3rem !important;padding-left:3rem !important}.py-xl-0{padding-top:0 !important;padding-bottom:0 !important}.py-xl-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.py-xl-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.py-xl-3{padding-top:1rem !important;padding-bottom:1rem !important}.py-xl-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.py-xl-5{padding-top:3rem !important;padding-bottom:3rem !important}.pt-xl-0{padding-top:0 !important}.pt-xl-1{padding-top:.25rem !important}.pt-xl-2{padding-top:.5rem !important}.pt-xl-3{padding-top:1rem !important}.pt-xl-4{padding-top:1.5rem !important}.pt-xl-5{padding-top:3rem !important}.pe-xl-0{padding-right:0 !important}.pe-xl-1{padding-right:.25rem !important}.pe-xl-2{padding-right:.5rem !important}.pe-xl-3{padding-right:1rem !important}.pe-xl-4{padding-right:1.5rem !important}.pe-xl-5{padding-right:3rem !important}.pb-xl-0{padding-bottom:0 !important}.pb-xl-1{padding-bottom:.25rem !important}.pb-xl-2{padding-bottom:.5rem !important}.pb-xl-3{padding-bottom:1rem !important}.pb-xl-4{padding-bottom:1.5rem !important}.pb-xl-5{padding-bottom:3rem !important}.ps-xl-0{padding-left:0 !important}.ps-xl-1{padding-left:.25rem !important}.ps-xl-2{padding-left:.5rem !important}.ps-xl-3{padding-left:1rem !important}.ps-xl-4{padding-left:1.5rem !important}.ps-xl-5{padding-left:3rem !important}.gap-xl-0{gap:0 !important}.gap-xl-1{gap:.25rem !important}.gap-xl-2{gap:.5rem !important}.gap-xl-3{gap:1rem !important}.gap-xl-4{gap:1.5rem !important}.gap-xl-5{gap:3rem !important}.row-gap-xl-0{row-gap:0 !important}.row-gap-xl-1{row-gap:.25rem !important}.row-gap-xl-2{row-gap:.5rem !important}.row-gap-xl-3{row-gap:1rem !important}.row-gap-xl-4{row-gap:1.5rem !important}.row-gap-xl-5{row-gap:3rem !important}.column-gap-xl-0{-moz-column-gap:0 !important;column-gap:0 !important}.column-gap-xl-1{-moz-column-gap:.25rem !important;column-gap:.25rem !important}.column-gap-xl-2{-moz-column-gap:.5rem !important;column-gap:.5rem !important}.column-gap-xl-3{-moz-column-gap:1rem !important;column-gap:1rem !important}.column-gap-xl-4{-moz-column-gap:1.5rem !important;column-gap:1.5rem !important}.column-gap-xl-5{-moz-column-gap:3rem !important;column-gap:3rem !important}.text-xl-start{text-align:left !important}.text-xl-end{text-align:right !important}.text-xl-center{text-align:center !important}}@media(min-width: 1400px){.float-xxl-start{float:left !important}.float-xxl-end{float:right !important}.float-xxl-none{float:none !important}.object-fit-xxl-contain{-o-object-fit:contain !important;object-fit:contain !important}.object-fit-xxl-cover{-o-object-fit:cover !important;object-fit:cover !important}.object-fit-xxl-fill{-o-object-fit:fill !important;object-fit:fill !important}.object-fit-xxl-scale{-o-object-fit:scale-down !important;object-fit:scale-down !important}.object-fit-xxl-none{-o-object-fit:none !important;object-fit:none !important}.d-xxl-inline{display:inline !important}.d-xxl-inline-block{display:inline-block !important}.d-xxl-block{display:block !important}.d-xxl-grid{display:grid !important}.d-xxl-inline-grid{display:inline-grid !important}.d-xxl-table{display:table !important}.d-xxl-table-row{display:table-row !important}.d-xxl-table-cell{display:table-cell !important}.d-xxl-flex{display:flex !important}.d-xxl-inline-flex{display:inline-flex !important}.d-xxl-none{display:none !important}.flex-xxl-fill{flex:1 1 auto !important}.flex-xxl-row{flex-direction:row !important}.flex-xxl-column{flex-direction:column !important}.flex-xxl-row-reverse{flex-direction:row-reverse !important}.flex-xxl-column-reverse{flex-direction:column-reverse !important}.flex-xxl-grow-0{flex-grow:0 !important}.flex-xxl-grow-1{flex-grow:1 !important}.flex-xxl-shrink-0{flex-shrink:0 !important}.flex-xxl-shrink-1{flex-shrink:1 !important}.flex-xxl-wrap{flex-wrap:wrap !important}.flex-xxl-nowrap{flex-wrap:nowrap !important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse !important}.justify-content-xxl-start{justify-content:flex-start !important}.justify-content-xxl-end{justify-content:flex-end !important}.justify-content-xxl-center{justify-content:center !important}.justify-content-xxl-between{justify-content:space-between !important}.justify-content-xxl-around{justify-content:space-around !important}.justify-content-xxl-evenly{justify-content:space-evenly !important}.align-items-xxl-start{align-items:flex-start !important}.align-items-xxl-end{align-items:flex-end !important}.align-items-xxl-center{align-items:center !important}.align-items-xxl-baseline{align-items:baseline !important}.align-items-xxl-stretch{align-items:stretch !important}.align-content-xxl-start{align-content:flex-start !important}.align-content-xxl-end{align-content:flex-end !important}.align-content-xxl-center{align-content:center !important}.align-content-xxl-between{align-content:space-between !important}.align-content-xxl-around{align-content:space-around !important}.align-content-xxl-stretch{align-content:stretch !important}.align-self-xxl-auto{align-self:auto !important}.align-self-xxl-start{align-self:flex-start !important}.align-self-xxl-end{align-self:flex-end !important}.align-self-xxl-center{align-self:center !important}.align-self-xxl-baseline{align-self:baseline !important}.align-self-xxl-stretch{align-self:stretch !important}.order-xxl-first{order:-1 !important}.order-xxl-0{order:0 !important}.order-xxl-1{order:1 !important}.order-xxl-2{order:2 !important}.order-xxl-3{order:3 !important}.order-xxl-4{order:4 !important}.order-xxl-5{order:5 !important}.order-xxl-last{order:6 !important}.m-xxl-0{margin:0 !important}.m-xxl-1{margin:.25rem !important}.m-xxl-2{margin:.5rem !important}.m-xxl-3{margin:1rem !important}.m-xxl-4{margin:1.5rem !important}.m-xxl-5{margin:3rem !important}.m-xxl-auto{margin:auto !important}.mx-xxl-0{margin-right:0 !important;margin-left:0 !important}.mx-xxl-1{margin-right:.25rem !important;margin-left:.25rem !important}.mx-xxl-2{margin-right:.5rem !important;margin-left:.5rem !important}.mx-xxl-3{margin-right:1rem !important;margin-left:1rem !important}.mx-xxl-4{margin-right:1.5rem !important;margin-left:1.5rem !important}.mx-xxl-5{margin-right:3rem !important;margin-left:3rem !important}.mx-xxl-auto{margin-right:auto !important;margin-left:auto !important}.my-xxl-0{margin-top:0 !important;margin-bottom:0 !important}.my-xxl-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.my-xxl-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.my-xxl-3{margin-top:1rem !important;margin-bottom:1rem !important}.my-xxl-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.my-xxl-5{margin-top:3rem !important;margin-bottom:3rem !important}.my-xxl-auto{margin-top:auto !important;margin-bottom:auto !important}.mt-xxl-0{margin-top:0 !important}.mt-xxl-1{margin-top:.25rem !important}.mt-xxl-2{margin-top:.5rem !important}.mt-xxl-3{margin-top:1rem !important}.mt-xxl-4{margin-top:1.5rem !important}.mt-xxl-5{margin-top:3rem !important}.mt-xxl-auto{margin-top:auto !important}.me-xxl-0{margin-right:0 !important}.me-xxl-1{margin-right:.25rem !important}.me-xxl-2{margin-right:.5rem !important}.me-xxl-3{margin-right:1rem !important}.me-xxl-4{margin-right:1.5rem !important}.me-xxl-5{margin-right:3rem !important}.me-xxl-auto{margin-right:auto !important}.mb-xxl-0{margin-bottom:0 !important}.mb-xxl-1{margin-bottom:.25rem !important}.mb-xxl-2{margin-bottom:.5rem !important}.mb-xxl-3{margin-bottom:1rem !important}.mb-xxl-4{margin-bottom:1.5rem !important}.mb-xxl-5{margin-bottom:3rem !important}.mb-xxl-auto{margin-bottom:auto !important}.ms-xxl-0{margin-left:0 !important}.ms-xxl-1{margin-left:.25rem !important}.ms-xxl-2{margin-left:.5rem !important}.ms-xxl-3{margin-left:1rem !important}.ms-xxl-4{margin-left:1.5rem !important}.ms-xxl-5{margin-left:3rem !important}.ms-xxl-auto{margin-left:auto !important}.p-xxl-0{padding:0 !important}.p-xxl-1{padding:.25rem !important}.p-xxl-2{padding:.5rem !important}.p-xxl-3{padding:1rem !important}.p-xxl-4{padding:1.5rem !important}.p-xxl-5{padding:3rem !important}.px-xxl-0{padding-right:0 !important;padding-left:0 !important}.px-xxl-1{padding-right:.25rem !important;padding-left:.25rem !important}.px-xxl-2{padding-right:.5rem !important;padding-left:.5rem !important}.px-xxl-3{padding-right:1rem !important;padding-left:1rem !important}.px-xxl-4{padding-right:1.5rem !important;padding-left:1.5rem !important}.px-xxl-5{padding-right:3rem !important;padding-left:3rem !important}.py-xxl-0{padding-top:0 !important;padding-bottom:0 !important}.py-xxl-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.py-xxl-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.py-xxl-3{padding-top:1rem !important;padding-bottom:1rem !important}.py-xxl-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.py-xxl-5{padding-top:3rem !important;padding-bottom:3rem !important}.pt-xxl-0{padding-top:0 !important}.pt-xxl-1{padding-top:.25rem !important}.pt-xxl-2{padding-top:.5rem !important}.pt-xxl-3{padding-top:1rem !important}.pt-xxl-4{padding-top:1.5rem !important}.pt-xxl-5{padding-top:3rem !important}.pe-xxl-0{padding-right:0 !important}.pe-xxl-1{padding-right:.25rem !important}.pe-xxl-2{padding-right:.5rem !important}.pe-xxl-3{padding-right:1rem !important}.pe-xxl-4{padding-right:1.5rem !important}.pe-xxl-5{padding-right:3rem !important}.pb-xxl-0{padding-bottom:0 !important}.pb-xxl-1{padding-bottom:.25rem !important}.pb-xxl-2{padding-bottom:.5rem !important}.pb-xxl-3{padding-bottom:1rem !important}.pb-xxl-4{padding-bottom:1.5rem !important}.pb-xxl-5{padding-bottom:3rem !important}.ps-xxl-0{padding-left:0 !important}.ps-xxl-1{padding-left:.25rem !important}.ps-xxl-2{padding-left:.5rem !important}.ps-xxl-3{padding-left:1rem !important}.ps-xxl-4{padding-left:1.5rem !important}.ps-xxl-5{padding-left:3rem !important}.gap-xxl-0{gap:0 !important}.gap-xxl-1{gap:.25rem !important}.gap-xxl-2{gap:.5rem !important}.gap-xxl-3{gap:1rem !important}.gap-xxl-4{gap:1.5rem !important}.gap-xxl-5{gap:3rem !important}.row-gap-xxl-0{row-gap:0 !important}.row-gap-xxl-1{row-gap:.25rem !important}.row-gap-xxl-2{row-gap:.5rem !important}.row-gap-xxl-3{row-gap:1rem !important}.row-gap-xxl-4{row-gap:1.5rem !important}.row-gap-xxl-5{row-gap:3rem !important}.column-gap-xxl-0{-moz-column-gap:0 !important;column-gap:0 !important}.column-gap-xxl-1{-moz-column-gap:.25rem !important;column-gap:.25rem !important}.column-gap-xxl-2{-moz-column-gap:.5rem !important;column-gap:.5rem !important}.column-gap-xxl-3{-moz-column-gap:1rem !important;column-gap:1rem !important}.column-gap-xxl-4{-moz-column-gap:1.5rem !important;column-gap:1.5rem !important}.column-gap-xxl-5{-moz-column-gap:3rem !important;column-gap:3rem !important}.text-xxl-start{text-align:left !important}.text-xxl-end{text-align:right !important}.text-xxl-center{text-align:center !important}}@media(min-width: 1200px){.fs-1{font-size:2.1875rem !important}.fs-2{font-size:1.75rem !important}.fs-3{font-size:1.53125rem !important}.fs-4{font-size:1.3125rem !important}}@media print{.d-print-inline{display:inline !important}.d-print-inline-block{display:inline-block !important}.d-print-block{display:block !important}.d-print-grid{display:grid !important}.d-print-inline-grid{display:inline-grid !important}.d-print-table{display:table !important}.d-print-table-row{display:table-row !important}.d-print-table-cell{display:table-cell !important}.d-print-flex{display:flex !important}.d-print-inline-flex{display:inline-flex !important}.d-print-none{display:none !important}}:root,[data-bs-theme=dark]{--elevation-0: 0 1px 1px 0 rgba(20, 23, 33, 0.8784313725), 0 0 1px 0 rgba(20, 23, 33, 0.8784313725);--elevation-0-inverted: 0 1px 1px 0 rgba(20, 23, 33, 0.0784313725), 0 0 1px 0 rgba(20, 23, 33, 0.3803921569);--elevation-1: 0 2px 4px -1px rgba(20, 23, 33, 0.3803921569), 0 1px 2px -1px rgba(20, 23, 33, 0.8784313725);--elevation-1-inverted: 0 2px 4px 1px rgba(20, 23, 33, 0.1215686275), 0 1px 2px 0 rgba(20, 23, 33, 0.1215686275);--elevation-2: 0 4px 8px 0 rgba(20, 23, 33, 0.3803921569), 0 2px 4px -1px rgba(20, 23, 33, 0.8784313725);--elevation-2-inverted: 0 4px 8px 0 rgba(20, 23, 33, 0.1215686275), 0 2px 4px -1px rgba(20, 23, 33, 0.0784313725);--elevation-3: 0 8px 12px 1px rgba(20, 23, 33, 0.3803921569), 0 4px 8px -1px rgba(20, 23, 33, 0.8784313725);--elevation-3-inverted: 0 8px 12px 1px rgba(20, 23, 33, 0.1215686275), 0 4px 8px -1px rgba(20, 23, 33, 0.0784313725);--elevation-4: 0 16px 32px 2px rgba(20, 23, 33, 0.3803921569), 0 8px 16px -2px rgba(20, 23, 33, 0.8784313725);--elevation-4-inverted: 0 16px 32px 2px rgba(20, 23, 33, 0.1215686275), 0 8px 16px -2px rgba(20, 23, 33, 0.0784313725);--primary-50: #373a44;--primary-50-inverted: white;--primary-100: #333640;--primary-100-inverted: #fafafb;--primary-200: #2f323c;--primary-200-inverted: #f5f6f6;--primary-300: #2b2e39;--primary-300-inverted: #f2f3f4;--primary-400: #272a35;--primary-400-inverted: #ebedee;--primary-500: #232632;--primary-500-inverted: #e6e8ea;--primary-600: #1f222e;--primary-600-inverted: #e2e4e6;--primary-700: #1b1e2a;--primary-700-inverted: #dddfe1;--primary-800: #181b26;--primary-800-inverted: #d8dadd;--primary-900: #141721;--primary-900-inverted: #d3d6d9;--text-placeholder: rgba(255, 255, 255, 0.3803921569);--text-placeholder-inverted: rgba(20, 23, 33, 0.3803921569);--text-secondary: rgba(255, 255, 255, 0.6);--text-secondary-inverted: rgba(20, 23, 33, 0.6);--text-primary: rgba(255, 255, 255, 0.8784313725);--text-primary-inverted: rgba(20, 23, 33, 0.8784313725);--text-primaryHover: white;--text-primaryHover-inverted: #141721;--text-disabled: rgba(255, 255, 255, 0.3019607843);--text-disabled-inverted: rgba(20, 23, 33, 0.3019607843);--fill-subtle: rgba(255, 255, 255, 0.1019607843);--fill-subtle-inverted: rgba(20, 23, 33, 0.1019607843);--fill-secondary: rgba(255, 255, 255, 0.6);--fill-secondary-inverted: rgba(20, 23, 33, 0.6);--fill-primary: rgba(255, 255, 255, 0.8784313725);--fill-primary-inverted: rgba(20, 23, 33, 0.8784313725);--fill-enabled: rgba(255, 255, 255, 0.6);--fill-enabled-inverted: rgba(20, 23, 33, 0.6);--fill-active: rgba(255, 255, 255, 0.8784313725);--fill-active-inverted: rgba(20, 23, 33, 0.8784313725);--fill-hoverSelected: white;--fill-hoverSelected-inverted: #141721;--fill-disabled: rgba(255, 255, 255, 0.3019607843);--fill-disabled-inverted: rgba(20, 23, 33, 0.3019607843);--border-subtleAlpha01: rgba(255, 255, 255, 0.1019607843);--border-subtleAlpha01-inverted: rgba(20, 23, 33, 0.1019607843);--border-subtleAlpha02: rgba(255, 255, 255, 0.1607843137);--border-subtleAlpha02-inverted: rgba(20, 23, 33, 0.1607843137);--border-subtleAlpha03: rgba(255, 255, 255, 0.2392156863);--border-subtleAlpha03-inverted: rgba(20, 23, 33, 0.2392156863);--border-enabled: rgba(255, 255, 255, 0.6);--border-enabled-inverted: rgba(20, 23, 33, 0.6);--border-hover: rgba(255, 255, 255, 0.8784313725);--border-hover-inverted: rgba(20, 23, 33, 0.8784313725);--border-disabled: rgba(255, 255, 255, 0.3019607843);--border-disabled-inverted: rgba(20, 23, 33, 0.3019607843);--border-selected: white;--border-selected-inverted: #141721;--border-selectedInverse: #141721;--border-selectedInverse-inverted: white;--stateOverlays-enabled: rgba(255, 255, 255, 0);--stateOverlays-enabled-inverted: rgba(20, 23, 33, 0);--stateOverlays-hover: rgba(255, 255, 255, 0.0392156863);--stateOverlays-hover-inverted: rgba(20, 23, 33, 0.0588235294);--stateOverlays-active: rgba(255, 255, 255, 0.0784313725);--stateOverlays-active-inverted: rgba(20, 23, 33, 0.1215686275);--stateOverlays-disabled: rgba(4, 19, 31, 0.1607843137);--stateOverlays-disabled-inverted: rgba(20, 23, 33, 0.1607843137);--stateOverlays-selected: #2b2e39;--stateOverlays-selected-inverted: #fafafb;--stateOverlays-selectedHover: rgba(255, 255, 255, 0.1607843137);--stateOverlays-selectedHover-inverted: rgba(20, 23, 33, 0.2392156863);--stateOverlays-selectedInverse: white;--stateOverlays-selectedInverse-inverted: #141721;--stateOverlays-selectedRange: rgba(255, 255, 255, 0.1019607843);--stateOverlays-selectedRange-inverted: rgba(20, 23, 33, 0.0784313725);--field-enabled: #2b2e39;--field-enabled-inverted: #fafafb;--field-hover: #373a44;--field-hover-inverted: white;--field-disabled: #272a35;--field-disabled-inverted: #f0f1f2;--status-success: #40d86e;--status-success-inverted: #26bf56;--status-error: #f56565;--status-error-inverted: #f03b3a;--status-information: #00b4ff;--status-information-inverted: #009eff;--status-warning: #ffc107;--status-warning-inverted: #f17c02;--focus: rgba(0, 133, 255, 0.6);--focus-inverted: rgba(0, 133, 255, 0.6);--surfaces-bg01: #232632;--surfaces-bg01-inverted: white;--surfaces-bg02: #1b1e2a;--surfaces-bg02-inverted: #f5f6f6;--surfaces-bg03: #141721;--surfaces-bg03-inverted: #e6e8ea;--categorical-01Cyan: #00b4ff;--categorical-01Cyan-inverted: #00b4ff;--categorical-02Orange: #ff9222;--categorical-02Orange-inverted: #ff9222;--categorical-03Purple: #3949ab;--categorical-03Purple-inverted: #3949ab;--categorical-04Red: #ff5267;--categorical-04Red-inverted: #ff5267;--categorical-05Teal: #08bdba;--categorical-05Teal-inverted: #08bdba;--categorical-06Amber: #fdc935;--categorical-06Amber-inverted: #fdc935;--categorical-07Green: #689f38;--categorical-07Green-inverted: #689f38;--categorical-08Purple: #976fd1;--categorical-08Purple-inverted: #976fd1;--categorical-09Pink: #f781bf;--categorical-09Pink-inverted: #f781bf;--categorical-10DarkGreen: #52733e;--categorical-10DarkGreen-inverted: #52733e;--sequentialCyan-100: #afe7f9;--sequentialCyan-100-inverted: #afe7f9;--sequentialCyan-200: #8bd0f6;--sequentialCyan-200-inverted: #8bd0f6;--sequentialCyan-300: #6cbaec;--sequentialCyan-300-inverted: #6cbaec;--sequentialCyan-400: #52a3dd;--sequentialCyan-400-inverted: #52a3dd;--sequentialCyan-500: #3b8dcb;--sequentialCyan-500-inverted: #3b8dcb;--sequentialCyan-600: #2777b7;--sequentialCyan-600-inverted: #2777b7;--sequentialCyan-700: #1661a2;--sequentialCyan-700-inverted: #1661a2;--sequentialCyan-800: #074c8c;--sequentialCyan-800-inverted: #074c8c;--sequentialCyan-900: #003875;--sequentialCyan-900-inverted: #003875;--sequentialOrange-100: #f9d8ac;--sequentialOrange-100-inverted: #f9d8ac;--sequentialOrange-200: #feb85b;--sequentialOrange-200-inverted: #feb85b;--sequentialOrange-300: #f09b32;--sequentialOrange-300-inverted: #f09b32;--sequentialOrange-400: #db811e;--sequentialOrange-400-inverted: #db811e;--sequentialOrange-500: #c76809;--sequentialOrange-500-inverted: #c76809;--sequentialOrange-600: #b05000;--sequentialOrange-600-inverted: #b05000;--sequentialOrange-700: #973a00;--sequentialOrange-700-inverted: #973a00;--sequentialOrange-800: #7e2400;--sequentialOrange-800-inverted: #7e2400;--sequentialOrange-900: #640d00;--sequentialOrange-900-inverted: #640d00;--sequentialIndigo-100: #dfd8fa;--sequentialIndigo-100-inverted: #dfd8fa;--sequentialIndigo-200: #c3c1ed;--sequentialIndigo-200-inverted: #c3c1ed;--sequentialIndigo-300: #aba8e0;--sequentialIndigo-300-inverted: #aba8e0;--sequentialIndigo-400: #9390d2;--sequentialIndigo-400-inverted: #9390d2;--sequentialIndigo-500: #7a79c4;--sequentialIndigo-500-inverted: #7a79c4;--sequentialIndigo-600: #6163b5;--sequentialIndigo-600-inverted: #6163b5;--sequentialIndigo-700: #474ea6;--sequentialIndigo-700-inverted: #474ea6;--sequentialIndigo-800: #2a3994;--sequentialIndigo-800-inverted: #2a3994;--sequentialIndigo-900: #002680;--sequentialIndigo-900-inverted: #002680;--sequentialYellow-100: #fff7cd;--sequentialYellow-100-inverted: #fff7cd;--sequentialYellow-200: #ffed9b;--sequentialYellow-200-inverted: #ffed9b;--sequentialYellow-300: #ffe16a;--sequentialYellow-300-inverted: #ffe16a;--sequentialYellow-400: #ffd545;--sequentialYellow-400-inverted: #ffd545;--sequentialYellow-500: #ffc107;--sequentialYellow-500-inverted: #ffc107;--sequentialYellow-600: #dba005;--sequentialYellow-600-inverted: #dba005;--sequentialYellow-700: #b78103;--sequentialYellow-700-inverted: #b78103;--sequentialYellow-800: #936402;--sequentialYellow-800-inverted: #936402;--sequentialYellow-900: #7a4f01;--sequentialYellow-900-inverted: #7a4f01;--sequentialTeal-100: #a5eae8;--sequentialTeal-100-inverted: #a5eae8;--sequentialTeal-200: #7dd5d3;--sequentialTeal-200-inverted: #7dd5d3;--sequentialTeal-300: #5ebfbc;--sequentialTeal-300-inverted: #5ebfbc;--sequentialTeal-400: #44a8a6;--sequentialTeal-400-inverted: #44a8a6;--sequentialTeal-500: #2e9190;--sequentialTeal-500-inverted: #2e9190;--sequentialTeal-600: #1b7b7a;--sequentialTeal-600-inverted: #1b7b7a;--sequentialTeal-700: #0c6565;--sequentialTeal-700-inverted: #0c6565;--sequentialTeal-800: #025050;--sequentialTeal-800-inverted: #025050;--sequentialTeal-900: #003b3c;--sequentialTeal-900-inverted: #003b3c;--sequentialRed-100: #f8d6da;--sequentialRed-100-inverted: #f8d6da;--sequentialRed-200: #fcb6ba;--sequentialRed-200-inverted: #fcb6ba;--sequentialRed-300: #f8989b;--sequentialRed-300-inverted: #f8989b;--sequentialRed-400: #ed7b7f;--sequentialRed-400-inverted: #ed7b7f;--sequentialRed-500: #dd6065;--sequentialRed-500-inverted: #dd6065;--sequentialRed-600: #c9474c;--sequentialRed-600-inverted: #c9474c;--sequentialRed-700: #b22f36;--sequentialRed-700-inverted: #b22f36;--sequentialRed-800: #981822;--sequentialRed-800-inverted: #981822;--sequentialRed-900: #7d000f;--sequentialRed-900-inverted: #7d000f;--surfaces-bg-card: #232632;--surfaces-bg-card-inverted: #F5F6F6;--bs-primary-rgb: #ffffffe0;--bs-secondary-color: #ffffff99;--bs-nav-link-color: #ffffff99;--bs-form-valid-color: #40d86eff;--bs-form-invalid-color: #f56565ff;--bs-body-bg: #141721}[data-bs-theme=light]{--elevation-0: 0 1px 1px 0 rgba(20, 23, 33, 0.0784313725), 0 0 1px 0 rgba(20, 23, 33, 0.3803921569);--elevation-0-inverted: 0 1px 1px 0 rgba(20, 23, 33, 0.8784313725), 0 0 1px 0 rgba(20, 23, 33, 0.8784313725);--elevation-1: 0 2px 4px 1px rgba(20, 23, 33, 0.1215686275), 0 1px 2px 0 rgba(20, 23, 33, 0.1215686275);--elevation-1-inverted: 0 2px 4px -1px rgba(20, 23, 33, 0.3803921569), 0 1px 2px -1px rgba(20, 23, 33, 0.8784313725);--elevation-2: 0 4px 8px 0 rgba(20, 23, 33, 0.1215686275), 0 2px 4px -1px rgba(20, 23, 33, 0.0784313725);--elevation-2-inverted: 0 4px 8px 0 rgba(20, 23, 33, 0.3803921569), 0 2px 4px -1px rgba(20, 23, 33, 0.8784313725);--elevation-3: 0 8px 12px 1px rgba(20, 23, 33, 0.1215686275), 0 4px 8px -1px rgba(20, 23, 33, 0.0784313725);--elevation-3-inverted: 0 8px 12px 1px rgba(20, 23, 33, 0.3803921569), 0 4px 8px -1px rgba(20, 23, 33, 0.8784313725);--elevation-4: 0 16px 32px 2px rgba(20, 23, 33, 0.1215686275), 0 8px 16px -2px rgba(20, 23, 33, 0.0784313725);--elevation-4-inverted: 0 16px 32px 2px rgba(20, 23, 33, 0.3803921569), 0 8px 16px -2px rgba(20, 23, 33, 0.8784313725);--primary-50: white;--primary-50-inverted: #373a44;--primary-100: #fafafb;--primary-100-inverted: #333640;--primary-200: #f5f6f6;--primary-200-inverted: #2f323c;--primary-300: #f2f3f4;--primary-300-inverted: #2b2e39;--primary-400: #ebedee;--primary-400-inverted: #272a35;--primary-500: #e6e8ea;--primary-500-inverted: #232632;--primary-600: #e2e4e6;--primary-600-inverted: #1f222e;--primary-700: #dddfe1;--primary-700-inverted: #1b1e2a;--primary-800: #d8dadd;--primary-800-inverted: #181b26;--primary-900: #d3d6d9;--primary-900-inverted: #141721;--text-placeholder: rgba(20, 23, 33, 0.3803921569);--text-placeholder-inverted: rgba(255, 255, 255, 0.3803921569);--text-secondary: rgba(20, 23, 33, 0.6);--text-secondary-inverted: rgba(255, 255, 255, 0.6);--text-primary: rgba(20, 23, 33, 0.8784313725);--text-primary-inverted: rgba(255, 255, 255, 0.8784313725);--text-primaryHover: #141721;--text-primaryHover-inverted: white;--text-disabled: rgba(20, 23, 33, 0.3019607843);--text-disabled-inverted: rgba(255, 255, 255, 0.3019607843);--fill-subtle: rgba(20, 23, 33, 0.1019607843);--fill-subtle-inverted: rgba(255, 255, 255, 0.1019607843);--fill-secondary: rgba(20, 23, 33, 0.6);--fill-secondary-inverted: rgba(255, 255, 255, 0.6);--fill-primary: rgba(20, 23, 33, 0.8784313725);--fill-primary-inverted: rgba(255, 255, 255, 0.8784313725);--fill-enabled: rgba(20, 23, 33, 0.6);--fill-enabled-inverted: rgba(255, 255, 255, 0.6);--fill-active: rgba(20, 23, 33, 0.8784313725);--fill-active-inverted: rgba(255, 255, 255, 0.8784313725);--fill-hoverSelected: #141721;--fill-hoverSelected-inverted: white;--fill-disabled: rgba(20, 23, 33, 0.3019607843);--fill-disabled-inverted: rgba(255, 255, 255, 0.3019607843);--border-subtleAlpha01: rgba(20, 23, 33, 0.1019607843);--border-subtleAlpha01-inverted: rgba(255, 255, 255, 0.1019607843);--border-subtleAlpha02: rgba(20, 23, 33, 0.1607843137);--border-subtleAlpha02-inverted: rgba(255, 255, 255, 0.1607843137);--border-subtleAlpha03: rgba(20, 23, 33, 0.2392156863);--border-subtleAlpha03-inverted: rgba(255, 255, 255, 0.2392156863);--border-enabled: rgba(20, 23, 33, 0.6);--border-enabled-inverted: rgba(255, 255, 255, 0.6);--border-hover: rgba(20, 23, 33, 0.8784313725);--border-hover-inverted: rgba(255, 255, 255, 0.8784313725);--border-disabled: rgba(20, 23, 33, 0.3019607843);--border-disabled-inverted: rgba(255, 255, 255, 0.3019607843);--border-selected: #141721;--border-selected-inverted: white;--border-selectedInverse: white;--border-selectedInverse-inverted: #141721;--stateOverlays-enabled: rgba(20, 23, 33, 0);--stateOverlays-enabled-inverted: rgba(255, 255, 255, 0);--stateOverlays-hover: rgba(20, 23, 33, 0.0588235294);--stateOverlays-hover-inverted: rgba(255, 255, 255, 0.0392156863);--stateOverlays-active: rgba(20, 23, 33, 0.1215686275);--stateOverlays-active-inverted: rgba(255, 255, 255, 0.0784313725);--stateOverlays-disabled: rgba(20, 23, 33, 0.1607843137);--stateOverlays-disabled-inverted: rgba(4, 19, 31, 0.1607843137);--stateOverlays-selected: #fafafb;--stateOverlays-selected-inverted: #2b2e39;--stateOverlays-selectedHover: rgba(20, 23, 33, 0.2392156863);--stateOverlays-selectedHover-inverted: rgba(255, 255, 255, 0.1607843137);--stateOverlays-selectedInverse: #141721;--stateOverlays-selectedInverse-inverted: white;--stateOverlays-selectedRange: rgba(20, 23, 33, 0.0784313725);--stateOverlays-selectedRange-inverted: rgba(255, 255, 255, 0.1019607843);--field-enabled: #fafafb;--field-enabled-inverted: #2b2e39;--field-hover: white;--field-hover-inverted: #373a44;--field-disabled: #f0f1f2;--field-disabled-inverted: #272a35;--status-success: #26bf56;--status-success-inverted: #40d86e;--status-error: #f03b3a;--status-error-inverted: #f56565;--status-information: #009eff;--status-information-inverted: #00b4ff;--status-warning: #f17c02;--status-warning-inverted: #ffc107;--focus: rgba(0, 133, 255, 0.6);--focus-inverted: rgba(0, 133, 255, 0.6);--surfaces-bg01: white;--surfaces-bg01-inverted: #232632;--surfaces-bg02: #f5f6f6;--surfaces-bg02-inverted: #1b1e2a;--surfaces-bg03: #e6e8ea;--surfaces-bg03-inverted: #141721;--categorical-01Cyan: #00b4ff;--categorical-01Cyan-inverted: #00b4ff;--categorical-02Orange: #ff9222;--categorical-02Orange-inverted: #ff9222;--categorical-03Purple: #3949ab;--categorical-03Purple-inverted: #3949ab;--categorical-04Red: #ff5267;--categorical-04Red-inverted: #ff5267;--categorical-05Teal: #08bdba;--categorical-05Teal-inverted: #08bdba;--categorical-06Amber: #fdc935;--categorical-06Amber-inverted: #fdc935;--categorical-07Green: #689f38;--categorical-07Green-inverted: #689f38;--categorical-08Purple: #976fd1;--categorical-08Purple-inverted: #976fd1;--categorical-09Pink: #f781bf;--categorical-09Pink-inverted: #f781bf;--categorical-10DarkGreen: #52733e;--categorical-10DarkGreen-inverted: #52733e;--sequentialCyan-100: #afe7f9;--sequentialCyan-100-inverted: #afe7f9;--sequentialCyan-200: #8bd0f6;--sequentialCyan-200-inverted: #8bd0f6;--sequentialCyan-300: #6cbaec;--sequentialCyan-300-inverted: #6cbaec;--sequentialCyan-400: #52a3dd;--sequentialCyan-400-inverted: #52a3dd;--sequentialCyan-500: #3b8dcb;--sequentialCyan-500-inverted: #3b8dcb;--sequentialCyan-600: #2777b7;--sequentialCyan-600-inverted: #2777b7;--sequentialCyan-700: #1661a2;--sequentialCyan-700-inverted: #1661a2;--sequentialCyan-800: #074c8c;--sequentialCyan-800-inverted: #074c8c;--sequentialCyan-900: #003875;--sequentialCyan-900-inverted: #003875;--sequentialOrange-100: #f9d8ac;--sequentialOrange-100-inverted: #f9d8ac;--sequentialOrange-200: #feb85b;--sequentialOrange-200-inverted: #feb85b;--sequentialOrange-300: #f09b32;--sequentialOrange-300-inverted: #f09b32;--sequentialOrange-400: #db811e;--sequentialOrange-400-inverted: #db811e;--sequentialOrange-500: #c76809;--sequentialOrange-500-inverted: #c76809;--sequentialOrange-600: #b05000;--sequentialOrange-600-inverted: #b05000;--sequentialOrange-700: #973a00;--sequentialOrange-700-inverted: #973a00;--sequentialOrange-800: #7e2400;--sequentialOrange-800-inverted: #7e2400;--sequentialOrange-900: #640d00;--sequentialOrange-900-inverted: #640d00;--sequentialIndigo-100: #dfd8fa;--sequentialIndigo-100-inverted: #dfd8fa;--sequentialIndigo-200: #c3c1ed;--sequentialIndigo-200-inverted: #c3c1ed;--sequentialIndigo-300: #aba8e0;--sequentialIndigo-300-inverted: #aba8e0;--sequentialIndigo-400: #9390d2;--sequentialIndigo-400-inverted: #9390d2;--sequentialIndigo-500: #7a79c4;--sequentialIndigo-500-inverted: #7a79c4;--sequentialIndigo-600: #6163b5;--sequentialIndigo-600-inverted: #6163b5;--sequentialIndigo-700: #474ea6;--sequentialIndigo-700-inverted: #474ea6;--sequentialIndigo-800: #2a3994;--sequentialIndigo-800-inverted: #2a3994;--sequentialIndigo-900: #002680;--sequentialIndigo-900-inverted: #002680;--sequentialYellow-100: #fff7cd;--sequentialYellow-100-inverted: #fff7cd;--sequentialYellow-200: #ffed9b;--sequentialYellow-200-inverted: #ffed9b;--sequentialYellow-300: #ffe16a;--sequentialYellow-300-inverted: #ffe16a;--sequentialYellow-400: #ffd545;--sequentialYellow-400-inverted: #ffd545;--sequentialYellow-500: #ffc107;--sequentialYellow-500-inverted: #ffc107;--sequentialYellow-600: #dba005;--sequentialYellow-600-inverted: #dba005;--sequentialYellow-700: #b78103;--sequentialYellow-700-inverted: #b78103;--sequentialYellow-800: #936402;--sequentialYellow-800-inverted: #936402;--sequentialYellow-900: #7a4f01;--sequentialYellow-900-inverted: #7a4f01;--sequentialTeal-100: #a5eae8;--sequentialTeal-100-inverted: #a5eae8;--sequentialTeal-200: #7dd5d3;--sequentialTeal-200-inverted: #7dd5d3;--sequentialTeal-300: #5ebfbc;--sequentialTeal-300-inverted: #5ebfbc;--sequentialTeal-400: #44a8a6;--sequentialTeal-400-inverted: #44a8a6;--sequentialTeal-500: #2e9190;--sequentialTeal-500-inverted: #2e9190;--sequentialTeal-600: #1b7b7a;--sequentialTeal-600-inverted: #1b7b7a;--sequentialTeal-700: #0c6565;--sequentialTeal-700-inverted: #0c6565;--sequentialTeal-800: #025050;--sequentialTeal-800-inverted: #025050;--sequentialTeal-900: #003b3c;--sequentialTeal-900-inverted: #003b3c;--sequentialRed-100: #f8d6da;--sequentialRed-100-inverted: #f8d6da;--sequentialRed-200: #fcb6ba;--sequentialRed-200-inverted: #fcb6ba;--sequentialRed-300: #f8989b;--sequentialRed-300-inverted: #f8989b;--sequentialRed-400: #ed7b7f;--sequentialRed-400-inverted: #ed7b7f;--sequentialRed-500: #dd6065;--sequentialRed-500-inverted: #dd6065;--sequentialRed-600: #c9474c;--sequentialRed-600-inverted: #c9474c;--sequentialRed-700: #b22f36;--sequentialRed-700-inverted: #b22f36;--sequentialRed-800: #981822;--sequentialRed-800-inverted: #981822;--sequentialRed-900: #7d000f;--sequentialRed-900-inverted: #7d000f;--surfaces-bg-card: #F5F6F6;--surfaces-bg-card-inverted: #232632;--bs-primary-rgb: #141721e0;--bs-secondary-color: #14172199;--bs-nav-link-color: #14172199;--bs-form-valid-color: #26bf56ff;--bs-form-invalid-color: #f03b3aff;--bs-body-bg: white}[data-bs-theme=dark]{--bs-primary: #1b0734;--bs-secondary: #808080;--bs-success: #222222;--bs-danger: #be3636;--bs-info: #163960;--bs-dark: #2a2a2a;--bs-light: #333333;--bs-primary-rgb: 27, 7, 52;--bs-secondary-rgb: 128, 128, 128;--bs-success-rgb: 34, 34, 34;--bs-danger-rgb: 190, 54, 54;--bs-info-rgb: 22, 57, 96;--bs-dark-rgb: 42, 42, 42;--bs-light-rgb: 51, 51, 51}[data-bs-theme=light]{--bs-primary: #1b0734;--bs-secondary: #808080;--bs-success: #222222;--bs-danger: #be3636;--bs-info: #163960;--bs-dark: #2a2a2a;--bs-light: #333333;--bs-primary-rgb: 27, 7, 52;--bs-secondary-rgb: 128, 128, 128;--bs-success-rgb: 34, 34, 34;--bs-danger-rgb: 190, 54, 54;--bs-info-rgb: 22, 57, 96;--bs-dark-rgb: 42, 42, 42;--bs-light-rgb: 51, 51, 51}h1,.h1{font-size:32px;text-decoration:none;font-weight:400;font-style:normal;font-stretch:normal;letter-spacing:-0.128px;line-height:40px;color:var(--text-primary)}h2,.h2{font-size:24px;text-decoration:none;font-weight:400;font-style:normal;font-stretch:normal;letter-spacing:-0.096px;line-height:32px;color:var(--text-primary)}h3,.h3,legend{font-size:20px;text-decoration:none;font-weight:400;font-style:normal;font-stretch:normal;letter-spacing:0px;line-height:28px;color:var(--text-primary)}h4,.h4{font-size:16px;text-decoration:none;font-weight:400;font-style:normal;font-stretch:normal;letter-spacing:-0.016px;line-height:20px;color:var(--text-primary)}h5,.h5{font-size:16px;text-decoration:none;font-weight:400;font-style:normal;font-stretch:normal;letter-spacing:-0.016px;line-height:20px;font-size:14px;color:var(--text-primary)}h6,.h6{font-size:16px;text-decoration:none;font-weight:400;font-style:normal;font-stretch:normal;letter-spacing:-0.016px;line-height:20px;font-size:12px;color:var(--text-primary)}p,label,ul,li,blockquote{color:var(--text-secondary)}.text-muted{color:var(--text-subtle)}.text-primary{color:var(--text-primary) !important}.text-secondary{color:var(--text-secondary) !important}blockquote{border-left:.25rem var(--text-secondary) solid;margin-bottom:.5rem;padding-left:.25rem}hr{border-bottom:1px solid var(--border-subtleAlpha01);width:100%}li{margin-bottom:.25rem}.btn{width:-moz-fit-content;width:fit-content;height:32px}.btn-large{height:40px}.btn-primary{color:var(--text-primary-inverted);font-size:14px;text-decoration:none;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;background:linear-gradient(var(--stateOverlays-enabled-inverted), var(--stateOverlays-enabled-inverted)),var(--fill-active);border:0 solid rgba(0,0,0,0);box-shadow:var(--elevation-0);transition:box-shadow .2s}.btn-primary:enabled,.btn-primary.enabled{border:0 solid rgba(0,0,0,0);box-shadow:var(--elevation-0)}.btn-primary:active,.btn-primary.active{color:var(--text-primary-inverted);font-size:14px;text-decoration:none;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;font-size:14px;text-decoration:underline;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;background:linear-gradient(var(--stateOverlays-active-inverted), var(--stateOverlays-active-inverted)),var(--fill-active);border:0 solid rgba(0,0,0,0);box-shadow:var(--elevation-0)}.btn-primary:hover,.btn-primary.hover{color:var(--text-primary-inverted);font-size:14px;text-decoration:none;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;font-size:14px;text-decoration:underline;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;background:linear-gradient(var(--stateOverlays-hover-inverted), var(--stateOverlays-hover-inverted)),var(--fill-active);border:0 solid rgba(0,0,0,0);box-shadow:var(--elevation-1)}.btn-primary:focus,.btn-primary.focus{color:var(--text-primary-inverted);font-size:14px;text-decoration:none;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;font-size:14px;text-decoration:underline;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;background:linear-gradient(var(--stateOverlays-enabled-inverted), var(--stateOverlays-enabled-inverted)),var(--fill-active);border:1px solid rgba(0,0,0,0);box-shadow:var(--elevation-0);outline:2px solid var(--focus-color)}.btn-primary:disabled,.btn-primary.disabled{color:var(--text-disabled-inverted);font-size:14px;text-decoration:none;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;background:linear-gradient(var(--stateOverlays-disabled-inverted), var(--stateOverlays-disabled-inverted)),var(--fill-active);border:0 solid rgba(0,0,0,0);box-shadow:var(--elevation-0)}.btn-primary:focus:not(:focus-visible,:disabled,.disabled){color:var(--text-primary-inverted);font-size:14px;text-decoration:none;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;font-size:14px;text-decoration:underline;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;background:linear-gradient(var(--stateOverlays-enabled-inverted), var(--stateOverlays-enabled-inverted)),var(--fill-active);border:0 solid rgba(0,0,0,0);box-shadow:var(--elevation-0)}.btn-secondary{color:var(--text-primary);font-size:14px;text-decoration:none;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;background:var(--stateOverlays-enabled);border:1px solid var(--border-enabled);box-shadow:var(--elevation-0);transition:box-shadow .2s}.btn-secondary:enabled,.btn-secondary.enabled{border:1px solid var(--border-enabled);box-shadow:var(--elevation-0)}.btn-secondary:active,.btn-secondary.active{color:var(--text-primary);font-size:14px;text-decoration:none;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;font-size:14px;text-decoration:underline;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;background:var(--stateOverlays-active);border:1px solid var(--border-selected);box-shadow:var(--elevation-0)}.btn-secondary:hover,.btn-secondary.hover{color:var(--text-primary);font-size:14px;text-decoration:none;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;font-size:14px;text-decoration:underline;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;background:var(--stateOverlays-hover);border:1px solid var(--border-hover);box-shadow:var(--elevation-1)}.btn-secondary:focus,.btn-secondary.focus{color:var(--text-primary);font-size:14px;text-decoration:none;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;font-size:14px;text-decoration:underline;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;background:var(--stateOverlays-enabled);border:1px solid var(--border-hover);box-shadow:var(--elevation-0);outline:2px solid var(--focus-color)}.btn-secondary:disabled,.btn-secondary.disabled{color:var(--text-disabled);font-size:14px;text-decoration:none;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;background:var(--stateOverlays-enabled);border:1px solid var(--border-disabled);box-shadow:var(--elevation-0)}.btn-secondary:focus:not(:focus-visible,:disabled,.disabled){color:var(--text-primary);font-size:14px;text-decoration:none;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;font-size:14px;text-decoration:underline;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;background:var(--stateOverlays-enabled);border:1px solid var(--border-enabled);box-shadow:var(--elevation-0)}.btn-tertiary,.btn-link{color:var(--text-primary);font-size:14px;text-decoration:none;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;background:var(--stateOverlays-enabled);border:0 solid rgba(0,0,0,0);box-shadow:None;transition:box-shadow .2s}.btn-tertiary:enabled,.btn-tertiary.enabled,.btn-link:enabled,.btn-link.enabled{border:0 solid rgba(0,0,0,0);box-shadow:None}.btn-tertiary:active,.btn-tertiary.active,.btn-link:active,.btn-link.active{color:var(--text-primary);font-size:14px;text-decoration:none;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;font-size:14px;text-decoration:underline;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;background:var(--stateOverlays-active);border:0 solid rgba(0,0,0,0);box-shadow:None}.btn-tertiary:hover,.btn-tertiary.hover,.btn-link:hover,.btn-link.hover{color:var(--text-primary);font-size:14px;text-decoration:none;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;font-size:14px;text-decoration:underline;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;background:var(--stateOverlays-hover);border:0 solid rgba(0,0,0,0);box-shadow:var(--elevation-1)}.btn-tertiary:focus,.btn-tertiary.focus,.btn-link:focus,.btn-link.focus{color:var(--text-primary);font-size:14px;text-decoration:none;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;font-size:14px;text-decoration:underline;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;background:var(--stateOverlays-enabled);border:1px solid rgba(0,0,0,0);box-shadow:None;outline:2px solid var(--focus-color)}.btn-tertiary:disabled,.btn-tertiary.disabled,.btn-link:disabled,.btn-link.disabled{color:var(--text-disabled);font-size:14px;text-decoration:none;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;background:var(--stateOverlays-enabled);border:0 solid rgba(0,0,0,0);box-shadow:None}.btn-tertiary:focus:not(:focus-visible,:disabled,.disabled),.btn-link:focus:not(:focus-visible,:disabled,.disabled){color:var(--text-primary);font-size:14px;text-decoration:none;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;font-size:14px;text-decoration:underline;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;background:var(--stateOverlays-enabled);border:0 solid rgba(0,0,0,0);box-shadow:None}.btn-tertiary,.btn-tertiary:disabled,.btn-tertiary.disabled,.btn-link,.btn-link:disabled,.btn-link.disabled{text-decoration:underline}.accordion{width:100%}.accordion-body{display:flex;flex-direction:column;gap:.5rem}.accordion-item{color:var(--text-primary);border-bottom:1px solid var(--border-subtleAlpha01)}.accordion-item .nav-link{padding:.5rem;color:var(--text-secondary)}.accordion-item .nav-link:last-child{margin-bottom:.75rem}.accordion-item .nav-link:active,.accordion-item .nav-link.active{background:var(--stateOverlays-active);color:var(--text-primary)}.accordion-item .nav-link:hover,.accordion-item .nav-link.hover{background:var(--stateOverlays-selectedHover);color:var(--text-primary)}.accordion-item .nav-link:disabled,.accordion-item .nav-link.disabled{color:var(--text-disabled)}.accordion-item .nav-link:focus:not(:focus-visible,:disabled,.disabled){background:var(--stateOverlays-active);color:var(--text-primary)}.accordion-button{font-size:14px;text-decoration:none;font-weight:400;font-style:normal;font-stretch:normal;letter-spacing:-0.112px;line-height:16px;color:var(--text-secondary);text-transform:uppercase;border:none;box-shadow:none}.accordion-button::after{filter:contrast(0);opacity:.4}.accordion-button:not(.collapsed){color:var(--text-primary);background-color:rgba(0,0,0,0);box-shadow:none}.accordion-button:not(.collapsed)::after{filter:contrast(0);opacity:1}.accordion-button:hover,.accordion-button.hover{color:var(--text-primary)}.accordion-button:hover::after,.accordion-button.hover::after{opacity:1}.accordion-button:focus,.accordion-button.focus{border:none;box-shadow:none}.card{background:var(--surfaces-bg-card);border:none;box-shadow:var(--elevation-1);width:100%;height:100%;overflow:auto;padding:1rem}.card-nav:hover,.card-nav.hover{background:var(--field-enabled);box-shadow:var(--elevation-2);transform:translate3d(0, -0.5rem, 0);will-change:transform}.card-title,.card-header{color:var(--text-primary)}.card-subtitle{color:var(--text-secondary)}.card-text{font-size:14px;text-decoration:none;font-weight:400;font-style:normal;font-stretch:normal;letter-spacing:-0.112px;line-height:16px;color:var(--text-secondary)}.card-text:last-child{margin-bottom:.5rem}.navbar{background:var(--surfaces-bg02) !important}.navbar .nav-link{padding:0;align-items:center;display:flex;height:4rem;justify-content:center;color:var(--text-secondary)}.navbar .nav-link:active,.navbar .nav-link.active{color:var(--text-primary)}.form-check{transition:all 150ms ease 0s;margin-bottom:12px;line-height:16px;min-height:auto;padding-left:28px}.form-check-inline{margin-right:12px}.form-check-input{margin:0;position:relative;border-color:var(--border-enabled);border-radius:0;color:var(--fill-active);width:16px;height:16px;outline:none;background-color:rgba(0,0,0,0);background-size:auto}.form-check-input:checked{background-color:rgba(0,0,0,0);border-color:var(--border-enabled);color:var(--fill-active)}.form-check-input[type=radio]{background-image:none}.form-check-input[type=radio]:checked::after{position:absolute;content:"";background-color:var(--fill-active);border-radius:50%;width:8px;height:8px;top:50%;left:50%;transform:translate(-50%, -50%)}.form-check-input:hover,.form-check-input.hover{border-color:var(--border-hover)}.form-check-input:active,.form-check-input.active{border-color:var(--border-selected)}.form-check-input:disabled,.form-check-input.disabled{border-color:var(--border-disabled)}.form-check-input:focus,.form-check-input.focus{border-color:var(--border-selected);box-shadow:none}.form-check-input:hover{border-color:var(--border-hover)}.form-check .form-check-input{margin-left:-24px}.form-check-label{font-size:14px;text-decoration:none;font-weight:400;font-style:normal;font-stretch:normal;letter-spacing:-0.112px;line-height:16px;color:var(--text-secondary)}.form-check-lg{line-height:20px;min-height:auto;padding-left:32px}.form-check-lg .form-check-input{width:20px;height:20px;border-width:2px;margin-left:-28px}.form-check-lg .form-check-label{line-height:20px}.form-check.disabled .form-check-label{color:var(--text-disabled)}[data-bs-theme=light] .form-check-input:checked[type=checkbox]{--bs-form-check-bg-image: url("data:image/svg+xml,%3Csvg width='8' height='7' viewBox='0 0 8 7' xmlns='http://www.w3.org/2000/svg' data-testid='checkmark-icon'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M7.5 1.25L6.25 0L2.5 3.75L1.25 2.5L0 3.75L2.5 6.25L7.5 1.25Z' fill='rgba(20, 23, 33, 0.8784313725)'/%3E%3C/svg%3E")}[data-bs-theme=light] .form-check-lg .form-check-input:checked[type=checkbox]{--bs-form-check-bg-image: url("data:image/svg+xml,%3Csvg width='11' height='9' viewBox='0 0 11 9' xmlns='http://www.w3.org/2000/svg' data-testid='checkmark-icon'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M10.5 1.25L9.25 0L3.5 5.75L1.25 3.5L0 4.75L3.5 8.25L10.5 1.25Z' fill='rgba(20, 23, 33, 0.8784313725)' fill-opacity='0.88'/%3E%3C/svg%3E")}[data-bs-theme=dark] .form-check-input:checked[type=checkbox]{--bs-form-check-bg-image: url("data:image/svg+xml,%3Csvg width='8' height='7' viewBox='0 0 8 7' xmlns='http://www.w3.org/2000/svg' data-testid='checkmark-icon'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M7.5 1.25L6.25 0L2.5 3.75L1.25 2.5L0 3.75L2.5 6.25L7.5 1.25Z' fill='rgba(255, 255, 255, 0.8784313725)'/%3E%3C/svg%3E")}[data-bs-theme=dark] .form-check-lg .form-check-input:checked[type=checkbox]{--bs-form-check-bg-image: url("data:image/svg+xml,%3Csvg width='11' height='9' viewBox='0 0 11 9' xmlns='http://www.w3.org/2000/svg' data-testid='checkmark-icon'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M10.5 1.25L9.25 0L3.5 5.75L1.25 3.5L0 4.75L3.5 8.25L10.5 1.25Z' fill='rgba(255, 255, 255, 0.8784313725)' fill-opacity='0.88'/%3E%3C/svg%3E")}[data-bs-theme=light] [data-bs-theme=dark] .form-check-input:checked[type=checkbox]{--bs-form-check-bg-image: url("data:image/svg+xml,%3Csvg width='8' height='7' viewBox='0 0 8 7' xmlns='http://www.w3.org/2000/svg' data-testid='checkmark-icon'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M7.5 1.25L6.25 0L2.5 3.75L1.25 2.5L0 3.75L2.5 6.25L7.5 1.25Z' fill='rgba(255, 255, 255, 0.8784313725)'/%3E%3C/svg%3E")}[data-bs-theme=light] [data-bs-theme=dark] .form-check-lg .form-check-input:checked[type=checkbox]{--bs-form-check-bg-image: url("data:image/svg+xml,%3Csvg width='11' height='9' viewBox='0 0 11 9' xmlns='http://www.w3.org/2000/svg' data-testid='checkmark-icon'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M10.5 1.25L9.25 0L3.5 5.75L1.25 3.5L0 4.75L3.5 8.25L10.5 1.25Z' fill='rgba(255, 255, 255, 0.8784313725)' fill-opacity='0.88'/%3E%3C/svg%3E")}[data-bs-theme=dark] [data-bs-theme=light] .form-check-input:checked[type=checkbox]{--bs-form-check-bg-image: url("data:image/svg+xml,%3Csvg width='8' height='7' viewBox='0 0 8 7' xmlns='http://www.w3.org/2000/svg' data-testid='checkmark-icon'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M7.5 1.25L6.25 0L2.5 3.75L1.25 2.5L0 3.75L2.5 6.25L7.5 1.25Z' fill='rgba(20, 23, 33, 0.8784313725)'/%3E%3C/svg%3E")}[data-bs-theme=dark] [data-bs-theme=light] .form-check-lg .form-check-input:checked[type=checkbox]{--bs-form-check-bg-image: url("data:image/svg+xml,%3Csvg width='11' height='9' viewBox='0 0 11 9' xmlns='http://www.w3.org/2000/svg' data-testid='checkmark-icon'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M10.5 1.25L9.25 0L3.5 5.75L1.25 3.5L0 4.75L3.5 8.25L10.5 1.25Z' fill='rgba(20, 23, 33, 0.8784313725)' fill-opacity='0.88'/%3E%3C/svg%3E")}.form-control,.input-group-text{font-size:14px;text-decoration:none;font-weight:400;font-style:normal;font-stretch:normal;letter-spacing:-0.112px;line-height:16px;background-color:var(--field-enabled);color:var(--text-primary);padding:.5rem;box-shadow:var(--elevation-0);border:none}.form-control::-moz-placeholder, .input-group-text::-moz-placeholder{color:var(--text-placeholder)}.form-control::placeholder,.input-group-text::placeholder{color:var(--text-placeholder)}.form-control:focus,.form-control.focus,.input-group-text:focus,.input-group-text.focus{background-color:var(--field-hover);color:var(--text-primary);box-shadow:0 0 0 2px var(--focus) inset}.form-control:hover,.form-control.hover,.input-group-text:hover,.input-group-text.hover{background-color:var(--field-hover)}.form-control:disabled,.form-control.disabled,.input-group-text:disabled,.input-group-text.disabled{background-color:var(--field-disabled);color:var(--text-disabled)}.form-control:focus-visible,.form-control.focus-visible,.input-group-text:focus-visible,.input-group-text.focus-visible{outline:none}.form-control.is-invalid{box-shadow:0 0 0 2px var(--status-error) inset;border:none}.form-control.is-invalid:focus,.form-control.is-invalid.focus{box-shadow:0 0 0 2px var(--status-error) inset}.form-control.is-valid{box-shadow:0 0 0 2px var(--status-success) inset}.form-control.is-valid:focus,.form-control.is-valid.focus{box-shadow:0 0 0 2px var(--status-success) inset}[data-bs-theme=light] .form-control.is-valid{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48' viewBox='0 -960 960 960'%3E%3Cpath fill='%2326bf56' d='M421-311.463 690.537-581l-34.845-34.23L421-380.153 302.539-498.615l-33.846 34.23L421-311.463Zm59.067 211.462q-78.221 0-147.397-29.92-69.176-29.92-120.989-81.71-51.814-51.791-81.747-120.936-29.933-69.146-29.933-147.366 0-78.836 29.92-148.204 29.92-69.369 81.71-120.682 51.791-51.314 120.936-81.247 69.146-29.933 147.366-29.933 78.836 0 148.204 29.92 69.369 29.92 120.682 81.21 51.314 51.291 81.247 120.629 29.933 69.337 29.933 148.173 0 78.221-29.92 147.397-29.92 69.176-81.21 120.989-51.291 51.814-120.629 81.747-69.337 29.933-148.173 29.933ZM480-145.385q139.692 0 237.154-97.769Q814.615-340.923 814.615-480q0-139.692-97.461-237.154Q619.692-814.615 480-814.615q-139.077 0-236.846 97.461Q145.385-619.692 145.385-480q0 139.077 97.769 236.846T480-145.385ZM480-480Z'%3E%3C/path%3E%3C/svg%3E"),url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48' viewBox='0 -960 960 960'%3E%3Cpath fill='%2326bf56' d='m421-298 283-283-46-45-237 237-120-120-45 45 165 166Zm59 218q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 31.5-156t86-127Q252-817 325-848.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 82-31.5 155T763-197.5q-54 54.5-127 86T480-80Zm0-60q142 0 241-99.5T820-480q0-142-99-241t-241-99q-141 0-240.5 99T140-480q0 141 99.5 240.5T480-140Zm0-340Z'%3E%3C/path%3E%3C/svg%3E")}[data-bs-theme=light] .form-control.is-invalid{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48' viewBox='0 -960 960 960'%3E%3Cpath fill='%23f03b3a' d='M331.539-299.539 480-448.001l148.461 148.462 32-32L511.999-480l148.462-148.461-32-32L480-511.999 331.539-660.461l-32 32L448.001-480 299.539-331.539l32 32Zm148.528 199.538q-78.221 0-147.397-29.92-69.176-29.92-120.989-81.71-51.814-51.791-81.747-120.936-29.933-69.146-29.933-147.366 0-78.836 29.92-148.204 29.92-69.369 81.71-120.682 51.791-51.314 120.936-81.247 69.146-29.933 147.366-29.933 78.836 0 148.204 29.92 69.369 29.92 120.682 81.21 51.314 51.291 81.247 120.629 29.933 69.337 29.933 148.173 0 78.221-29.92 147.397-29.92 69.176-81.21 120.989-51.291 51.814-120.629 81.747-69.337 29.933-148.173 29.933ZM480-145.385q139.692 0 237.154-97.769Q814.615-340.923 814.615-480q0-139.692-97.461-237.154Q619.692-814.615 480-814.615q-139.077 0-236.846 97.461Q145.385-619.692 145.385-480q0 139.077 97.769 236.846T480-145.385ZM480-480Z'%3E%3C/path%3E%3C/svg%3E"),url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48' viewBox='0 -960 960 960'%3E%3Cpath fill='%23f03b3a' d='m330-288 150-150 150 150 42-42-150-150 150-150-42-42-150 150-150-150-42 42 150 150-150 150 42 42ZM480-80q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 31.5-156t86-127Q252-817 325-848.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 82-31.5 155T763-197.5q-54 54.5-127 86T480-80Zm0-60q142 0 241-99.5T820-480q0-142-99-241t-241-99q-141 0-240.5 99T140-480q0 141 99.5 240.5T480-140Zm0-340Z'%3E%3C/path%3E%3C/svg%3E")}[data-bs-theme=dark] .form-control.is-valid{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48' viewBox='0 -960 960 960'%3E%3Cpath fill='%2340d86e' d='M421-311.463 690.537-581l-34.845-34.23L421-380.153 302.539-498.615l-33.846 34.23L421-311.463Zm59.067 211.462q-78.221 0-147.397-29.92-69.176-29.92-120.989-81.71-51.814-51.791-81.747-120.936-29.933-69.146-29.933-147.366 0-78.836 29.92-148.204 29.92-69.369 81.71-120.682 51.791-51.314 120.936-81.247 69.146-29.933 147.366-29.933 78.836 0 148.204 29.92 69.369 29.92 120.682 81.21 51.314 51.291 81.247 120.629 29.933 69.337 29.933 148.173 0 78.221-29.92 147.397-29.92 69.176-81.21 120.989-51.291 51.814-120.629 81.747-69.337 29.933-148.173 29.933ZM480-145.385q139.692 0 237.154-97.769Q814.615-340.923 814.615-480q0-139.692-97.461-237.154Q619.692-814.615 480-814.615q-139.077 0-236.846 97.461Q145.385-619.692 145.385-480q0 139.077 97.769 236.846T480-145.385ZM480-480Z'%3E%3C/path%3E%3C/svg%3E"),url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48' viewBox='0 -960 960 960'%3E%3Cpath fill='%2340d86e' d='m421-298 283-283-46-45-237 237-120-120-45 45 165 166Zm59 218q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 31.5-156t86-127Q252-817 325-848.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 82-31.5 155T763-197.5q-54 54.5-127 86T480-80Zm0-60q142 0 241-99.5T820-480q0-142-99-241t-241-99q-141 0-240.5 99T140-480q0 141 99.5 240.5T480-140Zm0-340Z'%3E%3C/path%3E%3C/svg%3E")}[data-bs-theme=dark] .form-control.is-invalid{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48' viewBox='0 -960 960 960'%3E%3Cpath fill='%23f56565' d='M331.539-299.539 480-448.001l148.461 148.462 32-32L511.999-480l148.462-148.461-32-32L480-511.999 331.539-660.461l-32 32L448.001-480 299.539-331.539l32 32Zm148.528 199.538q-78.221 0-147.397-29.92-69.176-29.92-120.989-81.71-51.814-51.791-81.747-120.936-29.933-69.146-29.933-147.366 0-78.836 29.92-148.204 29.92-69.369 81.71-120.682 51.791-51.314 120.936-81.247 69.146-29.933 147.366-29.933 78.836 0 148.204 29.92 69.369 29.92 120.682 81.21 51.314 51.291 81.247 120.629 29.933 69.337 29.933 148.173 0 78.221-29.92 147.397-29.92 69.176-81.21 120.989-51.291 51.814-120.629 81.747-69.337 29.933-148.173 29.933ZM480-145.385q139.692 0 237.154-97.769Q814.615-340.923 814.615-480q0-139.692-97.461-237.154Q619.692-814.615 480-814.615q-139.077 0-236.846 97.461Q145.385-619.692 145.385-480q0 139.077 97.769 236.846T480-145.385ZM480-480Z'%3E%3C/path%3E%3C/svg%3E"),url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48' viewBox='0 -960 960 960'%3E%3Cpath fill='%23f56565' d='m330-288 150-150 150 150 42-42-150-150 150-150-42-42-150 150-150-150-42 42 150 150-150 150 42 42ZM480-80q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 31.5-156t86-127Q252-817 325-848.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 82-31.5 155T763-197.5q-54 54.5-127 86T480-80Zm0-60q142 0 241-99.5T820-480q0-142-99-241t-241-99q-141 0-240.5 99T140-480q0 141 99.5 240.5T480-140Zm0-340Z'%3E%3C/path%3E%3C/svg%3E")}[data-bs-theme=light] [data-bs-theme=dark] .form-control.is-valid{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48' viewBox='0 -960 960 960'%3E%3Cpath fill='%2340d86e' d='M421-311.463 690.537-581l-34.845-34.23L421-380.153 302.539-498.615l-33.846 34.23L421-311.463Zm59.067 211.462q-78.221 0-147.397-29.92-69.176-29.92-120.989-81.71-51.814-51.791-81.747-120.936-29.933-69.146-29.933-147.366 0-78.836 29.92-148.204 29.92-69.369 81.71-120.682 51.791-51.314 120.936-81.247 69.146-29.933 147.366-29.933 78.836 0 148.204 29.92 69.369 29.92 120.682 81.21 51.314 51.291 81.247 120.629 29.933 69.337 29.933 148.173 0 78.221-29.92 147.397-29.92 69.176-81.21 120.989-51.291 51.814-120.629 81.747-69.337 29.933-148.173 29.933ZM480-145.385q139.692 0 237.154-97.769Q814.615-340.923 814.615-480q0-139.692-97.461-237.154Q619.692-814.615 480-814.615q-139.077 0-236.846 97.461Q145.385-619.692 145.385-480q0 139.077 97.769 236.846T480-145.385ZM480-480Z'%3E%3C/path%3E%3C/svg%3E"),url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48' viewBox='0 -960 960 960'%3E%3Cpath fill='%2340d86e' d='m421-298 283-283-46-45-237 237-120-120-45 45 165 166Zm59 218q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 31.5-156t86-127Q252-817 325-848.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 82-31.5 155T763-197.5q-54 54.5-127 86T480-80Zm0-60q142 0 241-99.5T820-480q0-142-99-241t-241-99q-141 0-240.5 99T140-480q0 141 99.5 240.5T480-140Zm0-340Z'%3E%3C/path%3E%3C/svg%3E")}[data-bs-theme=light] [data-bs-theme=dark] .form-control.is-invalid{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48' viewBox='0 -960 960 960'%3E%3Cpath fill='%23f56565' d='M331.539-299.539 480-448.001l148.461 148.462 32-32L511.999-480l148.462-148.461-32-32L480-511.999 331.539-660.461l-32 32L448.001-480 299.539-331.539l32 32Zm148.528 199.538q-78.221 0-147.397-29.92-69.176-29.92-120.989-81.71-51.814-51.791-81.747-120.936-29.933-69.146-29.933-147.366 0-78.836 29.92-148.204 29.92-69.369 81.71-120.682 51.791-51.314 120.936-81.247 69.146-29.933 147.366-29.933 78.836 0 148.204 29.92 69.369 29.92 120.682 81.21 51.314 51.291 81.247 120.629 29.933 69.337 29.933 148.173 0 78.221-29.92 147.397-29.92 69.176-81.21 120.989-51.291 51.814-120.629 81.747-69.337 29.933-148.173 29.933ZM480-145.385q139.692 0 237.154-97.769Q814.615-340.923 814.615-480q0-139.692-97.461-237.154Q619.692-814.615 480-814.615q-139.077 0-236.846 97.461Q145.385-619.692 145.385-480q0 139.077 97.769 236.846T480-145.385ZM480-480Z'%3E%3C/path%3E%3C/svg%3E"),url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48' viewBox='0 -960 960 960'%3E%3Cpath fill='%23f56565' d='m330-288 150-150 150 150 42-42-150-150 150-150-42-42-150 150-150-150-42 42 150 150-150 150 42 42ZM480-80q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 31.5-156t86-127Q252-817 325-848.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 82-31.5 155T763-197.5q-54 54.5-127 86T480-80Zm0-60q142 0 241-99.5T820-480q0-142-99-241t-241-99q-141 0-240.5 99T140-480q0 141 99.5 240.5T480-140Zm0-340Z'%3E%3C/path%3E%3C/svg%3E")}[data-bs-theme=dark] [data-bs-theme=light] .form-control.is-valid{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48' viewBox='0 -960 960 960'%3E%3Cpath fill='%2326bf56' d='M421-311.463 690.537-581l-34.845-34.23L421-380.153 302.539-498.615l-33.846 34.23L421-311.463Zm59.067 211.462q-78.221 0-147.397-29.92-69.176-29.92-120.989-81.71-51.814-51.791-81.747-120.936-29.933-69.146-29.933-147.366 0-78.836 29.92-148.204 29.92-69.369 81.71-120.682 51.791-51.314 120.936-81.247 69.146-29.933 147.366-29.933 78.836 0 148.204 29.92 69.369 29.92 120.682 81.21 51.314 51.291 81.247 120.629 29.933 69.337 29.933 148.173 0 78.221-29.92 147.397-29.92 69.176-81.21 120.989-51.291 51.814-120.629 81.747-69.337 29.933-148.173 29.933ZM480-145.385q139.692 0 237.154-97.769Q814.615-340.923 814.615-480q0-139.692-97.461-237.154Q619.692-814.615 480-814.615q-139.077 0-236.846 97.461Q145.385-619.692 145.385-480q0 139.077 97.769 236.846T480-145.385ZM480-480Z'%3E%3C/path%3E%3C/svg%3E"),url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48' viewBox='0 -960 960 960'%3E%3Cpath fill='%2326bf56' d='m421-298 283-283-46-45-237 237-120-120-45 45 165 166Zm59 218q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 31.5-156t86-127Q252-817 325-848.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 82-31.5 155T763-197.5q-54 54.5-127 86T480-80Zm0-60q142 0 241-99.5T820-480q0-142-99-241t-241-99q-141 0-240.5 99T140-480q0 141 99.5 240.5T480-140Zm0-340Z'%3E%3C/path%3E%3C/svg%3E")}[data-bs-theme=dark] [data-bs-theme=light] .form-control.is-invalid{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48' viewBox='0 -960 960 960'%3E%3Cpath fill='%23f03b3a' d='M331.539-299.539 480-448.001l148.461 148.462 32-32L511.999-480l148.462-148.461-32-32L480-511.999 331.539-660.461l-32 32L448.001-480 299.539-331.539l32 32Zm148.528 199.538q-78.221 0-147.397-29.92-69.176-29.92-120.989-81.71-51.814-51.791-81.747-120.936-29.933-69.146-29.933-147.366 0-78.836 29.92-148.204 29.92-69.369 81.71-120.682 51.791-51.314 120.936-81.247 69.146-29.933 147.366-29.933 78.836 0 148.204 29.92 69.369 29.92 120.682 81.21 51.314 51.291 81.247 120.629 29.933 69.337 29.933 148.173 0 78.221-29.92 147.397-29.92 69.176-81.21 120.989-51.291 51.814-120.629 81.747-69.337 29.933-148.173 29.933ZM480-145.385q139.692 0 237.154-97.769Q814.615-340.923 814.615-480q0-139.692-97.461-237.154Q619.692-814.615 480-814.615q-139.077 0-236.846 97.461Q145.385-619.692 145.385-480q0 139.077 97.769 236.846T480-145.385ZM480-480Z'%3E%3C/path%3E%3C/svg%3E"),url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48' viewBox='0 -960 960 960'%3E%3Cpath fill='%23f03b3a' d='m330-288 150-150 150 150 42-42-150-150 150-150-42-42-150 150-150-150-42 42 150 150-150 150 42 42ZM480-80q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 31.5-156t86-127Q252-817 325-848.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 82-31.5 155T763-197.5q-54 54.5-127 86T480-80Zm0-60q142 0 241-99.5T820-480q0-142-99-241t-241-99q-141 0-240.5 99T140-480q0 141 99.5 240.5T480-140Zm0-340Z'%3E%3C/path%3E%3C/svg%3E")}.form-label{font-size:14px;text-decoration:none;font-weight:400;font-style:normal;font-stretch:normal;letter-spacing:-0.112px;line-height:16px;color:var(--text-secondary);margin-bottom:12px;width:100%}.qb-container-bg-01{background-color:var(--surfaces-bg01);padding:40px;width:100%}.qb-container-bg-02{background-color:var(--surfaces-bg02);padding:40px;width:100%}.qb-container-bg-03{background-color:var(--surfaces-bg03);padding:40px;width:100%} + */:root,[data-bs-theme=light]{--bs-blue: #1a85ff;--bs-indigo: #6610f2;--bs-purple: #976fd1;--bs-pink: #d41159;--bs-red: #ff5267;--bs-orange: #ff9222;--bs-yellow: #fdc935;--bs-green: #689f38;--bs-teal: #08bdba;--bs-cyan: #00b4ff;--bs-black: #000;--bs-white: #fff;--bs-gray: #6c757d;--bs-gray-dark: #343a40;--bs-gray-100: #f8f9fa;--bs-gray-200: #e9ecef;--bs-gray-300: #dee2e6;--bs-gray-400: #ced4da;--bs-gray-500: #adb5bd;--bs-gray-600: #6c757d;--bs-gray-700: #495057;--bs-gray-800: #343a40;--bs-gray-900: #212529;--bs-primary: #1a85ff;--bs-secondary: #6c757d;--bs-success: #689f38;--bs-info: #00b4ff;--bs-warning: #fdc935;--bs-danger: #ff5267;--bs-light: #f8f9fa;--bs-dark: #212529;--bs-primary-rgb: 26, 133, 255;--bs-secondary-rgb: 108, 117, 125;--bs-success-rgb: 104, 159, 56;--bs-info-rgb: 0, 180, 255;--bs-warning-rgb: 253, 201, 53;--bs-danger-rgb: 255, 82, 103;--bs-light-rgb: 248, 249, 250;--bs-dark-rgb: 33, 37, 41;--bs-primary-text-emphasis: #0a3566;--bs-secondary-text-emphasis: #2b2f32;--bs-success-text-emphasis: #2a4016;--bs-info-text-emphasis: #004866;--bs-warning-text-emphasis: #655015;--bs-danger-text-emphasis: #662129;--bs-light-text-emphasis: #495057;--bs-dark-text-emphasis: #495057;--bs-primary-bg-subtle: #d1e7ff;--bs-secondary-bg-subtle: #e2e3e5;--bs-success-bg-subtle: #e1ecd7;--bs-info-bg-subtle: #ccf0ff;--bs-warning-bg-subtle: #fff4d7;--bs-danger-bg-subtle: #ffdce1;--bs-light-bg-subtle: #fcfcfd;--bs-dark-bg-subtle: #ced4da;--bs-primary-border-subtle: #a3ceff;--bs-secondary-border-subtle: #c4c8cb;--bs-success-border-subtle: #c3d9af;--bs-info-border-subtle: #99e1ff;--bs-warning-border-subtle: #fee9ae;--bs-danger-border-subtle: #ffbac2;--bs-light-border-subtle: #e9ecef;--bs-dark-border-subtle: #adb5bd;--bs-white-rgb: 255, 255, 255;--bs-black-rgb: 0, 0, 0;--bs-font-sans-serif: inter, sans-serif, arial, serif, "Segoe UI", roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family: var(--bs-font-sans-serif);--bs-body-font-size:0.875rem;--bs-body-font-weight: 400;--bs-body-line-height: 1.5;--bs-body-color: #212529;--bs-body-color-rgb: 33, 37, 41;--bs-body-bg: #fff;--bs-body-bg-rgb: 255, 255, 255;--bs-emphasis-color: #000;--bs-emphasis-color-rgb: 0, 0, 0;--bs-secondary-color: rgba(33, 37, 41, 0.75);--bs-secondary-color-rgb: 33, 37, 41;--bs-secondary-bg: #e9ecef;--bs-secondary-bg-rgb: 233, 236, 239;--bs-tertiary-color: rgba(33, 37, 41, 0.5);--bs-tertiary-color-rgb: 33, 37, 41;--bs-tertiary-bg: #f8f9fa;--bs-tertiary-bg-rgb: 248, 249, 250;--bs-heading-color: inherit;--bs-link-color: #1a85ff;--bs-link-color-rgb: 26, 133, 255;--bs-link-decoration: underline;--bs-link-hover-color: #156acc;--bs-link-hover-color-rgb: 21, 106, 204;--bs-code-color: #d41159;--bs-highlight-color: #212529;--bs-highlight-bg: #fff4d7;--bs-border-width: 1px;--bs-border-style: solid;--bs-border-color: #dee2e6;--bs-border-color-translucent: rgba(0, 0, 0, 0.175);--bs-border-radius: 0;--bs-border-radius-sm: 0;--bs-border-radius-lg: 0;--bs-border-radius-xl: 1rem;--bs-border-radius-xxl: 2rem;--bs-border-radius-2xl: var(--bs-border-radius-xxl);--bs-border-radius-pill: 50rem;--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);--bs-focus-ring-width: 0.25rem;--bs-focus-ring-opacity: 0.25;--bs-focus-ring-color: rgba(26, 133, 255, 0.25);--bs-form-valid-color: #689f38;--bs-form-valid-border-color: #689f38;--bs-form-invalid-color: #ff5267;--bs-form-invalid-border-color: #ff5267}[data-bs-theme=dark]{color-scheme:dark;--bs-body-color: #dee2e6;--bs-body-color-rgb: 222, 226, 230;--bs-body-bg: #212529;--bs-body-bg-rgb: 33, 37, 41;--bs-emphasis-color: #fff;--bs-emphasis-color-rgb: 255, 255, 255;--bs-secondary-color: rgba(222, 226, 230, 0.75);--bs-secondary-color-rgb: 222, 226, 230;--bs-secondary-bg: #343a40;--bs-secondary-bg-rgb: 52, 58, 64;--bs-tertiary-color: rgba(222, 226, 230, 0.5);--bs-tertiary-color-rgb: 222, 226, 230;--bs-tertiary-bg: #2b3035;--bs-tertiary-bg-rgb: 43, 48, 53;--bs-primary-text-emphasis: #76b6ff;--bs-secondary-text-emphasis: #a7acb1;--bs-success-text-emphasis: #a4c588;--bs-info-text-emphasis: #66d2ff;--bs-warning-text-emphasis: #fedf86;--bs-danger-text-emphasis: #ff97a4;--bs-light-text-emphasis: #f8f9fa;--bs-dark-text-emphasis: #dee2e6;--bs-primary-bg-subtle: #051b33;--bs-secondary-bg-subtle: #161719;--bs-success-bg-subtle: #15200b;--bs-info-bg-subtle: #002433;--bs-warning-bg-subtle: #33280b;--bs-danger-bg-subtle: #331015;--bs-light-bg-subtle: #343a40;--bs-dark-bg-subtle: #1a1d20;--bs-primary-border-subtle: #105099;--bs-secondary-border-subtle: #41464b;--bs-success-border-subtle: #3e5f22;--bs-info-border-subtle: #006c99;--bs-warning-border-subtle: #987920;--bs-danger-border-subtle: #99313e;--bs-light-border-subtle: #495057;--bs-dark-border-subtle: #343a40;--bs-heading-color: inherit;--bs-link-color: #76b6ff;--bs-link-hover-color: #91c5ff;--bs-link-color-rgb: 118, 182, 255;--bs-link-hover-color-rgb: 145, 197, 255;--bs-code-color: #e5709b;--bs-highlight-color: #dee2e6;--bs-highlight-bg: #655015;--bs-border-color: #495057;--bs-border-color-translucent: rgba(255, 255, 255, 0.15);--bs-form-valid-color: #a4c588;--bs-form-valid-border-color: #a4c588;--bs-form-invalid-color: #ff97a4;--bs-form-invalid-border-color: #ff97a4}*,*::before,*::after{box-sizing:border-box}@media(prefers-reduced-motion: no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0)}hr{margin:1rem 0;color:inherit;border:0;border-top:var(--bs-border-width) solid;opacity:.25}h6,.h6,h5,.h5,h4,.h4,h3,.h3,h2,.h2,h1,.h1{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2;color:var(--bs-heading-color)}h1,.h1{font-size:calc(1.34375rem + 1.125vw)}@media(min-width: 1200px){h1,.h1{font-size:2.1875rem}}h2,.h2{font-size:calc(1.3rem + 0.6vw)}@media(min-width: 1200px){h2,.h2{font-size:1.75rem}}h3,.h3{font-size:calc(1.278125rem + 0.3375vw)}@media(min-width: 1200px){h3,.h3{font-size:1.53125rem}}h4,.h4{font-size:calc(1.25625rem + 0.075vw)}@media(min-width: 1200px){h4,.h4{font-size:1.3125rem}}h5,.h5{font-size:1.09375rem}h6,.h6{font-size:0.875rem}p{margin-top:0;margin-bottom:.5rem}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}ol,ul,dl{margin-top:0;margin-bottom:1rem}ol ol,ul ul,ol ul,ul ol{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small,.small{font-size:0.875em}mark,.mark{padding:.1875em;color:var(--bs-highlight-color);background-color:var(--bs-highlight-bg)}sub,sup{position:relative;font-size:0.75em;line-height:0;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}a{color:rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));text-decoration:underline}a:hover{--bs-link-color-rgb: var(--bs-link-hover-color-rgb)}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}pre,code,kbd,samp{font-family:var(--bs-font-monospace);font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:0.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:0.875em;color:var(--bs-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:.1875rem .375rem;font-size:0.875em;color:var(--bs-body-bg);background-color:var(--bs-body-color)}kbd kbd{padding:0;font-size:1em}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-secondary-color);text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}thead,tbody,tfoot,tr,td,th{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}input,button,select,optgroup,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none !important}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}button:not(:disabled),[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + 0.3vw);line-height:inherit}@media(min-width: 1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-text,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none !important}.lead{font-size:1.09375rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media(min-width: 1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media(min-width: 1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media(min-width: 1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media(min-width: 1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media(min-width: 1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media(min-width: 1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:0.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.09375rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:0.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:var(--bs-body-bg);border:var(--bs-border-width) solid var(--bs-border-color);max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:0.875em;color:var(--bs-secondary-color)}.container,.container-fluid,.container-xxl,.container-xl,.container-lg,.container-md,.container-sm{--bs-gutter-x: 1.5rem;--bs-gutter-y: 0;width:100%;padding-right:calc(var(--bs-gutter-x)*.5);padding-left:calc(var(--bs-gutter-x)*.5);margin-right:auto;margin-left:auto}@media(min-width: 576px){.container-sm,.container{max-width:540px}}@media(min-width: 768px){.container-md,.container-sm,.container{max-width:720px}}@media(min-width: 992px){.container-lg,.container-md,.container-sm,.container{max-width:960px}}@media(min-width: 1200px){.container-xl,.container-lg,.container-md,.container-sm,.container{max-width:1140px}}@media(min-width: 1400px){.container-xxl,.container-xl,.container-lg,.container-md,.container-sm,.container{max-width:1320px}}:root{--bs-breakpoint-xs: 0;--bs-breakpoint-sm: 576px;--bs-breakpoint-md: 768px;--bs-breakpoint-lg: 992px;--bs-breakpoint-xl: 1200px;--bs-breakpoint-xxl: 1400px}.row{--bs-gutter-x: 1.5rem;--bs-gutter-y: 0;display:flex;flex-wrap:wrap;margin-top:calc(-1*var(--bs-gutter-y));margin-right:calc(-0.5*var(--bs-gutter-x));margin-left:calc(-0.5*var(--bs-gutter-x))}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x)*.5);padding-left:calc(var(--bs-gutter-x)*.5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.66666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x: 0}.g-0,.gy-0{--bs-gutter-y: 0}.g-1,.gx-1{--bs-gutter-x: 0.25rem}.g-1,.gy-1{--bs-gutter-y: 0.25rem}.g-2,.gx-2{--bs-gutter-x: 0.5rem}.g-2,.gy-2{--bs-gutter-y: 0.5rem}.g-3,.gx-3{--bs-gutter-x: 1rem}.g-3,.gy-3{--bs-gutter-y: 1rem}.g-4,.gx-4{--bs-gutter-x: 1.5rem}.g-4,.gy-4{--bs-gutter-y: 1.5rem}.g-5,.gx-5{--bs-gutter-x: 3rem}.g-5,.gy-5{--bs-gutter-y: 3rem}@media(min-width: 576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.66666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x: 0}.g-sm-0,.gy-sm-0{--bs-gutter-y: 0}.g-sm-1,.gx-sm-1{--bs-gutter-x: 0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y: 0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x: 0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y: 0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x: 1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y: 1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x: 1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y: 1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x: 3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y: 3rem}}@media(min-width: 768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.66666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x: 0}.g-md-0,.gy-md-0{--bs-gutter-y: 0}.g-md-1,.gx-md-1{--bs-gutter-x: 0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y: 0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x: 0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y: 0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x: 1rem}.g-md-3,.gy-md-3{--bs-gutter-y: 1rem}.g-md-4,.gx-md-4{--bs-gutter-x: 1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y: 1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x: 3rem}.g-md-5,.gy-md-5{--bs-gutter-y: 3rem}}@media(min-width: 992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.66666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x: 0}.g-lg-0,.gy-lg-0{--bs-gutter-y: 0}.g-lg-1,.gx-lg-1{--bs-gutter-x: 0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y: 0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x: 0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y: 0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x: 1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y: 1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x: 1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y: 1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x: 3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y: 3rem}}@media(min-width: 1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.66666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x: 0}.g-xl-0,.gy-xl-0{--bs-gutter-y: 0}.g-xl-1,.gx-xl-1{--bs-gutter-x: 0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y: 0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x: 0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y: 0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x: 1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y: 1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x: 1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y: 1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x: 3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y: 3rem}}@media(min-width: 1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.66666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x: 0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y: 0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x: 0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y: 0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x: 0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y: 0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x: 1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y: 1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x: 1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y: 1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x: 3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y: 3rem}}.table{--bs-table-color-type: initial;--bs-table-bg-type: initial;--bs-table-color-state: initial;--bs-table-bg-state: initial;--bs-table-color: var(--bs-emphasis-color);--bs-table-bg: var(--bs-body-bg);--bs-table-border-color: var(--bs-border-color);--bs-table-accent-bg: transparent;--bs-table-striped-color: var(--bs-emphasis-color);--bs-table-striped-bg: rgba(var(--bs-emphasis-color-rgb), 0.05);--bs-table-active-color: var(--bs-emphasis-color);--bs-table-active-bg: rgba(var(--bs-emphasis-color-rgb), 0.1);--bs-table-hover-color: var(--bs-emphasis-color);--bs-table-hover-bg: rgba(var(--bs-emphasis-color-rgb), 0.075);width:100%;margin-bottom:1rem;vertical-align:top;border-color:var(--bs-table-border-color)}.table>:not(caption)>*>*{padding:.5rem .5rem;color:var(--bs-table-color-state, var(--bs-table-color-type, var(--bs-table-color)));background-color:var(--bs-table-bg);border-bottom-width:var(--bs-border-width);box-shadow:inset 0 0 0 9999px var(--bs-table-bg-state, var(--bs-table-bg-type, var(--bs-table-accent-bg)))}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table-group-divider{border-top:calc(var(--bs-border-width)*2) solid currentcolor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:var(--bs-border-width) 0}.table-bordered>:not(caption)>*>*{border-width:0 var(--bs-border-width)}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-color-type: var(--bs-table-striped-color);--bs-table-bg-type: var(--bs-table-striped-bg)}.table-striped-columns>:not(caption)>tr>:nth-child(even){--bs-table-color-type: var(--bs-table-striped-color);--bs-table-bg-type: var(--bs-table-striped-bg)}.table-active{--bs-table-color-state: var(--bs-table-active-color);--bs-table-bg-state: var(--bs-table-active-bg)}.table-hover>tbody>tr:hover>*{--bs-table-color-state: var(--bs-table-hover-color);--bs-table-bg-state: var(--bs-table-hover-bg)}.table-primary{--bs-table-color: #000;--bs-table-bg: #d1e7ff;--bs-table-border-color: #a7b9cc;--bs-table-striped-bg: #c7dbf2;--bs-table-striped-color: #000;--bs-table-active-bg: #bcd0e6;--bs-table-active-color: #000;--bs-table-hover-bg: #c1d6ec;--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-secondary{--bs-table-color: #000;--bs-table-bg: #e2e3e5;--bs-table-border-color: #b5b6b7;--bs-table-striped-bg: #d7d8da;--bs-table-striped-color: #000;--bs-table-active-bg: #cbccce;--bs-table-active-color: #000;--bs-table-hover-bg: #d1d2d4;--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-success{--bs-table-color: #000;--bs-table-bg: #e1ecd7;--bs-table-border-color: #b4bdac;--bs-table-striped-bg: #d6e0cc;--bs-table-striped-color: #000;--bs-table-active-bg: #cbd4c2;--bs-table-active-color: #000;--bs-table-hover-bg: #d0dac7;--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-info{--bs-table-color: #000;--bs-table-bg: #ccf0ff;--bs-table-border-color: #a3c0cc;--bs-table-striped-bg: #c2e4f2;--bs-table-striped-color: #000;--bs-table-active-bg: #b8d8e6;--bs-table-active-color: #000;--bs-table-hover-bg: #bddeec;--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-warning{--bs-table-color: #000;--bs-table-bg: #fff4d7;--bs-table-border-color: #ccc3ac;--bs-table-striped-bg: #f2e8cc;--bs-table-striped-color: #000;--bs-table-active-bg: #e6dcc2;--bs-table-active-color: #000;--bs-table-hover-bg: #ece2c7;--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-danger{--bs-table-color: #000;--bs-table-bg: #ffdce1;--bs-table-border-color: #ccb0b4;--bs-table-striped-bg: #f2d1d6;--bs-table-striped-color: #000;--bs-table-active-bg: #e6c6cb;--bs-table-active-color: #000;--bs-table-hover-bg: #ecccd0;--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-light{--bs-table-color: #000;--bs-table-bg: #f8f9fa;--bs-table-border-color: #c6c7c8;--bs-table-striped-bg: #ecedee;--bs-table-striped-color: #000;--bs-table-active-bg: #dfe0e1;--bs-table-active-color: #000;--bs-table-hover-bg: #e5e6e7;--bs-table-hover-color: #000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-dark{--bs-table-color: #fff;--bs-table-bg: #212529;--bs-table-border-color: #4d5154;--bs-table-striped-bg: #2c3034;--bs-table-striped-color: #fff;--bs-table-active-bg: #373b3e;--bs-table-active-color: #fff;--bs-table-hover-bg: #323539;--bs-table-hover-color: #fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media(max-width: 575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media(max-width: 767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media(max-width: 991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media(max-width: 1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media(max-width: 1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(0.25rem + var(--bs-border-width));padding-bottom:calc(0.25rem + var(--bs-border-width));margin-bottom:0;font-size:inherit;line-height:1.6}.col-form-label-lg{padding-top:calc(0.5rem + var(--bs-border-width));padding-bottom:calc(0.5rem + var(--bs-border-width));font-size:0.875rem}.col-form-label-sm{padding-top:calc(0.25rem + var(--bs-border-width));padding-bottom:calc(0.25rem + var(--bs-border-width));font-size:0.65625rem}.form-text{margin-top:.25rem;font-size:0.875em;color:var(--bs-secondary-color)}.form-control{display:block;width:100%;padding:.25rem .5rem;font-size:0.875rem;font-weight:400;line-height:1.6;color:var(--bs-body-color);-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-body-bg);background-clip:padding-box;border:var(--bs-border-width) solid var(--bs-border-color);border-radius:0}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:var(--bs-body-color);background-color:var(--bs-body-bg);border-color:#8dc2ff;outline:0;box-shadow:0 0 0 .25rem rgba(26,133,255,.25)}.form-control::-webkit-date-and-time-value{min-width:85px;height:1.6em;margin:0}.form-control::-webkit-datetime-edit{display:block;padding:0}.form-control::-moz-placeholder{color:var(--bs-secondary-color);opacity:1}.form-control::placeholder{color:var(--bs-secondary-color);opacity:1}.form-control:disabled{background-color:var(--bs-secondary-bg);opacity:1}.form-control::file-selector-button{padding:.25rem .5rem;margin:-0.25rem -0.5rem;margin-inline-end:.5rem;color:var(--bs-body-color);background-color:var(--bs-tertiary-bg);pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:var(--bs-border-width);border-radius:0}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:var(--bs-secondary-bg)}.form-control-plaintext{display:block;width:100%;padding:.25rem 0;margin-bottom:0;line-height:1.6;color:var(--bs-body-color);background-color:rgba(0,0,0,0);border:solid rgba(0,0,0,0);border-width:var(--bs-border-width) 0}.form-control-plaintext:focus{outline:0}.form-control-plaintext.form-control-sm,.form-control-plaintext.form-control-lg{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.6em + 0.5rem + calc(var(--bs-border-width) * 2));padding:.25rem .5rem;font-size:0.65625rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-0.25rem -0.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.6em + 1rem + calc(var(--bs-border-width) * 2));padding:.5rem 1rem;font-size:0.875rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-0.5rem -1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.6em + 0.5rem + calc(var(--bs-border-width) * 2))}textarea.form-control-sm{min-height:calc(1.6em + 0.5rem + calc(var(--bs-border-width) * 2))}textarea.form-control-lg{min-height:calc(1.6em + 1rem + calc(var(--bs-border-width) * 2))}.form-control-color{width:3rem;height:calc(1.6em + 0.5rem + calc(var(--bs-border-width) * 2));padding:.25rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{border:0 !important}.form-control-color::-webkit-color-swatch{border:0 !important}.form-control-color.form-control-sm{height:calc(1.6em + 0.5rem + calc(var(--bs-border-width) * 2))}.form-control-color.form-control-lg{height:calc(1.6em + 1rem + calc(var(--bs-border-width) * 2))}.form-select{--bs-form-select-bg-img: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");display:block;width:100%;padding:.25rem 1.5rem .25rem .5rem;font-size:0.875rem;font-weight:400;line-height:1.6;color:var(--bs-body-color);-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-body-bg);background-image:var(--bs-form-select-bg-img),var(--bs-form-select-bg-icon, none);background-repeat:no-repeat;background-position:right .5rem center;background-size:16px 12px;border:var(--bs-border-width) solid var(--bs-border-color);border-radius:0}.form-select:focus{border-color:#8dc2ff;outline:0;box-shadow:0 0 0 .25rem rgba(26,133,255,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.5rem;background-image:none}.form-select:disabled{background-color:var(--bs-secondary-bg)}.form-select:-moz-focusring{color:rgba(0,0,0,0);text-shadow:0 0 0 var(--bs-body-color)}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:0.65625rem}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:0.875rem}[data-bs-theme=dark] .form-select{--bs-form-select-bg-img: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23dee2e6' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e")}.form-check{display:block;min-height:1.3125rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-reverse{padding-right:1.5em;padding-left:0;text-align:right}.form-check-reverse .form-check-input{float:right;margin-right:-1.5em;margin-left:0}.form-check-input{--bs-form-check-bg: var(--bs-body-bg);flex-shrink:0;width:1em;height:1em;margin-top:.25em;vertical-align:top;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-form-check-bg);background-image:var(--bs-form-check-bg-image);background-repeat:no-repeat;background-position:center;background-size:contain;border:var(--bs-border-width) solid var(--bs-border-color);-webkit-print-color-adjust:exact;print-color-adjust:exact}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#8dc2ff;outline:0;box-shadow:0 0 0 .25rem rgba(26,133,255,.25)}.form-check-input:checked{background-color:#1a85ff;border-color:#1a85ff}.form-check-input:checked[type=checkbox]{--bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{--bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#1a85ff;border-color:#1a85ff;--bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input[disabled]~.form-check-label,.form-check-input:disabled~.form-check-label{cursor:default;opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");width:2em;margin-left:-2.5em;background-image:var(--bs-form-switch-bg);background-position:left center;border-radius:0}.form-switch .form-check-input:focus{--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%238dc2ff'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-switch.form-check-reverse{padding-right:2.5em;padding-left:0}.form-switch.form-check-reverse .form-check-input{margin-right:-2.5em;margin-left:0}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0, 0, 0, 0);pointer-events:none}.btn-check[disabled]+.btn,.btn-check:disabled+.btn{pointer-events:none;filter:none;opacity:1}[data-bs-theme=dark] .form-switch .form-check-input:not(:checked):not(:focus){--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.25%29'/%3e%3c/svg%3e")}.form-range{width:100%;height:1.5rem;padding:0;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:rgba(0,0,0,0)}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(26,133,255,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(26,133,255,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-0.25rem;-webkit-appearance:none;appearance:none;background-color:#1a85ff;border:0}.form-range::-webkit-slider-thumb:active{background-color:#badaff}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:rgba(0,0,0,0);cursor:pointer;background-color:var(--bs-secondary-bg);border-color:rgba(0,0,0,0)}.form-range::-moz-range-thumb{width:1rem;height:1rem;-moz-appearance:none;appearance:none;background-color:#1a85ff;border:0}.form-range::-moz-range-thumb:active{background-color:#badaff}.form-range::-moz-range-track{width:100%;height:.5rem;color:rgba(0,0,0,0);cursor:pointer;background-color:var(--bs-secondary-bg);border-color:rgba(0,0,0,0)}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:var(--bs-secondary-color)}.form-range:disabled::-moz-range-thumb{background-color:var(--bs-secondary-color)}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-control-plaintext,.form-floating>.form-select{height:calc(3.5rem + calc(var(--bs-border-width) * 2));min-height:calc(3.5rem + calc(var(--bs-border-width) * 2));line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;z-index:2;height:100%;padding:1rem .5rem;overflow:hidden;text-align:start;text-overflow:ellipsis;white-space:nowrap;pointer-events:none;border:var(--bs-border-width) solid rgba(0,0,0,0);transform-origin:0 0}.form-floating>.form-control,.form-floating>.form-control-plaintext{padding:1rem .5rem}.form-floating>.form-control::-moz-placeholder, .form-floating>.form-control-plaintext::-moz-placeholder{color:rgba(0,0,0,0)}.form-floating>.form-control::placeholder,.form-floating>.form-control-plaintext::placeholder{color:rgba(0,0,0,0)}.form-floating>.form-control:not(:-moz-placeholder-shown), .form-floating>.form-control-plaintext:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown),.form-floating>.form-control-plaintext:focus,.form-floating>.form-control-plaintext:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:-webkit-autofill,.form-floating>.form-control-plaintext:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{color:rgba(var(--bs-body-color-rgb), 0.65);transform:scale(0.85) translateY(-0.5rem) translateX(0.15rem)}.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-control-plaintext~label,.form-floating>.form-select~label{color:rgba(var(--bs-body-color-rgb), 0.65);transform:scale(0.85) translateY(-0.5rem) translateX(0.15rem)}.form-floating>.form-control:not(:-moz-placeholder-shown)~label::after{position:absolute;inset:1rem .25rem;z-index:-1;height:1.5em;content:"";background-color:var(--bs-body-bg)}.form-floating>.form-control:focus~label::after,.form-floating>.form-control:not(:placeholder-shown)~label::after,.form-floating>.form-control-plaintext~label::after,.form-floating>.form-select~label::after{position:absolute;inset:1rem .25rem;z-index:-1;height:1.5em;content:"";background-color:var(--bs-body-bg)}.form-floating>.form-control:-webkit-autofill~label{color:rgba(var(--bs-body-color-rgb), 0.65);transform:scale(0.85) translateY(-0.5rem) translateX(0.15rem)}.form-floating>.form-control-plaintext~label{border-width:var(--bs-border-width) 0}.form-floating>:disabled~label,.form-floating>.form-control:disabled~label{color:#6c757d}.form-floating>:disabled~label::after,.form-floating>.form-control:disabled~label::after{background-color:var(--bs-secondary-bg)}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-select,.input-group>.form-floating{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-select:focus,.input-group>.form-floating:focus-within{z-index:5}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:5}.input-group-text{display:flex;align-items:center;padding:.25rem .5rem;font-size:0.875rem;font-weight:400;line-height:1.6;color:var(--bs-body-color);text-align:center;white-space:nowrap;background-color:var(--bs-tertiary-bg);border:var(--bs-border-width) solid var(--bs-border-color)}.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text,.input-group-lg>.btn{padding:.5rem 1rem;font-size:0.875rem}.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text,.input-group-sm>.btn{padding:.25rem .5rem;font-size:0.65625rem}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:2rem}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:calc(var(--bs-border-width)*-1)}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:0.875em;color:var(--bs-form-valid-color)}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:0.65625rem;color:#fff;background-color:var(--bs-success)}.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip,.is-valid~.valid-feedback,.is-valid~.valid-tooltip{display:block}.was-validated .form-control:valid,.form-control.is-valid{border-color:var(--bs-form-valid-border-color);padding-right:calc(1.6em + 0.5rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23689f38' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(0.4em + 0.125rem) center;background-size:calc(0.8em + 0.25rem) calc(0.8em + 0.25rem)}.was-validated .form-control:valid:focus,.form-control.is-valid:focus{border-color:var(--bs-form-valid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb), 0.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.6em + 0.5rem);background-position:top calc(0.4em + 0.125rem) right calc(0.4em + 0.125rem)}.was-validated .form-select:valid,.form-select.is-valid{border-color:var(--bs-form-valid-border-color)}.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"],.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"]{--bs-form-select-bg-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23689f38' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");padding-right:2.75rem;background-position:right .5rem center,center right 1.5rem;background-size:16px 12px,calc(0.8em + 0.25rem) calc(0.8em + 0.25rem)}.was-validated .form-select:valid:focus,.form-select.is-valid:focus{border-color:var(--bs-form-valid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb), 0.25)}.was-validated .form-control-color:valid,.form-control-color.is-valid{width:calc(3rem + calc(1.6em + 0.5rem))}.was-validated .form-check-input:valid,.form-check-input.is-valid{border-color:var(--bs-form-valid-border-color)}.was-validated .form-check-input:valid:checked,.form-check-input.is-valid:checked{background-color:var(--bs-form-valid-color)}.was-validated .form-check-input:valid:focus,.form-check-input.is-valid:focus{box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb), 0.25)}.was-validated .form-check-input:valid~.form-check-label,.form-check-input.is-valid~.form-check-label{color:var(--bs-form-valid-color)}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.was-validated .input-group>.form-control:not(:focus):valid,.input-group>.form-control:not(:focus).is-valid,.was-validated .input-group>.form-select:not(:focus):valid,.input-group>.form-select:not(:focus).is-valid,.was-validated .input-group>.form-floating:not(:focus-within):valid,.input-group>.form-floating:not(:focus-within).is-valid{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:0.875em;color:var(--bs-form-invalid-color)}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:0.65625rem;color:#fff;background-color:var(--bs-danger)}.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip,.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip{display:block}.was-validated .form-control:invalid,.form-control.is-invalid{border-color:var(--bs-form-invalid-border-color);padding-right:calc(1.6em + 0.5rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23ff5267'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23ff5267' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(0.4em + 0.125rem) center;background-size:calc(0.8em + 0.25rem) calc(0.8em + 0.25rem)}.was-validated .form-control:invalid:focus,.form-control.is-invalid:focus{border-color:var(--bs-form-invalid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb), 0.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.6em + 0.5rem);background-position:top calc(0.4em + 0.125rem) right calc(0.4em + 0.125rem)}.was-validated .form-select:invalid,.form-select.is-invalid{border-color:var(--bs-form-invalid-border-color)}.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"],.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"]{--bs-form-select-bg-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23ff5267'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23ff5267' stroke='none'/%3e%3c/svg%3e");padding-right:2.75rem;background-position:right .5rem center,center right 1.5rem;background-size:16px 12px,calc(0.8em + 0.25rem) calc(0.8em + 0.25rem)}.was-validated .form-select:invalid:focus,.form-select.is-invalid:focus{border-color:var(--bs-form-invalid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb), 0.25)}.was-validated .form-control-color:invalid,.form-control-color.is-invalid{width:calc(3rem + calc(1.6em + 0.5rem))}.was-validated .form-check-input:invalid,.form-check-input.is-invalid{border-color:var(--bs-form-invalid-border-color)}.was-validated .form-check-input:invalid:checked,.form-check-input.is-invalid:checked{background-color:var(--bs-form-invalid-color)}.was-validated .form-check-input:invalid:focus,.form-check-input.is-invalid:focus{box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb), 0.25)}.was-validated .form-check-input:invalid~.form-check-label,.form-check-input.is-invalid~.form-check-label{color:var(--bs-form-invalid-color)}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.was-validated .input-group>.form-control:not(:focus):invalid,.input-group>.form-control:not(:focus).is-invalid,.was-validated .input-group>.form-select:not(:focus):invalid,.input-group>.form-select:not(:focus).is-invalid,.was-validated .input-group>.form-floating:not(:focus-within):invalid,.input-group>.form-floating:not(:focus-within).is-invalid{z-index:4}.btn{--bs-btn-padding-x: 0.5rem;--bs-btn-padding-y: 0.25rem;--bs-btn-font-family: ;--bs-btn-font-size:0.875rem;--bs-btn-font-weight: 400;--bs-btn-line-height: 1.6;--bs-btn-color: var(--bs-body-color);--bs-btn-bg: transparent;--bs-btn-border-width: var(--bs-border-width);--bs-btn-border-color: transparent;--bs-btn-border-radius: var(--bs-border-radius);--bs-btn-hover-border-color: transparent;--bs-btn-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);--bs-btn-disabled-opacity: 1;--bs-btn-focus-box-shadow: 0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), .5);display:inline-block;padding:var(--bs-btn-padding-y) var(--bs-btn-padding-x);font-family:var(--bs-btn-font-family);font-size:var(--bs-btn-font-size);font-weight:var(--bs-btn-font-weight);line-height:var(--bs-btn-line-height);color:var(--bs-btn-color);text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;border:var(--bs-btn-border-width) solid var(--bs-btn-border-color);background-color:var(--bs-btn-bg)}.btn:hover{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color)}.btn-check+.btn:hover{color:var(--bs-btn-color);background-color:var(--bs-btn-bg);border-color:var(--bs-btn-border-color)}.btn:focus-visible{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:focus-visible+.btn{border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked+.btn,:not(.btn-check)+.btn:active,.btn:first-child:active,.btn.active,.btn.show{color:var(--bs-btn-active-color);background-color:var(--bs-btn-active-bg);border-color:var(--bs-btn-active-border-color)}.btn-check:checked+.btn:focus-visible,:not(.btn-check)+.btn:active:focus-visible,.btn:first-child:active:focus-visible,.btn.active:focus-visible,.btn.show:focus-visible{box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked:focus-visible+.btn{box-shadow:var(--bs-btn-focus-box-shadow)}.btn:disabled,.btn.disabled,fieldset:disabled .btn{color:var(--bs-btn-disabled-color);pointer-events:none;background-color:var(--bs-btn-disabled-bg);border-color:var(--bs-btn-disabled-border-color);opacity:var(--bs-btn-disabled-opacity)}.btn-primary{--bs-btn-color: #000;--bs-btn-bg: #1a85ff;--bs-btn-border-color: #1a85ff;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #3c97ff;--bs-btn-hover-border-color: #3191ff;--bs-btn-focus-shadow-rgb: 22, 113, 217;--bs-btn-active-color: #000;--bs-btn-active-bg: #489dff;--bs-btn-active-border-color: #3191ff;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #000;--bs-btn-disabled-bg: #1a85ff;--bs-btn-disabled-border-color: #1a85ff}.btn-secondary{--bs-btn-color: #fff;--bs-btn-bg: #6c757d;--bs-btn-border-color: #6c757d;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #5c636a;--bs-btn-hover-border-color: #565e64;--bs-btn-focus-shadow-rgb: 130, 138, 145;--bs-btn-active-color: #fff;--bs-btn-active-bg: #565e64;--bs-btn-active-border-color: #51585e;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #6c757d;--bs-btn-disabled-border-color: #6c757d}.btn-success{--bs-btn-color: #000;--bs-btn-bg: #689f38;--bs-btn-border-color: #689f38;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #7fad56;--bs-btn-hover-border-color: #77a94c;--bs-btn-focus-shadow-rgb: 88, 135, 48;--bs-btn-active-color: #000;--bs-btn-active-bg: #86b260;--bs-btn-active-border-color: #77a94c;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #000;--bs-btn-disabled-bg: #689f38;--bs-btn-disabled-border-color: #689f38}.btn-info{--bs-btn-color: #000;--bs-btn-bg: #00b4ff;--bs-btn-border-color: #00b4ff;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #26bfff;--bs-btn-hover-border-color: #1abcff;--bs-btn-focus-shadow-rgb: 0, 153, 217;--bs-btn-active-color: #000;--bs-btn-active-bg: #33c3ff;--bs-btn-active-border-color: #1abcff;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #000;--bs-btn-disabled-bg: #00b4ff;--bs-btn-disabled-border-color: #00b4ff}.btn-warning{--bs-btn-color: #000;--bs-btn-bg: #fdc935;--bs-btn-border-color: #fdc935;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #fdd153;--bs-btn-hover-border-color: #fdce49;--bs-btn-focus-shadow-rgb: 215, 171, 45;--bs-btn-active-color: #000;--bs-btn-active-bg: #fdd45d;--bs-btn-active-border-color: #fdce49;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #000;--bs-btn-disabled-bg: #fdc935;--bs-btn-disabled-border-color: #fdc935}.btn-danger{--bs-btn-color: #000;--bs-btn-bg: #ff5267;--bs-btn-border-color: #ff5267;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #ff6c7e;--bs-btn-hover-border-color: #ff6376;--bs-btn-focus-shadow-rgb: 217, 70, 88;--bs-btn-active-color: #000;--bs-btn-active-bg: #ff7585;--bs-btn-active-border-color: #ff6376;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #000;--bs-btn-disabled-bg: #ff5267;--bs-btn-disabled-border-color: #ff5267}.btn-light{--bs-btn-color: #000;--bs-btn-bg: #f8f9fa;--bs-btn-border-color: #f8f9fa;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #d3d4d5;--bs-btn-hover-border-color: #c6c7c8;--bs-btn-focus-shadow-rgb: 211, 212, 213;--bs-btn-active-color: #000;--bs-btn-active-bg: #c6c7c8;--bs-btn-active-border-color: #babbbc;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #000;--bs-btn-disabled-bg: #f8f9fa;--bs-btn-disabled-border-color: #f8f9fa}.btn-dark{--bs-btn-color: #fff;--bs-btn-bg: #212529;--bs-btn-border-color: #212529;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #424649;--bs-btn-hover-border-color: #373b3e;--bs-btn-focus-shadow-rgb: 66, 70, 73;--bs-btn-active-color: #fff;--bs-btn-active-bg: #4d5154;--bs-btn-active-border-color: #373b3e;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #212529;--bs-btn-disabled-border-color: #212529}.btn-outline-primary{--bs-btn-color: #1a85ff;--bs-btn-border-color: #1a85ff;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #1a85ff;--bs-btn-hover-border-color: #1a85ff;--bs-btn-focus-shadow-rgb: 26, 133, 255;--bs-btn-active-color: #000;--bs-btn-active-bg: #1a85ff;--bs-btn-active-border-color: #1a85ff;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #1a85ff;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #1a85ff;--bs-gradient: none}.btn-outline-secondary{--bs-btn-color: #6c757d;--bs-btn-border-color: #6c757d;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #6c757d;--bs-btn-hover-border-color: #6c757d;--bs-btn-focus-shadow-rgb: 108, 117, 125;--bs-btn-active-color: #fff;--bs-btn-active-bg: #6c757d;--bs-btn-active-border-color: #6c757d;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #6c757d;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #6c757d;--bs-gradient: none}.btn-outline-success{--bs-btn-color: #689f38;--bs-btn-border-color: #689f38;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #689f38;--bs-btn-hover-border-color: #689f38;--bs-btn-focus-shadow-rgb: 104, 159, 56;--bs-btn-active-color: #000;--bs-btn-active-bg: #689f38;--bs-btn-active-border-color: #689f38;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #689f38;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #689f38;--bs-gradient: none}.btn-outline-info{--bs-btn-color: #00b4ff;--bs-btn-border-color: #00b4ff;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #00b4ff;--bs-btn-hover-border-color: #00b4ff;--bs-btn-focus-shadow-rgb: 0, 180, 255;--bs-btn-active-color: #000;--bs-btn-active-bg: #00b4ff;--bs-btn-active-border-color: #00b4ff;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #00b4ff;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #00b4ff;--bs-gradient: none}.btn-outline-warning{--bs-btn-color: #fdc935;--bs-btn-border-color: #fdc935;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #fdc935;--bs-btn-hover-border-color: #fdc935;--bs-btn-focus-shadow-rgb: 253, 201, 53;--bs-btn-active-color: #000;--bs-btn-active-bg: #fdc935;--bs-btn-active-border-color: #fdc935;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #fdc935;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #fdc935;--bs-gradient: none}.btn-outline-danger{--bs-btn-color: #ff5267;--bs-btn-border-color: #ff5267;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #ff5267;--bs-btn-hover-border-color: #ff5267;--bs-btn-focus-shadow-rgb: 255, 82, 103;--bs-btn-active-color: #000;--bs-btn-active-bg: #ff5267;--bs-btn-active-border-color: #ff5267;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #ff5267;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #ff5267;--bs-gradient: none}.btn-outline-light{--bs-btn-color: #f8f9fa;--bs-btn-border-color: #f8f9fa;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #f8f9fa;--bs-btn-hover-border-color: #f8f9fa;--bs-btn-focus-shadow-rgb: 248, 249, 250;--bs-btn-active-color: #000;--bs-btn-active-bg: #f8f9fa;--bs-btn-active-border-color: #f8f9fa;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #f8f9fa;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #f8f9fa;--bs-gradient: none}.btn-outline-dark{--bs-btn-color: #212529;--bs-btn-border-color: #212529;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #212529;--bs-btn-hover-border-color: #212529;--bs-btn-focus-shadow-rgb: 33, 37, 41;--bs-btn-active-color: #fff;--bs-btn-active-bg: #212529;--bs-btn-active-border-color: #212529;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #212529;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #212529;--bs-gradient: none}.btn-link{--bs-btn-font-weight: 400;--bs-btn-color: var(--bs-link-color);--bs-btn-bg: transparent;--bs-btn-border-color: transparent;--bs-btn-hover-color: var(--bs-link-hover-color);--bs-btn-hover-border-color: transparent;--bs-btn-active-color: var(--bs-link-hover-color);--bs-btn-active-border-color: transparent;--bs-btn-disabled-color: #6c757d;--bs-btn-disabled-border-color: transparent;--bs-btn-box-shadow: 0 0 0 #000;--bs-btn-focus-shadow-rgb: 22, 113, 217;text-decoration:underline}.btn-link:focus-visible{color:var(--bs-btn-color)}.btn-link:hover{color:var(--bs-btn-hover-color)}.btn-lg,.btn-group-lg>.btn{--bs-btn-padding-y: 0.5rem;--bs-btn-padding-x: 1rem;--bs-btn-font-size:0.875rem;--bs-btn-border-radius: var(--bs-border-radius-lg)}.btn-sm,.btn-group-sm>.btn{--bs-btn-padding-y: 0.25rem;--bs-btn-padding-x: 0.5rem;--bs-btn-font-size:0.65625rem;--bs-btn-border-radius: var(--bs-border-radius-sm)}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden}.collapsing.collapse-horizontal{width:0;height:auto}.dropup,.dropend,.dropdown,.dropstart,.dropup-center,.dropdown-center{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid rgba(0,0,0,0);border-bottom:0;border-left:.3em solid rgba(0,0,0,0)}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{--bs-dropdown-zindex: 1000;--bs-dropdown-min-width: 10rem;--bs-dropdown-padding-x: 0;--bs-dropdown-padding-y: 0.5rem;--bs-dropdown-spacer: 0.125rem;--bs-dropdown-font-size:0.875rem;--bs-dropdown-color: var(--bs-body-color);--bs-dropdown-bg: var(--bs-body-bg);--bs-dropdown-border-color: var(--bs-border-color-translucent);--bs-dropdown-border-radius: var(--bs-border-radius);--bs-dropdown-border-width: var(--bs-border-width);--bs-dropdown-inner-border-radius: calc(var(--bs-border-radius) - var(--bs-border-width));--bs-dropdown-divider-bg: var(--bs-border-color-translucent);--bs-dropdown-divider-margin-y: 0.5rem;--bs-dropdown-box-shadow: var(--bs-box-shadow);--bs-dropdown-link-color: var(--bs-body-color);--bs-dropdown-link-hover-color: var(--bs-body-color);--bs-dropdown-link-hover-bg: var(--bs-tertiary-bg);--bs-dropdown-link-active-color: #fff;--bs-dropdown-link-active-bg: #1a85ff;--bs-dropdown-link-disabled-color: var(--bs-tertiary-color);--bs-dropdown-item-padding-x: 1rem;--bs-dropdown-item-padding-y: 0.25rem;--bs-dropdown-header-color: #6c757d;--bs-dropdown-header-padding-x: 1rem;--bs-dropdown-header-padding-y: 0.5rem;position:absolute;z-index:var(--bs-dropdown-zindex);display:none;min-width:var(--bs-dropdown-min-width);padding:var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x);margin:0;font-size:var(--bs-dropdown-font-size);color:var(--bs-dropdown-color);text-align:left;list-style:none;background-color:var(--bs-dropdown-bg);background-clip:padding-box;border:var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color)}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:var(--bs-dropdown-spacer)}.dropdown-menu-start{--bs-position: start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position: end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media(min-width: 576px){.dropdown-menu-sm-start{--bs-position: start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position: end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media(min-width: 768px){.dropdown-menu-md-start{--bs-position: start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position: end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media(min-width: 992px){.dropdown-menu-lg-start{--bs-position: start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position: end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media(min-width: 1200px){.dropdown-menu-xl-start{--bs-position: start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position: end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media(min-width: 1400px){.dropdown-menu-xxl-start{--bs-position: start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position: end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:var(--bs-dropdown-spacer)}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid rgba(0,0,0,0);border-bottom:.3em solid;border-left:.3em solid rgba(0,0,0,0)}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:var(--bs-dropdown-spacer)}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid rgba(0,0,0,0);border-right:0;border-bottom:.3em solid rgba(0,0,0,0);border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:var(--bs-dropdown-spacer)}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid rgba(0,0,0,0);border-right:.3em solid;border-bottom:.3em solid rgba(0,0,0,0)}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:var(--bs-dropdown-divider-margin-y) 0;overflow:hidden;border-top:1px solid var(--bs-dropdown-divider-bg);opacity:1}.dropdown-item{display:block;width:100%;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);clear:both;font-weight:400;color:var(--bs-dropdown-link-color);text-align:inherit;text-decoration:none;white-space:nowrap;background-color:rgba(0,0,0,0);border:0}.dropdown-item:hover,.dropdown-item:focus{color:var(--bs-dropdown-link-hover-color);background-color:var(--bs-dropdown-link-hover-bg)}.dropdown-item.active,.dropdown-item:active{color:var(--bs-dropdown-link-active-color);text-decoration:none;background-color:var(--bs-dropdown-link-active-bg)}.dropdown-item.disabled,.dropdown-item:disabled{color:var(--bs-dropdown-link-disabled-color);pointer-events:none;background-color:rgba(0,0,0,0)}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:var(--bs-dropdown-header-padding-y) var(--bs-dropdown-header-padding-x);margin-bottom:0;font-size:0.65625rem;color:var(--bs-dropdown-header-color);white-space:nowrap}.dropdown-item-text{display:block;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);color:var(--bs-dropdown-link-color)}.dropdown-menu-dark{--bs-dropdown-color: #dee2e6;--bs-dropdown-bg: #343a40;--bs-dropdown-border-color: var(--bs-border-color-translucent);--bs-dropdown-box-shadow: ;--bs-dropdown-link-color: #dee2e6;--bs-dropdown-link-hover-color: #fff;--bs-dropdown-divider-bg: var(--bs-border-color-translucent);--bs-dropdown-link-hover-bg: rgba(255, 255, 255, 0.15);--bs-dropdown-link-active-color: #fff;--bs-dropdown-link-active-bg: #1a85ff;--bs-dropdown-link-disabled-color: #adb5bd;--bs-dropdown-header-color: #adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;flex:1 1 auto}.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn:hover,.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn:hover,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn.active{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>:not(.btn-check:first-child)+.btn,.btn-group>.btn-group:not(:first-child){margin-left:calc(var(--bs-border-width)*-1)}.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-sm+.dropdown-toggle-split,.btn-group-sm>.btn+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-lg+.dropdown-toggle-split,.btn-group-lg>.btn+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn:not(:first-child),.btn-group-vertical>.btn-group:not(:first-child){margin-top:calc(var(--bs-border-width)*-1)}.nav{--bs-nav-link-padding-x: 0.5rem;--bs-nav-link-padding-y: 0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color: var(--bs-link-color);--bs-nav-link-hover-color: var(--bs-link-hover-color);--bs-nav-link-disabled-color: var(--bs-secondary-color);display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);font-size:var(--bs-nav-link-font-size);font-weight:var(--bs-nav-link-font-weight);color:var(--bs-nav-link-color);text-decoration:none;background:none;border:0}.nav-link:hover,.nav-link:focus{color:var(--bs-nav-link-hover-color)}.nav-link:focus-visible{outline:0;box-shadow:0 0 0 .25rem rgba(26,133,255,.25)}.nav-link.disabled,.nav-link:disabled{color:var(--bs-nav-link-disabled-color);pointer-events:none;cursor:default}.nav-tabs{--bs-nav-tabs-border-width: 0;--bs-nav-tabs-border-color: var(--bs-border-color);--bs-nav-tabs-border-radius: var(--bs-border-radius);--bs-nav-tabs-link-hover-border-color: var(--bs-secondary-bg) var(--bs-secondary-bg) var(--bs-border-color);--bs-nav-tabs-link-active-color: var(--bs-emphasis-color);--bs-nav-tabs-link-active-bg: var(--bs-body-bg);--bs-nav-tabs-link-active-border-color: var(--bs-border-color) var(--bs-border-color) var(--bs-body-bg);border-bottom:var(--bs-nav-tabs-border-width) solid var(--bs-nav-tabs-border-color)}.nav-tabs .nav-link{margin-bottom:calc(-1*var(--bs-nav-tabs-border-width));border:var(--bs-nav-tabs-border-width) solid rgba(0,0,0,0)}.nav-tabs .nav-link:hover,.nav-tabs .nav-link:focus{isolation:isolate;border-color:var(--bs-nav-tabs-link-hover-border-color)}.nav-tabs .nav-link.active,.nav-tabs .nav-item.show .nav-link{color:var(--bs-nav-tabs-link-active-color);background-color:var(--bs-nav-tabs-link-active-bg);border-color:var(--bs-nav-tabs-link-active-border-color)}.nav-tabs .dropdown-menu{margin-top:calc(-1*var(--bs-nav-tabs-border-width))}.nav-pills{--bs-nav-pills-border-radius: var(--bs-border-radius);--bs-nav-pills-link-active-color: #fff;--bs-nav-pills-link-active-bg: #1a85ff}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:var(--bs-nav-pills-link-active-color);background-color:var(--bs-nav-pills-link-active-bg)}.nav-underline{--bs-nav-underline-gap: 1rem;--bs-nav-underline-border-width: 0.125rem;--bs-nav-underline-link-active-color: var(--bs-emphasis-color);gap:var(--bs-nav-underline-gap)}.nav-underline .nav-link{padding-right:0;padding-left:0;border-bottom:var(--bs-nav-underline-border-width) solid rgba(0,0,0,0)}.nav-underline .nav-link:hover,.nav-underline .nav-link:focus{border-bottom-color:currentcolor}.nav-underline .nav-link.active,.nav-underline .show>.nav-link{font-weight:700;color:var(--bs-nav-underline-link-active-color);border-bottom-color:currentcolor}.nav-fill>.nav-link,.nav-fill .nav-item{flex:1 1 auto;text-align:center}.nav-justified>.nav-link,.nav-justified .nav-item{flex-basis:0;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{--bs-navbar-padding-x: 0;--bs-navbar-padding-y: 0.5rem;--bs-navbar-color: rgba(var(--bs-emphasis-color-rgb), 0.65);--bs-navbar-hover-color: rgba(var(--bs-emphasis-color-rgb), 0.8);--bs-navbar-disabled-color: rgba(var(--bs-emphasis-color-rgb), 0.3);--bs-navbar-active-color: rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-padding-y: 0.5rem;--bs-navbar-brand-margin-end: 1rem;--bs-navbar-brand-font-size: 0.875rem;--bs-navbar-brand-color: rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-hover-color: rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-nav-link-padding-x: 0.5rem;--bs-navbar-toggler-padding-y: 0.25rem;--bs-navbar-toggler-padding-x: 0.75rem;--bs-navbar-toggler-font-size: 0.875rem;--bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%2833, 37, 41, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");--bs-navbar-toggler-border-color: rgba(var(--bs-emphasis-color-rgb), 0.15);--bs-navbar-toggler-border-radius: var(--bs-border-radius);--bs-navbar-toggler-focus-width: 0.25rem;--bs-navbar-toggler-transition: box-shadow 0.15s ease-in-out;position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:var(--bs-navbar-padding-y) var(--bs-navbar-padding-x)}.navbar>.container,.navbar>.container-fluid,.navbar>.container-sm,.navbar>.container-md,.navbar>.container-lg,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:var(--bs-navbar-brand-padding-y);padding-bottom:var(--bs-navbar-brand-padding-y);margin-right:var(--bs-navbar-brand-margin-end);font-size:var(--bs-navbar-brand-font-size);color:var(--bs-navbar-brand-color);text-decoration:none;white-space:nowrap}.navbar-brand:hover,.navbar-brand:focus{color:var(--bs-navbar-brand-hover-color)}.navbar-nav{--bs-nav-link-padding-x: 0;--bs-nav-link-padding-y: 0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color: var(--bs-navbar-color);--bs-nav-link-hover-color: var(--bs-navbar-hover-color);--bs-nav-link-disabled-color: var(--bs-navbar-disabled-color);display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link.active,.navbar-nav .nav-link.show{color:var(--bs-navbar-active-color)}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-navbar-color)}.navbar-text a,.navbar-text a:hover,.navbar-text a:focus{color:var(--bs-navbar-active-color)}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:var(--bs-navbar-toggler-padding-y) var(--bs-navbar-toggler-padding-x);font-size:var(--bs-navbar-toggler-font-size);line-height:1;color:var(--bs-navbar-color);background-color:rgba(0,0,0,0);border:var(--bs-border-width) solid var(--bs-navbar-toggler-border-color)}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 var(--bs-navbar-toggler-focus-width)}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-image:var(--bs-navbar-toggler-icon-bg);background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height, 75vh);overflow-y:auto}@media(min-width: 576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex !important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto !important;height:auto !important;visibility:visible !important;background-color:rgba(0,0,0,0) !important;border:0 !important;transform:none !important}.navbar-expand-sm .offcanvas .offcanvas-header{display:none}.navbar-expand-sm .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media(min-width: 768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex !important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto !important;height:auto !important;visibility:visible !important;background-color:rgba(0,0,0,0) !important;border:0 !important;transform:none !important}.navbar-expand-md .offcanvas .offcanvas-header{display:none}.navbar-expand-md .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media(min-width: 992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex !important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto !important;height:auto !important;visibility:visible !important;background-color:rgba(0,0,0,0) !important;border:0 !important;transform:none !important}.navbar-expand-lg .offcanvas .offcanvas-header{display:none}.navbar-expand-lg .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media(min-width: 1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex !important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto !important;height:auto !important;visibility:visible !important;background-color:rgba(0,0,0,0) !important;border:0 !important;transform:none !important}.navbar-expand-xl .offcanvas .offcanvas-header{display:none}.navbar-expand-xl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media(min-width: 1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex !important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto !important;height:auto !important;visibility:visible !important;background-color:rgba(0,0,0,0) !important;border:0 !important;transform:none !important}.navbar-expand-xxl .offcanvas .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex !important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto !important;height:auto !important;visibility:visible !important;background-color:rgba(0,0,0,0) !important;border:0 !important;transform:none !important}.navbar-expand .offcanvas .offcanvas-header{display:none}.navbar-expand .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-dark,.navbar[data-bs-theme=dark]{--bs-navbar-color: rgba(255, 255, 255, 0.55);--bs-navbar-hover-color: rgba(255, 255, 255, 0.75);--bs-navbar-disabled-color: rgba(255, 255, 255, 0.25);--bs-navbar-active-color: #fff;--bs-navbar-brand-color: #fff;--bs-navbar-brand-hover-color: #fff;--bs-navbar-toggler-border-color: rgba(255, 255, 255, 0.1);--bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}[data-bs-theme=dark] .navbar-toggler-icon{--bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.card{--bs-card-spacer-y: 1rem;--bs-card-spacer-x: 1rem;--bs-card-title-spacer-y: 0.5rem;--bs-card-title-color: ;--bs-card-subtitle-color: ;--bs-card-border-width: var(--bs-border-width);--bs-card-border-color: var(--bs-border-color-translucent);--bs-card-border-radius: var(--bs-border-radius);--bs-card-box-shadow: ;--bs-card-inner-border-radius: calc(var(--bs-border-radius) - (var(--bs-border-width)));--bs-card-cap-padding-y: 0.5rem;--bs-card-cap-padding-x: 1rem;--bs-card-cap-bg: rgba(var(--bs-body-color-rgb), 0.03);--bs-card-cap-color: ;--bs-card-height: ;--bs-card-color: ;--bs-card-bg: var(--bs-body-bg);--bs-card-img-overlay-padding: 1rem;--bs-card-group-margin: 0.75rem;position:relative;display:flex;flex-direction:column;min-width:0;height:var(--bs-card-height);color:var(--bs-body-color);word-wrap:break-word;background-color:var(--bs-card-bg);background-clip:border-box;border:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0}.card>.list-group:last-child{border-bottom-width:0}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:var(--bs-card-spacer-y) var(--bs-card-spacer-x);color:var(--bs-card-color)}.card-title{margin-bottom:var(--bs-card-title-spacer-y);color:var(--bs-card-title-color)}.card-subtitle{margin-top:calc(-0.5*var(--bs-card-title-spacer-y));margin-bottom:0;color:var(--bs-card-subtitle-color)}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:var(--bs-card-spacer-x)}.card-header{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);margin-bottom:0;color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-bottom:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-footer{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-top:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-header-tabs{margin-right:calc(-0.5*var(--bs-card-cap-padding-x));margin-bottom:calc(-1*var(--bs-card-cap-padding-y));margin-left:calc(-0.5*var(--bs-card-cap-padding-x));border-bottom:0}.card-header-tabs .nav-link.active{background-color:var(--bs-card-bg);border-bottom-color:var(--bs-card-bg)}.card-header-pills{margin-right:calc(-0.5*var(--bs-card-cap-padding-x));margin-left:calc(-0.5*var(--bs-card-cap-padding-x))}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:var(--bs-card-img-overlay-padding)}.card-img,.card-img-top,.card-img-bottom{width:100%}.card-group>.card{margin-bottom:var(--bs-card-group-margin)}@media(min-width: 576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}}.accordion{--bs-accordion-color: var(--bs-body-color);--bs-accordion-bg: transparent;--bs-accordion-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, border-radius 0.15s ease;--bs-accordion-border-color: var(--bs-border-color);--bs-accordion-border-width: 0;--bs-accordion-border-radius: var(--bs-border-radius);--bs-accordion-inner-border-radius: calc(var(--bs-border-radius) - 0);--bs-accordion-btn-padding-x: 0;--bs-accordion-btn-padding-y: 1rem;--bs-accordion-btn-color: var(--bs-body-color);--bs-accordion-btn-bg: var(--bs-accordion-bg);--bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23212529' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M2 5L8 11L14 5'/%3e%3c/svg%3e");--bs-accordion-btn-icon-width: 0.75rem;--bs-accordion-btn-icon-transform: rotate(-180deg);--bs-accordion-btn-icon-transition: transform 0.2s ease-in-out;--bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%230a3566' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M2 5L8 11L14 5'/%3e%3c/svg%3e");--bs-accordion-btn-focus-box-shadow: 0 0 0 0.25rem rgba(26, 133, 255, 0.25);--bs-accordion-body-padding-x: 0;--bs-accordion-body-padding-y: 0;--bs-accordion-active-color: var(--bs-primary-text-emphasis);--bs-accordion-active-bg: var(--bs-primary-bg-subtle)}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:var(--bs-accordion-btn-padding-y) var(--bs-accordion-btn-padding-x);font-size:0.875rem;color:var(--bs-accordion-btn-color);text-align:left;background-color:var(--bs-accordion-btn-bg);border:0;overflow-anchor:none}.accordion-button:not(.collapsed){color:var(--bs-accordion-active-color);background-color:var(--bs-accordion-active-bg);box-shadow:inset 0 calc(-1*var(--bs-accordion-border-width)) 0 var(--bs-accordion-border-color)}.accordion-button:not(.collapsed)::after{background-image:var(--bs-accordion-btn-active-icon);transform:var(--bs-accordion-btn-icon-transform)}.accordion-button::after{flex-shrink:0;width:var(--bs-accordion-btn-icon-width);height:var(--bs-accordion-btn-icon-width);margin-left:auto;content:"";background-image:var(--bs-accordion-btn-icon);background-repeat:no-repeat;background-size:var(--bs-accordion-btn-icon-width)}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;outline:0;box-shadow:var(--bs-accordion-btn-focus-box-shadow)}.accordion-header{margin-bottom:0}.accordion-item{color:var(--bs-accordion-color);background-color:var(--bs-accordion-bg);border:var(--bs-accordion-border-width) solid var(--bs-accordion-border-color)}.accordion-item:not(:first-of-type){border-top:0}.accordion-body{padding:var(--bs-accordion-body-padding-y) var(--bs-accordion-body-padding-x)}.accordion-flush>.accordion-item{border-right:0;border-left:0}.accordion-flush>.accordion-item:first-child{border-top:0}.accordion-flush>.accordion-item:last-child{border-bottom:0}[data-bs-theme=dark] .accordion-button::after{--bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%2376b6ff'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");--bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%2376b6ff'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.breadcrumb{--bs-breadcrumb-padding-x: 0;--bs-breadcrumb-padding-y: 0;--bs-breadcrumb-margin-bottom: 1rem;--bs-breadcrumb-bg: ;--bs-breadcrumb-border-radius: ;--bs-breadcrumb-divider-color: var(--bs-secondary-color);--bs-breadcrumb-item-padding-x: 0.5rem;--bs-breadcrumb-item-active-color: var(--bs-secondary-color);display:flex;flex-wrap:wrap;padding:var(--bs-breadcrumb-padding-y) var(--bs-breadcrumb-padding-x);margin-bottom:var(--bs-breadcrumb-margin-bottom);font-size:var(--bs-breadcrumb-font-size);list-style:none;background-color:var(--bs-breadcrumb-bg)}.breadcrumb-item+.breadcrumb-item{padding-left:var(--bs-breadcrumb-item-padding-x)}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:var(--bs-breadcrumb-item-padding-x);color:var(--bs-breadcrumb-divider-color);content:var(--bs-breadcrumb-divider, "/") /* rtl: var(--bs-breadcrumb-divider, "/") */}.breadcrumb-item.active{color:var(--bs-breadcrumb-item-active-color)}.pagination{--bs-pagination-padding-x: 0.75rem;--bs-pagination-padding-y: 0.375rem;--bs-pagination-font-size:0.875rem;--bs-pagination-color: var(--bs-link-color);--bs-pagination-bg: var(--bs-body-bg);--bs-pagination-border-width: var(--bs-border-width);--bs-pagination-border-color: var(--bs-border-color);--bs-pagination-border-radius: var(--bs-border-radius);--bs-pagination-hover-color: var(--bs-link-hover-color);--bs-pagination-hover-bg: var(--bs-tertiary-bg);--bs-pagination-hover-border-color: var(--bs-border-color);--bs-pagination-focus-color: var(--bs-link-hover-color);--bs-pagination-focus-bg: var(--bs-secondary-bg);--bs-pagination-focus-box-shadow: 0 0 0 0.25rem rgba(26, 133, 255, 0.25);--bs-pagination-active-color: #fff;--bs-pagination-active-bg: #1a85ff;--bs-pagination-active-border-color: #1a85ff;--bs-pagination-disabled-color: var(--bs-secondary-color);--bs-pagination-disabled-bg: var(--bs-secondary-bg);--bs-pagination-disabled-border-color: var(--bs-border-color);display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;padding:var(--bs-pagination-padding-y) var(--bs-pagination-padding-x);font-size:var(--bs-pagination-font-size);color:var(--bs-pagination-color);text-decoration:none;background-color:var(--bs-pagination-bg);border:var(--bs-pagination-border-width) solid var(--bs-pagination-border-color)}.page-link:hover{z-index:2;color:var(--bs-pagination-hover-color);background-color:var(--bs-pagination-hover-bg);border-color:var(--bs-pagination-hover-border-color)}.page-link:focus{z-index:3;color:var(--bs-pagination-focus-color);background-color:var(--bs-pagination-focus-bg);outline:0;box-shadow:var(--bs-pagination-focus-box-shadow)}.page-link.active,.active>.page-link{z-index:3;color:var(--bs-pagination-active-color);background-color:var(--bs-pagination-active-bg);border-color:var(--bs-pagination-active-border-color)}.page-link.disabled,.disabled>.page-link{color:var(--bs-pagination-disabled-color);pointer-events:none;background-color:var(--bs-pagination-disabled-bg);border-color:var(--bs-pagination-disabled-border-color)}.page-item:not(:first-child) .page-link{margin-left:calc(var(--bs-border-width)*-1)}.pagination-lg{--bs-pagination-padding-x: 1.5rem;--bs-pagination-padding-y: 0.75rem;--bs-pagination-font-size:0.875rem;--bs-pagination-border-radius: var(--bs-border-radius-lg)}.pagination-sm{--bs-pagination-padding-x: 0.5rem;--bs-pagination-padding-y: 0.25rem;--bs-pagination-font-size:0.65625rem;--bs-pagination-border-radius: var(--bs-border-radius-sm)}.badge{--bs-badge-padding-x: 0.65em;--bs-badge-padding-y: 0.35em;--bs-badge-font-size:0.75em;--bs-badge-font-weight: 700;--bs-badge-color: #fff;--bs-badge-border-radius: var(--bs-border-radius);display:inline-block;padding:var(--bs-badge-padding-y) var(--bs-badge-padding-x);font-size:var(--bs-badge-font-size);font-weight:var(--bs-badge-font-weight);line-height:1;color:var(--bs-badge-color);text-align:center;white-space:nowrap;vertical-align:baseline}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{--bs-alert-bg: transparent;--bs-alert-padding-x: 1rem;--bs-alert-padding-y: 1rem;--bs-alert-margin-bottom: 1rem;--bs-alert-color: inherit;--bs-alert-border-color: transparent;--bs-alert-border: var(--bs-border-width) solid var(--bs-alert-border-color);--bs-alert-border-radius: var(--bs-border-radius);--bs-alert-link-color: inherit;position:relative;padding:var(--bs-alert-padding-y) var(--bs-alert-padding-x);margin-bottom:var(--bs-alert-margin-bottom);color:var(--bs-alert-color);background-color:var(--bs-alert-bg);border:var(--bs-alert-border)}.alert-heading{color:inherit}.alert-link{font-weight:700;color:var(--bs-alert-link-color)}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{--bs-alert-color: var(--bs-primary-text-emphasis);--bs-alert-bg: var(--bs-primary-bg-subtle);--bs-alert-border-color: var(--bs-primary-border-subtle);--bs-alert-link-color: var(--bs-primary-text-emphasis)}.alert-secondary{--bs-alert-color: var(--bs-secondary-text-emphasis);--bs-alert-bg: var(--bs-secondary-bg-subtle);--bs-alert-border-color: var(--bs-secondary-border-subtle);--bs-alert-link-color: var(--bs-secondary-text-emphasis)}.alert-success{--bs-alert-color: var(--bs-success-text-emphasis);--bs-alert-bg: var(--bs-success-bg-subtle);--bs-alert-border-color: var(--bs-success-border-subtle);--bs-alert-link-color: var(--bs-success-text-emphasis)}.alert-info{--bs-alert-color: var(--bs-info-text-emphasis);--bs-alert-bg: var(--bs-info-bg-subtle);--bs-alert-border-color: var(--bs-info-border-subtle);--bs-alert-link-color: var(--bs-info-text-emphasis)}.alert-warning{--bs-alert-color: var(--bs-warning-text-emphasis);--bs-alert-bg: var(--bs-warning-bg-subtle);--bs-alert-border-color: var(--bs-warning-border-subtle);--bs-alert-link-color: var(--bs-warning-text-emphasis)}.alert-danger{--bs-alert-color: var(--bs-danger-text-emphasis);--bs-alert-bg: var(--bs-danger-bg-subtle);--bs-alert-border-color: var(--bs-danger-border-subtle);--bs-alert-link-color: var(--bs-danger-text-emphasis)}.alert-light{--bs-alert-color: var(--bs-light-text-emphasis);--bs-alert-bg: var(--bs-light-bg-subtle);--bs-alert-border-color: var(--bs-light-border-subtle);--bs-alert-link-color: var(--bs-light-text-emphasis)}.alert-dark{--bs-alert-color: var(--bs-dark-text-emphasis);--bs-alert-bg: var(--bs-dark-bg-subtle);--bs-alert-border-color: var(--bs-dark-border-subtle);--bs-alert-link-color: var(--bs-dark-text-emphasis)}.progress,.progress-stacked{--bs-progress-height: 1rem;--bs-progress-font-size:0.65625rem;--bs-progress-bg: var(--bs-secondary-bg);--bs-progress-border-radius: var(--bs-border-radius);--bs-progress-box-shadow: var(--bs-box-shadow-inset);--bs-progress-bar-color: #fff;--bs-progress-bar-bg: #1a85ff;--bs-progress-bar-transition: width 0.6s ease;display:flex;height:var(--bs-progress-height);overflow:hidden;font-size:var(--bs-progress-font-size);background-color:var(--bs-progress-bg)}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:var(--bs-progress-bar-color);text-align:center;white-space:nowrap;background-color:var(--bs-progress-bar-bg)}.progress-bar-striped{background-image:linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-size:var(--bs-progress-height) var(--bs-progress-height)}.progress-stacked>.progress{overflow:visible}.progress-stacked>.progress>.progress-bar{width:100%}.list-group{--bs-list-group-color: var(--bs-body-color);--bs-list-group-bg: var(--bs-body-bg);--bs-list-group-border-color: var(--bs-border-color);--bs-list-group-border-width: var(--bs-border-width);--bs-list-group-border-radius: var(--bs-border-radius);--bs-list-group-item-padding-x: 1rem;--bs-list-group-item-padding-y: 0.5rem;--bs-list-group-action-color: var(--bs-secondary-color);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-tertiary-bg);--bs-list-group-action-active-color: var(--bs-body-color);--bs-list-group-action-active-bg: var(--bs-secondary-bg);--bs-list-group-disabled-color: var(--bs-secondary-color);--bs-list-group-disabled-bg: var(--bs-body-bg);--bs-list-group-active-color: #fff;--bs-list-group-active-bg: #1a85ff;--bs-list-group-active-border-color: #1a85ff;display:flex;flex-direction:column;padding-left:0;margin-bottom:0}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>.list-group-item::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:var(--bs-list-group-action-color);text-align:inherit}.list-group-item-action:hover,.list-group-item-action:focus{z-index:1;color:var(--bs-list-group-action-hover-color);text-decoration:none;background-color:var(--bs-list-group-action-hover-bg)}.list-group-item-action:active{color:var(--bs-list-group-action-active-color);background-color:var(--bs-list-group-action-active-bg)}.list-group-item{position:relative;display:block;padding:var(--bs-list-group-item-padding-y) var(--bs-list-group-item-padding-x);color:var(--bs-list-group-color);text-decoration:none;background-color:var(--bs-list-group-bg);border:var(--bs-list-group-border-width) solid var(--bs-list-group-border-color)}.list-group-item.disabled,.list-group-item:disabled{color:var(--bs-list-group-disabled-color);pointer-events:none;background-color:var(--bs-list-group-disabled-bg)}.list-group-item.active{z-index:2;color:var(--bs-list-group-active-color);background-color:var(--bs-list-group-active-bg);border-color:var(--bs-list-group-active-border-color)}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:calc(-1*var(--bs-list-group-border-width));border-top-width:var(--bs-list-group-border-width)}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:calc(-1*var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}@media(min-width: 576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:calc(-1*var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media(min-width: 768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:calc(-1*var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media(min-width: 992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:calc(-1*var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media(min-width: 1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:calc(-1*var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media(min-width: 1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:calc(-1*var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}.list-group-flush>.list-group-item{border-width:0 0 var(--bs-list-group-border-width)}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{--bs-list-group-color: var(--bs-primary-text-emphasis);--bs-list-group-bg: var(--bs-primary-bg-subtle);--bs-list-group-border-color: var(--bs-primary-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-primary-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-primary-border-subtle);--bs-list-group-active-color: var(--bs-primary-bg-subtle);--bs-list-group-active-bg: var(--bs-primary-text-emphasis);--bs-list-group-active-border-color: var(--bs-primary-text-emphasis)}.list-group-item-secondary{--bs-list-group-color: var(--bs-secondary-text-emphasis);--bs-list-group-bg: var(--bs-secondary-bg-subtle);--bs-list-group-border-color: var(--bs-secondary-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-secondary-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-secondary-border-subtle);--bs-list-group-active-color: var(--bs-secondary-bg-subtle);--bs-list-group-active-bg: var(--bs-secondary-text-emphasis);--bs-list-group-active-border-color: var(--bs-secondary-text-emphasis)}.list-group-item-success{--bs-list-group-color: var(--bs-success-text-emphasis);--bs-list-group-bg: var(--bs-success-bg-subtle);--bs-list-group-border-color: var(--bs-success-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-success-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-success-border-subtle);--bs-list-group-active-color: var(--bs-success-bg-subtle);--bs-list-group-active-bg: var(--bs-success-text-emphasis);--bs-list-group-active-border-color: var(--bs-success-text-emphasis)}.list-group-item-info{--bs-list-group-color: var(--bs-info-text-emphasis);--bs-list-group-bg: var(--bs-info-bg-subtle);--bs-list-group-border-color: var(--bs-info-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-info-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-info-border-subtle);--bs-list-group-active-color: var(--bs-info-bg-subtle);--bs-list-group-active-bg: var(--bs-info-text-emphasis);--bs-list-group-active-border-color: var(--bs-info-text-emphasis)}.list-group-item-warning{--bs-list-group-color: var(--bs-warning-text-emphasis);--bs-list-group-bg: var(--bs-warning-bg-subtle);--bs-list-group-border-color: var(--bs-warning-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-warning-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-warning-border-subtle);--bs-list-group-active-color: var(--bs-warning-bg-subtle);--bs-list-group-active-bg: var(--bs-warning-text-emphasis);--bs-list-group-active-border-color: var(--bs-warning-text-emphasis)}.list-group-item-danger{--bs-list-group-color: var(--bs-danger-text-emphasis);--bs-list-group-bg: var(--bs-danger-bg-subtle);--bs-list-group-border-color: var(--bs-danger-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-danger-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-danger-border-subtle);--bs-list-group-active-color: var(--bs-danger-bg-subtle);--bs-list-group-active-bg: var(--bs-danger-text-emphasis);--bs-list-group-active-border-color: var(--bs-danger-text-emphasis)}.list-group-item-light{--bs-list-group-color: var(--bs-light-text-emphasis);--bs-list-group-bg: var(--bs-light-bg-subtle);--bs-list-group-border-color: var(--bs-light-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-light-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-light-border-subtle);--bs-list-group-active-color: var(--bs-light-bg-subtle);--bs-list-group-active-bg: var(--bs-light-text-emphasis);--bs-list-group-active-border-color: var(--bs-light-text-emphasis)}.list-group-item-dark{--bs-list-group-color: var(--bs-dark-text-emphasis);--bs-list-group-bg: var(--bs-dark-bg-subtle);--bs-list-group-border-color: var(--bs-dark-border-subtle);--bs-list-group-action-hover-color: var(--bs-emphasis-color);--bs-list-group-action-hover-bg: var(--bs-dark-border-subtle);--bs-list-group-action-active-color: var(--bs-emphasis-color);--bs-list-group-action-active-bg: var(--bs-dark-border-subtle);--bs-list-group-active-color: var(--bs-dark-bg-subtle);--bs-list-group-active-bg: var(--bs-dark-text-emphasis);--bs-list-group-active-border-color: var(--bs-dark-text-emphasis)}.btn-close{--bs-btn-close-color: #000;--bs-btn-close-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3e%3c/svg%3e");--bs-btn-close-opacity: 0.5;--bs-btn-close-hover-opacity: 0.75;--bs-btn-close-focus-shadow: 0 0 0 0.25rem rgba(26, 133, 255, 0.25);--bs-btn-close-focus-opacity: 1;--bs-btn-close-disabled-opacity: 0.25;--bs-btn-close-white-filter: invert(1) grayscale(100%) brightness(200%);box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:var(--bs-btn-close-color);background:rgba(0,0,0,0) var(--bs-btn-close-bg) center/1em auto no-repeat;border:0;opacity:var(--bs-btn-close-opacity)}.btn-close:hover{color:var(--bs-btn-close-color);text-decoration:none;opacity:var(--bs-btn-close-hover-opacity)}.btn-close:focus{outline:0;box-shadow:var(--bs-btn-close-focus-shadow);opacity:var(--bs-btn-close-focus-opacity)}.btn-close:disabled,.btn-close.disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:var(--bs-btn-close-disabled-opacity)}.btn-close-white{filter:var(--bs-btn-close-white-filter)}[data-bs-theme=dark] .btn-close{filter:var(--bs-btn-close-white-filter)}.toast{--bs-toast-zindex: 1090;--bs-toast-padding-x: 0.75rem;--bs-toast-padding-y: 0.5rem;--bs-toast-spacing: 1.5rem;--bs-toast-max-width: 350px;--bs-toast-font-size:0.875rem;--bs-toast-color: ;--bs-toast-bg: rgba(var(--bs-body-bg-rgb), 0.85);--bs-toast-border-width: var(--bs-border-width);--bs-toast-border-color: var(--bs-border-color-translucent);--bs-toast-border-radius: var(--bs-border-radius);--bs-toast-box-shadow: var(--bs-box-shadow);--bs-toast-header-color: var(--bs-secondary-color);--bs-toast-header-bg: rgba(var(--bs-body-bg-rgb), 0.85);--bs-toast-header-border-color: var(--bs-border-color-translucent);width:var(--bs-toast-max-width);max-width:100%;font-size:var(--bs-toast-font-size);color:var(--bs-toast-color);pointer-events:auto;background-color:var(--bs-toast-bg);background-clip:padding-box;border:var(--bs-toast-border-width) solid var(--bs-toast-border-color);box-shadow:var(--bs-toast-box-shadow)}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{--bs-toast-zindex: 1090;position:absolute;z-index:var(--bs-toast-zindex);width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:var(--bs-toast-spacing)}.toast-header{display:flex;align-items:center;padding:var(--bs-toast-padding-y) var(--bs-toast-padding-x);color:var(--bs-toast-header-color);background-color:var(--bs-toast-header-bg);background-clip:padding-box;border-bottom:var(--bs-toast-border-width) solid var(--bs-toast-header-border-color)}.toast-header .btn-close{margin-right:calc(-0.5*var(--bs-toast-padding-x));margin-left:var(--bs-toast-padding-x)}.toast-body{padding:var(--bs-toast-padding-x);word-wrap:break-word}.modal{--bs-modal-zindex: 1055;--bs-modal-width: 500px;--bs-modal-padding: 1rem;--bs-modal-margin: 0.5rem;--bs-modal-color: ;--bs-modal-bg: var(--bs-body-bg);--bs-modal-border-color: var(--bs-border-color-translucent);--bs-modal-border-width: var(--bs-border-width);--bs-modal-border-radius: var(--bs-border-radius-lg);--bs-modal-box-shadow: var(--bs-box-shadow-sm);--bs-modal-inner-border-radius: calc(var(--bs-border-radius-lg) - (var(--bs-border-width)));--bs-modal-header-padding-x: 1rem;--bs-modal-header-padding-y: 1rem;--bs-modal-header-padding: 1rem 1rem;--bs-modal-header-border-color: var(--bs-border-color);--bs-modal-header-border-width: var(--bs-border-width);--bs-modal-title-line-height: 1.5;--bs-modal-footer-gap: 0.5rem;--bs-modal-footer-bg: ;--bs-modal-footer-border-color: var(--bs-border-color);--bs-modal-footer-border-width: var(--bs-border-width);position:fixed;top:0;left:0;z-index:var(--bs-modal-zindex);display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:var(--bs-modal-margin);pointer-events:none}.modal.fade .modal-dialog{transform:translate(0, -50px)}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - var(--bs-modal-margin)*2)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - var(--bs-modal-margin)*2)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;color:var(--bs-modal-color);pointer-events:auto;background-color:var(--bs-modal-bg);background-clip:padding-box;border:var(--bs-modal-border-width) solid var(--bs-modal-border-color);outline:0}.modal-backdrop{--bs-backdrop-zindex: 1050;--bs-backdrop-bg: #000;--bs-backdrop-opacity: 0.5;position:fixed;top:0;left:0;z-index:var(--bs-backdrop-zindex);width:100vw;height:100vh;background-color:var(--bs-backdrop-bg)}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:var(--bs-backdrop-opacity)}.modal-header{display:flex;flex-shrink:0;align-items:center;padding:var(--bs-modal-header-padding);border-bottom:var(--bs-modal-header-border-width) solid var(--bs-modal-header-border-color)}.modal-header .btn-close{padding:calc(var(--bs-modal-header-padding-y)*.5) calc(var(--bs-modal-header-padding-x)*.5);margin:calc(-0.5*var(--bs-modal-header-padding-y)) calc(-0.5*var(--bs-modal-header-padding-x)) calc(-0.5*var(--bs-modal-header-padding-y)) auto}.modal-title{margin-bottom:0;line-height:var(--bs-modal-title-line-height)}.modal-body{position:relative;flex:1 1 auto;padding:var(--bs-modal-padding)}.modal-footer{display:flex;flex-shrink:0;flex-wrap:wrap;align-items:center;justify-content:flex-end;padding:calc(var(--bs-modal-padding) - var(--bs-modal-footer-gap)*.5);background-color:var(--bs-modal-footer-bg);border-top:var(--bs-modal-footer-border-width) solid var(--bs-modal-footer-border-color)}.modal-footer>*{margin:calc(var(--bs-modal-footer-gap)*.5)}@media(min-width: 576px){.modal{--bs-modal-margin: 1.75rem;--bs-modal-box-shadow: var(--bs-box-shadow)}.modal-dialog{max-width:var(--bs-modal-width);margin-right:auto;margin-left:auto}.modal-sm{--bs-modal-width: 300px}}@media(min-width: 992px){.modal-lg,.modal-xl{--bs-modal-width: 800px}}@media(min-width: 1200px){.modal-xl{--bs-modal-width: 1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0}.modal-fullscreen .modal-body{overflow-y:auto}@media(max-width: 575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}}@media(max-width: 767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}}@media(max-width: 991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}}@media(max-width: 1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}}@media(max-width: 1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}}.tooltip{--bs-tooltip-zindex: 1080;--bs-tooltip-max-width: 180px;--bs-tooltip-padding-x: 0.5rem;--bs-tooltip-padding-y: 0.25rem;--bs-tooltip-margin: ;--bs-tooltip-font-size:0.65625rem;--bs-tooltip-color: var(--bs-body-bg);--bs-tooltip-bg: var(--bs-emphasis-color);--bs-tooltip-border-radius: var(--bs-border-radius);--bs-tooltip-opacity: 1;--bs-tooltip-arrow-width: 0.8rem;--bs-tooltip-arrow-height: 0.4rem;z-index:var(--bs-tooltip-zindex);display:block;margin:var(--bs-tooltip-margin);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-tooltip-font-size);word-wrap:break-word;opacity:0}.tooltip.show{opacity:var(--bs-tooltip-opacity)}.tooltip .tooltip-arrow{display:block;width:var(--bs-tooltip-arrow-width);height:var(--bs-tooltip-arrow-height)}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:rgba(0,0,0,0);border-style:solid}.bs-tooltip-top .tooltip-arrow,.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow{bottom:calc(-1*var(--bs-tooltip-arrow-height))}.bs-tooltip-top .tooltip-arrow::before,.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before{top:-1px;border-width:var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width)*.5) 0;border-top-color:var(--bs-tooltip-bg)}.bs-tooltip-end .tooltip-arrow,.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow{left:calc(-1*var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-end .tooltip-arrow::before,.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before{right:-1px;border-width:calc(var(--bs-tooltip-arrow-width)*.5) var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width)*.5) 0;border-right-color:var(--bs-tooltip-bg)}.bs-tooltip-bottom .tooltip-arrow,.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow{top:calc(-1*var(--bs-tooltip-arrow-height))}.bs-tooltip-bottom .tooltip-arrow::before,.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before{bottom:-1px;border-width:0 calc(var(--bs-tooltip-arrow-width)*.5) var(--bs-tooltip-arrow-height);border-bottom-color:var(--bs-tooltip-bg)}.bs-tooltip-start .tooltip-arrow,.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow{right:calc(-1*var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-start .tooltip-arrow::before,.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before{left:-1px;border-width:calc(var(--bs-tooltip-arrow-width)*.5) 0 calc(var(--bs-tooltip-arrow-width)*.5) var(--bs-tooltip-arrow-height);border-left-color:var(--bs-tooltip-bg)}.tooltip-inner{max-width:var(--bs-tooltip-max-width);padding:var(--bs-tooltip-padding-y) var(--bs-tooltip-padding-x);color:var(--bs-tooltip-color);text-align:center;background-color:var(--bs-tooltip-bg)}.popover{--bs-popover-zindex: 1070;--bs-popover-max-width: 276px;--bs-popover-font-size:0.65625rem;--bs-popover-bg: var(--bs-body-bg);--bs-popover-border-width: var(--bs-border-width);--bs-popover-border-color: var(--bs-border-color-translucent);--bs-popover-border-radius: var(--bs-border-radius-lg);--bs-popover-inner-border-radius: calc(var(--bs-border-radius-lg) - var(--bs-border-width));--bs-popover-box-shadow: var(--bs-box-shadow);--bs-popover-header-padding-x: 1rem;--bs-popover-header-padding-y: 0.5rem;--bs-popover-header-font-size:0.875rem;--bs-popover-header-color: inherit;--bs-popover-header-bg: var(--bs-secondary-bg);--bs-popover-body-padding-x: 1rem;--bs-popover-body-padding-y: 1rem;--bs-popover-body-color: var(--bs-body-color);--bs-popover-arrow-width: 1rem;--bs-popover-arrow-height: 0.5rem;--bs-popover-arrow-border: var(--bs-popover-border-color);z-index:var(--bs-popover-zindex);display:block;max-width:var(--bs-popover-max-width);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-popover-font-size);word-wrap:break-word;background-color:var(--bs-popover-bg);background-clip:padding-box;border:var(--bs-popover-border-width) solid var(--bs-popover-border-color)}.popover .popover-arrow{display:block;width:var(--bs-popover-arrow-width);height:var(--bs-popover-arrow-height)}.popover .popover-arrow::before,.popover .popover-arrow::after{position:absolute;display:block;content:"";border-color:rgba(0,0,0,0);border-style:solid;border-width:0}.bs-popover-top>.popover-arrow,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow{bottom:calc(-1*(var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-top>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after{border-width:var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width)*.5) 0}.bs-popover-top>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before{bottom:0;border-top-color:var(--bs-popover-arrow-border)}.bs-popover-top>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after{bottom:var(--bs-popover-border-width);border-top-color:var(--bs-popover-bg)}.bs-popover-end>.popover-arrow,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow{left:calc(-1*(var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-end>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after{border-width:calc(var(--bs-popover-arrow-width)*.5) var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width)*.5) 0}.bs-popover-end>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before{left:0;border-right-color:var(--bs-popover-arrow-border)}.bs-popover-end>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after{left:var(--bs-popover-border-width);border-right-color:var(--bs-popover-bg)}.bs-popover-bottom>.popover-arrow,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow{top:calc(-1*(var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-bottom>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after{border-width:0 calc(var(--bs-popover-arrow-width)*.5) var(--bs-popover-arrow-height)}.bs-popover-bottom>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before{top:0;border-bottom-color:var(--bs-popover-arrow-border)}.bs-popover-bottom>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after{top:var(--bs-popover-border-width);border-bottom-color:var(--bs-popover-bg)}.bs-popover-bottom .popover-header::before,.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before{position:absolute;top:0;left:50%;display:block;width:var(--bs-popover-arrow-width);margin-left:calc(-0.5*var(--bs-popover-arrow-width));content:"";border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-header-bg)}.bs-popover-start>.popover-arrow,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow{right:calc(-1*(var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-start>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after{border-width:calc(var(--bs-popover-arrow-width)*.5) 0 calc(var(--bs-popover-arrow-width)*.5) var(--bs-popover-arrow-height)}.bs-popover-start>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before{right:0;border-left-color:var(--bs-popover-arrow-border)}.bs-popover-start>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after{right:var(--bs-popover-border-width);border-left-color:var(--bs-popover-bg)}.popover-header{padding:var(--bs-popover-header-padding-y) var(--bs-popover-header-padding-x);margin-bottom:0;font-size:var(--bs-popover-header-font-size);color:var(--bs-popover-header-color);background-color:var(--bs-popover-header-bg);border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-border-color)}.popover-header:empty{display:none}.popover-body{padding:var(--bs-popover-body-padding-y) var(--bs-popover-body-padding-x);color:var(--bs-popover-body-color)}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;backface-visibility:hidden}.carousel-item.active,.carousel-item-next,.carousel-item-prev{display:block}.carousel-item-next:not(.carousel-item-start),.active.carousel-item-end{transform:translateX(100%)}.carousel-item-prev:not(.carousel-item-end),.active.carousel-item-start{transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item.active,.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end{z-index:1;opacity:1}.carousel-fade .active.carousel-item-start,.carousel-fade .active.carousel-item-end{z-index:0;opacity:0}.carousel-control-prev,.carousel-control-next{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:none;border:0;opacity:.5}.carousel-control-prev:hover,.carousel-control-prev:focus,.carousel-control-next:hover,.carousel-control-next:focus{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-prev-icon,.carousel-control-next-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e") /*rtl:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")*/}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e") /*rtl:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")*/}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid rgba(0,0,0,0);border-bottom:10px solid rgba(0,0,0,0);opacity:.5}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-prev-icon,.carousel-dark .carousel-control-next-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}[data-bs-theme=dark] .carousel .carousel-control-prev-icon,[data-bs-theme=dark] .carousel .carousel-control-next-icon,[data-bs-theme=dark].carousel .carousel-control-prev-icon,[data-bs-theme=dark].carousel .carousel-control-next-icon{filter:invert(1) grayscale(100)}[data-bs-theme=dark] .carousel .carousel-indicators [data-bs-target],[data-bs-theme=dark].carousel .carousel-indicators [data-bs-target]{background-color:#000}[data-bs-theme=dark] .carousel .carousel-caption,[data-bs-theme=dark].carousel .carousel-caption{color:#000}.spinner-grow,.spinner-border{display:inline-block;width:var(--bs-spinner-width);height:var(--bs-spinner-height);vertical-align:var(--bs-spinner-vertical-align);border-radius:50%;animation:var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name)}@keyframes spinner-border{to{transform:rotate(360deg) /* rtl:ignore */}}.spinner-border{--bs-spinner-width: 2rem;--bs-spinner-height: 2rem;--bs-spinner-vertical-align: -0.125em;--bs-spinner-border-width: 0.25em;--bs-spinner-animation-speed: 0.75s;--bs-spinner-animation-name: spinner-border;border:var(--bs-spinner-border-width) solid currentcolor;border-right-color:rgba(0,0,0,0)}.spinner-border-sm{--bs-spinner-width: 1rem;--bs-spinner-height: 1rem;--bs-spinner-border-width: 0.2em}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{--bs-spinner-width: 2rem;--bs-spinner-height: 2rem;--bs-spinner-vertical-align: -0.125em;--bs-spinner-animation-speed: 0.75s;--bs-spinner-animation-name: spinner-grow;background-color:currentcolor;opacity:0}.spinner-grow-sm{--bs-spinner-width: 1rem;--bs-spinner-height: 1rem}@media(prefers-reduced-motion: reduce){.spinner-border,.spinner-grow{--bs-spinner-animation-speed: 1.5s}}.offcanvas,.offcanvas-xxl,.offcanvas-xl,.offcanvas-lg,.offcanvas-md,.offcanvas-sm{--bs-offcanvas-zindex: 1045;--bs-offcanvas-width: 400px;--bs-offcanvas-height: 30vh;--bs-offcanvas-padding-x: 1rem;--bs-offcanvas-padding-y: 1rem;--bs-offcanvas-color: var(--bs-body-color);--bs-offcanvas-bg: var(--bs-body-bg);--bs-offcanvas-border-width: var(--bs-border-width);--bs-offcanvas-border-color: var(--bs-border-color-translucent);--bs-offcanvas-box-shadow: var(--bs-box-shadow-sm);--bs-offcanvas-transition: transform 0.3s ease-in-out;--bs-offcanvas-title-line-height: 1.5}@media(max-width: 575.98px){.offcanvas-sm{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0}.offcanvas-sm.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-sm.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-sm.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-sm.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-sm.showing,.offcanvas-sm.show:not(.hiding){transform:none}.offcanvas-sm.showing,.offcanvas-sm.hiding,.offcanvas-sm.show{visibility:visible}}@media(min-width: 576px){.offcanvas-sm{--bs-offcanvas-height: auto;--bs-offcanvas-border-width: 0;background-color:rgba(0,0,0,0) !important}.offcanvas-sm .offcanvas-header{display:none}.offcanvas-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:rgba(0,0,0,0) !important}}@media(max-width: 767.98px){.offcanvas-md{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0}.offcanvas-md.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-md.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-md.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-md.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-md.showing,.offcanvas-md.show:not(.hiding){transform:none}.offcanvas-md.showing,.offcanvas-md.hiding,.offcanvas-md.show{visibility:visible}}@media(min-width: 768px){.offcanvas-md{--bs-offcanvas-height: auto;--bs-offcanvas-border-width: 0;background-color:rgba(0,0,0,0) !important}.offcanvas-md .offcanvas-header{display:none}.offcanvas-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:rgba(0,0,0,0) !important}}@media(max-width: 991.98px){.offcanvas-lg{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0}.offcanvas-lg.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-lg.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-lg.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-lg.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-lg.showing,.offcanvas-lg.show:not(.hiding){transform:none}.offcanvas-lg.showing,.offcanvas-lg.hiding,.offcanvas-lg.show{visibility:visible}}@media(min-width: 992px){.offcanvas-lg{--bs-offcanvas-height: auto;--bs-offcanvas-border-width: 0;background-color:rgba(0,0,0,0) !important}.offcanvas-lg .offcanvas-header{display:none}.offcanvas-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:rgba(0,0,0,0) !important}}@media(max-width: 1199.98px){.offcanvas-xl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0}.offcanvas-xl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xl.showing,.offcanvas-xl.show:not(.hiding){transform:none}.offcanvas-xl.showing,.offcanvas-xl.hiding,.offcanvas-xl.show{visibility:visible}}@media(min-width: 1200px){.offcanvas-xl{--bs-offcanvas-height: auto;--bs-offcanvas-border-width: 0;background-color:rgba(0,0,0,0) !important}.offcanvas-xl .offcanvas-header{display:none}.offcanvas-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:rgba(0,0,0,0) !important}}@media(max-width: 1399.98px){.offcanvas-xxl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0}.offcanvas-xxl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xxl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xxl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xxl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xxl.showing,.offcanvas-xxl.show:not(.hiding){transform:none}.offcanvas-xxl.showing,.offcanvas-xxl.hiding,.offcanvas-xxl.show{visibility:visible}}@media(min-width: 1400px){.offcanvas-xxl{--bs-offcanvas-height: auto;--bs-offcanvas-border-width: 0;background-color:rgba(0,0,0,0) !important}.offcanvas-xxl .offcanvas-header{display:none}.offcanvas-xxl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:rgba(0,0,0,0) !important}}.offcanvas{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0}.offcanvas.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas.showing,.offcanvas.show:not(.hiding){transform:none}.offcanvas.showing,.offcanvas.hiding,.offcanvas.show{visibility:visible}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x)}.offcanvas-header .btn-close{padding:calc(var(--bs-offcanvas-padding-y)*.5) calc(var(--bs-offcanvas-padding-x)*.5);margin:calc(-0.5*var(--bs-offcanvas-padding-y)) calc(-0.5*var(--bs-offcanvas-padding-x)) calc(-0.5*var(--bs-offcanvas-padding-y)) auto}.offcanvas-title{margin-bottom:0;line-height:var(--bs-offcanvas-title-line-height)}.offcanvas-body{flex-grow:1;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x);overflow-y:auto}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentcolor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{animation:placeholder-glow 2s ease-in-out infinite}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg, #000 55%, rgba(0, 0, 0, 0.8) 75%, #000 95%);mask-image:linear-gradient(130deg, #000 55%, rgba(0, 0, 0, 0.8) 75%, #000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;animation:placeholder-wave 2s linear infinite}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.text-bg-primary{color:#000 !important;background-color:RGBA(var(--bs-primary-rgb), var(--bs-bg-opacity, 1)) !important}.text-bg-secondary{color:#fff !important;background-color:RGBA(var(--bs-secondary-rgb), var(--bs-bg-opacity, 1)) !important}.text-bg-success{color:#000 !important;background-color:RGBA(var(--bs-success-rgb), var(--bs-bg-opacity, 1)) !important}.text-bg-info{color:#000 !important;background-color:RGBA(var(--bs-info-rgb), var(--bs-bg-opacity, 1)) !important}.text-bg-warning{color:#000 !important;background-color:RGBA(var(--bs-warning-rgb), var(--bs-bg-opacity, 1)) !important}.text-bg-danger{color:#000 !important;background-color:RGBA(var(--bs-danger-rgb), var(--bs-bg-opacity, 1)) !important}.text-bg-light{color:#000 !important;background-color:RGBA(var(--bs-light-rgb), var(--bs-bg-opacity, 1)) !important}.text-bg-dark{color:#fff !important;background-color:RGBA(var(--bs-dark-rgb), var(--bs-bg-opacity, 1)) !important}.link-primary{color:RGBA(var(--bs-primary-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-primary-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-primary:hover,.link-primary:focus{color:RGBA(72, 157, 255, var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(72, 157, 255, var(--bs-link-underline-opacity, 1)) !important}.link-secondary{color:RGBA(var(--bs-secondary-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-secondary-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-secondary:hover,.link-secondary:focus{color:RGBA(86, 94, 100, var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(86, 94, 100, var(--bs-link-underline-opacity, 1)) !important}.link-success{color:RGBA(var(--bs-success-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-success-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-success:hover,.link-success:focus{color:RGBA(134, 178, 96, var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(134, 178, 96, var(--bs-link-underline-opacity, 1)) !important}.link-info{color:RGBA(var(--bs-info-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-info-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-info:hover,.link-info:focus{color:RGBA(51, 195, 255, var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(51, 195, 255, var(--bs-link-underline-opacity, 1)) !important}.link-warning{color:RGBA(var(--bs-warning-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-warning-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-warning:hover,.link-warning:focus{color:RGBA(253, 212, 93, var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(253, 212, 93, var(--bs-link-underline-opacity, 1)) !important}.link-danger{color:RGBA(var(--bs-danger-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-danger-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-danger:hover,.link-danger:focus{color:RGBA(255, 117, 133, var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(255, 117, 133, var(--bs-link-underline-opacity, 1)) !important}.link-light{color:RGBA(var(--bs-light-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-light-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-light:hover,.link-light:focus{color:RGBA(249, 250, 251, var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(249, 250, 251, var(--bs-link-underline-opacity, 1)) !important}.link-dark{color:RGBA(var(--bs-dark-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-dark-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-dark:hover,.link-dark:focus{color:RGBA(26, 30, 33, var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(26, 30, 33, var(--bs-link-underline-opacity, 1)) !important}.link-body-emphasis{color:RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-opacity, 1)) !important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-body-emphasis:hover,.link-body-emphasis:focus{color:RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-opacity, 0.75)) !important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-underline-opacity, 0.75)) !important}.focus-ring:focus{outline:0;box-shadow:var(--bs-focus-ring-x, 0) var(--bs-focus-ring-y, 0) var(--bs-focus-ring-blur, 0) var(--bs-focus-ring-width) var(--bs-focus-ring-color)}.icon-link{display:inline-flex;gap:.375rem;align-items:center;text-decoration-color:rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 0.5));text-underline-offset:.25em;backface-visibility:hidden}.icon-link>.bi{flex-shrink:0;width:1em;height:1em;fill:currentcolor}.icon-link-hover:hover>.bi,.icon-link-hover:focus-visible>.bi{transform:var(--bs-icon-link-transform, translate3d(0.25em, 0, 0))}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio: 100%}.ratio-4x3{--bs-aspect-ratio: 75%}.ratio-16x9{--bs-aspect-ratio: 56.25%}.ratio-21x9{--bs-aspect-ratio: 42.8571428571%}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:sticky;top:0;z-index:1020}.sticky-bottom{position:sticky;bottom:0;z-index:1020}@media(min-width: 576px){.sticky-sm-top{position:sticky;top:0;z-index:1020}.sticky-sm-bottom{position:sticky;bottom:0;z-index:1020}}@media(min-width: 768px){.sticky-md-top{position:sticky;top:0;z-index:1020}.sticky-md-bottom{position:sticky;bottom:0;z-index:1020}}@media(min-width: 992px){.sticky-lg-top{position:sticky;top:0;z-index:1020}.sticky-lg-bottom{position:sticky;bottom:0;z-index:1020}}@media(min-width: 1200px){.sticky-xl-top{position:sticky;top:0;z-index:1020}.sticky-xl-bottom{position:sticky;bottom:0;z-index:1020}}@media(min-width: 1400px){.sticky-xxl-top{position:sticky;top:0;z-index:1020}.sticky-xxl-bottom{position:sticky;bottom:0;z-index:1020}}.hstack{display:flex;flex-direction:row;align-items:center;align-self:stretch}.vstack{display:flex;flex:1 1 auto;flex-direction:column;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){width:1px !important;height:1px !important;padding:0 !important;margin:-1px !important;overflow:hidden !important;clip:rect(0, 0, 0, 0) !important;white-space:nowrap !important;border:0 !important}.visually-hidden:not(caption),.visually-hidden-focusable:not(:focus):not(:focus-within):not(caption){position:absolute !important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;width:var(--bs-border-width);min-height:1em;background-color:currentcolor;opacity:.25}.align-baseline{vertical-align:baseline !important}.align-top{vertical-align:top !important}.align-middle{vertical-align:middle !important}.align-bottom{vertical-align:bottom !important}.align-text-bottom{vertical-align:text-bottom !important}.align-text-top{vertical-align:text-top !important}.float-start{float:left !important}.float-end{float:right !important}.float-none{float:none !important}.object-fit-contain{-o-object-fit:contain !important;object-fit:contain !important}.object-fit-cover{-o-object-fit:cover !important;object-fit:cover !important}.object-fit-fill{-o-object-fit:fill !important;object-fit:fill !important}.object-fit-scale{-o-object-fit:scale-down !important;object-fit:scale-down !important}.object-fit-none{-o-object-fit:none !important;object-fit:none !important}.opacity-0{opacity:0 !important}.opacity-25{opacity:.25 !important}.opacity-50{opacity:.5 !important}.opacity-75{opacity:.75 !important}.opacity-100{opacity:1 !important}.overflow-auto{overflow:auto !important}.overflow-hidden{overflow:hidden !important}.overflow-visible{overflow:visible !important}.overflow-scroll{overflow:scroll !important}.overflow-x-auto{overflow-x:auto !important}.overflow-x-hidden{overflow-x:hidden !important}.overflow-x-visible{overflow-x:visible !important}.overflow-x-scroll{overflow-x:scroll !important}.overflow-y-auto{overflow-y:auto !important}.overflow-y-hidden{overflow-y:hidden !important}.overflow-y-visible{overflow-y:visible !important}.overflow-y-scroll{overflow-y:scroll !important}.d-inline{display:inline !important}.d-inline-block{display:inline-block !important}.d-block{display:block !important}.d-grid{display:grid !important}.d-inline-grid{display:inline-grid !important}.d-table{display:table !important}.d-table-row{display:table-row !important}.d-table-cell{display:table-cell !important}.d-flex{display:flex !important}.d-inline-flex{display:inline-flex !important}.d-none{display:none !important}.shadow{box-shadow:var(--bs-box-shadow) !important}.shadow-sm{box-shadow:var(--bs-box-shadow-sm) !important}.shadow-lg{box-shadow:var(--bs-box-shadow-lg) !important}.shadow-none{box-shadow:none !important}.focus-ring-primary{--bs-focus-ring-color: rgba(var(--bs-primary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-secondary{--bs-focus-ring-color: rgba(var(--bs-secondary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-success{--bs-focus-ring-color: rgba(var(--bs-success-rgb), var(--bs-focus-ring-opacity))}.focus-ring-info{--bs-focus-ring-color: rgba(var(--bs-info-rgb), var(--bs-focus-ring-opacity))}.focus-ring-warning{--bs-focus-ring-color: rgba(var(--bs-warning-rgb), var(--bs-focus-ring-opacity))}.focus-ring-danger{--bs-focus-ring-color: rgba(var(--bs-danger-rgb), var(--bs-focus-ring-opacity))}.focus-ring-light{--bs-focus-ring-color: rgba(var(--bs-light-rgb), var(--bs-focus-ring-opacity))}.focus-ring-dark{--bs-focus-ring-color: rgba(var(--bs-dark-rgb), var(--bs-focus-ring-opacity))}.position-static{position:static !important}.position-relative{position:relative !important}.position-absolute{position:absolute !important}.position-fixed{position:fixed !important}.position-sticky{position:sticky !important}.top-0{top:0 !important}.top-50{top:50% !important}.top-100{top:100% !important}.bottom-0{bottom:0 !important}.bottom-50{bottom:50% !important}.bottom-100{bottom:100% !important}.start-0{left:0 !important}.start-50{left:50% !important}.start-100{left:100% !important}.end-0{right:0 !important}.end-50{right:50% !important}.end-100{right:100% !important}.translate-middle{transform:translate(-50%, -50%) !important}.translate-middle-x{transform:translateX(-50%) !important}.translate-middle-y{transform:translateY(-50%) !important}.border{border:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important}.border-0{border:0 !important}.border-top{border-top:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important}.border-top-0{border-top:0 !important}.border-end{border-right:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important}.border-end-0{border-right:0 !important}.border-bottom{border-bottom:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important}.border-bottom-0{border-bottom:0 !important}.border-start{border-left:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important}.border-start-0{border-left:0 !important}.border-primary{--bs-border-opacity: 1;border-color:rgba(var(--bs-primary-rgb), var(--bs-border-opacity)) !important}.border-secondary{--bs-border-opacity: 1;border-color:rgba(var(--bs-secondary-rgb), var(--bs-border-opacity)) !important}.border-success{--bs-border-opacity: 1;border-color:rgba(var(--bs-success-rgb), var(--bs-border-opacity)) !important}.border-info{--bs-border-opacity: 1;border-color:rgba(var(--bs-info-rgb), var(--bs-border-opacity)) !important}.border-warning{--bs-border-opacity: 1;border-color:rgba(var(--bs-warning-rgb), var(--bs-border-opacity)) !important}.border-danger{--bs-border-opacity: 1;border-color:rgba(var(--bs-danger-rgb), var(--bs-border-opacity)) !important}.border-light{--bs-border-opacity: 1;border-color:rgba(var(--bs-light-rgb), var(--bs-border-opacity)) !important}.border-dark{--bs-border-opacity: 1;border-color:rgba(var(--bs-dark-rgb), var(--bs-border-opacity)) !important}.border-black{--bs-border-opacity: 1;border-color:rgba(var(--bs-black-rgb), var(--bs-border-opacity)) !important}.border-white{--bs-border-opacity: 1;border-color:rgba(var(--bs-white-rgb), var(--bs-border-opacity)) !important}.border-primary-subtle{border-color:var(--bs-primary-border-subtle) !important}.border-secondary-subtle{border-color:var(--bs-secondary-border-subtle) !important}.border-success-subtle{border-color:var(--bs-success-border-subtle) !important}.border-info-subtle{border-color:var(--bs-info-border-subtle) !important}.border-warning-subtle{border-color:var(--bs-warning-border-subtle) !important}.border-danger-subtle{border-color:var(--bs-danger-border-subtle) !important}.border-light-subtle{border-color:var(--bs-light-border-subtle) !important}.border-dark-subtle{border-color:var(--bs-dark-border-subtle) !important}.border-1{border-width:1px !important}.border-2{border-width:2px !important}.border-3{border-width:3px !important}.border-4{border-width:4px !important}.border-5{border-width:5px !important}.border-opacity-10{--bs-border-opacity: 0.1}.border-opacity-25{--bs-border-opacity: 0.25}.border-opacity-50{--bs-border-opacity: 0.5}.border-opacity-75{--bs-border-opacity: 0.75}.border-opacity-100{--bs-border-opacity: 1}.w-25{width:25% !important}.w-50{width:50% !important}.w-75{width:75% !important}.w-100{width:100% !important}.w-auto{width:auto !important}.mw-100{max-width:100% !important}.vw-100{width:100vw !important}.min-vw-100{min-width:100vw !important}.h-25{height:25% !important}.h-50{height:50% !important}.h-75{height:75% !important}.h-100{height:100% !important}.h-auto{height:auto !important}.mh-100{max-height:100% !important}.vh-100{height:100vh !important}.min-vh-100{min-height:100vh !important}.flex-fill{flex:1 1 auto !important}.flex-row{flex-direction:row !important}.flex-column{flex-direction:column !important}.flex-row-reverse{flex-direction:row-reverse !important}.flex-column-reverse{flex-direction:column-reverse !important}.flex-grow-0{flex-grow:0 !important}.flex-grow-1{flex-grow:1 !important}.flex-shrink-0{flex-shrink:0 !important}.flex-shrink-1{flex-shrink:1 !important}.flex-wrap{flex-wrap:wrap !important}.flex-nowrap{flex-wrap:nowrap !important}.flex-wrap-reverse{flex-wrap:wrap-reverse !important}.justify-content-start{justify-content:flex-start !important}.justify-content-end{justify-content:flex-end !important}.justify-content-center{justify-content:center !important}.justify-content-between{justify-content:space-between !important}.justify-content-around{justify-content:space-around !important}.justify-content-evenly{justify-content:space-evenly !important}.align-items-start{align-items:flex-start !important}.align-items-end{align-items:flex-end !important}.align-items-center{align-items:center !important}.align-items-baseline{align-items:baseline !important}.align-items-stretch{align-items:stretch !important}.align-content-start{align-content:flex-start !important}.align-content-end{align-content:flex-end !important}.align-content-center{align-content:center !important}.align-content-between{align-content:space-between !important}.align-content-around{align-content:space-around !important}.align-content-stretch{align-content:stretch !important}.align-self-auto{align-self:auto !important}.align-self-start{align-self:flex-start !important}.align-self-end{align-self:flex-end !important}.align-self-center{align-self:center !important}.align-self-baseline{align-self:baseline !important}.align-self-stretch{align-self:stretch !important}.order-first{order:-1 !important}.order-0{order:0 !important}.order-1{order:1 !important}.order-2{order:2 !important}.order-3{order:3 !important}.order-4{order:4 !important}.order-5{order:5 !important}.order-last{order:6 !important}.m-0{margin:0 !important}.m-1{margin:.25rem !important}.m-2{margin:.5rem !important}.m-3{margin:1rem !important}.m-4{margin:1.5rem !important}.m-5{margin:3rem !important}.m-auto{margin:auto !important}.mx-0{margin-right:0 !important;margin-left:0 !important}.mx-1{margin-right:.25rem !important;margin-left:.25rem !important}.mx-2{margin-right:.5rem !important;margin-left:.5rem !important}.mx-3{margin-right:1rem !important;margin-left:1rem !important}.mx-4{margin-right:1.5rem !important;margin-left:1.5rem !important}.mx-5{margin-right:3rem !important;margin-left:3rem !important}.mx-auto{margin-right:auto !important;margin-left:auto !important}.my-0{margin-top:0 !important;margin-bottom:0 !important}.my-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.my-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.my-3{margin-top:1rem !important;margin-bottom:1rem !important}.my-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.my-5{margin-top:3rem !important;margin-bottom:3rem !important}.my-auto{margin-top:auto !important;margin-bottom:auto !important}.mt-0{margin-top:0 !important}.mt-1{margin-top:.25rem !important}.mt-2{margin-top:.5rem !important}.mt-3{margin-top:1rem !important}.mt-4{margin-top:1.5rem !important}.mt-5{margin-top:3rem !important}.mt-auto{margin-top:auto !important}.me-0{margin-right:0 !important}.me-1{margin-right:.25rem !important}.me-2{margin-right:.5rem !important}.me-3{margin-right:1rem !important}.me-4{margin-right:1.5rem !important}.me-5{margin-right:3rem !important}.me-auto{margin-right:auto !important}.mb-0{margin-bottom:0 !important}.mb-1{margin-bottom:.25rem !important}.mb-2{margin-bottom:.5rem !important}.mb-3{margin-bottom:1rem !important}.mb-4{margin-bottom:1.5rem !important}.mb-5{margin-bottom:3rem !important}.mb-auto{margin-bottom:auto !important}.ms-0{margin-left:0 !important}.ms-1{margin-left:.25rem !important}.ms-2{margin-left:.5rem !important}.ms-3{margin-left:1rem !important}.ms-4{margin-left:1.5rem !important}.ms-5{margin-left:3rem !important}.ms-auto{margin-left:auto !important}.p-0{padding:0 !important}.p-1{padding:.25rem !important}.p-2{padding:.5rem !important}.p-3{padding:1rem !important}.p-4{padding:1.5rem !important}.p-5{padding:3rem !important}.px-0{padding-right:0 !important;padding-left:0 !important}.px-1{padding-right:.25rem !important;padding-left:.25rem !important}.px-2{padding-right:.5rem !important;padding-left:.5rem !important}.px-3{padding-right:1rem !important;padding-left:1rem !important}.px-4{padding-right:1.5rem !important;padding-left:1.5rem !important}.px-5{padding-right:3rem !important;padding-left:3rem !important}.py-0{padding-top:0 !important;padding-bottom:0 !important}.py-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.py-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.py-3{padding-top:1rem !important;padding-bottom:1rem !important}.py-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.py-5{padding-top:3rem !important;padding-bottom:3rem !important}.pt-0{padding-top:0 !important}.pt-1{padding-top:.25rem !important}.pt-2{padding-top:.5rem !important}.pt-3{padding-top:1rem !important}.pt-4{padding-top:1.5rem !important}.pt-5{padding-top:3rem !important}.pe-0{padding-right:0 !important}.pe-1{padding-right:.25rem !important}.pe-2{padding-right:.5rem !important}.pe-3{padding-right:1rem !important}.pe-4{padding-right:1.5rem !important}.pe-5{padding-right:3rem !important}.pb-0{padding-bottom:0 !important}.pb-1{padding-bottom:.25rem !important}.pb-2{padding-bottom:.5rem !important}.pb-3{padding-bottom:1rem !important}.pb-4{padding-bottom:1.5rem !important}.pb-5{padding-bottom:3rem !important}.ps-0{padding-left:0 !important}.ps-1{padding-left:.25rem !important}.ps-2{padding-left:.5rem !important}.ps-3{padding-left:1rem !important}.ps-4{padding-left:1.5rem !important}.ps-5{padding-left:3rem !important}.gap-0{gap:0 !important}.gap-1{gap:.25rem !important}.gap-2{gap:.5rem !important}.gap-3{gap:1rem !important}.gap-4{gap:1.5rem !important}.gap-5{gap:3rem !important}.row-gap-0{row-gap:0 !important}.row-gap-1{row-gap:.25rem !important}.row-gap-2{row-gap:.5rem !important}.row-gap-3{row-gap:1rem !important}.row-gap-4{row-gap:1.5rem !important}.row-gap-5{row-gap:3rem !important}.column-gap-0{-moz-column-gap:0 !important;column-gap:0 !important}.column-gap-1{-moz-column-gap:.25rem !important;column-gap:.25rem !important}.column-gap-2{-moz-column-gap:.5rem !important;column-gap:.5rem !important}.column-gap-3{-moz-column-gap:1rem !important;column-gap:1rem !important}.column-gap-4{-moz-column-gap:1.5rem !important;column-gap:1.5rem !important}.column-gap-5{-moz-column-gap:3rem !important;column-gap:3rem !important}.font-monospace{font-family:var(--bs-font-monospace) !important}.fs-1{font-size:calc(1.34375rem + 1.125vw) !important}.fs-2{font-size:calc(1.3rem + 0.6vw) !important}.fs-3{font-size:calc(1.278125rem + 0.3375vw) !important}.fs-4{font-size:calc(1.25625rem + 0.075vw) !important}.fs-5{font-size:1.09375rem !important}.fs-6{font-size:0.875rem !important}.fst-italic{font-style:italic !important}.fst-normal{font-style:normal !important}.fw-lighter{font-weight:lighter !important}.fw-light{font-weight:300 !important}.fw-normal{font-weight:400 !important}.fw-medium{font-weight:500 !important}.fw-semibold{font-weight:600 !important}.fw-bold{font-weight:700 !important}.fw-bolder{font-weight:bolder !important}.lh-1{line-height:1 !important}.lh-sm{line-height:1.25 !important}.lh-base{line-height:1.5 !important}.lh-lg{line-height:2 !important}.text-start{text-align:left !important}.text-end{text-align:right !important}.text-center{text-align:center !important}.text-decoration-none{text-decoration:none !important}.text-decoration-underline{text-decoration:underline !important}.text-decoration-line-through{text-decoration:line-through !important}.text-lowercase{text-transform:lowercase !important}.text-uppercase{text-transform:uppercase !important}.text-capitalize{text-transform:capitalize !important}.text-wrap{white-space:normal !important}.text-nowrap{white-space:nowrap !important}.text-break{word-wrap:break-word !important;word-break:break-word !important}.text-primary{--bs-text-opacity: 1;color:rgba(var(--bs-primary-rgb), var(--bs-text-opacity)) !important}.text-secondary{--bs-text-opacity: 1;color:rgba(var(--bs-secondary-rgb), var(--bs-text-opacity)) !important}.text-success{--bs-text-opacity: 1;color:rgba(var(--bs-success-rgb), var(--bs-text-opacity)) !important}.text-info{--bs-text-opacity: 1;color:rgba(var(--bs-info-rgb), var(--bs-text-opacity)) !important}.text-warning{--bs-text-opacity: 1;color:rgba(var(--bs-warning-rgb), var(--bs-text-opacity)) !important}.text-danger{--bs-text-opacity: 1;color:rgba(var(--bs-danger-rgb), var(--bs-text-opacity)) !important}.text-light{--bs-text-opacity: 1;color:rgba(var(--bs-light-rgb), var(--bs-text-opacity)) !important}.text-dark{--bs-text-opacity: 1;color:rgba(var(--bs-dark-rgb), var(--bs-text-opacity)) !important}.text-black{--bs-text-opacity: 1;color:rgba(var(--bs-black-rgb), var(--bs-text-opacity)) !important}.text-white{--bs-text-opacity: 1;color:rgba(var(--bs-white-rgb), var(--bs-text-opacity)) !important}.text-body{--bs-text-opacity: 1;color:rgba(var(--bs-body-color-rgb), var(--bs-text-opacity)) !important}.text-muted{--bs-text-opacity: 1;color:var(--bs-secondary-color) !important}.text-black-50{--bs-text-opacity: 1;color:rgba(0,0,0,.5) !important}.text-white-50{--bs-text-opacity: 1;color:rgba(255,255,255,.5) !important}.text-body-secondary{--bs-text-opacity: 1;color:var(--bs-secondary-color) !important}.text-body-tertiary{--bs-text-opacity: 1;color:var(--bs-tertiary-color) !important}.text-body-emphasis{--bs-text-opacity: 1;color:var(--bs-emphasis-color) !important}.text-reset{--bs-text-opacity: 1;color:inherit !important}.text-opacity-25{--bs-text-opacity: 0.25}.text-opacity-50{--bs-text-opacity: 0.5}.text-opacity-75{--bs-text-opacity: 0.75}.text-opacity-100{--bs-text-opacity: 1}.text-primary-emphasis{color:var(--bs-primary-text-emphasis) !important}.text-secondary-emphasis{color:var(--bs-secondary-text-emphasis) !important}.text-success-emphasis{color:var(--bs-success-text-emphasis) !important}.text-info-emphasis{color:var(--bs-info-text-emphasis) !important}.text-warning-emphasis{color:var(--bs-warning-text-emphasis) !important}.text-danger-emphasis{color:var(--bs-danger-text-emphasis) !important}.text-light-emphasis{color:var(--bs-light-text-emphasis) !important}.text-dark-emphasis{color:var(--bs-dark-text-emphasis) !important}.link-opacity-10{--bs-link-opacity: 0.1}.link-opacity-10-hover:hover{--bs-link-opacity: 0.1}.link-opacity-25{--bs-link-opacity: 0.25}.link-opacity-25-hover:hover{--bs-link-opacity: 0.25}.link-opacity-50{--bs-link-opacity: 0.5}.link-opacity-50-hover:hover{--bs-link-opacity: 0.5}.link-opacity-75{--bs-link-opacity: 0.75}.link-opacity-75-hover:hover{--bs-link-opacity: 0.75}.link-opacity-100{--bs-link-opacity: 1}.link-opacity-100-hover:hover{--bs-link-opacity: 1}.link-offset-1{text-underline-offset:.125em !important}.link-offset-1-hover:hover{text-underline-offset:.125em !important}.link-offset-2{text-underline-offset:.25em !important}.link-offset-2-hover:hover{text-underline-offset:.25em !important}.link-offset-3{text-underline-offset:.375em !important}.link-offset-3-hover:hover{text-underline-offset:.375em !important}.link-underline-primary{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-primary-rgb), var(--bs-link-underline-opacity)) !important}.link-underline-secondary{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-secondary-rgb), var(--bs-link-underline-opacity)) !important}.link-underline-success{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-success-rgb), var(--bs-link-underline-opacity)) !important}.link-underline-info{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-info-rgb), var(--bs-link-underline-opacity)) !important}.link-underline-warning{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-warning-rgb), var(--bs-link-underline-opacity)) !important}.link-underline-danger{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-danger-rgb), var(--bs-link-underline-opacity)) !important}.link-underline-light{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-light-rgb), var(--bs-link-underline-opacity)) !important}.link-underline-dark{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-dark-rgb), var(--bs-link-underline-opacity)) !important}.link-underline{--bs-link-underline-opacity: 1;text-decoration-color:rgba(var(--bs-link-color-rgb), var(--bs-link-underline-opacity, 1)) !important}.link-underline-opacity-0{--bs-link-underline-opacity: 0}.link-underline-opacity-0-hover:hover{--bs-link-underline-opacity: 0}.link-underline-opacity-10{--bs-link-underline-opacity: 0.1}.link-underline-opacity-10-hover:hover{--bs-link-underline-opacity: 0.1}.link-underline-opacity-25{--bs-link-underline-opacity: 0.25}.link-underline-opacity-25-hover:hover{--bs-link-underline-opacity: 0.25}.link-underline-opacity-50{--bs-link-underline-opacity: 0.5}.link-underline-opacity-50-hover:hover{--bs-link-underline-opacity: 0.5}.link-underline-opacity-75{--bs-link-underline-opacity: 0.75}.link-underline-opacity-75-hover:hover{--bs-link-underline-opacity: 0.75}.link-underline-opacity-100{--bs-link-underline-opacity: 1}.link-underline-opacity-100-hover:hover{--bs-link-underline-opacity: 1}.bg-primary{--bs-bg-opacity: 1;background-color:rgba(var(--bs-primary-rgb), var(--bs-bg-opacity)) !important}.bg-secondary{--bs-bg-opacity: 1;background-color:rgba(var(--bs-secondary-rgb), var(--bs-bg-opacity)) !important}.bg-success{--bs-bg-opacity: 1;background-color:rgba(var(--bs-success-rgb), var(--bs-bg-opacity)) !important}.bg-info{--bs-bg-opacity: 1;background-color:rgba(var(--bs-info-rgb), var(--bs-bg-opacity)) !important}.bg-warning{--bs-bg-opacity: 1;background-color:rgba(var(--bs-warning-rgb), var(--bs-bg-opacity)) !important}.bg-danger{--bs-bg-opacity: 1;background-color:rgba(var(--bs-danger-rgb), var(--bs-bg-opacity)) !important}.bg-light{--bs-bg-opacity: 1;background-color:rgba(var(--bs-light-rgb), var(--bs-bg-opacity)) !important}.bg-dark{--bs-bg-opacity: 1;background-color:rgba(var(--bs-dark-rgb), var(--bs-bg-opacity)) !important}.bg-black{--bs-bg-opacity: 1;background-color:rgba(var(--bs-black-rgb), var(--bs-bg-opacity)) !important}.bg-white{--bs-bg-opacity: 1;background-color:rgba(var(--bs-white-rgb), var(--bs-bg-opacity)) !important}.bg-body{--bs-bg-opacity: 1;background-color:rgba(var(--bs-body-bg-rgb), var(--bs-bg-opacity)) !important}.bg-transparent{--bs-bg-opacity: 1;background-color:rgba(0,0,0,0) !important}.bg-body-secondary{--bs-bg-opacity: 1;background-color:rgba(var(--bs-secondary-bg-rgb), var(--bs-bg-opacity)) !important}.bg-body-tertiary{--bs-bg-opacity: 1;background-color:rgba(var(--bs-tertiary-bg-rgb), var(--bs-bg-opacity)) !important}.bg-opacity-10{--bs-bg-opacity: 0.1}.bg-opacity-25{--bs-bg-opacity: 0.25}.bg-opacity-50{--bs-bg-opacity: 0.5}.bg-opacity-75{--bs-bg-opacity: 0.75}.bg-opacity-100{--bs-bg-opacity: 1}.bg-primary-subtle{background-color:var(--bs-primary-bg-subtle) !important}.bg-secondary-subtle{background-color:var(--bs-secondary-bg-subtle) !important}.bg-success-subtle{background-color:var(--bs-success-bg-subtle) !important}.bg-info-subtle{background-color:var(--bs-info-bg-subtle) !important}.bg-warning-subtle{background-color:var(--bs-warning-bg-subtle) !important}.bg-danger-subtle{background-color:var(--bs-danger-bg-subtle) !important}.bg-light-subtle{background-color:var(--bs-light-bg-subtle) !important}.bg-dark-subtle{background-color:var(--bs-dark-bg-subtle) !important}.bg-gradient{background-image:var(--bs-gradient) !important}.user-select-all{-webkit-user-select:all !important;-moz-user-select:all !important;user-select:all !important}.user-select-auto{-webkit-user-select:auto !important;-moz-user-select:auto !important;user-select:auto !important}.user-select-none{-webkit-user-select:none !important;-moz-user-select:none !important;user-select:none !important}.pe-none{pointer-events:none !important}.pe-auto{pointer-events:auto !important}.rounded{border-radius:var(--bs-border-radius) !important}.rounded-0{border-radius:0 !important}.rounded-1{border-radius:var(--bs-border-radius-sm) !important}.rounded-2{border-radius:var(--bs-border-radius) !important}.rounded-3{border-radius:var(--bs-border-radius-lg) !important}.rounded-4{border-radius:var(--bs-border-radius-xl) !important}.rounded-5{border-radius:var(--bs-border-radius-xxl) !important}.rounded-circle{border-radius:50% !important}.rounded-pill{border-radius:var(--bs-border-radius-pill) !important}.rounded-top{border-top-left-radius:var(--bs-border-radius) !important;border-top-right-radius:var(--bs-border-radius) !important}.rounded-top-0{border-top-left-radius:0 !important;border-top-right-radius:0 !important}.rounded-top-1{border-top-left-radius:var(--bs-border-radius-sm) !important;border-top-right-radius:var(--bs-border-radius-sm) !important}.rounded-top-2{border-top-left-radius:var(--bs-border-radius) !important;border-top-right-radius:var(--bs-border-radius) !important}.rounded-top-3{border-top-left-radius:var(--bs-border-radius-lg) !important;border-top-right-radius:var(--bs-border-radius-lg) !important}.rounded-top-4{border-top-left-radius:var(--bs-border-radius-xl) !important;border-top-right-radius:var(--bs-border-radius-xl) !important}.rounded-top-5{border-top-left-radius:var(--bs-border-radius-xxl) !important;border-top-right-radius:var(--bs-border-radius-xxl) !important}.rounded-top-circle{border-top-left-radius:50% !important;border-top-right-radius:50% !important}.rounded-top-pill{border-top-left-radius:var(--bs-border-radius-pill) !important;border-top-right-radius:var(--bs-border-radius-pill) !important}.rounded-end{border-top-right-radius:var(--bs-border-radius) !important;border-bottom-right-radius:var(--bs-border-radius) !important}.rounded-end-0{border-top-right-radius:0 !important;border-bottom-right-radius:0 !important}.rounded-end-1{border-top-right-radius:var(--bs-border-radius-sm) !important;border-bottom-right-radius:var(--bs-border-radius-sm) !important}.rounded-end-2{border-top-right-radius:var(--bs-border-radius) !important;border-bottom-right-radius:var(--bs-border-radius) !important}.rounded-end-3{border-top-right-radius:var(--bs-border-radius-lg) !important;border-bottom-right-radius:var(--bs-border-radius-lg) !important}.rounded-end-4{border-top-right-radius:var(--bs-border-radius-xl) !important;border-bottom-right-radius:var(--bs-border-radius-xl) !important}.rounded-end-5{border-top-right-radius:var(--bs-border-radius-xxl) !important;border-bottom-right-radius:var(--bs-border-radius-xxl) !important}.rounded-end-circle{border-top-right-radius:50% !important;border-bottom-right-radius:50% !important}.rounded-end-pill{border-top-right-radius:var(--bs-border-radius-pill) !important;border-bottom-right-radius:var(--bs-border-radius-pill) !important}.rounded-bottom{border-bottom-right-radius:var(--bs-border-radius) !important;border-bottom-left-radius:var(--bs-border-radius) !important}.rounded-bottom-0{border-bottom-right-radius:0 !important;border-bottom-left-radius:0 !important}.rounded-bottom-1{border-bottom-right-radius:var(--bs-border-radius-sm) !important;border-bottom-left-radius:var(--bs-border-radius-sm) !important}.rounded-bottom-2{border-bottom-right-radius:var(--bs-border-radius) !important;border-bottom-left-radius:var(--bs-border-radius) !important}.rounded-bottom-3{border-bottom-right-radius:var(--bs-border-radius-lg) !important;border-bottom-left-radius:var(--bs-border-radius-lg) !important}.rounded-bottom-4{border-bottom-right-radius:var(--bs-border-radius-xl) !important;border-bottom-left-radius:var(--bs-border-radius-xl) !important}.rounded-bottom-5{border-bottom-right-radius:var(--bs-border-radius-xxl) !important;border-bottom-left-radius:var(--bs-border-radius-xxl) !important}.rounded-bottom-circle{border-bottom-right-radius:50% !important;border-bottom-left-radius:50% !important}.rounded-bottom-pill{border-bottom-right-radius:var(--bs-border-radius-pill) !important;border-bottom-left-radius:var(--bs-border-radius-pill) !important}.rounded-start{border-bottom-left-radius:var(--bs-border-radius) !important;border-top-left-radius:var(--bs-border-radius) !important}.rounded-start-0{border-bottom-left-radius:0 !important;border-top-left-radius:0 !important}.rounded-start-1{border-bottom-left-radius:var(--bs-border-radius-sm) !important;border-top-left-radius:var(--bs-border-radius-sm) !important}.rounded-start-2{border-bottom-left-radius:var(--bs-border-radius) !important;border-top-left-radius:var(--bs-border-radius) !important}.rounded-start-3{border-bottom-left-radius:var(--bs-border-radius-lg) !important;border-top-left-radius:var(--bs-border-radius-lg) !important}.rounded-start-4{border-bottom-left-radius:var(--bs-border-radius-xl) !important;border-top-left-radius:var(--bs-border-radius-xl) !important}.rounded-start-5{border-bottom-left-radius:var(--bs-border-radius-xxl) !important;border-top-left-radius:var(--bs-border-radius-xxl) !important}.rounded-start-circle{border-bottom-left-radius:50% !important;border-top-left-radius:50% !important}.rounded-start-pill{border-bottom-left-radius:var(--bs-border-radius-pill) !important;border-top-left-radius:var(--bs-border-radius-pill) !important}.visible{visibility:visible !important}.invisible{visibility:hidden !important}.z-n1{z-index:-1 !important}.z-0{z-index:0 !important}.z-1{z-index:1 !important}.z-2{z-index:2 !important}.z-3{z-index:3 !important}@media(min-width: 576px){.float-sm-start{float:left !important}.float-sm-end{float:right !important}.float-sm-none{float:none !important}.object-fit-sm-contain{-o-object-fit:contain !important;object-fit:contain !important}.object-fit-sm-cover{-o-object-fit:cover !important;object-fit:cover !important}.object-fit-sm-fill{-o-object-fit:fill !important;object-fit:fill !important}.object-fit-sm-scale{-o-object-fit:scale-down !important;object-fit:scale-down !important}.object-fit-sm-none{-o-object-fit:none !important;object-fit:none !important}.d-sm-inline{display:inline !important}.d-sm-inline-block{display:inline-block !important}.d-sm-block{display:block !important}.d-sm-grid{display:grid !important}.d-sm-inline-grid{display:inline-grid !important}.d-sm-table{display:table !important}.d-sm-table-row{display:table-row !important}.d-sm-table-cell{display:table-cell !important}.d-sm-flex{display:flex !important}.d-sm-inline-flex{display:inline-flex !important}.d-sm-none{display:none !important}.flex-sm-fill{flex:1 1 auto !important}.flex-sm-row{flex-direction:row !important}.flex-sm-column{flex-direction:column !important}.flex-sm-row-reverse{flex-direction:row-reverse !important}.flex-sm-column-reverse{flex-direction:column-reverse !important}.flex-sm-grow-0{flex-grow:0 !important}.flex-sm-grow-1{flex-grow:1 !important}.flex-sm-shrink-0{flex-shrink:0 !important}.flex-sm-shrink-1{flex-shrink:1 !important}.flex-sm-wrap{flex-wrap:wrap !important}.flex-sm-nowrap{flex-wrap:nowrap !important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse !important}.justify-content-sm-start{justify-content:flex-start !important}.justify-content-sm-end{justify-content:flex-end !important}.justify-content-sm-center{justify-content:center !important}.justify-content-sm-between{justify-content:space-between !important}.justify-content-sm-around{justify-content:space-around !important}.justify-content-sm-evenly{justify-content:space-evenly !important}.align-items-sm-start{align-items:flex-start !important}.align-items-sm-end{align-items:flex-end !important}.align-items-sm-center{align-items:center !important}.align-items-sm-baseline{align-items:baseline !important}.align-items-sm-stretch{align-items:stretch !important}.align-content-sm-start{align-content:flex-start !important}.align-content-sm-end{align-content:flex-end !important}.align-content-sm-center{align-content:center !important}.align-content-sm-between{align-content:space-between !important}.align-content-sm-around{align-content:space-around !important}.align-content-sm-stretch{align-content:stretch !important}.align-self-sm-auto{align-self:auto !important}.align-self-sm-start{align-self:flex-start !important}.align-self-sm-end{align-self:flex-end !important}.align-self-sm-center{align-self:center !important}.align-self-sm-baseline{align-self:baseline !important}.align-self-sm-stretch{align-self:stretch !important}.order-sm-first{order:-1 !important}.order-sm-0{order:0 !important}.order-sm-1{order:1 !important}.order-sm-2{order:2 !important}.order-sm-3{order:3 !important}.order-sm-4{order:4 !important}.order-sm-5{order:5 !important}.order-sm-last{order:6 !important}.m-sm-0{margin:0 !important}.m-sm-1{margin:.25rem !important}.m-sm-2{margin:.5rem !important}.m-sm-3{margin:1rem !important}.m-sm-4{margin:1.5rem !important}.m-sm-5{margin:3rem !important}.m-sm-auto{margin:auto !important}.mx-sm-0{margin-right:0 !important;margin-left:0 !important}.mx-sm-1{margin-right:.25rem !important;margin-left:.25rem !important}.mx-sm-2{margin-right:.5rem !important;margin-left:.5rem !important}.mx-sm-3{margin-right:1rem !important;margin-left:1rem !important}.mx-sm-4{margin-right:1.5rem !important;margin-left:1.5rem !important}.mx-sm-5{margin-right:3rem !important;margin-left:3rem !important}.mx-sm-auto{margin-right:auto !important;margin-left:auto !important}.my-sm-0{margin-top:0 !important;margin-bottom:0 !important}.my-sm-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.my-sm-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.my-sm-3{margin-top:1rem !important;margin-bottom:1rem !important}.my-sm-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.my-sm-5{margin-top:3rem !important;margin-bottom:3rem !important}.my-sm-auto{margin-top:auto !important;margin-bottom:auto !important}.mt-sm-0{margin-top:0 !important}.mt-sm-1{margin-top:.25rem !important}.mt-sm-2{margin-top:.5rem !important}.mt-sm-3{margin-top:1rem !important}.mt-sm-4{margin-top:1.5rem !important}.mt-sm-5{margin-top:3rem !important}.mt-sm-auto{margin-top:auto !important}.me-sm-0{margin-right:0 !important}.me-sm-1{margin-right:.25rem !important}.me-sm-2{margin-right:.5rem !important}.me-sm-3{margin-right:1rem !important}.me-sm-4{margin-right:1.5rem !important}.me-sm-5{margin-right:3rem !important}.me-sm-auto{margin-right:auto !important}.mb-sm-0{margin-bottom:0 !important}.mb-sm-1{margin-bottom:.25rem !important}.mb-sm-2{margin-bottom:.5rem !important}.mb-sm-3{margin-bottom:1rem !important}.mb-sm-4{margin-bottom:1.5rem !important}.mb-sm-5{margin-bottom:3rem !important}.mb-sm-auto{margin-bottom:auto !important}.ms-sm-0{margin-left:0 !important}.ms-sm-1{margin-left:.25rem !important}.ms-sm-2{margin-left:.5rem !important}.ms-sm-3{margin-left:1rem !important}.ms-sm-4{margin-left:1.5rem !important}.ms-sm-5{margin-left:3rem !important}.ms-sm-auto{margin-left:auto !important}.p-sm-0{padding:0 !important}.p-sm-1{padding:.25rem !important}.p-sm-2{padding:.5rem !important}.p-sm-3{padding:1rem !important}.p-sm-4{padding:1.5rem !important}.p-sm-5{padding:3rem !important}.px-sm-0{padding-right:0 !important;padding-left:0 !important}.px-sm-1{padding-right:.25rem !important;padding-left:.25rem !important}.px-sm-2{padding-right:.5rem !important;padding-left:.5rem !important}.px-sm-3{padding-right:1rem !important;padding-left:1rem !important}.px-sm-4{padding-right:1.5rem !important;padding-left:1.5rem !important}.px-sm-5{padding-right:3rem !important;padding-left:3rem !important}.py-sm-0{padding-top:0 !important;padding-bottom:0 !important}.py-sm-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.py-sm-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.py-sm-3{padding-top:1rem !important;padding-bottom:1rem !important}.py-sm-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.py-sm-5{padding-top:3rem !important;padding-bottom:3rem !important}.pt-sm-0{padding-top:0 !important}.pt-sm-1{padding-top:.25rem !important}.pt-sm-2{padding-top:.5rem !important}.pt-sm-3{padding-top:1rem !important}.pt-sm-4{padding-top:1.5rem !important}.pt-sm-5{padding-top:3rem !important}.pe-sm-0{padding-right:0 !important}.pe-sm-1{padding-right:.25rem !important}.pe-sm-2{padding-right:.5rem !important}.pe-sm-3{padding-right:1rem !important}.pe-sm-4{padding-right:1.5rem !important}.pe-sm-5{padding-right:3rem !important}.pb-sm-0{padding-bottom:0 !important}.pb-sm-1{padding-bottom:.25rem !important}.pb-sm-2{padding-bottom:.5rem !important}.pb-sm-3{padding-bottom:1rem !important}.pb-sm-4{padding-bottom:1.5rem !important}.pb-sm-5{padding-bottom:3rem !important}.ps-sm-0{padding-left:0 !important}.ps-sm-1{padding-left:.25rem !important}.ps-sm-2{padding-left:.5rem !important}.ps-sm-3{padding-left:1rem !important}.ps-sm-4{padding-left:1.5rem !important}.ps-sm-5{padding-left:3rem !important}.gap-sm-0{gap:0 !important}.gap-sm-1{gap:.25rem !important}.gap-sm-2{gap:.5rem !important}.gap-sm-3{gap:1rem !important}.gap-sm-4{gap:1.5rem !important}.gap-sm-5{gap:3rem !important}.row-gap-sm-0{row-gap:0 !important}.row-gap-sm-1{row-gap:.25rem !important}.row-gap-sm-2{row-gap:.5rem !important}.row-gap-sm-3{row-gap:1rem !important}.row-gap-sm-4{row-gap:1.5rem !important}.row-gap-sm-5{row-gap:3rem !important}.column-gap-sm-0{-moz-column-gap:0 !important;column-gap:0 !important}.column-gap-sm-1{-moz-column-gap:.25rem !important;column-gap:.25rem !important}.column-gap-sm-2{-moz-column-gap:.5rem !important;column-gap:.5rem !important}.column-gap-sm-3{-moz-column-gap:1rem !important;column-gap:1rem !important}.column-gap-sm-4{-moz-column-gap:1.5rem !important;column-gap:1.5rem !important}.column-gap-sm-5{-moz-column-gap:3rem !important;column-gap:3rem !important}.text-sm-start{text-align:left !important}.text-sm-end{text-align:right !important}.text-sm-center{text-align:center !important}}@media(min-width: 768px){.float-md-start{float:left !important}.float-md-end{float:right !important}.float-md-none{float:none !important}.object-fit-md-contain{-o-object-fit:contain !important;object-fit:contain !important}.object-fit-md-cover{-o-object-fit:cover !important;object-fit:cover !important}.object-fit-md-fill{-o-object-fit:fill !important;object-fit:fill !important}.object-fit-md-scale{-o-object-fit:scale-down !important;object-fit:scale-down !important}.object-fit-md-none{-o-object-fit:none !important;object-fit:none !important}.d-md-inline{display:inline !important}.d-md-inline-block{display:inline-block !important}.d-md-block{display:block !important}.d-md-grid{display:grid !important}.d-md-inline-grid{display:inline-grid !important}.d-md-table{display:table !important}.d-md-table-row{display:table-row !important}.d-md-table-cell{display:table-cell !important}.d-md-flex{display:flex !important}.d-md-inline-flex{display:inline-flex !important}.d-md-none{display:none !important}.flex-md-fill{flex:1 1 auto !important}.flex-md-row{flex-direction:row !important}.flex-md-column{flex-direction:column !important}.flex-md-row-reverse{flex-direction:row-reverse !important}.flex-md-column-reverse{flex-direction:column-reverse !important}.flex-md-grow-0{flex-grow:0 !important}.flex-md-grow-1{flex-grow:1 !important}.flex-md-shrink-0{flex-shrink:0 !important}.flex-md-shrink-1{flex-shrink:1 !important}.flex-md-wrap{flex-wrap:wrap !important}.flex-md-nowrap{flex-wrap:nowrap !important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse !important}.justify-content-md-start{justify-content:flex-start !important}.justify-content-md-end{justify-content:flex-end !important}.justify-content-md-center{justify-content:center !important}.justify-content-md-between{justify-content:space-between !important}.justify-content-md-around{justify-content:space-around !important}.justify-content-md-evenly{justify-content:space-evenly !important}.align-items-md-start{align-items:flex-start !important}.align-items-md-end{align-items:flex-end !important}.align-items-md-center{align-items:center !important}.align-items-md-baseline{align-items:baseline !important}.align-items-md-stretch{align-items:stretch !important}.align-content-md-start{align-content:flex-start !important}.align-content-md-end{align-content:flex-end !important}.align-content-md-center{align-content:center !important}.align-content-md-between{align-content:space-between !important}.align-content-md-around{align-content:space-around !important}.align-content-md-stretch{align-content:stretch !important}.align-self-md-auto{align-self:auto !important}.align-self-md-start{align-self:flex-start !important}.align-self-md-end{align-self:flex-end !important}.align-self-md-center{align-self:center !important}.align-self-md-baseline{align-self:baseline !important}.align-self-md-stretch{align-self:stretch !important}.order-md-first{order:-1 !important}.order-md-0{order:0 !important}.order-md-1{order:1 !important}.order-md-2{order:2 !important}.order-md-3{order:3 !important}.order-md-4{order:4 !important}.order-md-5{order:5 !important}.order-md-last{order:6 !important}.m-md-0{margin:0 !important}.m-md-1{margin:.25rem !important}.m-md-2{margin:.5rem !important}.m-md-3{margin:1rem !important}.m-md-4{margin:1.5rem !important}.m-md-5{margin:3rem !important}.m-md-auto{margin:auto !important}.mx-md-0{margin-right:0 !important;margin-left:0 !important}.mx-md-1{margin-right:.25rem !important;margin-left:.25rem !important}.mx-md-2{margin-right:.5rem !important;margin-left:.5rem !important}.mx-md-3{margin-right:1rem !important;margin-left:1rem !important}.mx-md-4{margin-right:1.5rem !important;margin-left:1.5rem !important}.mx-md-5{margin-right:3rem !important;margin-left:3rem !important}.mx-md-auto{margin-right:auto !important;margin-left:auto !important}.my-md-0{margin-top:0 !important;margin-bottom:0 !important}.my-md-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.my-md-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.my-md-3{margin-top:1rem !important;margin-bottom:1rem !important}.my-md-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.my-md-5{margin-top:3rem !important;margin-bottom:3rem !important}.my-md-auto{margin-top:auto !important;margin-bottom:auto !important}.mt-md-0{margin-top:0 !important}.mt-md-1{margin-top:.25rem !important}.mt-md-2{margin-top:.5rem !important}.mt-md-3{margin-top:1rem !important}.mt-md-4{margin-top:1.5rem !important}.mt-md-5{margin-top:3rem !important}.mt-md-auto{margin-top:auto !important}.me-md-0{margin-right:0 !important}.me-md-1{margin-right:.25rem !important}.me-md-2{margin-right:.5rem !important}.me-md-3{margin-right:1rem !important}.me-md-4{margin-right:1.5rem !important}.me-md-5{margin-right:3rem !important}.me-md-auto{margin-right:auto !important}.mb-md-0{margin-bottom:0 !important}.mb-md-1{margin-bottom:.25rem !important}.mb-md-2{margin-bottom:.5rem !important}.mb-md-3{margin-bottom:1rem !important}.mb-md-4{margin-bottom:1.5rem !important}.mb-md-5{margin-bottom:3rem !important}.mb-md-auto{margin-bottom:auto !important}.ms-md-0{margin-left:0 !important}.ms-md-1{margin-left:.25rem !important}.ms-md-2{margin-left:.5rem !important}.ms-md-3{margin-left:1rem !important}.ms-md-4{margin-left:1.5rem !important}.ms-md-5{margin-left:3rem !important}.ms-md-auto{margin-left:auto !important}.p-md-0{padding:0 !important}.p-md-1{padding:.25rem !important}.p-md-2{padding:.5rem !important}.p-md-3{padding:1rem !important}.p-md-4{padding:1.5rem !important}.p-md-5{padding:3rem !important}.px-md-0{padding-right:0 !important;padding-left:0 !important}.px-md-1{padding-right:.25rem !important;padding-left:.25rem !important}.px-md-2{padding-right:.5rem !important;padding-left:.5rem !important}.px-md-3{padding-right:1rem !important;padding-left:1rem !important}.px-md-4{padding-right:1.5rem !important;padding-left:1.5rem !important}.px-md-5{padding-right:3rem !important;padding-left:3rem !important}.py-md-0{padding-top:0 !important;padding-bottom:0 !important}.py-md-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.py-md-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.py-md-3{padding-top:1rem !important;padding-bottom:1rem !important}.py-md-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.py-md-5{padding-top:3rem !important;padding-bottom:3rem !important}.pt-md-0{padding-top:0 !important}.pt-md-1{padding-top:.25rem !important}.pt-md-2{padding-top:.5rem !important}.pt-md-3{padding-top:1rem !important}.pt-md-4{padding-top:1.5rem !important}.pt-md-5{padding-top:3rem !important}.pe-md-0{padding-right:0 !important}.pe-md-1{padding-right:.25rem !important}.pe-md-2{padding-right:.5rem !important}.pe-md-3{padding-right:1rem !important}.pe-md-4{padding-right:1.5rem !important}.pe-md-5{padding-right:3rem !important}.pb-md-0{padding-bottom:0 !important}.pb-md-1{padding-bottom:.25rem !important}.pb-md-2{padding-bottom:.5rem !important}.pb-md-3{padding-bottom:1rem !important}.pb-md-4{padding-bottom:1.5rem !important}.pb-md-5{padding-bottom:3rem !important}.ps-md-0{padding-left:0 !important}.ps-md-1{padding-left:.25rem !important}.ps-md-2{padding-left:.5rem !important}.ps-md-3{padding-left:1rem !important}.ps-md-4{padding-left:1.5rem !important}.ps-md-5{padding-left:3rem !important}.gap-md-0{gap:0 !important}.gap-md-1{gap:.25rem !important}.gap-md-2{gap:.5rem !important}.gap-md-3{gap:1rem !important}.gap-md-4{gap:1.5rem !important}.gap-md-5{gap:3rem !important}.row-gap-md-0{row-gap:0 !important}.row-gap-md-1{row-gap:.25rem !important}.row-gap-md-2{row-gap:.5rem !important}.row-gap-md-3{row-gap:1rem !important}.row-gap-md-4{row-gap:1.5rem !important}.row-gap-md-5{row-gap:3rem !important}.column-gap-md-0{-moz-column-gap:0 !important;column-gap:0 !important}.column-gap-md-1{-moz-column-gap:.25rem !important;column-gap:.25rem !important}.column-gap-md-2{-moz-column-gap:.5rem !important;column-gap:.5rem !important}.column-gap-md-3{-moz-column-gap:1rem !important;column-gap:1rem !important}.column-gap-md-4{-moz-column-gap:1.5rem !important;column-gap:1.5rem !important}.column-gap-md-5{-moz-column-gap:3rem !important;column-gap:3rem !important}.text-md-start{text-align:left !important}.text-md-end{text-align:right !important}.text-md-center{text-align:center !important}}@media(min-width: 992px){.float-lg-start{float:left !important}.float-lg-end{float:right !important}.float-lg-none{float:none !important}.object-fit-lg-contain{-o-object-fit:contain !important;object-fit:contain !important}.object-fit-lg-cover{-o-object-fit:cover !important;object-fit:cover !important}.object-fit-lg-fill{-o-object-fit:fill !important;object-fit:fill !important}.object-fit-lg-scale{-o-object-fit:scale-down !important;object-fit:scale-down !important}.object-fit-lg-none{-o-object-fit:none !important;object-fit:none !important}.d-lg-inline{display:inline !important}.d-lg-inline-block{display:inline-block !important}.d-lg-block{display:block !important}.d-lg-grid{display:grid !important}.d-lg-inline-grid{display:inline-grid !important}.d-lg-table{display:table !important}.d-lg-table-row{display:table-row !important}.d-lg-table-cell{display:table-cell !important}.d-lg-flex{display:flex !important}.d-lg-inline-flex{display:inline-flex !important}.d-lg-none{display:none !important}.flex-lg-fill{flex:1 1 auto !important}.flex-lg-row{flex-direction:row !important}.flex-lg-column{flex-direction:column !important}.flex-lg-row-reverse{flex-direction:row-reverse !important}.flex-lg-column-reverse{flex-direction:column-reverse !important}.flex-lg-grow-0{flex-grow:0 !important}.flex-lg-grow-1{flex-grow:1 !important}.flex-lg-shrink-0{flex-shrink:0 !important}.flex-lg-shrink-1{flex-shrink:1 !important}.flex-lg-wrap{flex-wrap:wrap !important}.flex-lg-nowrap{flex-wrap:nowrap !important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse !important}.justify-content-lg-start{justify-content:flex-start !important}.justify-content-lg-end{justify-content:flex-end !important}.justify-content-lg-center{justify-content:center !important}.justify-content-lg-between{justify-content:space-between !important}.justify-content-lg-around{justify-content:space-around !important}.justify-content-lg-evenly{justify-content:space-evenly !important}.align-items-lg-start{align-items:flex-start !important}.align-items-lg-end{align-items:flex-end !important}.align-items-lg-center{align-items:center !important}.align-items-lg-baseline{align-items:baseline !important}.align-items-lg-stretch{align-items:stretch !important}.align-content-lg-start{align-content:flex-start !important}.align-content-lg-end{align-content:flex-end !important}.align-content-lg-center{align-content:center !important}.align-content-lg-between{align-content:space-between !important}.align-content-lg-around{align-content:space-around !important}.align-content-lg-stretch{align-content:stretch !important}.align-self-lg-auto{align-self:auto !important}.align-self-lg-start{align-self:flex-start !important}.align-self-lg-end{align-self:flex-end !important}.align-self-lg-center{align-self:center !important}.align-self-lg-baseline{align-self:baseline !important}.align-self-lg-stretch{align-self:stretch !important}.order-lg-first{order:-1 !important}.order-lg-0{order:0 !important}.order-lg-1{order:1 !important}.order-lg-2{order:2 !important}.order-lg-3{order:3 !important}.order-lg-4{order:4 !important}.order-lg-5{order:5 !important}.order-lg-last{order:6 !important}.m-lg-0{margin:0 !important}.m-lg-1{margin:.25rem !important}.m-lg-2{margin:.5rem !important}.m-lg-3{margin:1rem !important}.m-lg-4{margin:1.5rem !important}.m-lg-5{margin:3rem !important}.m-lg-auto{margin:auto !important}.mx-lg-0{margin-right:0 !important;margin-left:0 !important}.mx-lg-1{margin-right:.25rem !important;margin-left:.25rem !important}.mx-lg-2{margin-right:.5rem !important;margin-left:.5rem !important}.mx-lg-3{margin-right:1rem !important;margin-left:1rem !important}.mx-lg-4{margin-right:1.5rem !important;margin-left:1.5rem !important}.mx-lg-5{margin-right:3rem !important;margin-left:3rem !important}.mx-lg-auto{margin-right:auto !important;margin-left:auto !important}.my-lg-0{margin-top:0 !important;margin-bottom:0 !important}.my-lg-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.my-lg-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.my-lg-3{margin-top:1rem !important;margin-bottom:1rem !important}.my-lg-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.my-lg-5{margin-top:3rem !important;margin-bottom:3rem !important}.my-lg-auto{margin-top:auto !important;margin-bottom:auto !important}.mt-lg-0{margin-top:0 !important}.mt-lg-1{margin-top:.25rem !important}.mt-lg-2{margin-top:.5rem !important}.mt-lg-3{margin-top:1rem !important}.mt-lg-4{margin-top:1.5rem !important}.mt-lg-5{margin-top:3rem !important}.mt-lg-auto{margin-top:auto !important}.me-lg-0{margin-right:0 !important}.me-lg-1{margin-right:.25rem !important}.me-lg-2{margin-right:.5rem !important}.me-lg-3{margin-right:1rem !important}.me-lg-4{margin-right:1.5rem !important}.me-lg-5{margin-right:3rem !important}.me-lg-auto{margin-right:auto !important}.mb-lg-0{margin-bottom:0 !important}.mb-lg-1{margin-bottom:.25rem !important}.mb-lg-2{margin-bottom:.5rem !important}.mb-lg-3{margin-bottom:1rem !important}.mb-lg-4{margin-bottom:1.5rem !important}.mb-lg-5{margin-bottom:3rem !important}.mb-lg-auto{margin-bottom:auto !important}.ms-lg-0{margin-left:0 !important}.ms-lg-1{margin-left:.25rem !important}.ms-lg-2{margin-left:.5rem !important}.ms-lg-3{margin-left:1rem !important}.ms-lg-4{margin-left:1.5rem !important}.ms-lg-5{margin-left:3rem !important}.ms-lg-auto{margin-left:auto !important}.p-lg-0{padding:0 !important}.p-lg-1{padding:.25rem !important}.p-lg-2{padding:.5rem !important}.p-lg-3{padding:1rem !important}.p-lg-4{padding:1.5rem !important}.p-lg-5{padding:3rem !important}.px-lg-0{padding-right:0 !important;padding-left:0 !important}.px-lg-1{padding-right:.25rem !important;padding-left:.25rem !important}.px-lg-2{padding-right:.5rem !important;padding-left:.5rem !important}.px-lg-3{padding-right:1rem !important;padding-left:1rem !important}.px-lg-4{padding-right:1.5rem !important;padding-left:1.5rem !important}.px-lg-5{padding-right:3rem !important;padding-left:3rem !important}.py-lg-0{padding-top:0 !important;padding-bottom:0 !important}.py-lg-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.py-lg-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.py-lg-3{padding-top:1rem !important;padding-bottom:1rem !important}.py-lg-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.py-lg-5{padding-top:3rem !important;padding-bottom:3rem !important}.pt-lg-0{padding-top:0 !important}.pt-lg-1{padding-top:.25rem !important}.pt-lg-2{padding-top:.5rem !important}.pt-lg-3{padding-top:1rem !important}.pt-lg-4{padding-top:1.5rem !important}.pt-lg-5{padding-top:3rem !important}.pe-lg-0{padding-right:0 !important}.pe-lg-1{padding-right:.25rem !important}.pe-lg-2{padding-right:.5rem !important}.pe-lg-3{padding-right:1rem !important}.pe-lg-4{padding-right:1.5rem !important}.pe-lg-5{padding-right:3rem !important}.pb-lg-0{padding-bottom:0 !important}.pb-lg-1{padding-bottom:.25rem !important}.pb-lg-2{padding-bottom:.5rem !important}.pb-lg-3{padding-bottom:1rem !important}.pb-lg-4{padding-bottom:1.5rem !important}.pb-lg-5{padding-bottom:3rem !important}.ps-lg-0{padding-left:0 !important}.ps-lg-1{padding-left:.25rem !important}.ps-lg-2{padding-left:.5rem !important}.ps-lg-3{padding-left:1rem !important}.ps-lg-4{padding-left:1.5rem !important}.ps-lg-5{padding-left:3rem !important}.gap-lg-0{gap:0 !important}.gap-lg-1{gap:.25rem !important}.gap-lg-2{gap:.5rem !important}.gap-lg-3{gap:1rem !important}.gap-lg-4{gap:1.5rem !important}.gap-lg-5{gap:3rem !important}.row-gap-lg-0{row-gap:0 !important}.row-gap-lg-1{row-gap:.25rem !important}.row-gap-lg-2{row-gap:.5rem !important}.row-gap-lg-3{row-gap:1rem !important}.row-gap-lg-4{row-gap:1.5rem !important}.row-gap-lg-5{row-gap:3rem !important}.column-gap-lg-0{-moz-column-gap:0 !important;column-gap:0 !important}.column-gap-lg-1{-moz-column-gap:.25rem !important;column-gap:.25rem !important}.column-gap-lg-2{-moz-column-gap:.5rem !important;column-gap:.5rem !important}.column-gap-lg-3{-moz-column-gap:1rem !important;column-gap:1rem !important}.column-gap-lg-4{-moz-column-gap:1.5rem !important;column-gap:1.5rem !important}.column-gap-lg-5{-moz-column-gap:3rem !important;column-gap:3rem !important}.text-lg-start{text-align:left !important}.text-lg-end{text-align:right !important}.text-lg-center{text-align:center !important}}@media(min-width: 1200px){.float-xl-start{float:left !important}.float-xl-end{float:right !important}.float-xl-none{float:none !important}.object-fit-xl-contain{-o-object-fit:contain !important;object-fit:contain !important}.object-fit-xl-cover{-o-object-fit:cover !important;object-fit:cover !important}.object-fit-xl-fill{-o-object-fit:fill !important;object-fit:fill !important}.object-fit-xl-scale{-o-object-fit:scale-down !important;object-fit:scale-down !important}.object-fit-xl-none{-o-object-fit:none !important;object-fit:none !important}.d-xl-inline{display:inline !important}.d-xl-inline-block{display:inline-block !important}.d-xl-block{display:block !important}.d-xl-grid{display:grid !important}.d-xl-inline-grid{display:inline-grid !important}.d-xl-table{display:table !important}.d-xl-table-row{display:table-row !important}.d-xl-table-cell{display:table-cell !important}.d-xl-flex{display:flex !important}.d-xl-inline-flex{display:inline-flex !important}.d-xl-none{display:none !important}.flex-xl-fill{flex:1 1 auto !important}.flex-xl-row{flex-direction:row !important}.flex-xl-column{flex-direction:column !important}.flex-xl-row-reverse{flex-direction:row-reverse !important}.flex-xl-column-reverse{flex-direction:column-reverse !important}.flex-xl-grow-0{flex-grow:0 !important}.flex-xl-grow-1{flex-grow:1 !important}.flex-xl-shrink-0{flex-shrink:0 !important}.flex-xl-shrink-1{flex-shrink:1 !important}.flex-xl-wrap{flex-wrap:wrap !important}.flex-xl-nowrap{flex-wrap:nowrap !important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse !important}.justify-content-xl-start{justify-content:flex-start !important}.justify-content-xl-end{justify-content:flex-end !important}.justify-content-xl-center{justify-content:center !important}.justify-content-xl-between{justify-content:space-between !important}.justify-content-xl-around{justify-content:space-around !important}.justify-content-xl-evenly{justify-content:space-evenly !important}.align-items-xl-start{align-items:flex-start !important}.align-items-xl-end{align-items:flex-end !important}.align-items-xl-center{align-items:center !important}.align-items-xl-baseline{align-items:baseline !important}.align-items-xl-stretch{align-items:stretch !important}.align-content-xl-start{align-content:flex-start !important}.align-content-xl-end{align-content:flex-end !important}.align-content-xl-center{align-content:center !important}.align-content-xl-between{align-content:space-between !important}.align-content-xl-around{align-content:space-around !important}.align-content-xl-stretch{align-content:stretch !important}.align-self-xl-auto{align-self:auto !important}.align-self-xl-start{align-self:flex-start !important}.align-self-xl-end{align-self:flex-end !important}.align-self-xl-center{align-self:center !important}.align-self-xl-baseline{align-self:baseline !important}.align-self-xl-stretch{align-self:stretch !important}.order-xl-first{order:-1 !important}.order-xl-0{order:0 !important}.order-xl-1{order:1 !important}.order-xl-2{order:2 !important}.order-xl-3{order:3 !important}.order-xl-4{order:4 !important}.order-xl-5{order:5 !important}.order-xl-last{order:6 !important}.m-xl-0{margin:0 !important}.m-xl-1{margin:.25rem !important}.m-xl-2{margin:.5rem !important}.m-xl-3{margin:1rem !important}.m-xl-4{margin:1.5rem !important}.m-xl-5{margin:3rem !important}.m-xl-auto{margin:auto !important}.mx-xl-0{margin-right:0 !important;margin-left:0 !important}.mx-xl-1{margin-right:.25rem !important;margin-left:.25rem !important}.mx-xl-2{margin-right:.5rem !important;margin-left:.5rem !important}.mx-xl-3{margin-right:1rem !important;margin-left:1rem !important}.mx-xl-4{margin-right:1.5rem !important;margin-left:1.5rem !important}.mx-xl-5{margin-right:3rem !important;margin-left:3rem !important}.mx-xl-auto{margin-right:auto !important;margin-left:auto !important}.my-xl-0{margin-top:0 !important;margin-bottom:0 !important}.my-xl-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.my-xl-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.my-xl-3{margin-top:1rem !important;margin-bottom:1rem !important}.my-xl-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.my-xl-5{margin-top:3rem !important;margin-bottom:3rem !important}.my-xl-auto{margin-top:auto !important;margin-bottom:auto !important}.mt-xl-0{margin-top:0 !important}.mt-xl-1{margin-top:.25rem !important}.mt-xl-2{margin-top:.5rem !important}.mt-xl-3{margin-top:1rem !important}.mt-xl-4{margin-top:1.5rem !important}.mt-xl-5{margin-top:3rem !important}.mt-xl-auto{margin-top:auto !important}.me-xl-0{margin-right:0 !important}.me-xl-1{margin-right:.25rem !important}.me-xl-2{margin-right:.5rem !important}.me-xl-3{margin-right:1rem !important}.me-xl-4{margin-right:1.5rem !important}.me-xl-5{margin-right:3rem !important}.me-xl-auto{margin-right:auto !important}.mb-xl-0{margin-bottom:0 !important}.mb-xl-1{margin-bottom:.25rem !important}.mb-xl-2{margin-bottom:.5rem !important}.mb-xl-3{margin-bottom:1rem !important}.mb-xl-4{margin-bottom:1.5rem !important}.mb-xl-5{margin-bottom:3rem !important}.mb-xl-auto{margin-bottom:auto !important}.ms-xl-0{margin-left:0 !important}.ms-xl-1{margin-left:.25rem !important}.ms-xl-2{margin-left:.5rem !important}.ms-xl-3{margin-left:1rem !important}.ms-xl-4{margin-left:1.5rem !important}.ms-xl-5{margin-left:3rem !important}.ms-xl-auto{margin-left:auto !important}.p-xl-0{padding:0 !important}.p-xl-1{padding:.25rem !important}.p-xl-2{padding:.5rem !important}.p-xl-3{padding:1rem !important}.p-xl-4{padding:1.5rem !important}.p-xl-5{padding:3rem !important}.px-xl-0{padding-right:0 !important;padding-left:0 !important}.px-xl-1{padding-right:.25rem !important;padding-left:.25rem !important}.px-xl-2{padding-right:.5rem !important;padding-left:.5rem !important}.px-xl-3{padding-right:1rem !important;padding-left:1rem !important}.px-xl-4{padding-right:1.5rem !important;padding-left:1.5rem !important}.px-xl-5{padding-right:3rem !important;padding-left:3rem !important}.py-xl-0{padding-top:0 !important;padding-bottom:0 !important}.py-xl-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.py-xl-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.py-xl-3{padding-top:1rem !important;padding-bottom:1rem !important}.py-xl-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.py-xl-5{padding-top:3rem !important;padding-bottom:3rem !important}.pt-xl-0{padding-top:0 !important}.pt-xl-1{padding-top:.25rem !important}.pt-xl-2{padding-top:.5rem !important}.pt-xl-3{padding-top:1rem !important}.pt-xl-4{padding-top:1.5rem !important}.pt-xl-5{padding-top:3rem !important}.pe-xl-0{padding-right:0 !important}.pe-xl-1{padding-right:.25rem !important}.pe-xl-2{padding-right:.5rem !important}.pe-xl-3{padding-right:1rem !important}.pe-xl-4{padding-right:1.5rem !important}.pe-xl-5{padding-right:3rem !important}.pb-xl-0{padding-bottom:0 !important}.pb-xl-1{padding-bottom:.25rem !important}.pb-xl-2{padding-bottom:.5rem !important}.pb-xl-3{padding-bottom:1rem !important}.pb-xl-4{padding-bottom:1.5rem !important}.pb-xl-5{padding-bottom:3rem !important}.ps-xl-0{padding-left:0 !important}.ps-xl-1{padding-left:.25rem !important}.ps-xl-2{padding-left:.5rem !important}.ps-xl-3{padding-left:1rem !important}.ps-xl-4{padding-left:1.5rem !important}.ps-xl-5{padding-left:3rem !important}.gap-xl-0{gap:0 !important}.gap-xl-1{gap:.25rem !important}.gap-xl-2{gap:.5rem !important}.gap-xl-3{gap:1rem !important}.gap-xl-4{gap:1.5rem !important}.gap-xl-5{gap:3rem !important}.row-gap-xl-0{row-gap:0 !important}.row-gap-xl-1{row-gap:.25rem !important}.row-gap-xl-2{row-gap:.5rem !important}.row-gap-xl-3{row-gap:1rem !important}.row-gap-xl-4{row-gap:1.5rem !important}.row-gap-xl-5{row-gap:3rem !important}.column-gap-xl-0{-moz-column-gap:0 !important;column-gap:0 !important}.column-gap-xl-1{-moz-column-gap:.25rem !important;column-gap:.25rem !important}.column-gap-xl-2{-moz-column-gap:.5rem !important;column-gap:.5rem !important}.column-gap-xl-3{-moz-column-gap:1rem !important;column-gap:1rem !important}.column-gap-xl-4{-moz-column-gap:1.5rem !important;column-gap:1.5rem !important}.column-gap-xl-5{-moz-column-gap:3rem !important;column-gap:3rem !important}.text-xl-start{text-align:left !important}.text-xl-end{text-align:right !important}.text-xl-center{text-align:center !important}}@media(min-width: 1400px){.float-xxl-start{float:left !important}.float-xxl-end{float:right !important}.float-xxl-none{float:none !important}.object-fit-xxl-contain{-o-object-fit:contain !important;object-fit:contain !important}.object-fit-xxl-cover{-o-object-fit:cover !important;object-fit:cover !important}.object-fit-xxl-fill{-o-object-fit:fill !important;object-fit:fill !important}.object-fit-xxl-scale{-o-object-fit:scale-down !important;object-fit:scale-down !important}.object-fit-xxl-none{-o-object-fit:none !important;object-fit:none !important}.d-xxl-inline{display:inline !important}.d-xxl-inline-block{display:inline-block !important}.d-xxl-block{display:block !important}.d-xxl-grid{display:grid !important}.d-xxl-inline-grid{display:inline-grid !important}.d-xxl-table{display:table !important}.d-xxl-table-row{display:table-row !important}.d-xxl-table-cell{display:table-cell !important}.d-xxl-flex{display:flex !important}.d-xxl-inline-flex{display:inline-flex !important}.d-xxl-none{display:none !important}.flex-xxl-fill{flex:1 1 auto !important}.flex-xxl-row{flex-direction:row !important}.flex-xxl-column{flex-direction:column !important}.flex-xxl-row-reverse{flex-direction:row-reverse !important}.flex-xxl-column-reverse{flex-direction:column-reverse !important}.flex-xxl-grow-0{flex-grow:0 !important}.flex-xxl-grow-1{flex-grow:1 !important}.flex-xxl-shrink-0{flex-shrink:0 !important}.flex-xxl-shrink-1{flex-shrink:1 !important}.flex-xxl-wrap{flex-wrap:wrap !important}.flex-xxl-nowrap{flex-wrap:nowrap !important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse !important}.justify-content-xxl-start{justify-content:flex-start !important}.justify-content-xxl-end{justify-content:flex-end !important}.justify-content-xxl-center{justify-content:center !important}.justify-content-xxl-between{justify-content:space-between !important}.justify-content-xxl-around{justify-content:space-around !important}.justify-content-xxl-evenly{justify-content:space-evenly !important}.align-items-xxl-start{align-items:flex-start !important}.align-items-xxl-end{align-items:flex-end !important}.align-items-xxl-center{align-items:center !important}.align-items-xxl-baseline{align-items:baseline !important}.align-items-xxl-stretch{align-items:stretch !important}.align-content-xxl-start{align-content:flex-start !important}.align-content-xxl-end{align-content:flex-end !important}.align-content-xxl-center{align-content:center !important}.align-content-xxl-between{align-content:space-between !important}.align-content-xxl-around{align-content:space-around !important}.align-content-xxl-stretch{align-content:stretch !important}.align-self-xxl-auto{align-self:auto !important}.align-self-xxl-start{align-self:flex-start !important}.align-self-xxl-end{align-self:flex-end !important}.align-self-xxl-center{align-self:center !important}.align-self-xxl-baseline{align-self:baseline !important}.align-self-xxl-stretch{align-self:stretch !important}.order-xxl-first{order:-1 !important}.order-xxl-0{order:0 !important}.order-xxl-1{order:1 !important}.order-xxl-2{order:2 !important}.order-xxl-3{order:3 !important}.order-xxl-4{order:4 !important}.order-xxl-5{order:5 !important}.order-xxl-last{order:6 !important}.m-xxl-0{margin:0 !important}.m-xxl-1{margin:.25rem !important}.m-xxl-2{margin:.5rem !important}.m-xxl-3{margin:1rem !important}.m-xxl-4{margin:1.5rem !important}.m-xxl-5{margin:3rem !important}.m-xxl-auto{margin:auto !important}.mx-xxl-0{margin-right:0 !important;margin-left:0 !important}.mx-xxl-1{margin-right:.25rem !important;margin-left:.25rem !important}.mx-xxl-2{margin-right:.5rem !important;margin-left:.5rem !important}.mx-xxl-3{margin-right:1rem !important;margin-left:1rem !important}.mx-xxl-4{margin-right:1.5rem !important;margin-left:1.5rem !important}.mx-xxl-5{margin-right:3rem !important;margin-left:3rem !important}.mx-xxl-auto{margin-right:auto !important;margin-left:auto !important}.my-xxl-0{margin-top:0 !important;margin-bottom:0 !important}.my-xxl-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.my-xxl-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.my-xxl-3{margin-top:1rem !important;margin-bottom:1rem !important}.my-xxl-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.my-xxl-5{margin-top:3rem !important;margin-bottom:3rem !important}.my-xxl-auto{margin-top:auto !important;margin-bottom:auto !important}.mt-xxl-0{margin-top:0 !important}.mt-xxl-1{margin-top:.25rem !important}.mt-xxl-2{margin-top:.5rem !important}.mt-xxl-3{margin-top:1rem !important}.mt-xxl-4{margin-top:1.5rem !important}.mt-xxl-5{margin-top:3rem !important}.mt-xxl-auto{margin-top:auto !important}.me-xxl-0{margin-right:0 !important}.me-xxl-1{margin-right:.25rem !important}.me-xxl-2{margin-right:.5rem !important}.me-xxl-3{margin-right:1rem !important}.me-xxl-4{margin-right:1.5rem !important}.me-xxl-5{margin-right:3rem !important}.me-xxl-auto{margin-right:auto !important}.mb-xxl-0{margin-bottom:0 !important}.mb-xxl-1{margin-bottom:.25rem !important}.mb-xxl-2{margin-bottom:.5rem !important}.mb-xxl-3{margin-bottom:1rem !important}.mb-xxl-4{margin-bottom:1.5rem !important}.mb-xxl-5{margin-bottom:3rem !important}.mb-xxl-auto{margin-bottom:auto !important}.ms-xxl-0{margin-left:0 !important}.ms-xxl-1{margin-left:.25rem !important}.ms-xxl-2{margin-left:.5rem !important}.ms-xxl-3{margin-left:1rem !important}.ms-xxl-4{margin-left:1.5rem !important}.ms-xxl-5{margin-left:3rem !important}.ms-xxl-auto{margin-left:auto !important}.p-xxl-0{padding:0 !important}.p-xxl-1{padding:.25rem !important}.p-xxl-2{padding:.5rem !important}.p-xxl-3{padding:1rem !important}.p-xxl-4{padding:1.5rem !important}.p-xxl-5{padding:3rem !important}.px-xxl-0{padding-right:0 !important;padding-left:0 !important}.px-xxl-1{padding-right:.25rem !important;padding-left:.25rem !important}.px-xxl-2{padding-right:.5rem !important;padding-left:.5rem !important}.px-xxl-3{padding-right:1rem !important;padding-left:1rem !important}.px-xxl-4{padding-right:1.5rem !important;padding-left:1.5rem !important}.px-xxl-5{padding-right:3rem !important;padding-left:3rem !important}.py-xxl-0{padding-top:0 !important;padding-bottom:0 !important}.py-xxl-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.py-xxl-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.py-xxl-3{padding-top:1rem !important;padding-bottom:1rem !important}.py-xxl-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.py-xxl-5{padding-top:3rem !important;padding-bottom:3rem !important}.pt-xxl-0{padding-top:0 !important}.pt-xxl-1{padding-top:.25rem !important}.pt-xxl-2{padding-top:.5rem !important}.pt-xxl-3{padding-top:1rem !important}.pt-xxl-4{padding-top:1.5rem !important}.pt-xxl-5{padding-top:3rem !important}.pe-xxl-0{padding-right:0 !important}.pe-xxl-1{padding-right:.25rem !important}.pe-xxl-2{padding-right:.5rem !important}.pe-xxl-3{padding-right:1rem !important}.pe-xxl-4{padding-right:1.5rem !important}.pe-xxl-5{padding-right:3rem !important}.pb-xxl-0{padding-bottom:0 !important}.pb-xxl-1{padding-bottom:.25rem !important}.pb-xxl-2{padding-bottom:.5rem !important}.pb-xxl-3{padding-bottom:1rem !important}.pb-xxl-4{padding-bottom:1.5rem !important}.pb-xxl-5{padding-bottom:3rem !important}.ps-xxl-0{padding-left:0 !important}.ps-xxl-1{padding-left:.25rem !important}.ps-xxl-2{padding-left:.5rem !important}.ps-xxl-3{padding-left:1rem !important}.ps-xxl-4{padding-left:1.5rem !important}.ps-xxl-5{padding-left:3rem !important}.gap-xxl-0{gap:0 !important}.gap-xxl-1{gap:.25rem !important}.gap-xxl-2{gap:.5rem !important}.gap-xxl-3{gap:1rem !important}.gap-xxl-4{gap:1.5rem !important}.gap-xxl-5{gap:3rem !important}.row-gap-xxl-0{row-gap:0 !important}.row-gap-xxl-1{row-gap:.25rem !important}.row-gap-xxl-2{row-gap:.5rem !important}.row-gap-xxl-3{row-gap:1rem !important}.row-gap-xxl-4{row-gap:1.5rem !important}.row-gap-xxl-5{row-gap:3rem !important}.column-gap-xxl-0{-moz-column-gap:0 !important;column-gap:0 !important}.column-gap-xxl-1{-moz-column-gap:.25rem !important;column-gap:.25rem !important}.column-gap-xxl-2{-moz-column-gap:.5rem !important;column-gap:.5rem !important}.column-gap-xxl-3{-moz-column-gap:1rem !important;column-gap:1rem !important}.column-gap-xxl-4{-moz-column-gap:1.5rem !important;column-gap:1.5rem !important}.column-gap-xxl-5{-moz-column-gap:3rem !important;column-gap:3rem !important}.text-xxl-start{text-align:left !important}.text-xxl-end{text-align:right !important}.text-xxl-center{text-align:center !important}}@media(min-width: 1200px){.fs-1{font-size:2.1875rem !important}.fs-2{font-size:1.75rem !important}.fs-3{font-size:1.53125rem !important}.fs-4{font-size:1.3125rem !important}}@media print{.d-print-inline{display:inline !important}.d-print-inline-block{display:inline-block !important}.d-print-block{display:block !important}.d-print-grid{display:grid !important}.d-print-inline-grid{display:inline-grid !important}.d-print-table{display:table !important}.d-print-table-row{display:table-row !important}.d-print-table-cell{display:table-cell !important}.d-print-flex{display:flex !important}.d-print-inline-flex{display:inline-flex !important}.d-print-none{display:none !important}}:root,[data-bs-theme=dark]{--elevation-0: 0 1px 1px 0 rgba(20, 23, 33, 0.8784313725), 0 0 1px 0 rgba(20, 23, 33, 0.8784313725);--elevation-0-inverted: 0 1px 1px 0 rgba(20, 23, 33, 0.0784313725), 0 0 1px 0 rgba(20, 23, 33, 0.3803921569);--elevation-1: 0 2px 4px -1px rgba(20, 23, 33, 0.3803921569), 0 1px 2px -1px rgba(20, 23, 33, 0.8784313725);--elevation-1-inverted: 0 2px 4px 1px rgba(20, 23, 33, 0.1215686275), 0 1px 2px 0 rgba(20, 23, 33, 0.1215686275);--elevation-2: 0 4px 8px 0 rgba(20, 23, 33, 0.3803921569), 0 2px 4px -1px rgba(20, 23, 33, 0.8784313725);--elevation-2-inverted: 0 4px 8px 0 rgba(20, 23, 33, 0.1215686275), 0 2px 4px -1px rgba(20, 23, 33, 0.0784313725);--elevation-3: 0 8px 12px 1px rgba(20, 23, 33, 0.3803921569), 0 4px 8px -1px rgba(20, 23, 33, 0.8784313725);--elevation-3-inverted: 0 8px 12px 1px rgba(20, 23, 33, 0.1215686275), 0 4px 8px -1px rgba(20, 23, 33, 0.0784313725);--elevation-4: 0 16px 32px 2px rgba(20, 23, 33, 0.3803921569), 0 8px 16px -2px rgba(20, 23, 33, 0.8784313725);--elevation-4-inverted: 0 16px 32px 2px rgba(20, 23, 33, 0.1215686275), 0 8px 16px -2px rgba(20, 23, 33, 0.0784313725);--primary-50: #373a44;--primary-50-inverted: white;--primary-100: #333640;--primary-100-inverted: #fafafb;--primary-200: #2f323c;--primary-200-inverted: #f5f6f6;--primary-300: #2b2e39;--primary-300-inverted: #f2f3f4;--primary-400: #272a35;--primary-400-inverted: #ebedee;--primary-500: #232632;--primary-500-inverted: #e6e8ea;--primary-600: #1f222e;--primary-600-inverted: #e2e4e6;--primary-700: #1b1e2a;--primary-700-inverted: #dddfe1;--primary-800: #181b26;--primary-800-inverted: #d8dadd;--primary-900: #141721;--primary-900-inverted: #d3d6d9;--text-placeholder: rgba(255, 255, 255, 0.3803921569);--text-placeholder-inverted: rgba(20, 23, 33, 0.3803921569);--text-secondary: rgba(255, 255, 255, 0.6);--text-secondary-inverted: rgba(20, 23, 33, 0.6);--text-primary: rgba(255, 255, 255, 0.8784313725);--text-primary-inverted: rgba(20, 23, 33, 0.8784313725);--text-primaryHover: white;--text-primaryHover-inverted: #141721;--text-disabled: rgba(255, 255, 255, 0.3019607843);--text-disabled-inverted: rgba(20, 23, 33, 0.3019607843);--fill-subtle: rgba(255, 255, 255, 0.1019607843);--fill-subtle-inverted: rgba(20, 23, 33, 0.1019607843);--fill-secondary: rgba(255, 255, 255, 0.6);--fill-secondary-inverted: rgba(20, 23, 33, 0.6);--fill-primary: rgba(255, 255, 255, 0.8784313725);--fill-primary-inverted: rgba(20, 23, 33, 0.8784313725);--fill-enabled: rgba(255, 255, 255, 0.6);--fill-enabled-inverted: rgba(20, 23, 33, 0.6);--fill-active: rgba(255, 255, 255, 0.8784313725);--fill-active-inverted: rgba(20, 23, 33, 0.8784313725);--fill-hoverSelected: white;--fill-hoverSelected-inverted: #141721;--fill-disabled: rgba(255, 255, 255, 0.3019607843);--fill-disabled-inverted: rgba(20, 23, 33, 0.3019607843);--border-subtleAlpha01: rgba(255, 255, 255, 0.1019607843);--border-subtleAlpha01-inverted: rgba(20, 23, 33, 0.1019607843);--border-subtleAlpha02: rgba(255, 255, 255, 0.1607843137);--border-subtleAlpha02-inverted: rgba(20, 23, 33, 0.1607843137);--border-subtleAlpha03: rgba(255, 255, 255, 0.2392156863);--border-subtleAlpha03-inverted: rgba(20, 23, 33, 0.2392156863);--border-enabled: rgba(255, 255, 255, 0.6);--border-enabled-inverted: rgba(20, 23, 33, 0.6);--border-hover: rgba(255, 255, 255, 0.8784313725);--border-hover-inverted: rgba(20, 23, 33, 0.8784313725);--border-disabled: rgba(255, 255, 255, 0.3019607843);--border-disabled-inverted: rgba(20, 23, 33, 0.3019607843);--border-selected: white;--border-selected-inverted: #141721;--border-selectedInverse: #141721;--border-selectedInverse-inverted: white;--stateOverlays-enabled: rgba(255, 255, 255, 0);--stateOverlays-enabled-inverted: rgba(20, 23, 33, 0);--stateOverlays-hover: rgba(255, 255, 255, 0.0392156863);--stateOverlays-hover-inverted: rgba(20, 23, 33, 0.0588235294);--stateOverlays-active: rgba(255, 255, 255, 0.0784313725);--stateOverlays-active-inverted: rgba(20, 23, 33, 0.1215686275);--stateOverlays-disabled: rgba(4, 19, 31, 0.1607843137);--stateOverlays-disabled-inverted: rgba(20, 23, 33, 0.1607843137);--stateOverlays-selected: #2b2e39;--stateOverlays-selected-inverted: #fafafb;--stateOverlays-selectedHover: rgba(255, 255, 255, 0.1607843137);--stateOverlays-selectedHover-inverted: rgba(20, 23, 33, 0.2392156863);--stateOverlays-selectedInverse: white;--stateOverlays-selectedInverse-inverted: #141721;--stateOverlays-selectedRange: rgba(255, 255, 255, 0.1019607843);--stateOverlays-selectedRange-inverted: rgba(20, 23, 33, 0.0784313725);--field-enabled: #2b2e39;--field-enabled-inverted: #fafafb;--field-hover: #373a44;--field-hover-inverted: white;--field-disabled: #272a35;--field-disabled-inverted: #f0f1f2;--status-success: #40d86e;--status-success-inverted: #26bf56;--status-error: #f56565;--status-error-inverted: #f03b3a;--status-information: #00b4ff;--status-information-inverted: #009eff;--status-warning: #ffc107;--status-warning-inverted: #f17c02;--focus: rgba(0, 133, 255, 0.6);--focus-inverted: rgba(0, 133, 255, 0.6);--surfaces-bg01: #232632;--surfaces-bg01-inverted: white;--surfaces-bg02: #1b1e2a;--surfaces-bg02-inverted: #f5f6f6;--surfaces-bg03: #141721;--surfaces-bg03-inverted: #e6e8ea;--categorical-01Cyan: #00b4ff;--categorical-01Cyan-inverted: #00b4ff;--categorical-02Orange: #ff9222;--categorical-02Orange-inverted: #ff9222;--categorical-03Purple: #3949ab;--categorical-03Purple-inverted: #3949ab;--categorical-04Red: #ff5267;--categorical-04Red-inverted: #ff5267;--categorical-05Teal: #08bdba;--categorical-05Teal-inverted: #08bdba;--categorical-06Amber: #fdc935;--categorical-06Amber-inverted: #fdc935;--categorical-07Green: #689f38;--categorical-07Green-inverted: #689f38;--categorical-08Purple: #976fd1;--categorical-08Purple-inverted: #976fd1;--categorical-09Pink: #f781bf;--categorical-09Pink-inverted: #f781bf;--categorical-10DarkGreen: #52733e;--categorical-10DarkGreen-inverted: #52733e;--sequentialCyan-100: #afe7f9;--sequentialCyan-100-inverted: #afe7f9;--sequentialCyan-200: #8bd0f6;--sequentialCyan-200-inverted: #8bd0f6;--sequentialCyan-300: #6cbaec;--sequentialCyan-300-inverted: #6cbaec;--sequentialCyan-400: #52a3dd;--sequentialCyan-400-inverted: #52a3dd;--sequentialCyan-500: #3b8dcb;--sequentialCyan-500-inverted: #3b8dcb;--sequentialCyan-600: #2777b7;--sequentialCyan-600-inverted: #2777b7;--sequentialCyan-700: #1661a2;--sequentialCyan-700-inverted: #1661a2;--sequentialCyan-800: #074c8c;--sequentialCyan-800-inverted: #074c8c;--sequentialCyan-900: #003875;--sequentialCyan-900-inverted: #003875;--sequentialOrange-100: #f9d8ac;--sequentialOrange-100-inverted: #f9d8ac;--sequentialOrange-200: #feb85b;--sequentialOrange-200-inverted: #feb85b;--sequentialOrange-300: #f09b32;--sequentialOrange-300-inverted: #f09b32;--sequentialOrange-400: #db811e;--sequentialOrange-400-inverted: #db811e;--sequentialOrange-500: #c76809;--sequentialOrange-500-inverted: #c76809;--sequentialOrange-600: #b05000;--sequentialOrange-600-inverted: #b05000;--sequentialOrange-700: #973a00;--sequentialOrange-700-inverted: #973a00;--sequentialOrange-800: #7e2400;--sequentialOrange-800-inverted: #7e2400;--sequentialOrange-900: #640d00;--sequentialOrange-900-inverted: #640d00;--sequentialIndigo-100: #dfd8fa;--sequentialIndigo-100-inverted: #dfd8fa;--sequentialIndigo-200: #c3c1ed;--sequentialIndigo-200-inverted: #c3c1ed;--sequentialIndigo-300: #aba8e0;--sequentialIndigo-300-inverted: #aba8e0;--sequentialIndigo-400: #9390d2;--sequentialIndigo-400-inverted: #9390d2;--sequentialIndigo-500: #7a79c4;--sequentialIndigo-500-inverted: #7a79c4;--sequentialIndigo-600: #6163b5;--sequentialIndigo-600-inverted: #6163b5;--sequentialIndigo-700: #474ea6;--sequentialIndigo-700-inverted: #474ea6;--sequentialIndigo-800: #2a3994;--sequentialIndigo-800-inverted: #2a3994;--sequentialIndigo-900: #002680;--sequentialIndigo-900-inverted: #002680;--sequentialYellow-100: #fff7cd;--sequentialYellow-100-inverted: #fff7cd;--sequentialYellow-200: #ffed9b;--sequentialYellow-200-inverted: #ffed9b;--sequentialYellow-300: #ffe16a;--sequentialYellow-300-inverted: #ffe16a;--sequentialYellow-400: #ffd545;--sequentialYellow-400-inverted: #ffd545;--sequentialYellow-500: #ffc107;--sequentialYellow-500-inverted: #ffc107;--sequentialYellow-600: #dba005;--sequentialYellow-600-inverted: #dba005;--sequentialYellow-700: #b78103;--sequentialYellow-700-inverted: #b78103;--sequentialYellow-800: #936402;--sequentialYellow-800-inverted: #936402;--sequentialYellow-900: #7a4f01;--sequentialYellow-900-inverted: #7a4f01;--sequentialTeal-100: #a5eae8;--sequentialTeal-100-inverted: #a5eae8;--sequentialTeal-200: #7dd5d3;--sequentialTeal-200-inverted: #7dd5d3;--sequentialTeal-300: #5ebfbc;--sequentialTeal-300-inverted: #5ebfbc;--sequentialTeal-400: #44a8a6;--sequentialTeal-400-inverted: #44a8a6;--sequentialTeal-500: #2e9190;--sequentialTeal-500-inverted: #2e9190;--sequentialTeal-600: #1b7b7a;--sequentialTeal-600-inverted: #1b7b7a;--sequentialTeal-700: #0c6565;--sequentialTeal-700-inverted: #0c6565;--sequentialTeal-800: #025050;--sequentialTeal-800-inverted: #025050;--sequentialTeal-900: #003b3c;--sequentialTeal-900-inverted: #003b3c;--sequentialRed-100: #f8d6da;--sequentialRed-100-inverted: #f8d6da;--sequentialRed-200: #fcb6ba;--sequentialRed-200-inverted: #fcb6ba;--sequentialRed-300: #f8989b;--sequentialRed-300-inverted: #f8989b;--sequentialRed-400: #ed7b7f;--sequentialRed-400-inverted: #ed7b7f;--sequentialRed-500: #dd6065;--sequentialRed-500-inverted: #dd6065;--sequentialRed-600: #c9474c;--sequentialRed-600-inverted: #c9474c;--sequentialRed-700: #b22f36;--sequentialRed-700-inverted: #b22f36;--sequentialRed-800: #981822;--sequentialRed-800-inverted: #981822;--sequentialRed-900: #7d000f;--sequentialRed-900-inverted: #7d000f;--surfaces-bg-card: #232632;--surfaces-bg-card-inverted: #F5F6F6;--bs-primary-rgb: #ffffffe0;--bs-secondary-color: #ffffff99;--bs-nav-link-color: #ffffff99;--bs-form-valid-color: #40d86eff;--bs-form-invalid-color: #f56565ff;--bs-body-bg: #141721;--bs-tooltip-bg: white;--bs-tooltip-color: rgba(20, 23, 33, 0.8784313725)}[data-bs-theme=light]{--elevation-0: 0 1px 1px 0 rgba(20, 23, 33, 0.0784313725), 0 0 1px 0 rgba(20, 23, 33, 0.3803921569);--elevation-0-inverted: 0 1px 1px 0 rgba(20, 23, 33, 0.8784313725), 0 0 1px 0 rgba(20, 23, 33, 0.8784313725);--elevation-1: 0 2px 4px 1px rgba(20, 23, 33, 0.1215686275), 0 1px 2px 0 rgba(20, 23, 33, 0.1215686275);--elevation-1-inverted: 0 2px 4px -1px rgba(20, 23, 33, 0.3803921569), 0 1px 2px -1px rgba(20, 23, 33, 0.8784313725);--elevation-2: 0 4px 8px 0 rgba(20, 23, 33, 0.1215686275), 0 2px 4px -1px rgba(20, 23, 33, 0.0784313725);--elevation-2-inverted: 0 4px 8px 0 rgba(20, 23, 33, 0.3803921569), 0 2px 4px -1px rgba(20, 23, 33, 0.8784313725);--elevation-3: 0 8px 12px 1px rgba(20, 23, 33, 0.1215686275), 0 4px 8px -1px rgba(20, 23, 33, 0.0784313725);--elevation-3-inverted: 0 8px 12px 1px rgba(20, 23, 33, 0.3803921569), 0 4px 8px -1px rgba(20, 23, 33, 0.8784313725);--elevation-4: 0 16px 32px 2px rgba(20, 23, 33, 0.1215686275), 0 8px 16px -2px rgba(20, 23, 33, 0.0784313725);--elevation-4-inverted: 0 16px 32px 2px rgba(20, 23, 33, 0.3803921569), 0 8px 16px -2px rgba(20, 23, 33, 0.8784313725);--primary-50: white;--primary-50-inverted: #373a44;--primary-100: #fafafb;--primary-100-inverted: #333640;--primary-200: #f5f6f6;--primary-200-inverted: #2f323c;--primary-300: #f2f3f4;--primary-300-inverted: #2b2e39;--primary-400: #ebedee;--primary-400-inverted: #272a35;--primary-500: #e6e8ea;--primary-500-inverted: #232632;--primary-600: #e2e4e6;--primary-600-inverted: #1f222e;--primary-700: #dddfe1;--primary-700-inverted: #1b1e2a;--primary-800: #d8dadd;--primary-800-inverted: #181b26;--primary-900: #d3d6d9;--primary-900-inverted: #141721;--text-placeholder: rgba(20, 23, 33, 0.3803921569);--text-placeholder-inverted: rgba(255, 255, 255, 0.3803921569);--text-secondary: rgba(20, 23, 33, 0.6);--text-secondary-inverted: rgba(255, 255, 255, 0.6);--text-primary: rgba(20, 23, 33, 0.8784313725);--text-primary-inverted: rgba(255, 255, 255, 0.8784313725);--text-primaryHover: #141721;--text-primaryHover-inverted: white;--text-disabled: rgba(20, 23, 33, 0.3019607843);--text-disabled-inverted: rgba(255, 255, 255, 0.3019607843);--fill-subtle: rgba(20, 23, 33, 0.1019607843);--fill-subtle-inverted: rgba(255, 255, 255, 0.1019607843);--fill-secondary: rgba(20, 23, 33, 0.6);--fill-secondary-inverted: rgba(255, 255, 255, 0.6);--fill-primary: rgba(20, 23, 33, 0.8784313725);--fill-primary-inverted: rgba(255, 255, 255, 0.8784313725);--fill-enabled: rgba(20, 23, 33, 0.6);--fill-enabled-inverted: rgba(255, 255, 255, 0.6);--fill-active: rgba(20, 23, 33, 0.8784313725);--fill-active-inverted: rgba(255, 255, 255, 0.8784313725);--fill-hoverSelected: #141721;--fill-hoverSelected-inverted: white;--fill-disabled: rgba(20, 23, 33, 0.3019607843);--fill-disabled-inverted: rgba(255, 255, 255, 0.3019607843);--border-subtleAlpha01: rgba(20, 23, 33, 0.1019607843);--border-subtleAlpha01-inverted: rgba(255, 255, 255, 0.1019607843);--border-subtleAlpha02: rgba(20, 23, 33, 0.1607843137);--border-subtleAlpha02-inverted: rgba(255, 255, 255, 0.1607843137);--border-subtleAlpha03: rgba(20, 23, 33, 0.2392156863);--border-subtleAlpha03-inverted: rgba(255, 255, 255, 0.2392156863);--border-enabled: rgba(20, 23, 33, 0.6);--border-enabled-inverted: rgba(255, 255, 255, 0.6);--border-hover: rgba(20, 23, 33, 0.8784313725);--border-hover-inverted: rgba(255, 255, 255, 0.8784313725);--border-disabled: rgba(20, 23, 33, 0.3019607843);--border-disabled-inverted: rgba(255, 255, 255, 0.3019607843);--border-selected: #141721;--border-selected-inverted: white;--border-selectedInverse: white;--border-selectedInverse-inverted: #141721;--stateOverlays-enabled: rgba(20, 23, 33, 0);--stateOverlays-enabled-inverted: rgba(255, 255, 255, 0);--stateOverlays-hover: rgba(20, 23, 33, 0.0588235294);--stateOverlays-hover-inverted: rgba(255, 255, 255, 0.0392156863);--stateOverlays-active: rgba(20, 23, 33, 0.1215686275);--stateOverlays-active-inverted: rgba(255, 255, 255, 0.0784313725);--stateOverlays-disabled: rgba(20, 23, 33, 0.1607843137);--stateOverlays-disabled-inverted: rgba(4, 19, 31, 0.1607843137);--stateOverlays-selected: #fafafb;--stateOverlays-selected-inverted: #2b2e39;--stateOverlays-selectedHover: rgba(20, 23, 33, 0.2392156863);--stateOverlays-selectedHover-inverted: rgba(255, 255, 255, 0.1607843137);--stateOverlays-selectedInverse: #141721;--stateOverlays-selectedInverse-inverted: white;--stateOverlays-selectedRange: rgba(20, 23, 33, 0.0784313725);--stateOverlays-selectedRange-inverted: rgba(255, 255, 255, 0.1019607843);--field-enabled: #fafafb;--field-enabled-inverted: #2b2e39;--field-hover: white;--field-hover-inverted: #373a44;--field-disabled: #f0f1f2;--field-disabled-inverted: #272a35;--status-success: #26bf56;--status-success-inverted: #40d86e;--status-error: #f03b3a;--status-error-inverted: #f56565;--status-information: #009eff;--status-information-inverted: #00b4ff;--status-warning: #f17c02;--status-warning-inverted: #ffc107;--focus: rgba(0, 133, 255, 0.6);--focus-inverted: rgba(0, 133, 255, 0.6);--surfaces-bg01: white;--surfaces-bg01-inverted: #232632;--surfaces-bg02: #f5f6f6;--surfaces-bg02-inverted: #1b1e2a;--surfaces-bg03: #e6e8ea;--surfaces-bg03-inverted: #141721;--categorical-01Cyan: #00b4ff;--categorical-01Cyan-inverted: #00b4ff;--categorical-02Orange: #ff9222;--categorical-02Orange-inverted: #ff9222;--categorical-03Purple: #3949ab;--categorical-03Purple-inverted: #3949ab;--categorical-04Red: #ff5267;--categorical-04Red-inverted: #ff5267;--categorical-05Teal: #08bdba;--categorical-05Teal-inverted: #08bdba;--categorical-06Amber: #fdc935;--categorical-06Amber-inverted: #fdc935;--categorical-07Green: #689f38;--categorical-07Green-inverted: #689f38;--categorical-08Purple: #976fd1;--categorical-08Purple-inverted: #976fd1;--categorical-09Pink: #f781bf;--categorical-09Pink-inverted: #f781bf;--categorical-10DarkGreen: #52733e;--categorical-10DarkGreen-inverted: #52733e;--sequentialCyan-100: #afe7f9;--sequentialCyan-100-inverted: #afe7f9;--sequentialCyan-200: #8bd0f6;--sequentialCyan-200-inverted: #8bd0f6;--sequentialCyan-300: #6cbaec;--sequentialCyan-300-inverted: #6cbaec;--sequentialCyan-400: #52a3dd;--sequentialCyan-400-inverted: #52a3dd;--sequentialCyan-500: #3b8dcb;--sequentialCyan-500-inverted: #3b8dcb;--sequentialCyan-600: #2777b7;--sequentialCyan-600-inverted: #2777b7;--sequentialCyan-700: #1661a2;--sequentialCyan-700-inverted: #1661a2;--sequentialCyan-800: #074c8c;--sequentialCyan-800-inverted: #074c8c;--sequentialCyan-900: #003875;--sequentialCyan-900-inverted: #003875;--sequentialOrange-100: #f9d8ac;--sequentialOrange-100-inverted: #f9d8ac;--sequentialOrange-200: #feb85b;--sequentialOrange-200-inverted: #feb85b;--sequentialOrange-300: #f09b32;--sequentialOrange-300-inverted: #f09b32;--sequentialOrange-400: #db811e;--sequentialOrange-400-inverted: #db811e;--sequentialOrange-500: #c76809;--sequentialOrange-500-inverted: #c76809;--sequentialOrange-600: #b05000;--sequentialOrange-600-inverted: #b05000;--sequentialOrange-700: #973a00;--sequentialOrange-700-inverted: #973a00;--sequentialOrange-800: #7e2400;--sequentialOrange-800-inverted: #7e2400;--sequentialOrange-900: #640d00;--sequentialOrange-900-inverted: #640d00;--sequentialIndigo-100: #dfd8fa;--sequentialIndigo-100-inverted: #dfd8fa;--sequentialIndigo-200: #c3c1ed;--sequentialIndigo-200-inverted: #c3c1ed;--sequentialIndigo-300: #aba8e0;--sequentialIndigo-300-inverted: #aba8e0;--sequentialIndigo-400: #9390d2;--sequentialIndigo-400-inverted: #9390d2;--sequentialIndigo-500: #7a79c4;--sequentialIndigo-500-inverted: #7a79c4;--sequentialIndigo-600: #6163b5;--sequentialIndigo-600-inverted: #6163b5;--sequentialIndigo-700: #474ea6;--sequentialIndigo-700-inverted: #474ea6;--sequentialIndigo-800: #2a3994;--sequentialIndigo-800-inverted: #2a3994;--sequentialIndigo-900: #002680;--sequentialIndigo-900-inverted: #002680;--sequentialYellow-100: #fff7cd;--sequentialYellow-100-inverted: #fff7cd;--sequentialYellow-200: #ffed9b;--sequentialYellow-200-inverted: #ffed9b;--sequentialYellow-300: #ffe16a;--sequentialYellow-300-inverted: #ffe16a;--sequentialYellow-400: #ffd545;--sequentialYellow-400-inverted: #ffd545;--sequentialYellow-500: #ffc107;--sequentialYellow-500-inverted: #ffc107;--sequentialYellow-600: #dba005;--sequentialYellow-600-inverted: #dba005;--sequentialYellow-700: #b78103;--sequentialYellow-700-inverted: #b78103;--sequentialYellow-800: #936402;--sequentialYellow-800-inverted: #936402;--sequentialYellow-900: #7a4f01;--sequentialYellow-900-inverted: #7a4f01;--sequentialTeal-100: #a5eae8;--sequentialTeal-100-inverted: #a5eae8;--sequentialTeal-200: #7dd5d3;--sequentialTeal-200-inverted: #7dd5d3;--sequentialTeal-300: #5ebfbc;--sequentialTeal-300-inverted: #5ebfbc;--sequentialTeal-400: #44a8a6;--sequentialTeal-400-inverted: #44a8a6;--sequentialTeal-500: #2e9190;--sequentialTeal-500-inverted: #2e9190;--sequentialTeal-600: #1b7b7a;--sequentialTeal-600-inverted: #1b7b7a;--sequentialTeal-700: #0c6565;--sequentialTeal-700-inverted: #0c6565;--sequentialTeal-800: #025050;--sequentialTeal-800-inverted: #025050;--sequentialTeal-900: #003b3c;--sequentialTeal-900-inverted: #003b3c;--sequentialRed-100: #f8d6da;--sequentialRed-100-inverted: #f8d6da;--sequentialRed-200: #fcb6ba;--sequentialRed-200-inverted: #fcb6ba;--sequentialRed-300: #f8989b;--sequentialRed-300-inverted: #f8989b;--sequentialRed-400: #ed7b7f;--sequentialRed-400-inverted: #ed7b7f;--sequentialRed-500: #dd6065;--sequentialRed-500-inverted: #dd6065;--sequentialRed-600: #c9474c;--sequentialRed-600-inverted: #c9474c;--sequentialRed-700: #b22f36;--sequentialRed-700-inverted: #b22f36;--sequentialRed-800: #981822;--sequentialRed-800-inverted: #981822;--sequentialRed-900: #7d000f;--sequentialRed-900-inverted: #7d000f;--surfaces-bg-card: #F5F6F6;--surfaces-bg-card-inverted: #232632;--bs-primary-rgb: #141721e0;--bs-secondary-color: #14172199;--bs-nav-link-color: #14172199;--bs-form-valid-color: #26bf56ff;--bs-form-invalid-color: #f03b3aff;--bs-body-bg: white;--bs-tooltip-bg: #141721;--bs-tooltip-color: rgba(255, 255, 255, 0.8784313725)}[data-bs-theme=dark]{--bs-primary: #1b0734;--bs-secondary: #808080;--bs-success: #222222;--bs-danger: #be3636;--bs-info: #163960;--bs-dark: #2a2a2a;--bs-light: #333333;--bs-primary-rgb: 27, 7, 52;--bs-secondary-rgb: 128, 128, 128;--bs-success-rgb: 34, 34, 34;--bs-danger-rgb: 190, 54, 54;--bs-info-rgb: 22, 57, 96;--bs-dark-rgb: 42, 42, 42;--bs-light-rgb: 51, 51, 51}[data-bs-theme=light]{--bs-primary: #1b0734;--bs-secondary: #808080;--bs-success: #222222;--bs-danger: #be3636;--bs-info: #163960;--bs-dark: #2a2a2a;--bs-light: #333333;--bs-primary-rgb: 27, 7, 52;--bs-secondary-rgb: 128, 128, 128;--bs-success-rgb: 34, 34, 34;--bs-danger-rgb: 190, 54, 54;--bs-info-rgb: 22, 57, 96;--bs-dark-rgb: 42, 42, 42;--bs-light-rgb: 51, 51, 51}h1,.h1{font-size:32px;text-decoration:none;font-weight:400;font-style:normal;font-stretch:normal;letter-spacing:-0.128px;line-height:40px;color:var(--text-primary)}h2,.h2{font-size:24px;text-decoration:none;font-weight:400;font-style:normal;font-stretch:normal;letter-spacing:-0.096px;line-height:32px;color:var(--text-primary)}h3,.h3,legend{font-size:20px;text-decoration:none;font-weight:400;font-style:normal;font-stretch:normal;letter-spacing:0px;line-height:28px;color:var(--text-primary)}h4,.h4{font-size:16px;text-decoration:none;font-weight:400;font-style:normal;font-stretch:normal;letter-spacing:-0.016px;line-height:20px;color:var(--text-primary)}h5,.h5{font-size:16px;text-decoration:none;font-weight:400;font-style:normal;font-stretch:normal;letter-spacing:-0.016px;line-height:20px;font-size:14px;color:var(--text-primary)}h6,.h6{font-size:16px;text-decoration:none;font-weight:400;font-style:normal;font-stretch:normal;letter-spacing:-0.016px;line-height:20px;font-size:12px;color:var(--text-primary)}p,label,ul,li,blockquote{color:var(--text-secondary)}.text-muted{color:var(--text-subtle)}.text-primary{color:var(--text-primary) !important}.text-secondary{color:var(--text-secondary) !important}blockquote{border-left:.25rem var(--text-secondary) solid;margin-bottom:.5rem;padding-left:.25rem}hr{border-bottom:1px solid var(--border-subtleAlpha01);width:100%}.btn{display:inline-flex;justify-content:center;align-items:center;width:-moz-fit-content;width:fit-content;height:32px}.btn-large{height:40px}.btn-primary{color:var(--text-primary-inverted);font-size:14px;text-decoration:none;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;background:linear-gradient(var(--stateOverlays-enabled-inverted), var(--stateOverlays-enabled-inverted)),var(--fill-active);border:0 solid rgba(0,0,0,0);box-shadow:var(--elevation-0);transition:box-shadow .2s}.btn-primary:enabled,.btn-primary.enabled{border:0 solid rgba(0,0,0,0);box-shadow:var(--elevation-0)}.btn-primary:active,.btn-primary.active{color:var(--text-primary-inverted);font-size:14px;text-decoration:none;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;font-size:14px;text-decoration:underline;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;background:linear-gradient(var(--stateOverlays-active-inverted), var(--stateOverlays-active-inverted)),var(--fill-active);border:0 solid rgba(0,0,0,0);box-shadow:var(--elevation-0)}.btn-primary:hover,.btn-primary.hover{color:var(--text-primary-inverted);font-size:14px;text-decoration:none;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;font-size:14px;text-decoration:underline;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;background:linear-gradient(var(--stateOverlays-hover-inverted), var(--stateOverlays-hover-inverted)),var(--fill-active);border:0 solid rgba(0,0,0,0);box-shadow:var(--elevation-1)}.btn-primary:focus,.btn-primary.focus{color:var(--text-primary-inverted);font-size:14px;text-decoration:none;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;font-size:14px;text-decoration:underline;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;background:linear-gradient(var(--stateOverlays-enabled-inverted), var(--stateOverlays-enabled-inverted)),var(--fill-active);border:1px solid rgba(0,0,0,0);box-shadow:var(--elevation-0);outline:2px solid var(--focus-color)}.btn-primary:disabled,.btn-primary.disabled{color:var(--text-disabled-inverted);font-size:14px;text-decoration:none;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;background:linear-gradient(var(--stateOverlays-disabled-inverted), var(--stateOverlays-disabled-inverted)),var(--fill-active);border:0 solid rgba(0,0,0,0);box-shadow:var(--elevation-0)}.btn-primary:focus:not(:focus-visible,:disabled,.disabled){color:var(--text-primary-inverted);font-size:14px;text-decoration:none;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;font-size:14px;text-decoration:underline;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;background:linear-gradient(var(--stateOverlays-enabled-inverted), var(--stateOverlays-enabled-inverted)),var(--fill-active);border:0 solid rgba(0,0,0,0);box-shadow:var(--elevation-0)}.btn-secondary{color:var(--text-primary);font-size:14px;text-decoration:none;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;background:var(--stateOverlays-enabled);border:1px solid var(--border-enabled);box-shadow:var(--elevation-0);transition:box-shadow .2s}.btn-secondary:enabled,.btn-secondary.enabled{border:1px solid var(--border-enabled);box-shadow:var(--elevation-0)}.btn-secondary:active,.btn-secondary.active{color:var(--text-primary);font-size:14px;text-decoration:none;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;font-size:14px;text-decoration:underline;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;background:var(--stateOverlays-active);border:1px solid var(--border-selected);box-shadow:var(--elevation-0)}.btn-secondary:hover,.btn-secondary.hover{color:var(--text-primary);font-size:14px;text-decoration:none;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;font-size:14px;text-decoration:underline;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;background:var(--stateOverlays-hover);border:1px solid var(--border-hover);box-shadow:var(--elevation-1)}.btn-secondary:focus,.btn-secondary.focus{color:var(--text-primary);font-size:14px;text-decoration:none;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;font-size:14px;text-decoration:underline;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;background:var(--stateOverlays-enabled);border:1px solid var(--border-hover);box-shadow:var(--elevation-0);outline:2px solid var(--focus-color)}.btn-secondary:disabled,.btn-secondary.disabled{color:var(--text-disabled);font-size:14px;text-decoration:none;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;background:var(--stateOverlays-enabled);border:1px solid var(--border-disabled);box-shadow:var(--elevation-0)}.btn-secondary:focus:not(:focus-visible,:disabled,.disabled){color:var(--text-primary);font-size:14px;text-decoration:none;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;font-size:14px;text-decoration:underline;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;background:var(--stateOverlays-enabled);border:1px solid var(--border-enabled);box-shadow:var(--elevation-0)}.btn-tertiary,.btn-link{color:var(--text-primary);font-size:14px;text-decoration:none;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;background:var(--stateOverlays-enabled);border:0 solid rgba(0,0,0,0);box-shadow:None;transition:box-shadow .2s}.btn-tertiary:enabled,.btn-tertiary.enabled,.btn-link:enabled,.btn-link.enabled{border:0 solid rgba(0,0,0,0);box-shadow:None}.btn-tertiary:active,.btn-tertiary.active,.btn-link:active,.btn-link.active{color:var(--text-primary);font-size:14px;text-decoration:none;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;font-size:14px;text-decoration:underline;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;background:var(--stateOverlays-active);border:0 solid rgba(0,0,0,0);box-shadow:None}.btn-tertiary:hover,.btn-tertiary.hover,.btn-link:hover,.btn-link.hover{color:var(--text-primary);font-size:14px;text-decoration:none;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;font-size:14px;text-decoration:underline;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;background:var(--stateOverlays-hover);border:0 solid rgba(0,0,0,0);box-shadow:var(--elevation-1)}.btn-tertiary:focus,.btn-tertiary.focus,.btn-link:focus,.btn-link.focus{color:var(--text-primary);font-size:14px;text-decoration:none;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;font-size:14px;text-decoration:underline;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;background:var(--stateOverlays-enabled);border:1px solid rgba(0,0,0,0);box-shadow:None;outline:2px solid var(--focus-color)}.btn-tertiary:disabled,.btn-tertiary.disabled,.btn-link:disabled,.btn-link.disabled{color:var(--text-disabled);font-size:14px;text-decoration:none;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;background:var(--stateOverlays-enabled);border:0 solid rgba(0,0,0,0);box-shadow:None}.btn-tertiary:focus:not(:focus-visible,:disabled,.disabled),.btn-link:focus:not(:focus-visible,:disabled,.disabled){color:var(--text-primary);font-size:14px;text-decoration:none;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;font-size:14px;text-decoration:underline;font-weight:600;font-style:normal;font-stretch:normal;letter-spacing:-0.056px;line-height:16px;background:var(--stateOverlays-enabled);border:0 solid rgba(0,0,0,0);box-shadow:None}.btn-tertiary,.btn-tertiary:disabled,.btn-tertiary.disabled,.btn-link,.btn-link:disabled,.btn-link.disabled{text-decoration:underline}.accordion{width:100%}.accordion-body{display:flex;flex-direction:column;gap:.5rem}.accordion-item{color:var(--text-primary);border-bottom:1px solid var(--border-subtleAlpha01)}.accordion-item .nav-link{padding:.5rem;color:var(--text-secondary)}.accordion-item .nav-link:last-child{margin-bottom:.75rem}.accordion-item .nav-link:active,.accordion-item .nav-link.active{background:var(--stateOverlays-active);color:var(--text-primary)}.accordion-item .nav-link:hover,.accordion-item .nav-link.hover{background:var(--stateOverlays-selectedHover);color:var(--text-primary)}.accordion-item .nav-link:disabled,.accordion-item .nav-link.disabled{color:var(--text-disabled)}.accordion-item .nav-link:focus:not(:focus-visible,:disabled,.disabled){background:var(--stateOverlays-active);color:var(--text-primary)}.accordion-button{font-size:14px;text-decoration:none;font-weight:400;font-style:normal;font-stretch:normal;letter-spacing:-0.112px;line-height:16px;color:var(--text-secondary);text-transform:uppercase;border:none;box-shadow:none}.accordion-button::after{filter:contrast(0);opacity:.4}.accordion-button:not(.collapsed){color:var(--text-primary);background-color:rgba(0,0,0,0);box-shadow:none}.accordion-button:not(.collapsed)::after{filter:contrast(0);opacity:1}.accordion-button:hover,.accordion-button.hover{color:var(--text-primary)}.accordion-button:hover::after,.accordion-button.hover::after{opacity:1}.accordion-button:focus,.accordion-button.focus{border:none;box-shadow:none}.card{background:var(--surfaces-bg-card);border:none;box-shadow:var(--elevation-1);width:100%;height:100%;overflow:auto;padding:1rem}.card-nav:hover,.card-nav.hover{background:var(--field-enabled);box-shadow:var(--elevation-2);transform:translate3d(0, -0.5rem, 0);will-change:transform}.card-title,.card-header{color:var(--text-primary)}.card-subtitle{color:var(--text-secondary)}.card-text{font-size:14px;text-decoration:none;font-weight:400;font-style:normal;font-stretch:normal;letter-spacing:-0.112px;line-height:16px;color:var(--text-secondary)}.card-text:last-child{margin-bottom:.5rem}.navbar{background:var(--surfaces-bg02) !important}.navbar .nav-link{padding:0;align-items:center;display:flex;height:4rem;justify-content:center;color:var(--text-secondary)}.navbar .nav-link:active,.navbar .nav-link.active{color:var(--text-primary)}.form-check{transition:all 150ms ease 0s;margin-bottom:12px;line-height:16px;min-height:auto;padding-left:28px}.form-check-inline{margin-right:12px}.form-check-input{margin:0;position:relative;border-color:var(--border-enabled);border-radius:0;color:var(--fill-active);width:16px;height:16px;outline:none;background-color:rgba(0,0,0,0);background-size:auto}.form-check-input:checked{background-color:rgba(0,0,0,0);border-color:var(--border-enabled);color:var(--fill-active)}.form-check-input[type=radio]{background-image:none}.form-check-input[type=radio]:checked::after{position:absolute;content:"";background-color:var(--fill-active);border-radius:50%;width:8px;height:8px;top:50%;left:50%;transform:translate(-50%, -50%)}.form-check-input:hover,.form-check-input.hover{border-color:var(--border-hover)}.form-check-input:active,.form-check-input.active{border-color:var(--border-selected)}.form-check-input:disabled,.form-check-input.disabled{border-color:var(--border-disabled)}.form-check-input:focus,.form-check-input.focus{border-color:var(--border-selected);box-shadow:none}.form-check-input:hover{border-color:var(--border-hover)}.form-check .form-check-input{margin-left:-24px}.form-check-label{font-size:14px;text-decoration:none;font-weight:400;font-style:normal;font-stretch:normal;letter-spacing:-0.112px;line-height:16px;color:var(--text-secondary)}.form-check-lg{line-height:20px;min-height:auto;padding-left:32px}.form-check-lg .form-check-input{width:20px;height:20px;border-width:2px;margin-left:-28px}.form-check-lg .form-check-label{line-height:20px}.form-check.disabled .form-check-label{color:var(--text-disabled)}.form-switch{display:flex;gap:8px}.form-switch .form-check-input{background-color:var(--fill-subtle);border:1px solid var(--border-enabled);border-radius:16px;height:16px;width:32px}.form-switch .form-check-input:focus-visible,.form-switch .form-check-input.focus-visible{box-shadow:0 0 0 2px var(--focus)}.form-switch .form-check-input:disabled,.form-switch .form-check-input.disabled{border-color:var(--border-disabled)}.form-switch .form-check-input:checked{background-color:var(--fill-active);border:1px solid var(--border-selected)}.form-switch .form-check-input:checked:disabled,.form-switch .form-check-input:checked.disabled{background-color:var(--fill-subtle);border-color:var(--border-disabled)}[data-bs-theme=light] .form-check-input:checked[type=checkbox]{--bs-form-check-bg-image: url("data:image/svg+xml,%3Csvg width='8' height='7' viewBox='0 0 8 7' xmlns='http://www.w3.org/2000/svg' data-testid='checkmark-icon'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M7.5 1.25L6.25 0L2.5 3.75L1.25 2.5L0 3.75L2.5 6.25L7.5 1.25Z' fill='rgba(20, 23, 33, 0.8784313725)'/%3E%3C/svg%3E")}[data-bs-theme=light] .form-switch .form-check-input:disabled,[data-bs-theme=light] .form-switch .form-check-input.disabled{--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba(20, 23, 33, 0.3019607843)'/%3e%3c/svg%3e")}[data-bs-theme=light] .form-switch .form-check-input:checked[type=checkbox]:not(:disabled){--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='white'/%3e%3c/svg%3e")}[data-bs-theme=light] .form-switch .form-check-input:focus,[data-bs-theme=light] .form-switch .form-check-input:not(:checked):not(:focus){--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba(20, 23, 33, 0.6)'/%3e%3c/svg%3e")}[data-bs-theme=dark] .form-check-input:checked[type=checkbox]{--bs-form-check-bg-image: url("data:image/svg+xml,%3Csvg width='8' height='7' viewBox='0 0 8 7' xmlns='http://www.w3.org/2000/svg' data-testid='checkmark-icon'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M7.5 1.25L6.25 0L2.5 3.75L1.25 2.5L0 3.75L2.5 6.25L7.5 1.25Z' fill='rgba(255, 255, 255, 0.8784313725)'/%3E%3C/svg%3E")}[data-bs-theme=dark] .form-switch .form-check-input:disabled,[data-bs-theme=dark] .form-switch .form-check-input.disabled{--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba(255, 255, 255, 0.3019607843)'/%3e%3c/svg%3e")}[data-bs-theme=dark] .form-switch .form-check-input:checked[type=checkbox]:not(:disabled){--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill=''/%3e%3c/svg%3e")}[data-bs-theme=dark] .form-switch .form-check-input:focus,[data-bs-theme=dark] .form-switch .form-check-input:not(:checked):not(:focus){--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba(255, 255, 255, 0.6)'/%3e%3c/svg%3e")}[data-bs-theme=light] [data-bs-theme=dark] .form-check-input:checked[type=checkbox]{--bs-form-check-bg-image: url("data:image/svg+xml,%3Csvg width='8' height='7' viewBox='0 0 8 7' xmlns='http://www.w3.org/2000/svg' data-testid='checkmark-icon'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M7.5 1.25L6.25 0L2.5 3.75L1.25 2.5L0 3.75L2.5 6.25L7.5 1.25Z' fill='rgba(255, 255, 255, 0.8784313725)'/%3E%3C/svg%3E")}[data-bs-theme=light] [data-bs-theme=dark] .form-switch .form-check-input:disabled,[data-bs-theme=light] [data-bs-theme=dark] .form-switch .form-check-input.disabled{--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba(255, 255, 255, 0.3019607843)'/%3e%3c/svg%3e")}[data-bs-theme=light] [data-bs-theme=dark] .form-switch .form-check-input:checked[type=checkbox]:not(:disabled){--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill=''/%3e%3c/svg%3e")}[data-bs-theme=light] [data-bs-theme=dark] .form-switch .form-check-input:focus,[data-bs-theme=light] [data-bs-theme=dark] .form-switch .form-check-input:not(:checked):not(:focus){--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba(255, 255, 255, 0.6)'/%3e%3c/svg%3e")}[data-bs-theme=dark] [data-bs-theme=light] .form-check-input:checked[type=checkbox]{--bs-form-check-bg-image: url("data:image/svg+xml,%3Csvg width='8' height='7' viewBox='0 0 8 7' xmlns='http://www.w3.org/2000/svg' data-testid='checkmark-icon'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M7.5 1.25L6.25 0L2.5 3.75L1.25 2.5L0 3.75L2.5 6.25L7.5 1.25Z' fill='rgba(20, 23, 33, 0.8784313725)'/%3E%3C/svg%3E")}[data-bs-theme=dark] [data-bs-theme=light] .form-switch .form-check-input:disabled,[data-bs-theme=dark] [data-bs-theme=light] .form-switch .form-check-input.disabled{--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba(20, 23, 33, 0.3019607843)'/%3e%3c/svg%3e")}[data-bs-theme=dark] [data-bs-theme=light] .form-switch .form-check-input:checked[type=checkbox]:not(:disabled){--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill=''/%3e%3c/svg%3e")}[data-bs-theme=dark] [data-bs-theme=light] .form-switch .form-check-input:focus,[data-bs-theme=dark] [data-bs-theme=light] .form-switch .form-check-input:not(:checked):not(:focus){--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba(20, 23, 33, 0.6)'/%3e%3c/svg%3e")}.form-control,.input-group-text{font-size:14px;text-decoration:none;font-weight:400;font-style:normal;font-stretch:normal;letter-spacing:-0.112px;line-height:16px;background-color:var(--field-enabled);color:var(--text-primary);padding:.5rem;box-shadow:var(--elevation-0);border:none}.form-control::-moz-placeholder, .input-group-text::-moz-placeholder{color:var(--text-placeholder)}.form-control::placeholder,.input-group-text::placeholder{color:var(--text-placeholder)}.form-control:focus,.form-control.focus,.input-group-text:focus,.input-group-text.focus{background-color:var(--field-hover);color:var(--text-primary);box-shadow:0 0 0 2px var(--focus) inset}.form-control:hover,.form-control.hover,.input-group-text:hover,.input-group-text.hover{background-color:var(--field-hover)}.form-control:disabled,.form-control.disabled,.input-group-text:disabled,.input-group-text.disabled{background-color:var(--field-disabled);color:var(--text-disabled)}.form-control:focus-visible,.form-control.focus-visible,.input-group-text:focus-visible,.input-group-text.focus-visible{outline:none}.form-control.is-invalid{box-shadow:0 0 0 2px var(--status-error) inset;border:none}.form-control.is-invalid:focus,.form-control.is-invalid.focus{box-shadow:0 0 0 2px var(--status-error) inset}.form-control.is-valid{box-shadow:0 0 0 2px var(--status-success) inset}.form-control.is-valid:focus,.form-control.is-valid.focus{box-shadow:0 0 0 2px var(--status-success) inset}[data-bs-theme=light] .form-control.is-valid{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48' viewBox='0 -960 960 960'%3E%3Cpath fill='%2326bf56' d='M421-311.463 690.537-581l-34.845-34.23L421-380.153 302.539-498.615l-33.846 34.23L421-311.463Zm59.067 211.462q-78.221 0-147.397-29.92-69.176-29.92-120.989-81.71-51.814-51.791-81.747-120.936-29.933-69.146-29.933-147.366 0-78.836 29.92-148.204 29.92-69.369 81.71-120.682 51.791-51.314 120.936-81.247 69.146-29.933 147.366-29.933 78.836 0 148.204 29.92 69.369 29.92 120.682 81.21 51.314 51.291 81.247 120.629 29.933 69.337 29.933 148.173 0 78.221-29.92 147.397-29.92 69.176-81.21 120.989-51.291 51.814-120.629 81.747-69.337 29.933-148.173 29.933ZM480-145.385q139.692 0 237.154-97.769Q814.615-340.923 814.615-480q0-139.692-97.461-237.154Q619.692-814.615 480-814.615q-139.077 0-236.846 97.461Q145.385-619.692 145.385-480q0 139.077 97.769 236.846T480-145.385ZM480-480Z'%3E%3C/path%3E%3C/svg%3E"),url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48' viewBox='0 -960 960 960'%3E%3Cpath fill='%2326bf56' d='m421-298 283-283-46-45-237 237-120-120-45 45 165 166Zm59 218q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 31.5-156t86-127Q252-817 325-848.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 82-31.5 155T763-197.5q-54 54.5-127 86T480-80Zm0-60q142 0 241-99.5T820-480q0-142-99-241t-241-99q-141 0-240.5 99T140-480q0 141 99.5 240.5T480-140Zm0-340Z'%3E%3C/path%3E%3C/svg%3E")}[data-bs-theme=light] .form-control.is-invalid{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48' viewBox='0 -960 960 960'%3E%3Cpath fill='%23f03b3a' d='M331.539-299.539 480-448.001l148.461 148.462 32-32L511.999-480l148.462-148.461-32-32L480-511.999 331.539-660.461l-32 32L448.001-480 299.539-331.539l32 32Zm148.528 199.538q-78.221 0-147.397-29.92-69.176-29.92-120.989-81.71-51.814-51.791-81.747-120.936-29.933-69.146-29.933-147.366 0-78.836 29.92-148.204 29.92-69.369 81.71-120.682 51.791-51.314 120.936-81.247 69.146-29.933 147.366-29.933 78.836 0 148.204 29.92 69.369 29.92 120.682 81.21 51.314 51.291 81.247 120.629 29.933 69.337 29.933 148.173 0 78.221-29.92 147.397-29.92 69.176-81.21 120.989-51.291 51.814-120.629 81.747-69.337 29.933-148.173 29.933ZM480-145.385q139.692 0 237.154-97.769Q814.615-340.923 814.615-480q0-139.692-97.461-237.154Q619.692-814.615 480-814.615q-139.077 0-236.846 97.461Q145.385-619.692 145.385-480q0 139.077 97.769 236.846T480-145.385ZM480-480Z'%3E%3C/path%3E%3C/svg%3E"),url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48' viewBox='0 -960 960 960'%3E%3Cpath fill='%23f03b3a' d='m330-288 150-150 150 150 42-42-150-150 150-150-42-42-150 150-150-150-42 42 150 150-150 150 42 42ZM480-80q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 31.5-156t86-127Q252-817 325-848.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 82-31.5 155T763-197.5q-54 54.5-127 86T480-80Zm0-60q142 0 241-99.5T820-480q0-142-99-241t-241-99q-141 0-240.5 99T140-480q0 141 99.5 240.5T480-140Zm0-340Z'%3E%3C/path%3E%3C/svg%3E")}[data-bs-theme=dark] .form-control.is-valid{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48' viewBox='0 -960 960 960'%3E%3Cpath fill='%2340d86e' d='M421-311.463 690.537-581l-34.845-34.23L421-380.153 302.539-498.615l-33.846 34.23L421-311.463Zm59.067 211.462q-78.221 0-147.397-29.92-69.176-29.92-120.989-81.71-51.814-51.791-81.747-120.936-29.933-69.146-29.933-147.366 0-78.836 29.92-148.204 29.92-69.369 81.71-120.682 51.791-51.314 120.936-81.247 69.146-29.933 147.366-29.933 78.836 0 148.204 29.92 69.369 29.92 120.682 81.21 51.314 51.291 81.247 120.629 29.933 69.337 29.933 148.173 0 78.221-29.92 147.397-29.92 69.176-81.21 120.989-51.291 51.814-120.629 81.747-69.337 29.933-148.173 29.933ZM480-145.385q139.692 0 237.154-97.769Q814.615-340.923 814.615-480q0-139.692-97.461-237.154Q619.692-814.615 480-814.615q-139.077 0-236.846 97.461Q145.385-619.692 145.385-480q0 139.077 97.769 236.846T480-145.385ZM480-480Z'%3E%3C/path%3E%3C/svg%3E"),url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48' viewBox='0 -960 960 960'%3E%3Cpath fill='%2340d86e' d='m421-298 283-283-46-45-237 237-120-120-45 45 165 166Zm59 218q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 31.5-156t86-127Q252-817 325-848.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 82-31.5 155T763-197.5q-54 54.5-127 86T480-80Zm0-60q142 0 241-99.5T820-480q0-142-99-241t-241-99q-141 0-240.5 99T140-480q0 141 99.5 240.5T480-140Zm0-340Z'%3E%3C/path%3E%3C/svg%3E")}[data-bs-theme=dark] .form-control.is-invalid{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48' viewBox='0 -960 960 960'%3E%3Cpath fill='%23f56565' d='M331.539-299.539 480-448.001l148.461 148.462 32-32L511.999-480l148.462-148.461-32-32L480-511.999 331.539-660.461l-32 32L448.001-480 299.539-331.539l32 32Zm148.528 199.538q-78.221 0-147.397-29.92-69.176-29.92-120.989-81.71-51.814-51.791-81.747-120.936-29.933-69.146-29.933-147.366 0-78.836 29.92-148.204 29.92-69.369 81.71-120.682 51.791-51.314 120.936-81.247 69.146-29.933 147.366-29.933 78.836 0 148.204 29.92 69.369 29.92 120.682 81.21 51.314 51.291 81.247 120.629 29.933 69.337 29.933 148.173 0 78.221-29.92 147.397-29.92 69.176-81.21 120.989-51.291 51.814-120.629 81.747-69.337 29.933-148.173 29.933ZM480-145.385q139.692 0 237.154-97.769Q814.615-340.923 814.615-480q0-139.692-97.461-237.154Q619.692-814.615 480-814.615q-139.077 0-236.846 97.461Q145.385-619.692 145.385-480q0 139.077 97.769 236.846T480-145.385ZM480-480Z'%3E%3C/path%3E%3C/svg%3E"),url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48' viewBox='0 -960 960 960'%3E%3Cpath fill='%23f56565' d='m330-288 150-150 150 150 42-42-150-150 150-150-42-42-150 150-150-150-42 42 150 150-150 150 42 42ZM480-80q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 31.5-156t86-127Q252-817 325-848.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 82-31.5 155T763-197.5q-54 54.5-127 86T480-80Zm0-60q142 0 241-99.5T820-480q0-142-99-241t-241-99q-141 0-240.5 99T140-480q0 141 99.5 240.5T480-140Zm0-340Z'%3E%3C/path%3E%3C/svg%3E")}[data-bs-theme=light] [data-bs-theme=dark] .form-control.is-valid{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48' viewBox='0 -960 960 960'%3E%3Cpath fill='%2340d86e' d='M421-311.463 690.537-581l-34.845-34.23L421-380.153 302.539-498.615l-33.846 34.23L421-311.463Zm59.067 211.462q-78.221 0-147.397-29.92-69.176-29.92-120.989-81.71-51.814-51.791-81.747-120.936-29.933-69.146-29.933-147.366 0-78.836 29.92-148.204 29.92-69.369 81.71-120.682 51.791-51.314 120.936-81.247 69.146-29.933 147.366-29.933 78.836 0 148.204 29.92 69.369 29.92 120.682 81.21 51.314 51.291 81.247 120.629 29.933 69.337 29.933 148.173 0 78.221-29.92 147.397-29.92 69.176-81.21 120.989-51.291 51.814-120.629 81.747-69.337 29.933-148.173 29.933ZM480-145.385q139.692 0 237.154-97.769Q814.615-340.923 814.615-480q0-139.692-97.461-237.154Q619.692-814.615 480-814.615q-139.077 0-236.846 97.461Q145.385-619.692 145.385-480q0 139.077 97.769 236.846T480-145.385ZM480-480Z'%3E%3C/path%3E%3C/svg%3E"),url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48' viewBox='0 -960 960 960'%3E%3Cpath fill='%2340d86e' d='m421-298 283-283-46-45-237 237-120-120-45 45 165 166Zm59 218q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 31.5-156t86-127Q252-817 325-848.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 82-31.5 155T763-197.5q-54 54.5-127 86T480-80Zm0-60q142 0 241-99.5T820-480q0-142-99-241t-241-99q-141 0-240.5 99T140-480q0 141 99.5 240.5T480-140Zm0-340Z'%3E%3C/path%3E%3C/svg%3E")}[data-bs-theme=light] [data-bs-theme=dark] .form-control.is-invalid{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48' viewBox='0 -960 960 960'%3E%3Cpath fill='%23f56565' d='M331.539-299.539 480-448.001l148.461 148.462 32-32L511.999-480l148.462-148.461-32-32L480-511.999 331.539-660.461l-32 32L448.001-480 299.539-331.539l32 32Zm148.528 199.538q-78.221 0-147.397-29.92-69.176-29.92-120.989-81.71-51.814-51.791-81.747-120.936-29.933-69.146-29.933-147.366 0-78.836 29.92-148.204 29.92-69.369 81.71-120.682 51.791-51.314 120.936-81.247 69.146-29.933 147.366-29.933 78.836 0 148.204 29.92 69.369 29.92 120.682 81.21 51.314 51.291 81.247 120.629 29.933 69.337 29.933 148.173 0 78.221-29.92 147.397-29.92 69.176-81.21 120.989-51.291 51.814-120.629 81.747-69.337 29.933-148.173 29.933ZM480-145.385q139.692 0 237.154-97.769Q814.615-340.923 814.615-480q0-139.692-97.461-237.154Q619.692-814.615 480-814.615q-139.077 0-236.846 97.461Q145.385-619.692 145.385-480q0 139.077 97.769 236.846T480-145.385ZM480-480Z'%3E%3C/path%3E%3C/svg%3E"),url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48' viewBox='0 -960 960 960'%3E%3Cpath fill='%23f56565' d='m330-288 150-150 150 150 42-42-150-150 150-150-42-42-150 150-150-150-42 42 150 150-150 150 42 42ZM480-80q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 31.5-156t86-127Q252-817 325-848.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 82-31.5 155T763-197.5q-54 54.5-127 86T480-80Zm0-60q142 0 241-99.5T820-480q0-142-99-241t-241-99q-141 0-240.5 99T140-480q0 141 99.5 240.5T480-140Zm0-340Z'%3E%3C/path%3E%3C/svg%3E")}[data-bs-theme=dark] [data-bs-theme=light] .form-control.is-valid{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48' viewBox='0 -960 960 960'%3E%3Cpath fill='%2326bf56' d='M421-311.463 690.537-581l-34.845-34.23L421-380.153 302.539-498.615l-33.846 34.23L421-311.463Zm59.067 211.462q-78.221 0-147.397-29.92-69.176-29.92-120.989-81.71-51.814-51.791-81.747-120.936-29.933-69.146-29.933-147.366 0-78.836 29.92-148.204 29.92-69.369 81.71-120.682 51.791-51.314 120.936-81.247 69.146-29.933 147.366-29.933 78.836 0 148.204 29.92 69.369 29.92 120.682 81.21 51.314 51.291 81.247 120.629 29.933 69.337 29.933 148.173 0 78.221-29.92 147.397-29.92 69.176-81.21 120.989-51.291 51.814-120.629 81.747-69.337 29.933-148.173 29.933ZM480-145.385q139.692 0 237.154-97.769Q814.615-340.923 814.615-480q0-139.692-97.461-237.154Q619.692-814.615 480-814.615q-139.077 0-236.846 97.461Q145.385-619.692 145.385-480q0 139.077 97.769 236.846T480-145.385ZM480-480Z'%3E%3C/path%3E%3C/svg%3E"),url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48' viewBox='0 -960 960 960'%3E%3Cpath fill='%2326bf56' d='m421-298 283-283-46-45-237 237-120-120-45 45 165 166Zm59 218q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 31.5-156t86-127Q252-817 325-848.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 82-31.5 155T763-197.5q-54 54.5-127 86T480-80Zm0-60q142 0 241-99.5T820-480q0-142-99-241t-241-99q-141 0-240.5 99T140-480q0 141 99.5 240.5T480-140Zm0-340Z'%3E%3C/path%3E%3C/svg%3E")}[data-bs-theme=dark] [data-bs-theme=light] .form-control.is-invalid{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48' viewBox='0 -960 960 960'%3E%3Cpath fill='%23f03b3a' d='M331.539-299.539 480-448.001l148.461 148.462 32-32L511.999-480l148.462-148.461-32-32L480-511.999 331.539-660.461l-32 32L448.001-480 299.539-331.539l32 32Zm148.528 199.538q-78.221 0-147.397-29.92-69.176-29.92-120.989-81.71-51.814-51.791-81.747-120.936-29.933-69.146-29.933-147.366 0-78.836 29.92-148.204 29.92-69.369 81.71-120.682 51.791-51.314 120.936-81.247 69.146-29.933 147.366-29.933 78.836 0 148.204 29.92 69.369 29.92 120.682 81.21 51.314 51.291 81.247 120.629 29.933 69.337 29.933 148.173 0 78.221-29.92 147.397-29.92 69.176-81.21 120.989-51.291 51.814-120.629 81.747-69.337 29.933-148.173 29.933ZM480-145.385q139.692 0 237.154-97.769Q814.615-340.923 814.615-480q0-139.692-97.461-237.154Q619.692-814.615 480-814.615q-139.077 0-236.846 97.461Q145.385-619.692 145.385-480q0 139.077 97.769 236.846T480-145.385ZM480-480Z'%3E%3C/path%3E%3C/svg%3E"),url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48' viewBox='0 -960 960 960'%3E%3Cpath fill='%23f03b3a' d='m330-288 150-150 150 150 42-42-150-150 150-150-42-42-150 150-150-150-42 42 150 150-150 150 42 42ZM480-80q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 31.5-156t86-127Q252-817 325-848.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 82-31.5 155T763-197.5q-54 54.5-127 86T480-80Zm0-60q142 0 241-99.5T820-480q0-142-99-241t-241-99q-141 0-240.5 99T140-480q0 141 99.5 240.5T480-140Zm0-340Z'%3E%3C/path%3E%3C/svg%3E")}.form-label{font-size:14px;text-decoration:none;font-weight:400;font-style:normal;font-stretch:normal;letter-spacing:-0.112px;line-height:16px;color:var(--text-secondary);margin-bottom:12px;width:100%}.tooltip{display:flex;justify-content:center;align-items:center;box-shadow:var(--elevation-2-inverted)}.tooltip-inner{font-size:12px;text-decoration:none;font-weight:300;font-style:normal;font-stretch:normal;letter-spacing:.024px;line-height:16px;text-align:left}.nav-tabs{align-items:flex-start;align-self:stretch;flex-wrap:nowrap;border-bottom:1px solid var(--border-subtleAlpha01);display:flex;gap:1rem}.nav-tabs .nav-link{color:var(--text-secondary);height:2rem;padding:0 0 .5rem;margin-bottom:-1px}.nav-tabs .nav-link:active,.nav-tabs .nav-link.active{border-bottom:1px solid var(--border-selected);color:var(--text-primary)}.nav-tabs .nav-link:hover,.nav-tabs .nav-link.hover{border-bottom:1px solid var(--border-hover);color:var(--text-primaryHover)}.nav-tabs .nav-link:focus-visible,.nav-tabs .nav-link.focus-visible{border-bottom:1px solid var(--focus);box-shadow:none;color:var(--text-primaryHover)}.nav-tabs .nav-link:disabled,.nav-tabs .nav-link.disabled{color:var(--text-disabled)}.qb-container-bg-01{background-color:var(--surfaces-bg01);padding:40px;width:100%}.qb-container-bg-02{background-color:var(--surfaces-bg02);padding:40px;width:100%}.qb-container-bg-03{background-color:var(--surfaces-bg03);padding:40px;width:100%} diff --git a/vizro-core/src/vizro/static/js/models/range_slider.js b/vizro-core/src/vizro/static/js/models/range_slider.js index 5eb1892aa..8aafdde7f 100644 --- a/vizro-core/src/vizro/static/js/models/range_slider.js +++ b/vizro-core/src/vizro/static/js/models/range_slider.js @@ -17,6 +17,8 @@ function update_range_slider_values( trigger_id = dash_clientside.callback_context.triggered[0]["prop_id"].split(".")[0]; } + + // text form component is the trigger if ( trigger_id === `${self_data["id"]}_start_value` || trigger_id === `${self_data["id"]}_end_value` @@ -24,21 +26,36 @@ function update_range_slider_values( if (isNaN(start) || isNaN(end)) { return dash_clientside.no_update; } - [start_text_value, end_text_value] = [start, end]; + return [start, end, [start, end], [start, end]]; + + // slider component is the trigger } else if (trigger_id === self_data["id"]) { - [start_text_value, end_text_value] = [slider[0], slider[1]]; - } else { - [start_text_value, end_text_value] = - input_store !== null ? input_store : [slider[0], slider[1]]; + return [slider[0], slider[1], slider, slider]; } - - start_value = Math.min(start_text_value, end_text_value); - end_value = Math.max(start_text_value, end_text_value); - start_value = Math.max(self_data["min"], start_value); - end_value = Math.min(self_data["max"], end_value); - slider_value = [start_value, end_value]; - - return [start_value, end_value, slider_value, [start_value, end_value]]; + // on_page_load is the trigger + if (input_store === null) { + return [ + dash_clientside.no_update, + dash_clientside.no_update, + dash_clientside.no_update, + slider, + ]; + } + if ( + slider[0] === start && + input_store[0] === start && + slider[1] === end && + input_store[1] === end + ) { + // To prevent filter_action to be triggered after on_page_load + return [ + dash_clientside.no_update, + dash_clientside.no_update, + dash_clientside.no_update, + dash_clientside.no_update, + ]; + } + return [input_store[0], input_store[1], input_store, input_store]; } window.dash_clientside = { diff --git a/vizro-core/src/vizro/static/js/models/slider.js b/vizro-core/src/vizro/static/js/models/slider.js index bc572cffe..1b15d78ae 100644 --- a/vizro-core/src/vizro/static/js/models/slider.js +++ b/vizro-core/src/vizro/static/js/models/slider.js @@ -6,20 +6,31 @@ function update_slider_values(start, slider, input_store, self_data) { trigger_id = dash_clientside.callback_context.triggered[0]["prop_id"].split(".")[0]; } + + // text form component is the trigger if (trigger_id === `${self_data["id"]}_end_value`) { if (isNaN(start)) { return dash_clientside.no_update; } - end_value = start; + return [start, start, start]; + + // slider component is the trigger } else if (trigger_id === self_data["id"]) { - end_value = slider; - } else { - end_value = input_store !== null ? input_store : self_data["min"]; + return [slider, slider, slider]; } - - end_value = Math.min(Math.max(self_data["min"], end_value), self_data["max"]); - - return [end_value, end_value, end_value]; + // on_page_load is the trigger + if (input_store === null) { + return [dash_clientside.no_update, dash_clientside.no_update, slider]; + } + if (slider === start && start === input_store) { + // To prevent filter_action to be triggered after on_page_load + return [ + dash_clientside.no_update, + dash_clientside.no_update, + dash_clientside.no_update, + ]; + } + return [input_store, input_store, input_store]; } window.dash_clientside = { diff --git a/vizro-core/src/vizro/tables/_dash_table.py b/vizro-core/src/vizro/tables/_dash_table.py index 4335b68ef..f3b93f16b 100644 --- a/vizro-core/src/vizro/tables/_dash_table.py +++ b/vizro-core/src/vizro/tables/_dash_table.py @@ -31,17 +31,17 @@ def dash_data_table(data_frame: pd.DataFrame, **kwargs: Any) -> dash_table.DataT "columns": [{"name": col, "id": col} for col in data_frame.columns], "style_as_list_view": True, "style_cell": {"position": "static"}, - "style_data": {"border_bottom": "1px solid var(--border-subtle-alpha-01)", "height": "40px"}, + "style_data": {"border_bottom": "1px solid var(--border-subtleAlpha01)", "height": "40px"}, "style_header": { - "border_bottom": "1px solid var(--state-overlays-selected-hover)", - "border_top": "1px solid var(--main-container-bg-color)", + "border_bottom": "1px solid var(--stateOverlays-selectedHover)", + "border_top": "1px solid var(--right-side-bg)", "height": "32px", }, "style_data_conditional": [ { "if": {"state": "active"}, - "backgroundColor": "var(--state-overlays-selected)", - "border": "1px solid var(--state-overlays-selected)", + "backgroundColor": "var(--stateOverlays-selected)", + "border": "1px solid var(--stateOverlays-selected)", } ], } diff --git a/vizro-core/tests/e2e/screenshots/main_kpi_card_component_library.png b/vizro-core/tests/e2e/screenshots/main_kpi_card_component_library.png new file mode 100644 index 000000000..bc9550578 Binary files /dev/null and b/vizro-core/tests/e2e/screenshots/main_kpi_card_component_library.png differ diff --git a/vizro-core/tests/e2e/test_component_library.py b/vizro-core/tests/e2e/test_component_library.py new file mode 100644 index 000000000..553a6878c --- /dev/null +++ b/vizro-core/tests/e2e/test_component_library.py @@ -0,0 +1,89 @@ +import dash_bootstrap_components as dbc +import pandas as pd +from dash import Dash, html +from e2e_asserts import assert_image_equal, make_screenshot_and_paths + +from vizro.figures.library import kpi_card, kpi_card_reference + +df_kpi = pd.DataFrame( + { + "Actual": [100, 200, 700], + "Reference": [100, 300, 500], + "Category": ["A", "B", "C"], + } +) + +example_cards = [ + kpi_card(data_frame=df_kpi, value_column="Actual", title="KPI with value"), + kpi_card( + data_frame=df_kpi, + value_column="Actual", + title="KPI with aggregation", + agg_func="median", + ), + kpi_card( + data_frame=df_kpi, + value_column="Actual", + title="KPI formatted", + value_format="${value:.2f}", + ), + kpi_card( + data_frame=df_kpi, + value_column="Actual", + title="KPI with icon", + icon="shopping_cart", + ), +] + +example_reference_cards = [ + kpi_card_reference( + data_frame=df_kpi, + value_column="Actual", + reference_column="Reference", + title="KPI ref. (pos)", + ), + kpi_card_reference( + data_frame=df_kpi, + value_column="Actual", + reference_column="Reference", + agg_func="median", + title="KPI ref. (neg)", + ), + kpi_card_reference( + data_frame=df_kpi, + value_column="Actual", + reference_column="Reference", + title="KPI ref. formatted", + value_format="{value}€", + reference_format="{delta}€ vs. last year ({reference}€)", + ), + kpi_card_reference( + data_frame=df_kpi, + value_column="Actual", + reference_column="Reference", + title="KPI ref. with icon", + icon="shopping_cart", + ), +] + + +def test_kpi_card_component_library(dash_duo, request): + app = Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP]) + app.layout = dbc.Container( + [ + html.H1(children="KPI Cards"), + dbc.Stack( + children=[ + dbc.Row([dbc.Col(kpi_card) for kpi_card in example_cards]), + dbc.Row([dbc.Col(kpi_card) for kpi_card in example_reference_cards]), + ], + gap=4, + ), + ] + ) + dash_duo.start_server(app) + dash_duo.wait_for_page(timeout=20) + dash_duo.wait_for_element("div[class='card-kpi card']") + result_image_path, expected_image_path = make_screenshot_and_paths(dash_duo.driver, request.node.name) + assert_image_equal(result_image_path, expected_image_path) + assert dash_duo.get_logs() == [], "browser console should contain no error" diff --git a/vizro-core/tests/integration/test_examples.py b/vizro-core/tests/integration/test_examples.py index a983469c3..1acdd4372 100644 --- a/vizro-core/tests/integration/test_examples.py +++ b/vizro-core/tests/integration/test_examples.py @@ -1,4 +1,3 @@ -# ruff: noqa: F403, F405 import os import runpy from pathlib import Path @@ -40,17 +39,15 @@ def dashboard(request, monkeypatch): examples_path = Path(__file__).parents[2] / "examples" -# Ignore deprecation warning until this is solved: https://github.com/plotly/dash/issues/2590 -# The `features` examples do add_type, which ideally we would clean up afterwards to restore vizro.models to -# its previous state. Since we don't currently do this, `hatch run test` fails. -# This is difficult to fix fully by un-importing vizro.models though, since we use `import vizro.models as vm` - see -# https://stackoverflow.com/questions/437589/how-do-i-unload-reload-a-python-module. -@pytest.mark.filterwarnings("ignore:HTTPResponse.getheader():DeprecationWarning") # Ignore as it doesn't affect the test run @pytest.mark.filterwarnings("ignore::pytest.PytestUnhandledThreadExceptionWarning") @pytest.mark.filterwarnings("ignore:unclosed file:ResourceWarning") # Ignore for lower bounds because of plotly==5.12.0 @pytest.mark.filterwarnings("ignore:The behavior of DatetimeProperties.to_pydatetime is deprecated:FutureWarning") +# The `features` examples do add_type, which ideally we would clean up afterwards to restore vizro.models to +# its previous state. Since we don't currently do this, `hatch run test` fails. +# This is difficult to fix fully by un-importing vizro.models though, since we use `import vizro.models as vm` - see +# https://stackoverflow.com/questions/437589/how-do-i-unload-reload-a-python-module. @pytest.mark.parametrize( "example_path, version", [ diff --git a/vizro-core/tests/tests_utils/e2e_asserts.py b/vizro-core/tests/tests_utils/e2e_asserts.py new file mode 100644 index 000000000..e16eb40eb --- /dev/null +++ b/vizro-core/tests/tests_utils/e2e_asserts.py @@ -0,0 +1,69 @@ +import shutil +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(result_image, (x, y), (x + width, y + height), (0, 0, 255), 2) + return result_image + + +def make_screenshot_and_paths(browserdriver, 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" + browserdriver.save_screenshot(result_image_path) + return result_image_path, expected_image_path + + +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, result_image) + # Deleting created branch image to leave only failed for github artifacts + 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: + shutil.copy(result_image_path, expected_image_name) + raise cv2.error("pictures has different sizes") from exc diff --git a/vizro-core/tests/unit/vizro/actions/_action_loop/test_get_action_loop_components.py b/vizro-core/tests/unit/vizro/actions/_action_loop/test_get_action_loop_components.py index 9e649188d..55838f186 100644 --- a/vizro-core/tests/unit/vizro/actions/_action_loop/test_get_action_loop_components.py +++ b/vizro-core/tests/unit/vizro/actions/_action_loop/test_get_action_loop_components.py @@ -1,8 +1,9 @@ """Unit tests for vizro.actions._action_loop._get_action_loop_components file.""" import pytest -from asserts import assert_component_equal +from asserts import STRIP_ALL, assert_component_equal from dash import dcc, html +from dash._utils import stringify_id import vizro.models as vm import vizro.plotly.express as px @@ -27,7 +28,7 @@ def gateway_components(request): components = request.param actions_chain_ids = [model_manager[component].actions[0].id for component in components] return [ - dcc.Store(id={"type": "gateway_input", "trigger_id": actions_chain_id}, data=f"{actions_chain_id}") + dcc.Store(id={"type": "gateway_input", "trigger_id": actions_chain_id}, data=actions_chain_id) for actions_chain_id in actions_chain_ids ] @@ -153,11 +154,26 @@ def test_all_action_loop_components( result = _get_action_loop_components() expected = html.Div( id="action_loop_components_div", - children=fundamental_components - + gateway_components - + action_trigger_components - + [action_trigger_actions_id_component] - + [trigger_to_actions_chain_mapper_component], + children=[ + *fundamental_components, + *gateway_components, + *action_trigger_components, + action_trigger_actions_id_component, + trigger_to_actions_chain_mapper_component, + ], ) - assert_component_equal(result, expected) + # Data in these dcc.Stores is arbitrarily order. Sort in advance to ensure that assert_component_equal + # is order-agnostic for their data. + for key in ("action_trigger_actions_id", "trigger_to_actions_chain_mapper"): + result[key].data = sorted(result[key].data) + expected[key].data = sorted(expected[key].data) + + # Validate the action_loop_components_div wrapper. + assert_component_equal(result, expected, keys_to_strip=STRIP_ALL) + + # Order of dcc.Stores inside div wrapper is arbitrary so sort by stringified id to do order-agnostic comparison. + assert_component_equal( + sorted(result.children, key=lambda component: stringify_id(component.id)), + sorted(expected.children, key=lambda component: stringify_id(component.id)), + ) diff --git a/vizro-core/tests/unit/vizro/actions/_callback_mapping/test_get_action_callback_mapping.py b/vizro-core/tests/unit/vizro/actions/_callback_mapping/test_get_action_callback_mapping.py index 27adbb223..c32b8e1c7 100644 --- a/vizro-core/tests/unit/vizro/actions/_callback_mapping/test_get_action_callback_mapping.py +++ b/vizro-core/tests/unit/vizro/actions/_callback_mapping/test_get_action_callback_mapping.py @@ -9,6 +9,7 @@ from vizro import Vizro from vizro.actions import export_data, filter_interaction from vizro.actions._callback_mapping._get_action_callback_mapping import _get_action_callback_mapping +from vizro.managers import model_manager from vizro.models.types import capture @@ -185,7 +186,7 @@ class TestCallbackMapping: ], ) def test_action_callback_mapping_inputs(self, action_id, action_callback_inputs_expected): - result = _get_action_callback_mapping(action_id=action_id, argument="inputs") + result = _get_action_callback_mapping(action=model_manager[action_id], argument="inputs") assert result == action_callback_inputs_expected @pytest.mark.parametrize( @@ -242,14 +243,14 @@ def test_action_callback_mapping_inputs(self, action_id, action_callback_inputs_ indirect=["action_callback_outputs_expected"], ) def test_action_callback_mapping_outputs(self, action_id, action_callback_outputs_expected): - result = _get_action_callback_mapping(action_id=action_id, argument="outputs") + result = _get_action_callback_mapping(action=model_manager[action_id], argument="outputs") assert result == action_callback_outputs_expected @pytest.mark.parametrize( "export_data_outputs_expected", [("scatter_chart", "scatter_chart_2", "vizro_table")], indirect=True ) def test_export_data_no_targets_set_mapping_outputs(self, export_data_outputs_expected): - result = _get_action_callback_mapping(action_id="export_data_action", argument="outputs") + result = _get_action_callback_mapping(action=model_manager["export_data_action"], argument="outputs") assert result == export_data_outputs_expected @@ -266,7 +267,7 @@ def test_export_data_no_targets_set_mapping_outputs(self, export_data_outputs_ex def test_export_data_targets_set_mapping_outputs( self, config_for_testing_all_components_with_actions, export_data_outputs_expected ): - result = _get_action_callback_mapping(action_id="export_data_action", argument="outputs") + result = _get_action_callback_mapping(action=model_manager["export_data_action"], argument="outputs") assert result == export_data_outputs_expected @@ -274,7 +275,9 @@ def test_export_data_targets_set_mapping_outputs( "export_data_components_expected", [("scatter_chart", "scatter_chart_2", "vizro_table")], indirect=True ) def test_export_data_no_targets_set_mapping_components(self, export_data_components_expected): - result_components = _get_action_callback_mapping(action_id="export_data_action", argument="components") + result_components = _get_action_callback_mapping( + action=model_manager["export_data_action"], argument="components" + ) assert_component_equal(result_components, export_data_components_expected) @pytest.mark.parametrize( @@ -290,11 +293,13 @@ def test_export_data_no_targets_set_mapping_components(self, export_data_compone def test_export_data_targets_set_mapping_components( self, config_for_testing_all_components_with_actions, export_data_components_expected ): - result_components = _get_action_callback_mapping(action_id="export_data_action", argument="components") + result_components = _get_action_callback_mapping( + action=model_manager["export_data_action"], argument="components" + ) assert_component_equal(result_components, export_data_components_expected) def test_known_action_unknown_argument(self): - result = _get_action_callback_mapping(action_id="export_data_action", argument="unknown-argument") + result = _get_action_callback_mapping(action=model_manager["export_data_action"], argument="unknown-argument") assert result == {} # "export_data_custom_action" represents a unique scenario within custom actions, where the function's name @@ -305,5 +310,5 @@ def test_known_action_unknown_argument(self): "argument, expected", [("inputs", {}), ("outputs", {}), ("components", []), ("unknown-argument", {})] ) def test_custom_action_mapping(self, action_id, argument, expected): - result = _get_action_callback_mapping(action_id=action_id, argument=argument) + result = _get_action_callback_mapping(action=model_manager[action_id], argument=argument) assert result == expected diff --git a/vizro-core/tests/unit/vizro/actions/conftest.py b/vizro-core/tests/unit/vizro/actions/conftest.py index 902ca042f..3b7bc5337 100644 --- a/vizro-core/tests/unit/vizro/actions/conftest.py +++ b/vizro-core/tests/unit/vizro/actions/conftest.py @@ -1,4 +1,3 @@ -import pandas as pd import pytest import vizro.models as vm @@ -25,37 +24,11 @@ def iris(): return px.data.iris() -@pytest.fixture -def gapminder_dynamic_first_n_last_n_function(gapminder): - return lambda first_n=None, last_n=None: ( - pd.concat([gapminder[:first_n], gapminder[-last_n:]]) - if last_n - else gapminder[:first_n] - if first_n - else gapminder - ) - - -@pytest.fixture -def box_params(): - return {"x": "continent", "y": "lifeExp", "custom_data": ["continent"]} - - @pytest.fixture def box_chart(gapminder_2007, box_params): return px.box(gapminder_2007, **box_params) -@pytest.fixture -def box_chart_dynamic_data_frame(box_params): - return px.box("gapminder_dynamic_first_n_last_n", **box_params) - - -@pytest.fixture -def scatter_params(): - return {"x": "gdpPercap", "y": "lifeExp"} - - @pytest.fixture def scatter_chart(gapminder_2007, scatter_params): return px.scatter(gapminder_2007, **scatter_params) @@ -71,11 +44,6 @@ def scatter_matrix_chart(iris, scatter_matrix_params): return px.scatter_matrix(iris, **scatter_matrix_params) -@pytest.fixture -def scatter_chart_dynamic_data_frame(scatter_params): - return px.scatter("gapminder_dynamic_first_n_last_n", **scatter_params) - - @pytest.fixture def target_scatter_filtered_continent(request, gapminder_2007, scatter_params): continent = request.param @@ -105,21 +73,6 @@ def managers_one_page_two_graphs_one_button(box_chart, scatter_chart): Vizro._pre_build() -@pytest.fixture -def managers_one_page_two_graphs_with_dynamic_data(box_chart_dynamic_data_frame, scatter_chart_dynamic_data_frame): - """Instantiates a simple model_manager and data_manager with a page, two graph models and the button component.""" - vm.Page( - id="test_page", - title="My first dashboard", - components=[ - vm.Graph(id="box_chart", figure=box_chart_dynamic_data_frame), - vm.Graph(id="scatter_chart", figure=scatter_chart_dynamic_data_frame), - vm.Button(id="button"), - ], - ) - Vizro._pre_build() - - @pytest.fixture def managers_one_page_two_graphs_one_table_one_aggrid_one_button( box_chart, scatter_chart, dash_data_table_with_id, ag_grid_with_id diff --git a/vizro-core/tests/unit/vizro/conftest.py b/vizro-core/tests/unit/vizro/conftest.py index cba2bccc7..dd15c9fa4 100644 --- a/vizro-core/tests/unit/vizro/conftest.py +++ b/vizro-core/tests/unit/vizro/conftest.py @@ -1,5 +1,6 @@ """Fixtures to be shared across several tests.""" +import pandas as pd import plotly.graph_objects as go import pytest @@ -20,6 +21,17 @@ def stocks(): return px.data.stocks() +@pytest.fixture +def gapminder_dynamic_first_n_last_n_function(gapminder): + return lambda first_n=None, last_n=None: ( + pd.concat([gapminder[:first_n], gapminder[-last_n:]]) + if last_n + else gapminder[:first_n] + if first_n + else gapminder + ) + + @pytest.fixture def standard_px_chart(gapminder): return px.scatter( @@ -33,6 +45,26 @@ def standard_px_chart(gapminder): ) +@pytest.fixture +def scatter_params(): + return {"x": "gdpPercap", "y": "lifeExp"} + + +@pytest.fixture +def scatter_chart_dynamic_data_frame(scatter_params): + return px.scatter("gapminder_dynamic_first_n_last_n", **scatter_params) + + +@pytest.fixture +def box_params(): + return {"x": "continent", "y": "lifeExp", "custom_data": ["continent"]} + + +@pytest.fixture +def box_chart_dynamic_data_frame(box_params): + return px.box("gapminder_dynamic_first_n_last_n", **box_params) + + @pytest.fixture def standard_ag_grid(gapminder): return dash_ag_grid(data_frame=gapminder) @@ -88,6 +120,21 @@ def page_2(): return vm.Page(title="Page 2", components=[vm.Button()]) +@pytest.fixture +def managers_one_page_two_graphs_with_dynamic_data(box_chart_dynamic_data_frame, scatter_chart_dynamic_data_frame): + """Instantiates a simple model_manager and data_manager with a page, two graph models and the button component.""" + vm.Page( + id="test_page", + title="My first dashboard", + components=[ + vm.Graph(id="box_chart", figure=box_chart_dynamic_data_frame), + vm.Graph(id="scatter_chart", figure=scatter_chart_dynamic_data_frame), + vm.Button(id="button"), + ], + ) + Vizro._pre_build() + + @pytest.fixture() def vizro_app(): """Fixture to instantiate Vizro/Dash app. diff --git a/vizro-core/tests/unit/vizro/models/_components/form/test_range_slider.py b/vizro-core/tests/unit/vizro/models/_components/form/test_range_slider.py index e0c9a9f13..4879aafc3 100644 --- a/vizro-core/tests/unit/vizro/models/_components/form/test_range_slider.py +++ b/vizro-core/tests/unit/vizro/models/_components/form/test_range_slider.py @@ -48,7 +48,7 @@ def expected_range_slider_default(): persistence_type="session", className="slider-text-input-field", ), - dcc.Store(id="range_slider_input_store", storage_type="session", data=[None, None]), + dcc.Store(id="range_slider_input_store", storage_type="session"), ], className="slider-text-input-container", ), @@ -105,7 +105,7 @@ def expected_range_slider_with_optional(): persistence_type="session", className="slider-text-input-field", ), - dcc.Store(id="range_slider_input_store", storage_type="session", data=[0, 10]), + dcc.Store(id="range_slider_input_store", storage_type="session"), ], className="slider-text-input-container", ), diff --git a/vizro-core/tests/unit/vizro/models/_components/form/test_slider.py b/vizro-core/tests/unit/vizro/models/_components/form/test_slider.py index 56d2c5f24..92b34429e 100755 --- a/vizro-core/tests/unit/vizro/models/_components/form/test_slider.py +++ b/vizro-core/tests/unit/vizro/models/_components/form/test_slider.py @@ -35,7 +35,7 @@ def expected_slider(): persistence_type="session", className="slider-text-input-field", ), - dcc.Store(id="slider_id_input_store", storage_type="session", data=5.0), + dcc.Store(id="slider_id_input_store", storage_type="session"), ], className="slider-text-input-container", ), diff --git a/vizro-core/tests/unit/vizro/models/_components/test_button.py b/vizro-core/tests/unit/vizro/models/_components/test_button.py index fa793e443..1118c8de6 100644 --- a/vizro-core/tests/unit/vizro/models/_components/test_button.py +++ b/vizro-core/tests/unit/vizro/models/_components/test_button.py @@ -16,14 +16,28 @@ def test_create_default_button(self): assert hasattr(button, "id") assert button.type == "button" assert button.text == "Click me!" + assert button.href == "" assert button.actions == [] - @pytest.mark.parametrize("text", ["Test", 123, 1.23, True, """# Header""", """

Hello

"""]) - def test_create_button_with_optional(self, text): - button = vm.Button(text=text) - assert hasattr(button, "id") + @pytest.mark.parametrize( + "text, href", + [ + ("Test", "/page_1_reference"), + ("Test", "https://www.google.de/"), + (123, "/"), + ("""# Header""", "/"), + (1.23, "/"), + ("""

Hello

""", "/"), + (True, "/"), + ], + ) + def test_create_button_with_optional(self, text, href): + button = vm.Button(id="button-id", text=text, href=href) + + assert button.id == "button-id" assert button.type == "button" assert button.text == str(text) + assert button.href == href assert button.actions == [] def test_set_action_via_validator(self): @@ -33,7 +47,12 @@ def test_set_action_via_validator(self): class TestBuildMethod: - def test_button_build(self): + def test_button_build_wo_href(self): button = vm.Button(id="button_id", text="My text").build() - expected = dbc.Button(id="button_id", children="My text") + expected = dbc.Button(id="button_id", children="My text", href="", target="_top") + assert_component_equal(button, expected) + + def test_button_build_with_href(self): + button = vm.Button(id="button_id", text="My text", href="https://www.google.com").build() + expected = dbc.Button(id="button_id", children="My text", href="https://www.google.com", target="_top") assert_component_equal(button, expected) diff --git a/vizro-core/tests/unit/vizro/models/_components/test_card.py b/vizro-core/tests/unit/vizro/models/_components/test_card.py index 44c9d24ab..9b4274653 100755 --- a/vizro-core/tests/unit/vizro/models/_components/test_card.py +++ b/vizro-core/tests/unit/vizro/models/_components/test_card.py @@ -77,8 +77,8 @@ def test_card_build_wo_href(self): ("Text to test card", "Text to test card"), ("", ""), ( - """![](assets/images/icons/content/hypotheses.svg#icon-top)""", - "![](assets/images/icons/content/hypotheses.svg#icon-top)", + """![](assets/images/icons/content/hypotheses.svg)""", + "![](assets/images/icons/content/hypotheses.svg)", ), ("""Code block: ```python print(1)```""", "Code block: ```python print(1)```"), ("""[Example page](/test_page)""", "[Example page](/test_page)"), 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 d2f8bb39f..7a83dcfc0 100644 --- a/vizro-core/tests/unit/vizro/models/_components/test_tabs.py +++ b/vizro-core/tests/unit/vizro/models/_components/test_tabs.py @@ -1,6 +1,6 @@ """Unit tests for vizro.models.Container.""" -import dash_mantine_components as dmc +import dash_bootstrap_components as dbc import pytest from asserts import assert_component_equal from dash import html @@ -41,42 +41,20 @@ def test_tabs_build(self, containers): # We want to test the component itself but not all its children assert_component_equal( result, - dmc.Tabs(id="tabs-id", value="container-1", persistence=True, persistence_type="session", className="tabs"), + dbc.Tabs(id="tabs-id", persistence=True, persistence_type="session"), keys_to_strip={"children"}, ) # We want to test the children created in the Tabs.build but not e.g. the # vm.Container.build() as it's tested elsewhere already assert_component_equal( - result.children, - [dmc.TabsList(), dmc.TabsPanel(value="container-1"), dmc.TabsPanel(value="container-2")], - keys_to_strip={"children", "className"}, - ) - # So we go down the tree and ignore the children selectively - assert_component_equal( - result.children[0], - dmc.TabsList( - children=[ - dmc.Tab(value="container-1", children="Title-1", className="tab-title"), - dmc.Tab(value="container-2", children="Title-2", className="tab-title"), - ], - className="tabs-list", - ), - ) - # This one removes the need for duplication of tests as the output is similar - assert_component_equal( - result.children[1:], - [ - dmc.TabsPanel(className="tabs-panel", value="container-1"), - dmc.TabsPanel(className="tabs-panel", value="container-2"), - ], - keys_to_strip={"children"}, + result.children, [dbc.Tab(label="Title-1"), dbc.Tab(label="Title-2")], keys_to_strip={"children"} ) # We still check that the html.Div for the Containers are created, but we don't need to check its content assert_component_equal( - [tab.children.children for tab in result.children[1:]], + [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")], + html.Div(id="container-1", className="page-component-container"), + html.Div(id="container-2", className="page-component-container"), ], keys_to_strip={"children"}, ) diff --git a/vizro-core/tests/unit/vizro/models/_controls/test_filter.py b/vizro-core/tests/unit/vizro/models/_controls/test_filter.py index 3f3d909e9..2b64666e9 100644 --- a/vizro-core/tests/unit/vizro/models/_controls/test_filter.py +++ b/vizro-core/tests/unit/vizro/models/_controls/test_filter.py @@ -4,11 +4,12 @@ import pandas as pd import pytest from asserts import assert_component_equal +from dash import dcc import vizro.models as vm import vizro.plotly.express as px from vizro import Vizro -from vizro.managers import model_manager +from vizro.managers import data_manager, model_manager from vizro.models._action._actions_chain import ActionsChain from vizro.models._controls.filter import Filter, _filter_between, _filter_isin from vizro.models.types import CapturedCallable @@ -52,6 +53,32 @@ def managers_column_only_exists_in_some(): Vizro._pre_build() +@pytest.fixture +def target_to_data_frame(): + return { + "column_numerical_exists_1": pd.DataFrame( + { + "column_numerical": [1, 2], + } + ), + "column_numerical_exists_2": pd.DataFrame( + { + "column_numerical": [2, 3], + } + ), + "column_categorical_exists_1": pd.DataFrame( + { + "column_categorical": ["a", "b"], + } + ), + "column_categorical_exists_2": pd.DataFrame( + { + "column_categorical": ["b", "c"], + } + ), + } + + class TestFilterFunctions: @pytest.mark.parametrize( "data, value, expected", @@ -219,6 +246,167 @@ def test_filter_isin_date(self, data, value, expected): pd.testing.assert_series_equal(result, expected) +class TestFilterStaticMethods: + """Tests static methods of the Filter class.""" + + @pytest.mark.parametrize( + "data_columns, expected", + [ + ([[]], []), + ([["A", "B", "A"]], ["A", "B"]), + ([[1, 2, 1]], [1, 2]), + ([[1.1, 2.2, 1.1]], [1.1, 2.2]), + ( + [ + [ + datetime(2024, 1, 1), + datetime(2024, 1, 2), + datetime(2024, 1, 1), + ] + ], + [ + datetime(2024, 1, 1), + datetime(2024, 1, 2), + ], + ), + ([[], []], []), + ([["A"], []], ["A"]), + ([[], ["A"]], ["A"]), + ([["A"], ["B"]], ["A", "B"]), + ([["A", "B"], ["B", "C"]], ["A", "B", "C"]), + ], + ) + def test_get_options(self, data_columns, expected): + targeted_data = pd.DataFrame({f"target_{i}": pd.Series(data) for i, data in enumerate(data_columns)}) + result = Filter._get_options(targeted_data) + assert result == expected + + @pytest.mark.parametrize( + "data_columns, current_value, expected", + [ + ([[]], None, []), + ([[]], "ALL", []), + ([[]], ["ALL", "A"], ["A"]), + ([["A"]], ["ALL", "B"], ["A", "B"]), + ([[]], "A", ["A"]), + ([[]], ["A", "B"], ["A", "B"]), + ([["A"]], "B", ["A", "B"]), + ([["A"]], ["B", "C"], ["A", "B", "C"]), + ([[1]], 2, [1, 2]), + ([[1]], [2, 3], [1, 2, 3]), + ([[1.1]], 2.2, [1.1, 2.2]), + ([[1.1]], [2.2, 3.3], [1.1, 2.2, 3.3]), + ( + [ + [ + datetime(2024, 1, 1), + ] + ], + datetime(2024, 1, 2), + [ + datetime(2024, 1, 1), + datetime(2024, 1, 2), + ], + ), + ( + [ + [ + datetime(2024, 1, 1), + ] + ], + [ + datetime(2024, 1, 2), + datetime(2024, 1, 3), + ], + [ + datetime(2024, 1, 1), + datetime(2024, 1, 2), + datetime(2024, 1, 3), + ], + ), + ], + ) + def test_get_options_with_current_value(self, data_columns, current_value, expected): + targeted_data = pd.DataFrame({f"target_{i}": pd.Series(data) for i, data in enumerate(data_columns)}) + result = Filter._get_options(targeted_data, current_value) + assert result == expected + + @pytest.mark.parametrize( + "data_columns, expected", + [ + ([[1, 2, 1]], (1, 2)), + ([[1.1, 2.2, 1.1]], (1.1, 2.2)), + ( + [ + [ + datetime(2024, 1, 1), + datetime(2024, 1, 2), + datetime(2024, 1, 1), + ] + ], + ( + datetime(2024, 1, 1), + datetime(2024, 1, 2), + ), + ), + ([[1], []], (1, 1)), + ([[1, 2], []], (1, 2)), + ([[1, 2], [2, 3]], (1, 3)), + ], + ) + def test_get_min_max(self, data_columns, expected): + targeted_data = pd.DataFrame({f"target_{i}": pd.Series(data) for i, data in enumerate(data_columns)}) + result = Filter._get_min_max(targeted_data) + assert result == expected + + @pytest.mark.parametrize( + "data_columns, current_value, expected", + [ + ([[1, 2]], 3, (1, 3)), + ([[1, 2]], [3, 4], (1, 4)), + ([[1.1, 2.2]], 3.3, (1.1, 3.3)), + ([[1.1, 2.2]], [3.3, 4.4], (1.1, 4.4)), + ( + [ + [ + datetime(2024, 1, 1), + datetime(2024, 1, 2), + ] + ], + datetime(2024, 1, 3), + ( + datetime(2024, 1, 1), + datetime(2024, 1, 3), + ), + ), + ( + [ + [ + datetime(2024, 1, 1), + datetime(2024, 1, 2), + ] + ], + [ + datetime(2024, 1, 3), + datetime(2024, 1, 4), + ], + ( + datetime(2024, 1, 1), + datetime(2024, 1, 4), + ), + ), + ([[1], []], 2, (1, 2)), + ([[1], []], [2, 3], (1, 3)), + ([[1], [2]], 3, (1, 3)), + ([[1], [2]], [3, 4], (1, 4)), + ], + ) + def test_get_min_max_with_current_value(self, data_columns, current_value, expected): + targeted_data = pd.DataFrame({f"target_{i}": pd.Series(data) for i, data in enumerate(data_columns)}) + result = Filter._get_min_max(targeted_data, current_value) + assert result == expected + + @pytest.mark.usefixtures("managers_one_page_two_graphs") class TestFilterInstantiation: """Tests model instantiation and the validators run at that time.""" @@ -244,6 +432,73 @@ def test_check_target_present_invalid(self): Filter(column="foo", targets=["invalid_target"]) +@pytest.mark.usefixtures("managers_column_only_exists_in_some") +class TestFilterCall: + """Test Filter.__call__() method with target_to_data_frame and current_value inputs.""" + + def test_filter_call_categorical_valid(self, target_to_data_frame): + filter = vm.Filter( + column="column_categorical", + targets=["column_categorical_exists_1", "column_categorical_exists_2"], + selector=vm.Dropdown(id="test_selector_id"), + ) + filter.pre_build() + + selector_build = filter(target_to_data_frame=target_to_data_frame, current_value=["a", "b"])["test_selector_id"] + assert selector_build.options == ["ALL", "a", "b", "c"] + + def test_filter_call_numerical_valid(self, target_to_data_frame): + filter = vm.Filter( + column="column_numerical", + targets=["column_numerical_exists_1", "column_numerical_exists_2"], + selector=vm.RangeSlider(id="test_selector_id"), + ) + filter.pre_build() + + selector_build = filter(target_to_data_frame=target_to_data_frame, current_value=[1, 2])["test_selector_id"] + assert selector_build.min == 1 + assert selector_build.max == 3 + + def test_filter_call_column_is_changed(self, target_to_data_frame): + filter = vm.Filter( + column="column_categorical", targets=["column_categorical_exists_1", "column_categorical_exists_2"] + ) + filter.pre_build() + + filter._column_type = "numerical" + + with pytest.raises( + ValueError, + match="column_categorical has changed type from numerical to categorical. " + "A filtered column cannot change type while the dashboard is running.", + ): + filter(target_to_data_frame=target_to_data_frame, current_value=["a", "b"]) + + def test_filter_call_selected_column_not_found_in_target(self): + filter = vm.Filter(column="column_categorical", targets=["column_categorical_exists_1"]) + filter.pre_build() + + with pytest.raises( + ValueError, + match="Selected column column_categorical not found in dataframe for column_categorical_exists_1.", + ): + filter(target_to_data_frame={"column_categorical_exists_1": pd.DataFrame()}, current_value=["a", "b"]) + + def test_filter_call_targeted_data_empty(self): + filter = vm.Filter(column="column_categorical", targets=["column_categorical_exists_1"]) + filter.pre_build() + + with pytest.raises( + ValueError, + match="Selected column column_categorical does not contain anything in any dataframe " + "for column_categorical_exists_1.", + ): + filter( + target_to_data_frame={"column_categorical_exists_1": pd.DataFrame({"column_categorical": []})}, + current_value=["a", "b"], + ) + + class TestPreBuildMethod: def test_targets_default_valid(self, managers_column_only_exists_in_some): # Core of tests is still interface level @@ -387,6 +642,72 @@ def test_validate_column_type(self, targets, managers_column_different_type): ): filter.pre_build() + @pytest.mark.usefixtures("managers_one_page_two_graphs") + def test_filter_is_not_dynamic(self): + filter = vm.Filter(column="continent") + model_manager["test_page"].controls = [filter] + filter.pre_build() + # Filter is not dynamic because it does not target a figure that uses dynamic data + assert not filter._dynamic + assert not filter.selector._dynamic + + @pytest.mark.usefixtures("managers_one_page_two_graphs_with_dynamic_data") + @pytest.mark.parametrize( + "test_column, test_selector", + [ + ("continent", vm.Checklist()), + ("continent", vm.Dropdown()), + ("continent", vm.Dropdown(multi=False)), + ("continent", vm.RadioItems()), + ("pop", vm.Slider()), + ("pop", vm.RangeSlider()), + ], + ) + def test_filter_is_dynamic_with_dynamic_selectors( + self, test_column, test_selector, gapminder_dynamic_first_n_last_n_function + ): + data_manager["gapminder_dynamic_first_n_last_n"] = gapminder_dynamic_first_n_last_n_function + filter = vm.Filter(column=test_column, selector=test_selector) + model_manager["test_page"].controls = [filter] + filter.pre_build() + # Filter is dynamic because it targets a figure that uses dynamic data + assert filter._dynamic + assert filter.selector._dynamic + + @pytest.mark.usefixtures("managers_one_page_two_graphs_with_dynamic_data") + def test_filter_is_not_dynamic_with_non_dynamic_selectors(self, gapminder_dynamic_first_n_last_n_function): + data_manager["gapminder_dynamic_first_n_last_n"] = gapminder_dynamic_first_n_last_n_function + filter = vm.Filter(column="year", selector=vm.DatePicker()) + model_manager["test_page"].controls = [filter] + filter.pre_build() + assert not filter._dynamic + + @pytest.mark.usefixtures("managers_one_page_two_graphs_with_dynamic_data") + @pytest.mark.parametrize( + "test_column ,test_selector", + [ + ("continent", vm.Checklist(options=["Africa", "Europe"])), + ("continent", vm.Dropdown(options=["Africa", "Europe"])), + ("continent", vm.Dropdown(multi=False, options=["Africa", "Europe"])), + ("continent", vm.RadioItems(options=["Africa", "Europe"])), + ("pop", vm.Slider(min=2002)), + ("pop", vm.Slider(max=2007)), + ("pop", vm.Slider(min=2002, max=2007)), + ("pop", vm.RangeSlider(min=2002)), + ("pop", vm.RangeSlider(max=2007)), + ("pop", vm.RangeSlider(min=2002, max=2007)), + ], + ) + def test_filter_is_not_dynamic_with_options_min_max_specified( + self, test_column, test_selector, gapminder_dynamic_first_n_last_n_function + ): + data_manager["gapminder_dynamic_first_n_last_n"] = gapminder_dynamic_first_n_last_n_function + filter = vm.Filter(column=test_column, selector=test_selector) + model_manager["test_page"].controls = [filter] + filter.pre_build() + assert not filter._dynamic + assert not filter.selector._dynamic + @pytest.mark.parametrize("selector", [vm.Slider, vm.RangeSlider]) def test_numerical_min_max_default(self, selector, gapminder, managers_one_page_two_graphs): filter = vm.Filter(column="lifeExp", selector=selector()) @@ -500,18 +821,19 @@ def build(self): assert default_action.actions[0].id == f"filter_action_{filter.id}" -@pytest.mark.usefixtures("managers_one_page_two_graphs") class TestFilterBuild: """Tests filter build method.""" + @pytest.mark.usefixtures("managers_one_page_two_graphs") @pytest.mark.parametrize( - "test_column,test_selector", + "test_column ,test_selector", [ ("continent", vm.Checklist()), ("continent", vm.Dropdown()), + ("continent", vm.Dropdown(multi=False)), ("continent", vm.RadioItems()), - ("pop", vm.RangeSlider()), ("pop", vm.Slider()), + ("pop", vm.RangeSlider()), ("year", vm.DatePicker()), ("year", vm.DatePicker(range=False)), ], @@ -519,7 +841,52 @@ class TestFilterBuild: def test_filter_build(self, test_column, test_selector): filter = vm.Filter(column=test_column, selector=test_selector) model_manager["test_page"].controls = [filter] + + filter.pre_build() + result = filter.build() + expected = test_selector.build() + + assert_component_equal(result, expected) + + @pytest.mark.usefixtures("managers_one_page_two_graphs_with_dynamic_data") + @pytest.mark.parametrize( + "test_column, test_selector", + [ + ("continent", vm.Checklist()), + ("continent", vm.Dropdown()), + ("continent", vm.Dropdown(multi=False)), + ("continent", vm.RadioItems()), + ("pop", vm.Slider()), + ("pop", vm.RangeSlider()), + ], + ) + def test_dynamic_filter_build(self, test_column, test_selector, gapminder_dynamic_first_n_last_n_function): + # Adding dynamic data_frame to data_manager + data_manager["gapminder_dynamic_first_n_last_n"] = gapminder_dynamic_first_n_last_n_function + filter = vm.Filter(id="filter_id", column=test_column, selector=test_selector) + model_manager["test_page"].controls = [filter] filter.pre_build() + + result = filter.build() + expected = dcc.Loading( + id="filter_id", + children=test_selector.build(), + color="grey", + overlay_style={"visibility": "visible"}, + ) + + assert_component_equal(result, expected, keys_to_strip={"className"}) + + @pytest.mark.usefixtures("managers_one_page_two_graphs_with_dynamic_data") + def test_dynamic_filter_build_with_non_dynamic_selectors(self, gapminder_dynamic_first_n_last_n_function): + # Adding dynamic data_frame to data_manager + data_manager["gapminder_dynamic_first_n_last_n"] = gapminder_dynamic_first_n_last_n_function + + test_selector = vm.DatePicker() + filter = vm.Filter(column="year", selector=test_selector) + model_manager["test_page"].controls = [filter] + filter.pre_build() + result = filter.build() expected = test_selector.build() diff --git a/vizro-core/tests/unit/vizro/models/test_dashboard.py b/vizro-core/tests/unit/vizro/models/test_dashboard.py index e70ed4933..7d9ca5f08 100644 --- a/vizro-core/tests/unit/vizro/models/test_dashboard.py +++ b/vizro-core/tests/unit/vizro/models/test_dashboard.py @@ -2,7 +2,6 @@ import dash import dash_bootstrap_components as dbc -import dash_mantine_components as dmc import plotly.io as pio import pytest from asserts import assert_component_equal @@ -58,7 +57,7 @@ def test_field_invalid_pages_empty_list(self): vm.Dashboard(pages=[]) def test_field_invalid_pages_input_type(self): - with pytest.raises(ValidationError, match="4 validation errors for Dashboard"): + with pytest.raises(ValidationError, match="5 validation errors for Dashboard"): vm.Dashboard(pages=[vm.Button()]) def test_field_invalid_theme_input_type(self, page_1): @@ -244,36 +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=dmc.Switch( - id="theme_selector", - checked=False, - persistence=True, - persistence_type="session", - className="toggle-switch", - ), - 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"}) diff --git a/vizro-core/tests/unit/vizro/tables/test_dash_table.py b/vizro-core/tests/unit/vizro/tables/test_dash_table.py index 490610618..d903050cb 100644 --- a/vizro-core/tests/unit/vizro/tables/test_dash_table.py +++ b/vizro-core/tests/unit/vizro/tables/test_dash_table.py @@ -36,17 +36,17 @@ def test_dash_data_table(self): data=data_in_table, style_as_list_view=True, style_cell={"position": "static"}, - style_data={"border_bottom": "1px solid var(--border-subtle-alpha-01)", "height": "40px"}, + style_data={"border_bottom": "1px solid var(--border-subtleAlpha01)", "height": "40px"}, style_header={ - "border_bottom": "1px solid var(--state-overlays-selected-hover)", - "border_top": "1px solid var(--main-container-bg-color)", + "border_bottom": "1px solid var(--stateOverlays-selectedHover)", + "border_top": "1px solid var(--right-side-bg)", "height": "32px", }, style_data_conditional=[ { "if": {"state": "active"}, - "backgroundColor": "var(--state-overlays-selected)", - "border": "1px solid var(--state-overlays-selected)", + "backgroundColor": "var(--stateOverlays-selected)", + "border": "1px solid var(--stateOverlays-selected)", } ], ),