From 21a34929e0a4ed109235b97088a3ba9a42f11b40 Mon Sep 17 00:00:00 2001 From: petar-qb Date: Mon, 11 Sep 2023 11:14:47 +0200 Subject: [PATCH 01/40] First version optimised action loop mechanism --- vizro-core/examples/default/app.py | 26 ++- .../_build_action_loop_callbacks.py | 178 ++++++------------ .../_get_action_loop_components.py | 6 +- 3 files changed, 82 insertions(+), 128 deletions(-) diff --git a/vizro-core/examples/default/app.py b/vizro-core/examples/default/app.py index a4efb6643..dcfbd53e3 100644 --- a/vizro-core/examples/default/app.py +++ b/vizro-core/examples/default/app.py @@ -454,6 +454,16 @@ def create_country_analysis(): targets=["bar_country"], ) ), + vm.Action( + function=export_data( + targets=["bar_country"], + ) + ), + vm.Action( + function=export_data( + targets=["bar_country"], + ) + ), ], ), ], @@ -516,22 +526,20 @@ def create_home_page(): return page_home -create_country_analysis() - dashboard = vm.Dashboard( pages=[ create_home_page(), create_variable_analysis(), create_relation_analysis(), create_continent_summary(), - # create_country_analysis(), + create_country_analysis(), ], - navigation=vm.Navigation( - pages={ - "Analysis": ["Homepage", "Variable Analysis", "Relationship Analysis", "Country Analysis"], - "Summary": ["Continent Summary"], - } - ), + # navigation=vm.Navigation( + # pages={ + # "Analysis": ["Homepage", "Variable Analysis", "Relationship Analysis", "Country Analysis"], + # "Summary": ["Continent Summary"], + # } + # ), ) if __name__ == "__main__": 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 a4fef6c78..f5cac2e2f 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 @@ -23,10 +23,28 @@ def _build_action_loop_callbacks() -> None: actions_chains = _get_actions_chains_on_registered_pages() actions = _get_actions_on_registered_pages() - gateway_triggers: List[Input] = [] + if not actions_chains: + return + + gateway_inputs: Dict[str, Any] = { + "gateway_triggers": [], + "cycle_breaker_div": Input("cycle_breaker_div", "n_clicks"), + "remaining_actions": State("remaining_actions", "data"), + + } + gateway_outputs: Dict[str, Any] = { + "action_triggers": [Output({"type": "action_trigger", "action_name": action.id}, "data") for action in actions], + "remaining_actions": Output("remaining_actions", "data"), + } + for actions_chain in actions_chains: - # TODO: Potentially convert to clientside callback - @callback( + # Callback that enables gateway callback to work in the multiple page app + clientside_callback( + """ + function(input, data) { + return data; + } + """, Output({"type": "gateway_input", "trigger_id": actions_chain.id}, "data"), Input( component_id=actions_chain.trigger.component_id, @@ -35,148 +53,72 @@ def _build_action_loop_callbacks() -> None: State({"type": "gateway_input", "trigger_id": actions_chain.id}, "data"), prevent_initial_call=True, ) - def trigger_to_global_store(_, data): - return data - gateway_triggers.append( + gateway_inputs["gateway_triggers"].append( Input( component_id={"type": "gateway_input", "trigger_id": actions_chain.id}, component_property="data", ) ) - # TODO: don't create any components or callback if there's no actions configured - if not gateway_triggers: - gateway_triggers.append(Input("empty_input_store", "data")) - - action_triggers = [Output({"type": "action_trigger", "action_name": action.id}, "data") for action in actions] - if not action_triggers: - action_triggers.append(Output("empty_output_store", "data", allow_duplicate=True)) - - @callback( - Output("set_remaining", "data"), - gateway_triggers, - prevent_initial_call=True, - ) - def gateway(*gateway_triggers: List[dcc.Store]) -> List[Optional[str]]: - """Determines the final sequence of actions to be triggered. - - Args: - gateway_triggers: Each 'gateway_trigger' (ctx.triggered_id) provides the 'id' (or trigger_id) from the - 'actions_chain' that should be executed. - - Returns: - List of final action sequence names which need to be triggered in order. - - Raises: - PreventUpdate: - If screen with triggers is rendered but component isn't triggered. - """ - triggered_actions_chains_ids = [ - json.loads(triggered["prop_id"].split(".")[0])["trigger_id"] for triggered in ctx.triggered - ] - - # Trigger only the on_page_load action if exists. - # Otherwise, a single regular (non on_page_load) action is triggered - actions_chain_to_trigger = next( - ( - actions_chain_id - for actions_chain_id in triggered_actions_chains_ids - if ON_PAGE_LOAD_ACTION_PREFIX in actions_chain_id - ), - triggered_actions_chains_ids[0], - ) - logger.debug("=========== ACTION ===============") - logger.debug(f"Triggered component: {triggered_actions_chains_ids[0]}.") - final_action_sequence = [ - {"Action ID": action.id, "Action name": action_functions[action.function._function]} - for action in model_manager[actions_chain_to_trigger].actions # type: ignore[attr-defined] - ] - logger.debug(f"Actions to be executed as part of the triggered ActionsChain: {final_action_sequence}") - return [action_dict["Action ID"] for action_dict in final_action_sequence] - @callback( - Output("remaining_actions", "data"), - Input("cycle_breaker_div", "n_clicks"), - Input("set_remaining", "data"), - State("remaining_actions", "data"), + output=gateway_outputs, + inputs=gateway_inputs, prevent_initial_call=True, ) - def update_remaining_actions( - action_finished: Optional[Dict[str, Any]], - set_remaining: List[str], - remaining_actions: List[str], - ) -> List[str]: - """Updates remaining action sequence that should be performed. - - Args: - action_finished: - Input that signalise action callback has finished - set_remaining: - Input that pass action sequence set in 'gateway' callback - remaining_actions: - State represents remaining actions sequence - Returns: - Initial or diminished list of remaining actions needed to be triggered. - """ - # Propagate sequence of actions from gateway callback - triggered_id = ctx.triggered_id - if triggered_id == "set_remaining": - return set_remaining - # Pop first action - if triggered_id == "cycle_breaker_div": - return remaining_actions[1:] - return [] - - @callback( - *action_triggers, - Input("remaining_actions", "data"), - prevent_initial_call=True, - ) - def executor(remaining_actions: List[str]) -> List[Any]: - """Triggers callback of first action of remaining_actions list. - - Args: - remaining_actions: - Action sequence needed to be triggered. - - Returns: - List of dash.no_update objects for all outputs except for next action. + def gateway(**inputs: Dict[str, Any]): + """GATEWAY.""" + remaining_actions = inputs["remaining_actions"] + if ctx.triggered_id == "cycle_breaker_div": + remaining_actions = remaining_actions[1:] + else: + triggered_actions_chains_ids = [ + json.loads(triggered["prop_id"].split(".")[0])["trigger_id"] for triggered in ctx.triggered + ] + + # Trigger only the on_page_load action if exists. + # Otherwise, a single regular (non on_page_load) action is triggered + actions_chain_to_trigger = next( + ( + actions_chain_id + for actions_chain_id in triggered_actions_chains_ids + if ON_PAGE_LOAD_ACTION_PREFIX in actions_chain_id + ), + triggered_actions_chains_ids[0], + ) + logger.debug("=========== ACTION ===============") + logger.debug(f"Triggered component: {triggered_actions_chains_ids[0]}.") + final_action_sequence = [ + {"Action ID": action.id, "Action name": action_functions[action.function._function]} + for action in model_manager[actions_chain_to_trigger].actions # type: ignore[attr-defined] + ] + logger.debug(f"Actions to be executed as part of the triggered ActionsChain: {final_action_sequence}") + remaining_actions = [action_dict["Action ID"] for action_dict in final_action_sequence] - Raises: - PreventUpdate: - If there is no more remaining_actions needs to be triggered. - """ if not remaining_actions: raise PreventUpdate next_action = remaining_actions[0] - output_list = ctx.outputs_list if isinstance(ctx.outputs_list, list) else [ctx.outputs_list] + output_list = ctx.outputs_grouping["action_triggers"] # Return dash.no_update for all outputs except for the next action trigger_next = [no_update if output["id"]["action_name"] != next_action else None for output in output_list] logger.debug(f"Starting execution of Action: {next_action}") - return trigger_next - # Callback called after an action is finished - @callback( - Output("cycle_breaker_div", "children"), - Input("action_finished", "data"), - prevent_initial_call=True, - ) - def after_action(*_) -> None: - """Triggers clientside callback responsible for starting a new iteration.""" - logger.debug("Finished Action execution.") + return { + "action_triggers": trigger_next, + "remaining_actions": remaining_actions + } # Callback that triggers the next iteration clientside_callback( """ - function(children) { + function(data) { document.getElementById("cycle_breaker_div").click() - return children; + return []; } """, Output("cycle_breaker_empty_output_store", "data"), - Input("cycle_breaker_div", "children"), + Input("action_finished", "data"), prevent_initial_call=True, ) 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 64c5111a4..64321b825 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 @@ -19,13 +19,17 @@ def _get_action_loop_components() -> List[Union[dcc.Store, html.Div]]: actions_chains = _get_actions_chains_on_registered_pages() actions = _get_actions_on_registered_pages() + if not actions_chains: + return [] + + # TODO: update after optimisation # Fundamental components required for the smooth operation of the loop mechanism. components = [ dcc.Store(id="empty_input_store"), dcc.Store(id="empty_output_store"), dcc.Store(id="action_finished"), dcc.Store(id="set_remaining"), - dcc.Store(id="remaining_actions"), + dcc.Store(id="remaining_actions", data=[]), html.Div(id="cycle_breaker_div", style={"display": "hidden"}), dcc.Store(id="cycle_breaker_empty_output_store"), ] From 780254168f6c3aea38832279013e90174aa9fcad Mon Sep 17 00:00:00 2001 From: petar-qb Date: Mon, 11 Sep 2023 13:19:27 +0200 Subject: [PATCH 02/40] _update_theme callback overwritten in clientside_callback --- vizro-core/src/vizro/models/_dashboard.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/vizro-core/src/vizro/models/_dashboard.py b/vizro-core/src/vizro/models/_dashboard.py index 037928e26..a7baafec0 100644 --- a/vizro-core/src/vizro/models/_dashboard.py +++ b/vizro-core/src/vizro/models/_dashboard.py @@ -6,7 +6,7 @@ import dash import dash_bootstrap_components as dbc import plotly.io as pio -from dash import Input, Output, callback, html +from dash import Input, Output, clientside_callback, html from pydantic import Field, validator from vizro._constants import MODULE_PAGE_404, STATIC_URL_PREFIX @@ -99,12 +99,15 @@ def build(self): @staticmethod def _update_theme(): - @callback( + clientside_callback( + """ + function(on) { + return on ? 'vizro_dark' : 'vizro_light'; + } + """, Output("dashboard_container", "className"), Input("theme_selector", "on"), ) - def callback_update_theme(on: bool): - return update_theme(on) @staticmethod def _create_error_page_404(): From 911f19f037ca92c726fb771b1af64024cf2ca957 Mon Sep 17 00:00:00 2001 From: petar-qb Date: Mon, 11 Sep 2023 16:43:05 +0200 Subject: [PATCH 03/40] =?UTF-8?q?First=20version=20where=20component=20cal?= =?UTF-8?q?lbacks=20are=20overwritten=20in=20clientside=E2=80=93callbacks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../_build_action_loop_callbacks.py | 4 +- .../models/_components/form/range_slider.py | 55 +++++++++++++------ .../vizro/models/_components/form/slider.py | 48 +++++++++++----- vizro-core/src/vizro/models/_dashboard.py | 2 +- 4 files changed, 75 insertions(+), 34 deletions(-) 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 f5cac2e2f..a8ef6ca90 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 @@ -41,7 +41,7 @@ def _build_action_loop_callbacks() -> None: # Callback that enables gateway callback to work in the multiple page app clientside_callback( """ - function(input, data) { + function trigger_to_global_store(input, data) { return data; } """, @@ -113,7 +113,7 @@ def gateway(**inputs: Dict[str, Any]): # Callback that triggers the next iteration clientside_callback( """ - function(data) { + function after_action_cycle_breaker(data) { document.getElementById("cycle_breaker_div").click() return []; } 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 13ecb0687..d644e6da4 100644 --- a/vizro-core/src/vizro/models/_components/form/range_slider.py +++ b/vizro-core/src/vizro/models/_components/form/range_slider.py @@ -1,6 +1,6 @@ from typing import Dict, List, Literal, Optional -from dash import Input, Output, State, callback, callback_context, dcc, html +from dash import Input, Output, State, clientside_callback, callback_context, dcc, html from pydantic import Field, validator from vizro.models import Action, VizroBaseModel @@ -54,32 +54,51 @@ def build(self): Output(self.id, "value"), Output(f"temp-store-range_slider-{self.id}", "data"), ] - input = [ + inputs = [ Input(f"{self.id}_start_value", "value"), Input(f"{self.id}_end_value", "value"), Input(self.id, "value"), State(f"temp-store-range_slider-{self.id}", "data"), + State(f"{self.id}_data", "data"), ] - @callback(output=output, inputs=input) - def update_slider_values(start, end, slider, input_store): - trigger_id = callback_context.triggered_id - if trigger_id == f"{self.id}_start_value" or trigger_id == f"{self.id}_end_value": - start_text_value, end_text_value = start, end - elif trigger_id == self.id: - start_text_value, end_text_value = slider - else: - start_text_value, end_text_value = input_store if input_store is not None else value - - start_value = min(start_text_value, end_text_value) - end_value = max(start_text_value, end_text_value) - start_value = max(self.min, start_value) - end_value = min(self.max, end_value) - slider_value = [start_value, end_value] - return start_value, end_value, slider_value, (start_value, end_value) + clientside_callback( + """ + function update_slider_values(start, end, slider, input_store, self_data) { + var end_text_value, end_value, slider_value, start_text_value, start_value, trigger_id; + + trigger_id = dash_clientside.callback_context.triggered + if (trigger_id.length != 0) { + trigger_id = dash_clientside.callback_context.triggered[0]['prop_id'].split('.')[0]; + } + if (trigger_id === `${self_data["id"]}_start_value` || trigger_id === `${self_data["id"]}_end_value`) { + [start_text_value, end_text_value] = [start, end]; + } 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 : value; + } + + 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]]; + } + """, + output=output, + inputs=inputs, + ) return html.Div( [ + dcc.Store(f"{self.id}_data", storage_type="local", data={ + "id": self.id, + "min": self.min, + "max": self.max, + }), html.P(self.title, id="range_slider_title") if self.title else None, html.Div( [ diff --git a/vizro-core/src/vizro/models/_components/form/slider.py b/vizro-core/src/vizro/models/_components/form/slider.py index c69fd6b27..dc42c5659 100644 --- a/vizro-core/src/vizro/models/_components/form/slider.py +++ b/vizro-core/src/vizro/models/_components/form/slider.py @@ -1,6 +1,6 @@ from typing import Dict, List, Literal, Optional -from dash import Input, Output, State, callback, callback_context, dcc, html +from dash import Input, Output, State, clientside_callback, callback_context, dcc, html from pydantic import Field, validator from vizro.models import Action, VizroBaseModel @@ -44,32 +44,54 @@ def set_default_marks(cls, v, values): @_log_call def build(self): + output = [ Output(f"{self.id}_text_value", "value"), Output(self.id, "value"), Output(f"temp-store-slider-{self.id}", "data"), ] - input = [ + inputs = [ Input(f"{self.id}_text_value", "value"), Input(self.id, "value"), State(f"temp-store-slider-{self.id}", "data"), + State(f"{self.id}_data", "data"), ] - @callback(output=output, inputs=input) - def update_slider_value(start, slider, input_store): - trigger_id = callback_context.triggered_id - if trigger_id == f"{self.id}_text_value": - text_value = start - elif trigger_id == f"{self.id}": - text_value = slider - else: - text_value = input_store or self.value or self.min - text_value = min(max(self.min, text_value), self.max) + clientside_callback( + """ + function update_slider_values(start, slider, input_store, self_data) { + var text_value, trigger_id; + + trigger_id = dash_clientside.callback_context.triggered + if (trigger_id.length != 0) { + trigger_id = dash_clientside.callback_context.triggered[0]['prop_id'].split('.')[0]; + } + if (trigger_id === `${self_data["id"]}_text_value`) { + text_value = start; + } else if (trigger_id === self_data["id"]) { + text_value = slider; + } else { + text_value = input_store !== null ? input_store : self_data["min"]; + } + + text_value = Math.min(Math.max(self_data["min"], text_value), self_data["max"]); - return text_value, text_value, text_value + console.log(text_value); + + return [text_value, text_value, text_value] + } + """, + output=output, + inputs=inputs, + ) return html.Div( [ + dcc.Store(f"{self.id}_data", storage_type="local", data={ + "id": self.id, + "min": self.min, + "max": self.max, + }), html.P(self.title, id="slider_title") if self.title else None, html.Div( [ diff --git a/vizro-core/src/vizro/models/_dashboard.py b/vizro-core/src/vizro/models/_dashboard.py index a7baafec0..ca3f5badb 100644 --- a/vizro-core/src/vizro/models/_dashboard.py +++ b/vizro-core/src/vizro/models/_dashboard.py @@ -101,7 +101,7 @@ def build(self): def _update_theme(): clientside_callback( """ - function(on) { + function update_dashboard_theme(on) { return on ? 'vizro_dark' : 'vizro_light'; } """, From 3aee009ef308244f0b4677c4b8dcde8a5ab6a66c Mon Sep 17 00:00:00 2001 From: petar-qb Date: Wed, 13 Sep 2023 14:15:21 +0200 Subject: [PATCH 04/40] Added commented out gateway clientside_callback --- .../_build_action_loop_callbacks.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) 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 a8ef6ca90..298e0551c 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 @@ -61,6 +61,49 @@ def _build_action_loop_callbacks() -> None: ) ) + # TODO: need to be finished + # clientside_callback( + # """ + # function gateway(remaining_actions, cycle_breaker_div, ...gateway_triggers) { + # var ctx_triggered, triggered_actions_chains_ids, actions_chain_to_trigger, final_action_sequence, next_action, output_list, trigger_next; + # + # ctx_triggered = dash_clientside.callback_context.triggered + # triggered_actions_chains_ids = [] + # + # console.log("ctx_triggered") + # console.log(ctx_triggered) + # + # for (let i = 0; i < ctx_triggered.length; i++) { + # triggered_actions_chains_ids.push(JSON.parse(ctx_triggered[i]['prop_id'].split('.')[0])['trigger_id']); + # } + # + # console.log("triggered_actions_chains_ids"); + # console.log(triggered_actions_chains_ids); + # + # if (trigger_id === 'cycle_breaker_div') { + # remaining_actions = remaining_actions.split(1); + # } + # else { + # + # } + # + # console.log("remaining_actions"); + # console.log(remaining_actions); + # console.log("cycle_breaker_div"); + # console.log(cycle_breaker_div); + # console.log("gateway_triggers"); + # console.log(gateway_triggers); + # console.log(''); + # } + # """, + # # output=gateway_outputs, + # output=[Output("remaining_actions", "data")] + [Output({"type": "action_trigger", "action_name": action.id}, "data") for action in actions], + # # inputs=gateway_inputs, + # inputs=[State("remaining_actions", "data"), Input("cycle_breaker_div", "n_clicks")] + gateway_inputs["gateway_triggers"], + # prevent_initial_call=True, + # ) + + # TODO: Move it to the clientside callback @callback( output=gateway_outputs, inputs=gateway_inputs, From 307d9475ac3799782acf696351d454e9c8ad842a Mon Sep 17 00:00:00 2001 From: petar-qb Date: Thu, 14 Sep 2023 15:04:24 +0200 Subject: [PATCH 05/40] gateway overwritten into javascript code --- .../_build_action_loop_callbacks.py | 223 +++++++++++------- .../_get_action_loop_components.py | 21 ++ 2 files changed, 155 insertions(+), 89 deletions(-) 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 298e0551c..e7e82936b 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 @@ -61,97 +61,142 @@ def _build_action_loop_callbacks() -> None: ) ) - # TODO: need to be finished - # clientside_callback( - # """ - # function gateway(remaining_actions, cycle_breaker_div, ...gateway_triggers) { - # var ctx_triggered, triggered_actions_chains_ids, actions_chain_to_trigger, final_action_sequence, next_action, output_list, trigger_next; - # - # ctx_triggered = dash_clientside.callback_context.triggered - # triggered_actions_chains_ids = [] - # - # console.log("ctx_triggered") - # console.log(ctx_triggered) - # - # for (let i = 0; i < ctx_triggered.length; i++) { - # triggered_actions_chains_ids.push(JSON.parse(ctx_triggered[i]['prop_id'].split('.')[0])['trigger_id']); - # } - # - # console.log("triggered_actions_chains_ids"); - # console.log(triggered_actions_chains_ids); - # - # if (trigger_id === 'cycle_breaker_div') { - # remaining_actions = remaining_actions.split(1); - # } - # else { - # - # } - # - # console.log("remaining_actions"); - # console.log(remaining_actions); - # console.log("cycle_breaker_div"); - # console.log(cycle_breaker_div); - # console.log("gateway_triggers"); - # console.log(gateway_triggers); - # console.log(''); - # } - # """, - # # output=gateway_outputs, - # output=[Output("remaining_actions", "data")] + [Output({"type": "action_trigger", "action_name": action.id}, "data") for action in actions], - # # inputs=gateway_inputs, - # inputs=[State("remaining_actions", "data"), Input("cycle_breaker_div", "n_clicks")] + gateway_inputs["gateway_triggers"], - # prevent_initial_call=True, - # ) - - # TODO: Move it to the clientside callback - @callback( - output=gateway_outputs, - inputs=gateway_inputs, + clientside_callback( + """ + function gateway(remaining_actions, actions_chains_trigger_mapper, action_trigger_actions_id, cycle_breaker_div, ...gateway_triggers) { + var ctx_triggered, triggered_actions_chains_ids, actions_chain_to_trigger, actions_to_trigger, next_action, trigger_next; + + ctx_triggered = dash_clientside.callback_context.triggered + + console.log("ctx_triggered") + console.log(ctx_triggered) + + if (ctx_triggered.length == 1 && ctx_triggered[0]['prop_id'].split('.')[0] === 'cycle_breaker_div') { + if (remaining_actions.length == 0) { + console.log("remaining_actions.length == 0. PreventUpdate") + return dash_clientside.PreventUpdate + } + } + else { + triggered_actions_chains_ids = [] + for (let i = 0; i < ctx_triggered.length; i++) { + triggered_actions_chains_ids.push(JSON.parse(ctx_triggered[i]['prop_id'].split('.')[0])['trigger_id']); + } + + // Trigger only the on_page_load action if exists. + // Otherwise, a single regular (non on_page_load) action is triggered. + function findStringInList(list, string) { + for (let i = 0; i < list.length; i++) { + if (list[i].indexOf(string) !== -1) { + // The on_page_load action found + return list[i]; + } + } + // A single regular (non on_page_load) action is triggered. + return list[0]; + } + + actions_chain_to_trigger = findStringInList(triggered_actions_chains_ids, "on_page_load"); + + remaining_actions = actions_chains_trigger_mapper[actions_chain_to_trigger]; + } + + console.log("remaining_actions"); + console.log(remaining_actions); + + next_action = remaining_actions.shift() + + console.log("action_trigger_actions_id"); + console.log(action_trigger_actions_id); + + trigger_next = [] + for (let i = 0; i < action_trigger_actions_id.length; i++) { + if (next_action === action_trigger_actions_id[i]) { + trigger_next.push(123) + } + else { + trigger_next.push(dash_clientside.no_update) + } + } + + console.log('trigger_next'); + console.log(trigger_next); + + var return_value = [remaining_actions].concat(trigger_next) + console.log('return_value'); + console.log(return_value); + + console.log(); + console.log(); + console.log(); + console.log(); + console.log(); + + return return_value + + } + """, + # output=gateway_outputs, + output=[Output("remaining_actions", "data")] + [Output({"type": "action_trigger", "action_name": action.id}, "data") for action in actions], + # inputs=gateway_inputs, + inputs=[ + State("remaining_actions", "data"), + State("actions_chains_trigger_mapper", "data"), + State("action_trigger_actions_id", "data"), + Input("cycle_breaker_div", "n_clicks")] + gateway_inputs["gateway_triggers"], prevent_initial_call=True, ) - def gateway(**inputs: Dict[str, Any]): - """GATEWAY.""" - remaining_actions = inputs["remaining_actions"] - if ctx.triggered_id == "cycle_breaker_div": - remaining_actions = remaining_actions[1:] - else: - triggered_actions_chains_ids = [ - json.loads(triggered["prop_id"].split(".")[0])["trigger_id"] for triggered in ctx.triggered - ] - - # Trigger only the on_page_load action if exists. - # Otherwise, a single regular (non on_page_load) action is triggered - actions_chain_to_trigger = next( - ( - actions_chain_id - for actions_chain_id in triggered_actions_chains_ids - if ON_PAGE_LOAD_ACTION_PREFIX in actions_chain_id - ), - triggered_actions_chains_ids[0], - ) - logger.debug("=========== ACTION ===============") - logger.debug(f"Triggered component: {triggered_actions_chains_ids[0]}.") - final_action_sequence = [ - {"Action ID": action.id, "Action name": action_functions[action.function._function]} - for action in model_manager[actions_chain_to_trigger].actions # type: ignore[attr-defined] - ] - logger.debug(f"Actions to be executed as part of the triggered ActionsChain: {final_action_sequence}") - remaining_actions = [action_dict["Action ID"] for action_dict in final_action_sequence] - - if not remaining_actions: - raise PreventUpdate - - next_action = remaining_actions[0] - output_list = ctx.outputs_grouping["action_triggers"] - - # Return dash.no_update for all outputs except for the next action - trigger_next = [no_update if output["id"]["action_name"] != next_action else None for output in output_list] - logger.debug(f"Starting execution of Action: {next_action}") - - return { - "action_triggers": trigger_next, - "remaining_actions": remaining_actions - } + + def create_gateway(): + # TODO: Move it to the clientside callback + @callback( + output=gateway_outputs, + inputs=gateway_inputs, + prevent_initial_call=True, + ) + def gateway(**inputs: Dict[str, Any]): + """GATEWAY.""" + remaining_actions = inputs["remaining_actions"] + if ctx.triggered_id == "cycle_breaker_div": + remaining_actions = remaining_actions[1:] + else: + triggered_actions_chains_ids = [ + json.loads(triggered["prop_id"].split(".")[0])["trigger_id"] for triggered in ctx.triggered + ] + + # Trigger only the on_page_load action if exists. + # Otherwise, a single regular (non on_page_load) action is triggered + actions_chain_to_trigger = next( + ( + actions_chain_id + for actions_chain_id in triggered_actions_chains_ids + if ON_PAGE_LOAD_ACTION_PREFIX in actions_chain_id + ), + triggered_actions_chains_ids[0], + ) + logger.debug("=========== ACTION ===============") + logger.debug(f"Triggered component: {triggered_actions_chains_ids[0]}.") + final_action_sequence = [ + {"Action ID": action.id, "Action name": action_functions[action.function._function]} + for action in model_manager[actions_chain_to_trigger].actions # type: ignore[attr-defined] + ] + logger.debug(f"Actions to be executed as part of the triggered ActionsChain: {final_action_sequence}") + remaining_actions = [action_dict["Action ID"] for action_dict in final_action_sequence] + + if not remaining_actions: + raise PreventUpdate + + next_action = remaining_actions[0] + output_list = ctx.outputs_grouping["action_triggers"] + + # Return dash.no_update for all outputs except for the next action + trigger_next = [no_update if output["id"]["action_name"] != next_action else None for output in output_list] + logger.debug(f"Starting execution of Action: {next_action}") + + return { + "action_triggers": trigger_next, + "remaining_actions": remaining_actions + } # Callback that triggers the next iteration clientside_callback( 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 64321b825..93db4f766 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 @@ -48,4 +48,25 @@ def _get_action_loop_components() -> List[Union[dcc.Store, html.Div]]: # Additional component for every Action in the system components.extend([dcc.Store(id={"type": "action_trigger", "action_name": action.id}) for action in actions]) + # Add a mapper that binds actions chain trigger id and actions ids on the client-side. + actions_chains_trigger_mapper = { + actions_chain.id: [action.id for action in actions_chain.actions] + for actions_chain in actions_chains + } + components.append( + dcc.Store( + id="actions_chains_trigger_mapper", + data=actions_chains_trigger_mapper, + ) + ) + + # Add a store with all action_triggers ids + components.append( + dcc.Store( + id="action_trigger_actions_id", + data=[action.id for action in actions], + ) + + ) + return components From d9358a98c1ad48a09e36194b71810ac9a17d5a0a Mon Sep 17 00:00:00 2001 From: petar-qb Date: Fri, 15 Sep 2023 15:02:40 +0200 Subject: [PATCH 06/40] First fully workable version --- vizro-core/examples/default/app.py | 12 +- .../_build_action_loop_callbacks.py | 124 ++++-------------- .../_get_action_loop_components.py | 52 ++++---- 3 files changed, 53 insertions(+), 135 deletions(-) diff --git a/vizro-core/examples/default/app.py b/vizro-core/examples/default/app.py index dcfbd53e3..01c785642 100644 --- a/vizro-core/examples/default/app.py +++ b/vizro-core/examples/default/app.py @@ -534,12 +534,12 @@ def create_home_page(): create_continent_summary(), create_country_analysis(), ], - # navigation=vm.Navigation( - # pages={ - # "Analysis": ["Homepage", "Variable Analysis", "Relationship Analysis", "Country Analysis"], - # "Summary": ["Continent Summary"], - # } - # ), + navigation=vm.Navigation( + pages={ + "Analysis": ["Homepage", "Variable Analysis", "Relationship Analysis", "Country Analysis"], + "Summary": ["Continent Summary"], + } + ), ) if __name__ == "__main__": 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 e7e82936b..d4f729f9b 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 @@ -19,24 +19,13 @@ def _build_action_loop_callbacks() -> None: """Creates all required dash callbacks for the action loop.""" - # TODO - Reduce the number of the callbacks in the action loop mechanism actions_chains = _get_actions_chains_on_registered_pages() actions = _get_actions_on_registered_pages() if not actions_chains: return - gateway_inputs: Dict[str, Any] = { - "gateway_triggers": [], - "cycle_breaker_div": Input("cycle_breaker_div", "n_clicks"), - "remaining_actions": State("remaining_actions", "data"), - - } - gateway_outputs: Dict[str, Any] = { - "action_triggers": [Output({"type": "action_trigger", "action_name": action.id}, "data") for action in actions], - "remaining_actions": Output("remaining_actions", "data"), - } - + gateway_inputs: List[Input] = [] for actions_chain in actions_chains: # Callback that enables gateway callback to work in the multiple page app clientside_callback( @@ -54,7 +43,7 @@ def _build_action_loop_callbacks() -> None: prevent_initial_call=True, ) - gateway_inputs["gateway_triggers"].append( + gateway_inputs.append( Input( component_id={"type": "gateway_input", "trigger_id": actions_chain.id}, component_property="data", @@ -63,20 +52,26 @@ def _build_action_loop_callbacks() -> None: clientside_callback( """ - function gateway(remaining_actions, actions_chains_trigger_mapper, action_trigger_actions_id, cycle_breaker_div, ...gateway_triggers) { - var ctx_triggered, triggered_actions_chains_ids, actions_chain_to_trigger, actions_to_trigger, next_action, trigger_next; + function gateway( + remaining_actions, + trigger_to_actions_chain_mapper, + action_trigger_actions_id, + cycle_breaker_div, + ...gateway_triggers) + { + // Based on the triggered input, determines what is the next action to execute. + var ctx_triggered, triggered_actions_chains_ids, actions_chain_to_trigger, next_action, trigger_next; ctx_triggered = dash_clientside.callback_context.triggered - console.log("ctx_triggered") - console.log(ctx_triggered) - + // If the 'cycle_breaker_div' is triggered that means that at least one action is already executed. if (ctx_triggered.length == 1 && ctx_triggered[0]['prop_id'].split('.')[0] === 'cycle_breaker_div') { + // If there's no more actions to execute, stop the loop perform. if (remaining_actions.length == 0) { - console.log("remaining_actions.length == 0. PreventUpdate") return dash_clientside.PreventUpdate } } + // Actions chain is triggered from the UI, find the list of actions that should be executed. else { triggered_actions_chains_ids = [] for (let i = 0; i < ctx_triggered.length; i++) { @@ -84,7 +79,7 @@ def _build_action_loop_callbacks() -> None: } // Trigger only the on_page_load action if exists. - // Otherwise, a single regular (non on_page_load) action is triggered. + // Otherwise, a single regular (non on_page_load) actions chain is triggered. function findStringInList(list, string) { for (let i = 0; i < list.length; i++) { if (list[i].indexOf(string) !== -1) { @@ -95,109 +90,36 @@ def _build_action_loop_callbacks() -> None: // A single regular (non on_page_load) action is triggered. return list[0]; } - actions_chain_to_trigger = findStringInList(triggered_actions_chains_ids, "on_page_load"); - - remaining_actions = actions_chains_trigger_mapper[actions_chain_to_trigger]; + remaining_actions = trigger_to_actions_chain_mapper[actions_chain_to_trigger]; } - console.log("remaining_actions"); - console.log(remaining_actions); - - next_action = remaining_actions.shift() - - console.log("action_trigger_actions_id"); - console.log(action_trigger_actions_id); + next_action = remaining_actions[0] + // Return dash.no_update for all outputs except for the next action trigger_next = [] for (let i = 0; i < action_trigger_actions_id.length; i++) { if (next_action === action_trigger_actions_id[i]) { - trigger_next.push(123) + trigger_next.push(null) } else { trigger_next.push(dash_clientside.no_update) } } - console.log('trigger_next'); - console.log(trigger_next); - - var return_value = [remaining_actions].concat(trigger_next) - console.log('return_value'); - console.log(return_value); - - console.log(); - console.log(); - console.log(); - console.log(); - console.log(); - - return return_value - + return [remaining_actions.slice(1)].concat(trigger_next) } """, - # output=gateway_outputs, output=[Output("remaining_actions", "data")] + [Output({"type": "action_trigger", "action_name": action.id}, "data") for action in actions], - # inputs=gateway_inputs, inputs=[ State("remaining_actions", "data"), - State("actions_chains_trigger_mapper", "data"), + State("trigger_to_actions_chain_mapper", "data"), State("action_trigger_actions_id", "data"), - Input("cycle_breaker_div", "n_clicks")] + gateway_inputs["gateway_triggers"], + Input("cycle_breaker_div", "n_clicks") + ] + gateway_inputs, prevent_initial_call=True, ) - def create_gateway(): - # TODO: Move it to the clientside callback - @callback( - output=gateway_outputs, - inputs=gateway_inputs, - prevent_initial_call=True, - ) - def gateway(**inputs: Dict[str, Any]): - """GATEWAY.""" - remaining_actions = inputs["remaining_actions"] - if ctx.triggered_id == "cycle_breaker_div": - remaining_actions = remaining_actions[1:] - else: - triggered_actions_chains_ids = [ - json.loads(triggered["prop_id"].split(".")[0])["trigger_id"] for triggered in ctx.triggered - ] - - # Trigger only the on_page_load action if exists. - # Otherwise, a single regular (non on_page_load) action is triggered - actions_chain_to_trigger = next( - ( - actions_chain_id - for actions_chain_id in triggered_actions_chains_ids - if ON_PAGE_LOAD_ACTION_PREFIX in actions_chain_id - ), - triggered_actions_chains_ids[0], - ) - logger.debug("=========== ACTION ===============") - logger.debug(f"Triggered component: {triggered_actions_chains_ids[0]}.") - final_action_sequence = [ - {"Action ID": action.id, "Action name": action_functions[action.function._function]} - for action in model_manager[actions_chain_to_trigger].actions # type: ignore[attr-defined] - ] - logger.debug(f"Actions to be executed as part of the triggered ActionsChain: {final_action_sequence}") - remaining_actions = [action_dict["Action ID"] for action_dict in final_action_sequence] - - if not remaining_actions: - raise PreventUpdate - - next_action = remaining_actions[0] - output_list = ctx.outputs_grouping["action_triggers"] - - # Return dash.no_update for all outputs except for the next action - trigger_next = [no_update if output["id"]["action_name"] != next_action else None for output in output_list] - logger.debug(f"Starting execution of Action: {next_action}") - - return { - "action_triggers": trigger_next, - "remaining_actions": remaining_actions - } - # Callback that triggers the next iteration clientside_callback( """ 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 93db4f766..79bb2444b 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 @@ -9,7 +9,6 @@ ) -# TODO - Return only components for selected dashboard pages (not for all) def _get_action_loop_components() -> List[Union[dcc.Store, html.Div]]: """Gets all required components for the action loop. @@ -22,51 +21,48 @@ def _get_action_loop_components() -> List[Union[dcc.Store, html.Div]]: if not actions_chains: return [] - # TODO: update after optimisation - # Fundamental components required for the smooth operation of the loop mechanism. + # Fundamental components required for the smooth operation of the action loop mechanism. components = [ - dcc.Store(id="empty_input_store"), - dcc.Store(id="empty_output_store"), dcc.Store(id="action_finished"), - dcc.Store(id="set_remaining"), dcc.Store(id="remaining_actions", data=[]), html.Div(id="cycle_breaker_div", style={"display": "hidden"}), dcc.Store(id="cycle_breaker_empty_output_store"), ] - # Additional component for every ActionChain in the system - components.extend( - [ - dcc.Store( - id={"type": "gateway_input", "trigger_id": actions_chain.id}, - data=f"{actions_chain.id}", - ) - for actions_chain in actions_chains - ] - ) + # Additional component for every ActionChain in the system. + # Represents a proxy component between visible UI component and the gateway of the action loop mechanism. + # Required to avoid the "Unknown callback Input" issue for multiple page app examples. + components.extend([ + dcc.Store( + id={"type": "gateway_input", "trigger_id": actions_chain.id}, + data=f"{actions_chain.id}", + ) + for actions_chain in actions_chains + ]) - # Additional component for every Action in the system + # Additional component for every Action in the system. + # This component is injected as the only Input (trigger) inside each Action. + # It enables that the action can be triggered only from the action loop mechanism. components.extend([dcc.Store(id={"type": "action_trigger", "action_name": action.id}) for action in actions]) - # Add a mapper that binds actions chain trigger id and actions ids on the client-side. - actions_chains_trigger_mapper = { - actions_chain.id: [action.id for action in actions_chain.actions] - for actions_chain in actions_chains - } + # Additional store with all action_triggers ids. components.append( dcc.Store( - id="actions_chains_trigger_mapper", - data=actions_chains_trigger_mapper, + id="action_trigger_actions_id", + data=[action.id for action in actions], ) + ) - # Add a store with all action_triggers ids + # Additional store that maps the actions chain trigger id and the list of action ids that should be executed. components.append( dcc.Store( - id="action_trigger_actions_id", - data=[action.id for action in actions], + id="trigger_to_actions_chain_mapper", + data={ + actions_chain.id: [action.id for action in actions_chain.actions] + for actions_chain in actions_chains + } ) - ) return components From 68b1c2124f3a696a7146965f945f5918d1cd46ed Mon Sep 17 00:00:00 2001 From: petar-qb Date: Fri, 15 Sep 2023 15:14:59 +0200 Subject: [PATCH 07/40] Linting --- .../_build_action_loop_callbacks.py | 31 +++++++++---------- .../_get_action_loop_components.py | 22 ++++++------- .../models/_components/form/range_slider.py | 24 ++++++++------ .../vizro/models/_components/form/slider.py | 14 +++++++-- 4 files changed, 50 insertions(+), 41 deletions(-) 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 d4f729f9b..fd9abd9e7 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 @@ -1,18 +1,13 @@ """Contains utilities to create required dash callbacks for the action loop.""" -import json import logging -from typing import Any, Dict, List, Optional +from typing import List -from dash import Input, Output, State, callback, clientside_callback, ctx, dcc, no_update -from dash.exceptions import PreventUpdate +from dash import Input, Output, State, clientside_callback -from vizro._constants import ON_PAGE_LOAD_ACTION_PREFIX -from vizro.actions import action_functions from vizro.actions._action_loop._action_loop_utils import ( _get_actions_chains_on_registered_pages, _get_actions_on_registered_pages, ) -from vizro.managers import model_manager logger = logging.getLogger(__name__) @@ -53,20 +48,20 @@ def _build_action_loop_callbacks() -> None: clientside_callback( """ function gateway( - remaining_actions, - trigger_to_actions_chain_mapper, - action_trigger_actions_id, + remaining_actions, + trigger_to_actions_chain_mapper, + action_trigger_actions_id, cycle_breaker_div, - ...gateway_triggers) + ...gateway_triggers) { // Based on the triggered input, determines what is the next action to execute. var ctx_triggered, triggered_actions_chains_ids, actions_chain_to_trigger, next_action, trigger_next; ctx_triggered = dash_clientside.callback_context.triggered - // If the 'cycle_breaker_div' is triggered that means that at least one action is already executed. + // If the 'cycle_breaker_div' is triggered that means that at least one action is already executed. if (ctx_triggered.length == 1 && ctx_triggered[0]['prop_id'].split('.')[0] === 'cycle_breaker_div') { - // If there's no more actions to execute, stop the loop perform. + // If there's no more actions to execute, stop the loop perform. if (remaining_actions.length == 0) { return dash_clientside.PreventUpdate } @@ -77,7 +72,7 @@ def _build_action_loop_callbacks() -> None: for (let i = 0; i < ctx_triggered.length; i++) { triggered_actions_chains_ids.push(JSON.parse(ctx_triggered[i]['prop_id'].split('.')[0])['trigger_id']); } - + // Trigger only the on_page_load action if exists. // Otherwise, a single regular (non on_page_load) actions chain is triggered. function findStringInList(list, string) { @@ -110,13 +105,15 @@ def _build_action_loop_callbacks() -> None: return [remaining_actions.slice(1)].concat(trigger_next) } """, - output=[Output("remaining_actions", "data")] + [Output({"type": "action_trigger", "action_name": action.id}, "data") for action in actions], + output=[Output("remaining_actions", "data")] + + [Output({"type": "action_trigger", "action_name": action.id}, "data") for action in actions], inputs=[ State("remaining_actions", "data"), State("trigger_to_actions_chain_mapper", "data"), State("action_trigger_actions_id", "data"), - Input("cycle_breaker_div", "n_clicks") - ] + gateway_inputs, + Input("cycle_breaker_div", "n_clicks"), + *gateway_inputs, + ], prevent_initial_call=True, ) 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 79bb2444b..40c07d8fd 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 @@ -32,13 +32,15 @@ def _get_action_loop_components() -> List[Union[dcc.Store, html.Div]]: # Additional component for every ActionChain in the system. # Represents a proxy component between visible UI component and the gateway of the action loop mechanism. # Required to avoid the "Unknown callback Input" issue for multiple page app examples. - components.extend([ - dcc.Store( - id={"type": "gateway_input", "trigger_id": actions_chain.id}, - data=f"{actions_chain.id}", - ) - for actions_chain in actions_chains - ]) + components.extend( + [ + dcc.Store( + id={"type": "gateway_input", "trigger_id": actions_chain.id}, + data=f"{actions_chain.id}", + ) + for actions_chain in actions_chains + ] + ) # Additional component for every Action in the system. # This component is injected as the only Input (trigger) inside each Action. @@ -51,7 +53,6 @@ def _get_action_loop_components() -> List[Union[dcc.Store, html.Div]]: id="action_trigger_actions_id", data=[action.id for action in actions], ) - ) # Additional store that maps the actions chain trigger id and the list of action ids that should be executed. @@ -59,9 +60,8 @@ def _get_action_loop_components() -> List[Union[dcc.Store, html.Div]]: dcc.Store( id="trigger_to_actions_chain_mapper", data={ - actions_chain.id: [action.id for action in actions_chain.actions] - for actions_chain in actions_chains - } + actions_chain.id: [action.id for action in actions_chain.actions] for actions_chain in actions_chains + }, ) ) 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 decf4af4c..6ef7c5ec2 100644 --- a/vizro-core/src/vizro/models/_components/form/range_slider.py +++ b/vizro-core/src/vizro/models/_components/form/range_slider.py @@ -1,6 +1,6 @@ from typing import Dict, List, Literal, Optional -from dash import Input, Output, State, clientside_callback, callback_context, dcc, html +from dash import Input, Output, State, clientside_callback, dcc, html from pydantic import Field, validator from vizro.models import Action, VizroBaseModel @@ -59,14 +59,14 @@ def build(self): Input(f"{self.id}_end_value", "value"), Input(self.id, "value"), State(f"temp-store-range_slider-{self.id}", "data"), - State(f"{self.id}_data", "data"), + State(f"{self.id}_callback_data", "data"), ] clientside_callback( """ function update_slider_values(start, end, slider, input_store, self_data) { var end_text_value, end_value, slider_value, start_text_value, start_value, trigger_id; - + trigger_id = dash_clientside.callback_context.triggered if (trigger_id.length != 0) { trigger_id = dash_clientside.callback_context.triggered[0]['prop_id'].split('.')[0]; @@ -78,13 +78,13 @@ def build(self): } else { [start_text_value, end_text_value] = input_store !== null ? input_store : value; } - + 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]]; } """, @@ -94,11 +94,15 @@ def build(self): return html.Div( [ - dcc.Store(f"{self.id}_data", storage_type="local", data={ - "id": self.id, - "min": self.min, - "max": self.max, - }), + dcc.Store( + f"{self.id}_callback_data", + storage_type="local", + data={ + "id": self.id, + "min": self.min, + "max": self.max, + }, + ), html.P(self.title) if self.title else None, html.Div( [ diff --git a/vizro-core/src/vizro/models/_components/form/slider.py b/vizro-core/src/vizro/models/_components/form/slider.py index cf425144b..92e825d44 100644 --- a/vizro-core/src/vizro/models/_components/form/slider.py +++ b/vizro-core/src/vizro/models/_components/form/slider.py @@ -1,6 +1,6 @@ from typing import Dict, List, Literal, Optional -from dash import Input, Output, State, clientside_callback, callback_context, dcc, html +from dash import Input, Output, State, clientside_callback, dcc, html from pydantic import Field, validator from vizro.models import Action, VizroBaseModel @@ -50,7 +50,6 @@ def set_default_marks(cls, v, values): @_log_call def build(self): - output = [ Output(f"{self.id}_text_value", "value"), Output(self.id, "value"), @@ -60,7 +59,7 @@ def build(self): Input(f"{self.id}_text_value", "value"), Input(self.id, "value"), State(f"{self.id}_temp_store", "data"), - State(f"{self.id}_data", "data"), + State(f"{self.id}_callback_data", "data"), ] clientside_callback( @@ -93,6 +92,15 @@ def build(self): return html.Div( [ + dcc.Store( + f"{self.id}_callback_data", + storage_type="local", + data={ + "id": self.id, + "min": self.min, + "max": self.max, + }, + ), html.P(self.title) if self.title else None, html.Div( [ From bebfa52dd9bb28fc61c8a651d43e59d9ffb62011 Mon Sep 17 00:00:00 2001 From: petar-qb Date: Mon, 18 Sep 2023 10:59:13 +0200 Subject: [PATCH 08/40] Remove callback test for slider --- .../models/_components/form/test_slider.py | 46 ++++--------------- 1 file changed, 9 insertions(+), 37 deletions(-) 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 8b3bd58fb..684676611 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 @@ -13,6 +13,15 @@ def expected_slider(): return html.Div( [ + dcc.Store( + f"slider_id_callback_data", + storage_type="local", + data={ + "id": "slider_id", + "min": 0, + "max": 10, + }, + ), html.P("Test title"), html.Div( [ @@ -187,40 +196,3 @@ def test_slider_build(self, expected_slider): expected = json.loads(json.dumps(expected_slider, cls=plotly.utils.PlotlyJSONEncoder)) assert result == expected - - -class TestCallbackMethod: - @pytest.mark.parametrize( - "trigger, start_value, slider_value, input_store_value, expected", - [ - ("_text_value", 3, 1, 1, (3, 3, 3)), # set new value by start - ("", 1, 4, 1, (4, 4, 4)), # set new value by slider - ("_input_store", 1, 1, 5, (5, 5, 5)), # set new value by input store - ("_text_value", 0, 1, 1, (0, 0, 0)), # set to minimum value - ("_text_value", 12, 1, 1, (10, 10, 10)), # set outside of possible range - ("_text_value", -1, 1, 1, (0, 0, 0)), # set outside of possible range - ("_text_value", 1, 8, 1, (1, 1, 1)), # triggerdID value is only used - ], - ) - def test_update_slider_value_triggered( # noqa - self, trigger, start_value, slider_value, input_store_value, expected - ): - slider = vm.Slider(min=0, max=10, value=1) - result = slider._update_slider_value(f"{slider.id}{trigger}", start_value, slider_value, input_store_value) - - assert result == expected - - @pytest.mark.parametrize( - "trigger, start_value, slider_value, input_store_value", - [ - ("_text_value", None, 1, 1), # set new value by start - ], - ) - def test_update_slider_invalid(self, trigger, start_value, slider_value, input_store_value): - slider = vm.Slider(min=0, max=10, value=1) - - with pytest.raises( - TypeError, - match="'>' not supported between instances of 'NoneType' and 'float'", - ): - slider._update_slider_value(f"{slider.id}{trigger}", start_value, slider_value, input_store_value) From c84db574d0a8cf4c08b6869cabd46715076d0f52 Mon Sep 17 00:00:00 2001 From: petar-qb Date: Mon, 18 Sep 2023 11:09:18 +0200 Subject: [PATCH 09/40] Minor lint change --- .../tests/unit/vizro/models/_components/form/test_slider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 684676611..724c1d254 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 @@ -14,7 +14,7 @@ def expected_slider(): return html.Div( [ dcc.Store( - f"slider_id_callback_data", + "slider_id_callback_data", storage_type="local", data={ "id": "slider_id", From f7e8805ce5f9e00ec65dc4781a8ce095680a26e1 Mon Sep 17 00:00:00 2001 From: petar-qb Date: Mon, 18 Sep 2023 11:12:38 +0200 Subject: [PATCH 10/40] Adding the changelog file --- ...tar_pejovic_optimise_app_rendering_time.md | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 vizro-core/changelog.d/20230918_111226_petar_pejovic_optimise_app_rendering_time.md diff --git a/vizro-core/changelog.d/20230918_111226_petar_pejovic_optimise_app_rendering_time.md b/vizro-core/changelog.d/20230918_111226_petar_pejovic_optimise_app_rendering_time.md new file mode 100644 index 000000000..d57e34cc2 --- /dev/null +++ b/vizro-core/changelog.d/20230918_111226_petar_pejovic_optimise_app_rendering_time.md @@ -0,0 +1,42 @@ + + + + + + + + From b4f1bc2d69b6b55cf8e389c46ca22e42dd5f8f48 Mon Sep 17 00:00:00 2001 From: petar-qb Date: Mon, 18 Sep 2023 13:12:31 +0200 Subject: [PATCH 11/40] Removing changes made in the example --- vizro-core/examples/default/app.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/vizro-core/examples/default/app.py b/vizro-core/examples/default/app.py index 01c785642..f866fab4c 100644 --- a/vizro-core/examples/default/app.py +++ b/vizro-core/examples/default/app.py @@ -454,16 +454,6 @@ def create_country_analysis(): targets=["bar_country"], ) ), - vm.Action( - function=export_data( - targets=["bar_country"], - ) - ), - vm.Action( - function=export_data( - targets=["bar_country"], - ) - ), ], ), ], From 48a114dd2e987c9d1b7e3e25f02b82b3b9c29c7f Mon Sep 17 00:00:00 2001 From: petar-qb Date: Mon, 18 Sep 2023 13:16:49 +0200 Subject: [PATCH 12/40] Remove a console.log --- vizro-core/src/vizro/models/_components/form/slider.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/vizro-core/src/vizro/models/_components/form/slider.py b/vizro-core/src/vizro/models/_components/form/slider.py index 92e825d44..f86fd2109 100644 --- a/vizro-core/src/vizro/models/_components/form/slider.py +++ b/vizro-core/src/vizro/models/_components/form/slider.py @@ -81,8 +81,6 @@ def build(self): text_value = Math.min(Math.max(self_data["min"], text_value), self_data["max"]); - console.log(text_value); - return [text_value, text_value, text_value] } """, From 58aa705d18c0a2ed8179ae970d1c507641d57fb1 Mon Sep 17 00:00:00 2001 From: petar-qb Date: Mon, 18 Sep 2023 13:40:15 +0200 Subject: [PATCH 13/40] Improved logs --- vizro-core/src/vizro/models/_action/_action.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/vizro-core/src/vizro/models/_action/_action.py b/vizro-core/src/vizro/models/_action/_action.py index 0bedbb8c6..cd2982f26 100644 --- a/vizro-core/src/vizro/models/_action/_action.py +++ b/vizro-core/src/vizro/models/_action/_action.py @@ -74,7 +74,10 @@ def build(self): @callback(output=callback_outputs, inputs=callback_inputs, prevent_initial_call=True) def callback_wrapper(trigger: None, **inputs: Dict[str, Any]): - logger.debug(f"Inputs to Action: {inputs}") + logger.debug("=============== ACTION ===============") + logger.debug(f'Action ID: "{self.id}"') + logger.debug(f'Action name: "{action_functions[self.function._function]}"') + logger.debug(f"Action inputs: {inputs}") return_value = self.function(**inputs) or {} if isinstance(return_value, dict): return {"action_finished": None, **return_value} From 4761753c94e5d674e684acb808594e3ed729dfd7 Mon Sep 17 00:00:00 2001 From: petar-qb Date: Mon, 18 Sep 2023 14:07:31 +0200 Subject: [PATCH 14/40] Fixing undefined value RangeSlider callback bug --- vizro-core/src/vizro/models/_components/form/range_slider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 6ef7c5ec2..4e727aa83 100644 --- a/vizro-core/src/vizro/models/_components/form/range_slider.py +++ b/vizro-core/src/vizro/models/_components/form/range_slider.py @@ -76,7 +76,7 @@ def build(self): } 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 : value; + [start_text_value, end_text_value] = input_store !== null ? input_store : [slider[0], slider[1]]; } start_value = Math.min(start_text_value, end_text_value); From ef4129d24ce3cf7d21bc98a2300ab95dbdfa55eb Mon Sep 17 00:00:00 2001 From: petar-qb Date: Mon, 18 Sep 2023 14:41:53 +0200 Subject: [PATCH 15/40] Naive way of handling when a new actions chain is started before the previous one has finished. --- .../actions/_action_loop/_build_action_loop_callbacks.py | 4 ++++ 1 file changed, 4 insertions(+) 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 fd9abd9e7..ff59120a7 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 @@ -70,6 +70,10 @@ def _build_action_loop_callbacks() -> None: else { triggered_actions_chains_ids = [] for (let i = 0; i < ctx_triggered.length; i++) { + if (ctx_triggered[i]['prop_id'].split('.')[0] === 'cycle_breaker_div') { + // TODO: handle when a new action chain is started before the previous one has finished. + continue; + } triggered_actions_chains_ids.push(JSON.parse(ctx_triggered[i]['prop_id'].split('.')[0])['trigger_id']); } From e502cc957567545c8f88ed65134d9d9ae25cedee Mon Sep 17 00:00:00 2001 From: petar-qb Date: Tue, 19 Sep 2023 08:11:48 +0200 Subject: [PATCH 16/40] Removing local session type from the stores where it's unnecessary --- ...0230918_111226_petar_pejovic_optimise_app_rendering_time.md | 3 ++- vizro-core/src/vizro/models/_components/form/range_slider.py | 1 - vizro-core/src/vizro/models/_components/form/slider.py | 1 - .../tests/unit/vizro/models/_components/form/test_slider.py | 1 - 4 files changed, 2 insertions(+), 4 deletions(-) diff --git a/vizro-core/changelog.d/20230918_111226_petar_pejovic_optimise_app_rendering_time.md b/vizro-core/changelog.d/20230918_111226_petar_pejovic_optimise_app_rendering_time.md index d57e34cc2..d3f5da6d8 100644 --- a/vizro-core/changelog.d/20230918_111226_petar_pejovic_optimise_app_rendering_time.md +++ b/vizro-core/changelog.d/20230918_111226_petar_pejovic_optimise_app_rendering_time.md @@ -17,9 +17,10 @@ Uncomment the section that is right (remove the HTML comment wrapper). --> - From 1b0b0cf48cd81b0c6cad910d441fa7cbde59c95c Mon Sep 17 00:00:00 2001 From: petar-qb Date: Mon, 25 Sep 2023 14:51:56 +0200 Subject: [PATCH 40/40] Linting --- vizro-core/docs/pages/development/contributing.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/vizro-core/docs/pages/development/contributing.md b/vizro-core/docs/pages/development/contributing.md index 3ba678e1a..38470b652 100644 --- a/vizro-core/docs/pages/development/contributing.md +++ b/vizro-core/docs/pages/development/contributing.md @@ -87,8 +87,7 @@ Arguments are passed through to the underlying `npx jest` command, e.g. hatch run test-js --help ``` -executes `npx jest --help` and shows all jest optional arguments you can also propagate through `hatch run test-js`. - +executes `npx jest --help` and shows all jest optional arguments you can also propagate through `hatch run test-js`.