Skip to content

Commit

Permalink
Init tabs/containers backend support
Browse files Browse the repository at this point in the history
  • Loading branch information
petar-qb committed Dec 20, 2023
1 parent cce47b1 commit 5c7404c
Show file tree
Hide file tree
Showing 9 changed files with 610 additions and 611 deletions.
960 changes: 492 additions & 468 deletions vizro-core/examples/default/app.py

Large diffs are not rendered by default.

22 changes: 1 addition & 21 deletions vizro-core/src/vizro/actions/_action_loop/_action_loop_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,17 @@

from __future__ import annotations

from itertools import chain
from typing import TYPE_CHECKING, List

from dash import page_registry

from vizro.managers import model_manager
from vizro.models import VizroBaseModel

if TYPE_CHECKING:
from vizro.models import Action, Page
from vizro.models._action._actions_chain import ActionsChain


def _get_actions(model: VizroBaseModel) -> List[ActionsChain]:
"""Gets the list of the ActionsChain models for any VizroBaseModel model."""
if hasattr(model, "selector"):
return model.selector.actions
elif hasattr(model, "actions"):
return model.actions
return []


def _get_all_actions_chains_on_page(page: Page) -> List[ActionsChain]:
"""Gets the list of the ActionsChain models for the Page model."""
return [
actions_chain
for page_item in chain([page], page.components, page.controls)
for actions_chain in _get_actions(model=page_item)
]


def _get_actions_chains_on_registered_pages() -> List[ActionsChain]:
"""Gets list of ActionsChain models for registered pages."""
actions_chains: List[ActionsChain] = []
Expand All @@ -41,7 +21,7 @@ def _get_actions_chains_on_registered_pages() -> List[ActionsChain]:
page: Page = model_manager[registered_page["module"]]
except KeyError:
continue
actions_chains.extend(_get_all_actions_chains_on_page(page=page))
actions_chains.extend(page._get_page_actions_chains())
return actions_chains


Expand Down
Original file line number Diff line number Diff line change
@@ -1,91 +1,30 @@
"""Contains utilities to create the action_callback_mapping."""

from itertools import chain
from typing import Any, Callable, Dict, List, NamedTuple
from typing import Any, Callable, Dict, List, Union

from dash import Output, State, dcc

from vizro.actions import _on_page_load, _parameter, export_data, filter_interaction
from vizro.managers import data_manager, model_manager
from vizro.actions import _parameter, export_data, filter_interaction
from vizro.managers import model_manager
from vizro.managers._model_manager import ModelID
from vizro.models import Action, Page, Table, VizroBaseModel
from vizro.models._action._actions_chain import ActionsChain
from vizro.models import Action, Page, Table
from vizro.models._controls import Filter, Parameter
from vizro.models.types import ControlType


class ModelActionsChains(NamedTuple):
model: VizroBaseModel
actions_chains: List[ActionsChain]


def _get_actions(model) -> List[ActionsChain]:
"""Gets the list of trigger action chains in the `action` parameter for any model."""
if hasattr(model, "selector"):
return model.selector.actions
elif hasattr(model, "actions"):
return model.actions
return []


def _get_all_actions_chains_on_page(page: Page) -> chain: # type: ignore[type-arg]
"""Creates an itertools chain of all ActionsChains present on selected Page."""
return chain(*(_get_actions(page_item) for page_item in chain([page], page.components, page.controls)))


def _get_model_actions_chains_mapping(page: Page) -> Dict[str, ModelActionsChains]:
"""Creates a mapping of model ids and ModelActionsChains for selected Page."""
model_actions_chains_mapping = {}
for page_item in chain([page], page.components, page.controls):
model_actions_chains_mapping[page_item.id] = ModelActionsChains(
model=page_item, actions_chains=_get_actions(page_item)
)
return model_actions_chains_mapping


def _get_triggered_page(action_id: ModelID) -> Page: # type: ignore[return]
"""Gets the page where the provided `action_id` has been triggered."""
for _, page in model_manager._items_with_type(Page):
if any(
action.id == action_id
for actions_chain in _get_all_actions_chains_on_page(page)
for action in actions_chain.actions
):
return page


def _get_triggered_model(action_id: ModelID) -> VizroBaseModel: # type: ignore[return]
"""Gets the model where the provided `action_id` has been triggered."""
for _, page in model_manager._items_with_type(Page):
for model_id, model_actions_chains in _get_model_actions_chains_mapping(page).items():
if any(
action.id == action_id
for actions_chain in model_actions_chains.actions_chains
for action in actions_chain.actions
):
return model_actions_chains.model


def _get_components_with_data(action_id: ModelID) -> List[str]:
"""Gets all components that have a registered dataframe on the page where `action_id` was triggered."""
page = _get_triggered_page(action_id=action_id)
return [component.id for component in page.components if data_manager._has_registered_data(component.id)]


def _get_matching_actions_by_function(page: Page, action_function: Callable[[Any], Dict[str, Any]]) -> List[Action]:
"""Gets list of Actions on triggered page that match the provided action function."""
return [
action
for actions_chain in _get_all_actions_chains_on_page(page)
for actions_chain in page._get_page_actions_chains()
for action in actions_chain.actions
if action.function._function == action_function
]


# CALLBACK STATES --------------
def _get_inputs_of_controls(action_id: ModelID, control_type: ControlType) -> List[State]:
def _get_inputs_of_controls(page: Page, control_type: ControlType) -> List[State]:
"""Gets list of States for selected control_type of triggered page."""
page = _get_triggered_page(action_id=action_id)
return [
State(
component_id=control.selector.id,
Expand All @@ -97,17 +36,16 @@ def _get_inputs_of_controls(action_id: ModelID, control_type: ControlType) -> Li


def _get_inputs_of_figure_interactions(
action_id: ModelID, action_function: Callable[[Any], Dict[str, Any]]
page: Page, action_function: Callable[[Any], Dict[str, Any]]
) -> List[Dict[str, State]]:
"""Gets list of States for selected chart interaction `action_name` of triggered page."""
figure_interactions_on_page = _get_matching_actions_by_function(
page=_get_triggered_page(action_id=action_id),
page=page,
action_function=action_function,
)
inputs = []
for action in figure_interactions_on_page:
# TODO: Consider do we want to move the following logic into Model implementation
triggered_model = _get_triggered_model(action_id=ModelID(str(action.id)))
triggered_model = model_manager._get_action_trigger(action_id=ModelID(str(action.id)))
if isinstance(triggered_model, Table):
inputs.append(
{
Expand All @@ -130,27 +68,25 @@ def _get_inputs_of_figure_interactions(
return inputs


def _get_action_callback_inputs(action_id: ModelID) -> Dict[str, Any]:
# TODO: Refactor this and util functions once we implement "_get_input_property" method in VizroBaseModel models
def _get_action_callback_inputs(action_id: ModelID) -> Dict[str, List[Union[State, Dict[str, State]]]]:
"""Creates mapping of pre-defined action names and a list of States."""
action_function = model_manager[action_id].function._function
page: Page = model_manager._get_model_page(model_id=action_id)

if action_function == export_data.__wrapped__:
include_inputs = ["filters", "filter_interaction"]
else:
include_inputs = ["filters", "parameters", "filter_interaction", "theme_selector"]

action_input_mapping = {
"filters": (
_get_inputs_of_controls(action_id=action_id, control_type=Filter) if "filters" in include_inputs else []
),
"filters": (_get_inputs_of_controls(page=page, control_type=Filter) if "filters" in include_inputs else []),
"parameters": (
_get_inputs_of_controls(action_id=action_id, control_type=Parameter)
if "parameters" in include_inputs
else []
_get_inputs_of_controls(page=page, control_type=Parameter) if "parameters" in include_inputs else []
),
# TODO: Probably need to adjust other inputs to follow the same structure List[Dict[str, State]]
"filter_interaction": (
_get_inputs_of_figure_interactions(action_id=action_id, action_function=filter_interaction.__wrapped__)
_get_inputs_of_figure_interactions(page=page, action_function=filter_interaction.__wrapped__)
if "filter_interaction" in include_inputs
else []
),
Expand All @@ -177,9 +113,6 @@ def _get_action_callback_outputs(action_id: ModelID) -> Dict[str, Output]:
if action_function == _parameter.__wrapped__:
targets = [target.split(".")[0] for target in targets]

if action_function == _on_page_load.__wrapped__:
targets = _get_components_with_data(action_id=action_id)

return {
target: Output(
component_id=target,
Expand All @@ -200,7 +133,7 @@ def _get_export_data_callback_outputs(action_id: ModelID) -> Dict[str, List[Stat
targets = None

if not targets:
targets = _get_components_with_data(action_id=action_id)
targets = model_manager._get_model_page(model_id=action_id)._get_page_model_ids_with_figure()

return {
f"download_dataframe_{target}": Output(
Expand All @@ -226,7 +159,7 @@ def _get_export_data_callback_components(action_id: ModelID) -> List[dcc.Downloa
targets = None

if not targets:
targets = _get_components_with_data(action_id=action_id)
targets = model_manager._get_model_page(model_id=action_id)._get_page_model_ids_with_figure()

return [
dcc.Download(
Expand Down
13 changes: 3 additions & 10 deletions vizro-core/src/vizro/actions/_on_page_load_action.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,28 @@
"""Pre-defined action function "_on_page_load" to be reused in `action` parameter of VizroBaseModels."""

from typing import Any, Dict
from typing import Any, Dict, List

from dash import ctx

from vizro.actions._actions_utils import (
_get_modified_page_figures,
)
from vizro.managers import data_manager, model_manager
from vizro.managers._model_manager import ModelID
from vizro.models.types import capture


@capture("action")
def _on_page_load(page_id: ModelID, **inputs: Dict[str, Any]) -> Dict[str, Any]:
def _on_page_load(targets: List[ModelID], **inputs: Dict[str, Any]) -> Dict[str, Any]:
"""Applies controls to charts on page once the page is opened (or refreshed).
Args:
page_id: Page ID of relevant page
targets: List of target component ids to apply on page load mechanism to
inputs: Dict mapping action function names with their inputs e.g.
inputs = {'filters': [], 'parameters': ['gdpPercap'], 'filter_interaction': [], 'theme_selector': True}
Returns:
Dict mapping target chart ids to modified figures e.g. {'my_scatter': Figure({})}
"""
targets = [
component.id
for component in model_manager[page_id].components
if data_manager._has_registered_data(component.id)
]

return _get_modified_page_figures(
targets=targets,
ctds_filter=ctx.args_grouping["external"]["filters"],
Expand Down
7 changes: 0 additions & 7 deletions vizro-core/src/vizro/managers/_data_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,6 @@ def _get_component_data(self, component_id: ComponentID) -> pd.DataFrame:
# to not do any inplace=True operations, but probably safest to leave it here.
return self.__original_data[dataset_name].copy()

def _has_registered_data(self, component_id: ComponentID) -> bool:
try:
self._get_component_data(component_id)
return True
except KeyError:
return False

def _clear(self):
self.__init__() # type: ignore[misc]

Expand Down
57 changes: 55 additions & 2 deletions vizro-core/src/vizro/managers/_model_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@

import random
import uuid
from typing import TYPE_CHECKING, Dict, Generator, NewType, Tuple, Type, TypeVar, cast
from typing import TYPE_CHECKING, Dict, Generator, List, NewType, Tuple, Type, TypeVar, cast

from vizro.managers._managers_utils import _state_modifier

if TYPE_CHECKING:
from vizro.models import VizroBaseModel
from vizro.models import Page, VizroBaseModel

rd = random.Random(0)

Expand All @@ -25,6 +25,8 @@ def __init__(self):
self.__models: Dict[ModelID, VizroBaseModel] = {}
self._frozen_state = False

# TODO: Consider do we need to save "page_id=None, parent_model_id=None" eagerly to the model itself
# and make all searching helper methods much easier?
@_state_modifier
def __setitem__(self, model_id: ModelID, model: Model):
if model_id in self.__models:
Expand All @@ -46,12 +48,63 @@ def __iter__(self) -> Generator[ModelID, None, None]:
"""
yield from self.__models

# TODO: Consider do we need to add additional argument "model_page_id=None"
def _items_with_type(self, model_type: Type[Model]) -> Generator[Tuple[ModelID, Model], None, None]:
"""Iterates through all models of type `model_type` (including subclasses)."""
for model_id in self:
if isinstance(self[model_id], model_type):
yield model_id, cast(Model, self[model_id])

# TODO: Consider moving this one to the _callback_mapping_utils.py since it's only used there
def _get_action_trigger(self, action_id: ModelID) -> VizroBaseModel: # type: ignore[return]
from vizro.models._action._actions_chain import ActionsChain

for _, actions_chain in model_manager._items_with_type(ActionsChain):
if action_id in [action.id for action in actions_chain.actions]:
return self[ModelID(str(actions_chain.trigger.component_id))]

# TODO: consider returning with yield
def _get_model_children(self, model_id: ModelID) -> List[ModelID]:
"""Gets all components and tabs recursively of the model with the `model_id`."""
model_children = []

def __get_model_children_helper(model: VizroBaseModel) -> None:
model_children.append(ModelID(str(model.id)))
if hasattr(model, "components"):
for sub_model in model.components:
__get_model_children_helper(model=sub_model)
if hasattr(model, "tabs"):
for sub_model in model.tabs:
__get_model_children_helper(model=sub_model)

__get_model_children_helper(model=self.__models[model_id])
return model_children

# TODO: Consider moving this one into Dashboard or some util file
def _get_model_page(self, model_id: ModelID) -> Page: # type: ignore[return]
"""Gets the page id of the page that contains the model with the `model_id`."""
from vizro.models import Page

for page_id, _ in model_manager._items_with_type(Page):
page_model_ids = [page_id, self._get_model_children(model_id=page_id)]
page: Page = cast(Page, self.__models[page_id])

if hasattr(page, "actions"):
for actions_chain in page._get_page_actions_chains():
page_model_ids.append(actions_chain.id)
for action in actions_chain.actions:
page_model_ids.append(action.id)

for control in page.controls:
page_model_ids.append(control.id)
if hasattr(control, "selector") and control.selector:
page_model_ids.append(control.selector.id)

# TODO: Add navigation, accordions and other page objects

if model_id in page_model_ids:
return page

@staticmethod
def _generate_id() -> ModelID:
return ModelID(str(uuid.UUID(int=rd.getrandbits(128))))
Expand Down
Loading

0 comments on commit 5c7404c

Please sign in to comment.