diff --git a/vizro-core/examples/dev/app.py b/vizro-core/examples/dev/app.py index 1265ab20c..74ade57b7 100644 --- a/vizro-core/examples/dev/app.py +++ b/vizro-core/examples/dev/app.py @@ -820,13 +820,13 @@ def multiple_cards(data_frame: pd.DataFrame, n_rows: Optional[int] = 1) -> html. if __name__ == "__main__": 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", - ) + + banner = 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", ) + app.dash.layout.children = [app.dash.layout.children, banner] server = app.dash.server app.run() diff --git a/vizro-core/examples/scratch_dev/app.py b/vizro-core/examples/scratch_dev/app.py index b4aca4dd7..5c9dd7d9e 100644 --- a/vizro-core/examples/scratch_dev/app.py +++ b/vizro-core/examples/scratch_dev/app.py @@ -1,43 +1,22 @@ """Dev app to try things out.""" -import dash_bootstrap_components as dbc - -import vizro.models as vm from vizro import Vizro +import vizro.models as vm import vizro.plotly.express as px - -from typing import Literal - -gapminder = px.data.gapminder() - - -class NumberInput(vm.VizroBaseModel): - type: Literal["number_input"] = "number_input" - - def build(self): - return ( - dbc.Input( - id="number-input", - type="number", - min=0, - max=10, - step=1, - value=5, - debounce=True, - ), - ) - - -vm.Page.add_type("components", NumberInput) +stocks = px.data.stocks(datetimes=True) page = vm.Page( - title="Charts UI", + title="Page", components=[ - NumberInput(), - vm.Graph(figure=px.box(gapminder, x="year", y="gdpPercap", color="continent")), + vm.Graph( + figure=px.line(stocks, x="date", y="GOOG", title="Stocks Data"), + ), + ], + controls=[ + vm.Filter(column="GOOG"), + vm.Filter(column="date", selector=vm.DatePicker(title="Date Picker (Stocks - date)")), ], - controls=[vm.Filter(column="year")], ) dashboard = vm.Dashboard(pages=[page]) diff --git a/vizro-core/pyproject.toml b/vizro-core/pyproject.toml index 1bf2b9577..2da2d4566 100644 --- a/vizro-core/pyproject.toml +++ b/vizro-core/pyproject.toml @@ -21,7 +21,7 @@ dependencies = [ "pandas>=2", "plotly>=5.12.0", "pydantic>=1.10.16", # must be synced with pre-commit mypy hook manually - "dash_mantine_components<0.13.0", # 0.13.0 is not compatible with 0.12, + "dash_mantine_components~=0.15.1", "flask_caching>=2", "wrapt>=1", "black", diff --git a/vizro-core/src/vizro/_vizro.py b/vizro-core/src/vizro/_vizro.py index f648d053f..f2fa41a27 100644 --- a/vizro-core/src/vizro/_vizro.py +++ b/vizro-core/src/vizro/_vizro.py @@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, TypedDict, cast import dash +import dash_mantine_components as dmc import plotly.io as pio from dash.development.base_component import ComponentRegistry from flask_caching import SimpleCache @@ -17,6 +18,9 @@ from vizro.managers import data_manager, model_manager from vizro.models import Dashboard, Filter +# this can be removed when Dash uses React 18 as a default (likely V3.0 https://github.com/plotly/dash/pull/3093) +dash._dash_renderer._set_react_version("18.2.0") + logger = logging.getLogger(__name__) if TYPE_CHECKING: @@ -49,6 +53,13 @@ def __init__(self, **kwargs): use_pages=True, ) + # Ensure external_stylesheets is a list and append the additional stylesheet + external_stylesheets = self.dash.config.external_stylesheets + self.dash.config.external_stylesheets = ( + external_stylesheets if isinstance(external_stylesheets, list) else [external_stylesheets] + ) + self.dash.config.external_stylesheets.append(dmc.styles.DATES) + # When Vizro is used as a framework, we want to include the library and framework resources. # Dash serves resources in the order 1. external_stylesheets/scripts; 2. library resources from the # ComponentRegistry; 3. resources added by append_css/scripts. diff --git a/vizro-core/src/vizro/models/_components/form/date_picker.py b/vizro-core/src/vizro/models/_components/form/date_picker.py index b3dc416fd..fefa81625 100644 --- a/vizro-core/src/vizro/models/_components/form/date_picker.py +++ b/vizro-core/src/vizro/models/_components/form/date_picker.py @@ -1,7 +1,7 @@ from typing import Literal, Optional, Union import dash_mantine_components as dmc -from dash import ClientsideFunction, Input, Output, State, clientside_callback, dcc, html +from dash import html try: from pydantic.v1 import Field, PrivateAttr, validator @@ -9,7 +9,6 @@ from pydantic import Field, PrivateAttr, validator -import datetime from datetime import date import dash_bootstrap_components as dbc @@ -42,6 +41,7 @@ class DatePicker(VizroBaseModel): max: Optional[date] = Field(None, description="End date for date picker.") value: Optional[Union[list[date], date]] = Field(None, description="Default date for date picker") title: str = Field("", description="Title to be displayed.") + range: bool = Field(True, description="Boolean flag for displaying range picker.") actions: list[Action] = [] @@ -55,57 +55,24 @@ class DatePicker(VizroBaseModel): def build(self): init_value = self.value or ([self.min, self.max] if self.range else self.min) # type: ignore[list-item] - date_range_picker_kwargs = {"allowSingleDateInRange": True} if self.range else {} - - output = [ - Output(self.id, "value"), - Output(f"{self.id}_input_store", "data"), - ] - inputs = [ - Input(self.id, "value"), - State(f"{self.id}_input_store", "data"), - ] - - clientside_callback( - ClientsideFunction(namespace="date_picker", function_name="update_date_picker_values"), - output=output, - inputs=inputs, - ) - # clientside callback is required as a workaround when the date-picker is overflowing its parent container - # if there is not enough space. Caused by another workaround for this issue: - # https://github.com/snehilvj/dash-mantine-components/issues/219 - clientside_callback( - ClientsideFunction(namespace="date_picker", function_name="update_date_picker_position"), - output=Output(self.id, "dropdownPosition"), - inputs=Input(self.id, "n_clicks"), - ) - - date_picker_class = dmc.DateRangePicker if self.range else dmc.DatePicker - # dropdownPosition must be set to bottom-start as a workaround for issue: - # https://github.com/snehilvj/dash-mantine-components/issues/219 - # clearable must be set to False as a workaround for issue: - # https://github.com/snehilvj/dash-mantine-components/issues/212 - # maxDate must be increased by one day, and later on disabledDates must be set as maxDate + 1 day - # as a workaround for issue: https://github.com/snehilvj/dash-mantine-components/issues/230 - date_picker = date_picker_class( + date_picker = dmc.DatePickerInput( id=self.id, minDate=self.min, value=init_value, - maxDate=self.max + datetime.timedelta(days=1) if self.max else None, + maxDate=self.max, persistence=True, persistence_type="session", - dropdownPosition="bottom-start", - clearable=False, - disabledDates=self.max + datetime.timedelta(days=1) if self.max else None, + type="range" if self.range else "default", + allowSingleDateInRange=True, className="datepicker", - **date_range_picker_kwargs, + # removes the default red color for weekend days + styles={"day": {"color": "var(--mantine-color-text"}}, ) return html.Div( children=[ dbc.Label(children=self.title, html_for=self.id) if self.title else None, date_picker, - dcc.Store(id=f"{self.id}_input_store", storage_type="session", data=init_value), ], ) diff --git a/vizro-core/src/vizro/models/_dashboard.py b/vizro-core/src/vizro/models/_dashboard.py index b635b8e95..94d319612 100644 --- a/vizro-core/src/vizro/models/_dashboard.py +++ b/vizro-core/src/vizro/models/_dashboard.py @@ -8,6 +8,7 @@ import dash import dash_bootstrap_components as dbc +import dash_mantine_components as dmc import plotly.io as pio from dash import ( ClientsideFunction, @@ -156,7 +157,7 @@ def build(self): State("collapsable-left-side", "is_open"), ) - return html.Div( + layout = html.Div( id="dashboard-container", children=[ html.Div(id="vizro_version", children=vizro.__version__, hidden=True), @@ -171,6 +172,11 @@ def build(self): dash.page_container, ], ) + return dmc.MantineProvider( + layout, + # Use the `theme` to style all Mantine components with a Vizro theme. For more info see https://www.dash-mantine-components.com/components/mantineprovider + theme={"primaryColor": "gray"}, + ) def _validate_logos(self): logo_img = self._infer_image(filename="logo") diff --git a/vizro-core/src/vizro/static/css/datepicker.css b/vizro-core/src/vizro/static/css/datepicker.css deleted file mode 100644 index 3e167d5d0..000000000 --- a/vizro-core/src/vizro/static/css/datepicker.css +++ /dev/null @@ -1,109 +0,0 @@ -.datepicker .mantine-Input-wrapper { - font-family: unset; - height: 2rem; -} - -.datepicker .mantine-DateRangePicker-input, -.datepicker .mantine-DatePicker-input { - background-color: var(--field-enabled); - border: none; - border-radius: 0; - box-shadow: var(--elevation-0); - color: var(--text-secondary); - font-size: 0.875rem; - height: 2rem; - line-height: 1rem; - min-height: 2rem; - padding: 0 0.5rem; -} - -.datepicker .mantine-DateRangePicker-input:hover, -.datepicker .mantine-DatePicker-input:hover { - color: var(--text-primary); -} - -.datepicker .mantine-DateRangePicker-dropdown, -.datepicker .mantine-DatePicker-dropdown { - background: var(--field-enabled); - border: none; - border-radius: 0; - box-shadow: var(--elevation-1); - padding: 1rem 11px; /* 11px otherwise not aligned with controls */ -} - -.datepicker .mantine-UnstyledButton-root { - border-radius: 0; - color: var(--text-secondary); - font-family: unset; - font-weight: 400; -} - -.datepicker .mantine-UnstyledButton-root:hover { - background: var(--stateOverlays-hover); - color: var(--text-primary); -} - -.datepicker .mantine-DateRangePicker-weekday, -.datepicker .mantine-DatePicker-weekday { - color: var(--text-secondary); - font-family: unset; - padding: 0.5rem; -} - -.datepicker .mantine-DateRangePicker-cell, -.datepicker .mantine-DatePicker-cell { - border: none; -} - -.datepicker .mantine-DateRangePicker-day, -.datepicker .mantine-DatePicker-day { - background: var(--field-enabled); - border-radius: 0; - color: var(--text-secondary); - font-family: unset; -} - -.datepicker .mantine-DateRangePicker-day[data-outside], -.datepicker .mantine-DateRangePicker-day:disabled, -.datepicker .mantine-DatePicker-day[data-outside], -.datepicker .mantine-DatePicker-day:disabled, -.datepicker .mantine-UnstyledButton-root:disabled { - color: var(--text-disabled); -} - -.datepicker .mantine-DateRangePicker-day:hover, -.datepicker .mantine-DateRangePicker-day:focus-visible, -.datepicker .mantine-DatePicker-day:hover, -.datepicker .mantine-DatePicker-day:focus-visible { - background: var(--stateOverlays-hover); - border: none; - color: var(--text-primary); - outline: none; - text-decoration: none; -} - -.datepicker .mantine-DateRangePicker-day[data-in-range] { - background: var(--stateOverlays-selected); - color: var(--text-primary); -} - -.datepicker .mantine-DateRangePicker-day[data-selected], -.datepicker .mantine-DateRangePicker-yearPickerControlActive, -.datepicker .mantine-DateRangePicker-monthPickerControlActive, -.datepicker .mantine-DatePicker-day[data-selected], -.datepicker .mantine-DatePicker-yearPickerControlActive, -.datepicker .mantine-DatePicker-monthPickerControlActive { - background: var(--stateOverlays-selected-inverted); - color: var(--text-primary-inverted); - text-decoration: underline; -} - -.datepicker - .mantine-DateRangePicker-calendarHeader - .mantine-UnstyledButton-root:hover, -.datepicker - .mantine-DatePicker-calendarHeader - .mantine-UnstyledButton-root:hover { - background: transparent; - color: var(--text-primary); -} diff --git a/vizro-core/src/vizro/static/js/models/dashboard.js b/vizro-core/src/vizro/static/js/models/dashboard.js index e04084788..5a278e1ac 100644 --- a/vizro-core/src/vizro/static/js/models/dashboard.js +++ b/vizro-core/src/vizro/static/js/models/dashboard.js @@ -1,8 +1,10 @@ function update_dashboard_theme(theme_selector_checked) { - document.documentElement.setAttribute( - "data-bs-theme", - theme_selector_checked ? "light" : "dark", - ); + const theme = theme_selector_checked ? "light" : "dark"; + + // Update theme attributes for Bootstrap and Mantine + document.documentElement.setAttribute("data-bs-theme", theme); + document.documentElement.setAttribute("data-mantine-color-scheme", theme); + return window.dash_clientside.no_update; } diff --git a/vizro-core/src/vizro/static/js/models/date_picker.js b/vizro-core/src/vizro/static/js/models/date_picker.js deleted file mode 100644 index 1073acacb..000000000 --- a/vizro-core/src/vizro/static/js/models/date_picker.js +++ /dev/null @@ -1,27 +0,0 @@ -function update_date_picker_values(value, input_store) { - if ( - value === null || - dash_clientside.callback_context.triggered.length === 0 - ) { - return [input_store, input_store]; - } - return [value, value]; -} - -function update_date_picker_position(clicks) { - var element_id = window.dash_clientside.callback_context.inputs_list[0]["id"]; - var element = document.getElementById(element_id); - var rect = element.getBoundingClientRect(); - var position = - rect.y + 360 > window.innerHeight ? "top-start" : "bottom-start"; - - return position; -} - -window.dash_clientside = { - ...window.dash_clientside, - date_picker: { - update_date_picker_values: update_date_picker_values, - update_date_picker_position: update_date_picker_position, - }, -}; diff --git a/vizro-core/tests/unit/vizro/models/_components/form/test_date_picker.py b/vizro-core/tests/unit/vizro/models/_components/form/test_date_picker.py index 4c794541b..538353e47 100644 --- a/vizro-core/tests/unit/vizro/models/_components/form/test_date_picker.py +++ b/vizro-core/tests/unit/vizro/models/_components/form/test_date_picker.py @@ -6,7 +6,7 @@ import dash_mantine_components as dmc import pytest from asserts import assert_component_equal -from dash import dcc, html +from dash import html try: from pydantic.v1 import ValidationError @@ -117,25 +117,22 @@ def test_datepicker_build(self, range, value): min="2023-01-01", max="2023-07-01", range=range, value=value, id="datepicker_id", title="Test title" ).build() - date_picker_class = dmc.DateRangePicker if range else dmc.DatePicker - additional_kwargs = {"allowSingleDateInRange": True} if range else {} expected_datepicker = html.Div( [ dbc.Label("Test title", html_for="datepicker_id"), - date_picker_class( + dmc.DatePickerInput( id="datepicker_id", minDate="2023-01-01", - maxDate="2023-07-02", value=value, + maxDate="2023-07-01", persistence=True, persistence_type="session", - dropdownPosition="bottom-start", - disabledDates="2023-07-02", - clearable=False, + type="range" if range else "default", + allowSingleDateInRange=True, className="datepicker", - **additional_kwargs, + # removes the default red color for weekend days + styles={"day": {"color": "var(--mantine-color-text"}}, ), - dcc.Store(id="datepicker_id_input_store", storage_type="session", data=value), ], ) assert_component_equal(date_picker, expected_datepicker) diff --git a/vizro-core/tests/unit/vizro/models/test_dashboard.py b/vizro-core/tests/unit/vizro/models/test_dashboard.py index 7d9ca5f08..9e321ac85 100644 --- a/vizro-core/tests/unit/vizro/models/test_dashboard.py +++ b/vizro-core/tests/unit/vizro/models/test_dashboard.py @@ -2,6 +2,7 @@ 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 @@ -275,20 +276,23 @@ def test_dashboard_build(self, vizro_app, page_1, page_2): layout={"title": {"pad_l": 0, "pad_r": 0}, "margin_l": 24, "margin_t": 24, "margin_b": 16} ) - expected_dashboard_container = html.Div( - id="dashboard-container", - children=[ - html.Div(id="vizro_version", children=vizro.__version__, hidden=True), - dcc.Store( - id="vizro_themes", - data={ - "vizro_dark": dashboard_vizro_dark, - "vizro_light": dashboard_vizro_light, - }, - ), - ActionLoop._create_app_callbacks(), - dash.page_container, - ], + expected_dashboard_container = dmc.MantineProvider( + html.Div( + id="dashboard-container", + children=[ + html.Div(id="vizro_version", children=vizro.__version__, hidden=True), + dcc.Store( + id="vizro_themes", + data={ + "vizro_dark": dashboard_vizro_dark, + "vizro_light": dashboard_vizro_light, + }, + ), + ActionLoop._create_app_callbacks(), + dash.page_container, + ], + ), + theme={"primaryColor": "gray"}, ) assert_component_equal(dashboard.build(), expected_dashboard_container)