diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e85739768..d0596372c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -94,7 +94,7 @@ repos: --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 + 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 diff --git a/vizro-core/changelog.d/20241203_145037_antony.milne_mdformat.md b/vizro-core/changelog.d/20241203_145037_antony.milne_mdformat.md new file mode 100644 index 000000000..4abc0f11e --- /dev/null +++ b/vizro-core/changelog.d/20241203_145037_antony.milne_mdformat.md @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + diff --git a/vizro-core/changelog.d/20241203_145221_antony.milne_mdformat.md b/vizro-core/changelog.d/20241203_145221_antony.milne_mdformat.md new file mode 100644 index 000000000..4abc0f11e --- /dev/null +++ b/vizro-core/changelog.d/20241203_145221_antony.milne_mdformat.md @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + 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/src/vizro/__init__.py b/vizro-core/src/vizro/__init__.py index 6a77ff59c..cf2b583d7 100644 --- a/vizro-core/src/vizro/__init__.py +++ b/vizro-core/src/vizro/__init__.py @@ -14,7 +14,7 @@ __all__ = ["Vizro"] -__version__ = "0.1.30.dev0" +__version__ = "0.1.29.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/actions/_actions_utils.py b/vizro-core/src/vizro/actions/_actions_utils.py index 984359fc9..1873b6620 100644 --- a/vizro-core/src/vizro/actions/_actions_utils.py +++ b/vizro-core/src/vizro/actions/_actions_utils.py @@ -49,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"]]) @@ -162,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. @@ -185,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"] @@ -221,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 ) @@ -229,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"])) @@ -250,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/_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/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/checklist.py b/vizro-core/src/vizro/models/_components/form/checklist.py index 68cb26ad1..ed746dec3 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,13 @@ def build(self): ), ] ) + + def _build_dynamic_placeholder(self): + if self.value is None: + self.value = [get_options_and_default(self.options, multi=True)[1]] + + 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..a56c13c47 100755 --- a/vizro-core/src/vizro/models/_components/form/dropdown.py +++ b/vizro-core/src/vizro/models/_components/form/dropdown.py @@ -10,6 +10,7 @@ from pydantic import Field, PrivateAttr, StrictBool, root_validator, validator import dash_bootstrap_components as dbc +import dash_mantine_components as dmc from vizro.models import Action, VizroBaseModel from vizro.models._action._actions_chain import _action_validator_factory @@ -65,6 +66,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 +87,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 +99,35 @@ 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 + + # TODO-NEXT: Replace this with the "universal Vizro placeholder" component. + return html.Div( + children=[ + html.Legend(children=self.title, className="form-label") if self.title else None, + dmc.DateRangePicker( + id=self.id, + value=self.value, + persistence=True, persistence_type="session", ), ] ) + + @_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..48b8bc6bc 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,13 @@ def build(self): ), ] ) + + def _build_dynamic_placeholder(self): + if self.value is None: + self.value = get_options_and_default(self.options, multi=False)[1] + + 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/_controls/parameter.py b/vizro-core/src/vizro/models/_controls/parameter.py index c77c7c7bf..cdc76936c 100644 --- a/vizro-core/src/vizro/models/_controls/parameter.py +++ b/vizro-core/src/vizro/models/_controls/parameter.py @@ -56,6 +56,7 @@ 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") diff --git a/vizro-core/src/vizro/models/_dashboard.py b/vizro-core/src/vizro/models/_dashboard.py index b635b8e95..2fd7c7b00 100644 --- a/vizro-core/src/vizro/models/_dashboard.py +++ b/vizro-core/src/vizro/models/_dashboard.py @@ -308,18 +308,31 @@ 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. - dbc.Switch( - id="theme-selector", - value=self.theme == "vizro_light", - persistence=True, - persistence_type="session", + html.Div( + children=dbc.Switch( + id="theme-selector", + value=self.theme == "vizro_light", + persistence=True, + persistence_type="session", + ), + id="settings", ), html.Img(src=f"data:image/svg+xml;base64,{error_404_svg}"), - 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"), + 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", + ), ], - className="d-flex flex-column align-items-center justify-content-center min-vh-100", + className="page-error-container", ) @staticmethod diff --git a/vizro-core/src/vizro/static/css/layout.css b/vizro-core/src/vizro/static/css/layout.css index a7c66f864..996c1108c 100644 --- a/vizro-core/src/vizro/static/css/layout.css +++ b/vizro-core/src/vizro/static/css/layout.css @@ -85,6 +85,31 @@ border-bottom: 1px solid var(--border-subtleAlpha01); } +.page-error-container { + align-items: center; + display: flex; + flex-direction: column; + height: 100vh; + justify-content: center; + width: 100vw; +} + +.error-content-container { + align-items: center; + display: inline-flex; + flex-direction: column; + gap: 24px; + margin-top: -32px; +} + +.error-text-container { + display: flex; + flex-direction: column; + gap: 8px; + text-align: center; + width: 336px; +} + .dashboard_title { display: flex; flex-direction: column; diff --git a/vizro-core/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 = {