Skip to content

Commit

Permalink
Merge branch 'fix/decimator-zoom-vertical-horizontal' of https://gith…
Browse files Browse the repository at this point in the history
…ub.com/Avaiga/taipy into fix/decimator-zoom-vertical-horizontal
  • Loading branch information
dinhlongviolin1 committed Mar 6, 2024
2 parents 97e99a0 + f291275 commit 1566abf
Show file tree
Hide file tree
Showing 20 changed files with 262 additions and 176 deletions.
2 changes: 1 addition & 1 deletion doc/gui/examples/charts/treemap-simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
for i in range(2, n_numbers):
fibonacci.append(fibonacci[i - 1] + fibonacci[i - 2])

data = {"index": [i for i in range(1, n_numbers + 1)], "fibonacci": fibonacci}
data = {"index": list(range(1, n_numbers + 1)), "fibonacci": fibonacci}

page = """
# TreeMap - Simple
Expand Down
303 changes: 168 additions & 135 deletions frontend/taipy/package-lock.json

Large diffs are not rendered by default.

11 changes: 8 additions & 3 deletions frontend/taipy/src/ScenarioDag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { DisplayModel, TaskStatuses } from "./utils/types";
import { addStatusToDisplayModel, createDagreEngine, initDiagram, populateModel, relayoutDiagram } from "./utils/diagram";
import {
createRequestUpdateAction,
createSendActionNameAction,
createSendUpdateAction,
getUpdateVar,
useDispatch,
Expand All @@ -38,6 +39,8 @@ interface ScenarioDagProps {
libClassName?: string;
className?: string;
dynamicClassName?: string;
onAction?: string;
onSelect?: string;
}

const titleSx = { ml: 2, flex: 1 };
Expand Down Expand Up @@ -67,7 +70,7 @@ const getValidScenario = (scenar: DisplayModel | DisplayModel[]) =>
: undefined;

const ScenarioDag = (props: ScenarioDagProps) => {
const { showToolbar = true } = props;
const { showToolbar = true, onSelect, onAction } = props;
const [scenarioId, setScenarioId] = useState("");
const [engine] = useState(createEngine);
const [dagreEngine] = useState(createDagreEngine);
Expand Down Expand Up @@ -121,8 +124,10 @@ const ScenarioDag = (props: ScenarioDagProps) => {

const zoomToFit = useCallback(() => engine.zoomToFit(), [engine]);

const onClick = useCallback((id: string) => onAction && dispatch(createSendActionNameAction(props.id, module, onSelect, id, onAction)), [props.id, onAction, onSelect, module, dispatch]);

useEffect(() => {
const model = new TaipyDiagramModel();
const model = new TaipyDiagramModel(onClick);
initDiagram(engine);
let doLayout = false;
if (displayModel) {
Expand All @@ -135,7 +140,7 @@ const ScenarioDag = (props: ScenarioDagProps) => {
//engine.getActionEventBus().registerAction(new DeleteItemsAction({ keyCodes: [1] }));
model.setLocked(true);
doLayout && setTimeout(relayout, 500);
}, [displayModel, engine, relayout]);
}, [displayModel, engine, relayout, onClick]);

useEffect(() => {
const showVar = getUpdateVar(props.updateVars, "show");
Expand Down
4 changes: 4 additions & 0 deletions frontend/taipy/src/projectstorm/NodeWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ namespace S {
interface NodeProps {
node: TaipyNodeModel;
engine: DiagramEngine;
onAction?: string;
}

const getStatusLabel = (status?: TaskStatus) => status == TaskStatus.Running ? "Running" : status == TaskStatus.Pending ? "Pending" : undefined
Expand All @@ -110,13 +111,16 @@ const NodeWidget = ({ node, engine }: NodeProps) => {
[engine]
);

const onClick = useCallback(() => node.onClick && node.onClick(node.getID()), [node]);

return (
<S.Node
data-default-node-name={node.getOptions().name}
selected={node.isSelected()}
background={node.getOptions().color}
title={getStatusLabel(node.status)}
$status={node.status}
onClick={onClick}
>
<S.Title>
<S.TitleIcon className="icon" title={node.getType()}>
Expand Down
17 changes: 13 additions & 4 deletions frontend/taipy/src/projectstorm/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,30 @@ import { DefaultNodeModel, DefaultNodeModelOptions, DefaultPortModel, DefaultPor
import { IN_PORT_NAME, OUT_PORT_NAME } from "../utils/diagram";
import { getChildType } from "../utils/childtype";
import { DataNode, Task } from "../utils/names";
import { TaskStatus } from "../utils/types";
import { OnClick, TaskStatus } from "../utils/types";

export class TaipyDiagramModel extends DiagramModel {}
export class TaipyDiagramModel extends DiagramModel {
onClick?: OnClick;
constructor(onClick?: OnClick) {
super();
this.onClick = onClick
}
}

export interface TaipyNodeModelOptions extends DefaultNodeModelOptions {
subtype?: string;
status?: TaskStatus;
onClick?: OnClick;
}
export class TaipyNodeModel extends DefaultNodeModel {
subtype: string | undefined;
status: TaskStatus | undefined;
subtype?: string;
status?: TaskStatus;
onClick?: OnClick;
constructor(options?: TaipyNodeModelOptions) {
super(options);
this.subtype = options?.subtype;
this.status = options?.status
this.onClick = options?.onClick;
}
}

Expand Down
7 changes: 4 additions & 3 deletions frontend/taipy/src/utils/diagram.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { getNodeColor } from "./config";
import { TaipyDiagramModel, TaipyNodeModel } from "../projectstorm/models";
import { TaipyNodeFactory, TaipyPortFactory } from "../projectstorm/factories";
import { nodeTypes } from "./config";
import { DisplayModel, TaskStatus, TaskStatuses } from "./types";
import { DisplayModel, OnClick, TaskStatus, TaskStatuses } from "./types";

export const createDagreEngine = () =>
new DagreEngine({
Expand Down Expand Up @@ -59,14 +59,15 @@ export const getLinkId = (link: LinkModel) =>
)}`;
export const getNodeId = (node: DefaultNodeModel) => `${node.getType()}.${node.getID()}`;

export const createNode = (nodeType: string, id: string, name: string, subtype: string, status?: TaskStatus) =>
export const createNode = (nodeType: string, id: string, name: string, subtype: string, status?: TaskStatus, onClick?: OnClick) =>
new TaipyNodeModel({
id: id,
type: nodeType,
name: name,
color: getNodeColor(nodeType),
subtype: subtype,
status: status,
onClick: onClick,
});

export const createLink = (outPort: DefaultPortModel, inPort: DefaultPortModel) =>
Expand Down Expand Up @@ -147,7 +148,7 @@ export const populateModel = (displayModel: DisplayModel, model: TaipyDiagramMod
displayModel[1] &&
Object.entries(displayModel[1]).forEach(([nodeType, n]) => {
Object.entries(n).forEach(([id, detail]) => {
const node = createNode(nodeType, id, detail.name, detail.type, detail.status);
const node = createNode(nodeType, id, detail.name, detail.type, detail.status, model.onClick);
nodeModels[nodeType] = nodeModels[nodeType] || {};
nodeModels[nodeType][id] = node;
});
Expand Down
4 changes: 4 additions & 0 deletions frontend/taipy/src/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,7 @@ export enum TaskStatus {
}

export type TaskStatuses = Record<string, TaskStatus>;

export interface OnClick {
(id: string): void;
}
2 changes: 0 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,9 @@ select = [
"I001", # isort import order
]
ignore = [ # TODO: to be removed
"C401", # Unnecessary generator (rewrite as a `set` comprehension)
"C405", # Unnecessary list literal - rewrite as a literal
"C408", # Unnecessary dict call - rewrite as a literal
"C409", # Unnecessary list passed to tuple() - rewrite as a tuple literal
"C416", # Unnecessary `set` comprehension (rewrite using `set()`)
]

# Allow fix for all enabled rules (when `--fix`) is provided.
Expand Down
2 changes: 1 addition & 1 deletion taipy/core/_entity/_dag.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def __init__(self, src: _Node, dest: _Node):

class _DAG:
def __init__(self, dag: nx.DiGraph):
self._sorted_nodes = [nodes for nodes in nx.topological_generations(dag)]
self._sorted_nodes = list(nx.topological_generations(dag))
self._length, self._width = self.__compute_size()
self._grid_length, self._grid_width = self.__compute_grid_size()
self._nodes = self.__compute_nodes()
Expand Down
2 changes: 1 addition & 1 deletion taipy/core/data/_abstract_sql.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ def _read_as_pandas_dataframe(

# On pandas 1.3.5 there's a bug that makes that the dataframe from sqlalchemy query is
# created without headers
keys = [col for col in result.keys()]
keys = list(result.keys())
if columns:
return pd.DataFrame(result, columns=keys)[columns]
return pd.DataFrame(result, columns=keys)
Expand Down
29 changes: 16 additions & 13 deletions taipy/core/scenario/scenario.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,9 @@ def __init__(
self._properties = _Properties(self, **properties)
self._sequences: Dict[str, Dict] = sequences or {}

_scenario_task_ids = set(task.id if isinstance(task, Task) else task for task in self._tasks)
_scenario_task_ids = {task.id if isinstance(task, Task) else task for task in self._tasks}
for sequence_name, sequence_data in self._sequences.items():
sequence_task_ids = set(
task.id if isinstance(task, Task) else task for task in sequence_data.get("tasks", [])
)
sequence_task_ids = {task.id if isinstance(task, Task) else task for task in sequence_data.get("tasks", [])}
self.__check_sequence_tasks_exist_in_scenario_tasks(
sequence_name, sequence_task_ids, self.id, _scenario_task_ids
)
Expand Down Expand Up @@ -233,10 +231,11 @@ def _set_sequence(
subscribers: Optional[List[_Subscriber]] = None,
) -> Sequence:
_scenario = _Reloader()._reload(self._MANAGER_NAME, self)
_scenario_task_ids = set(task.id if isinstance(task, Task) else task for task in _scenario._tasks)
_sequence_task_ids: Set[TaskId] = set(task.id if isinstance(task, Task) else task for task in tasks)
_scenario_task_ids = {task.id if isinstance(task, Task) else task for task in _scenario._tasks}
_sequence_task_ids: Set[TaskId] = {task.id if isinstance(task, Task) else task for task in tasks}
self.__check_sequence_tasks_exist_in_scenario_tasks(name, _sequence_task_ids, self.id, _scenario_task_ids)
from taipy.core.sequence._sequence_manager_factory import _SequenceManagerFactory

seq_manager = _SequenceManagerFactory._build_manager()
seq = seq_manager._create(name, tasks, subscribers or [], properties or {}, self.id, self.version)
if not seq._is_consistent():
Expand Down Expand Up @@ -270,9 +269,9 @@ def add_sequences(self, sequences: Dict[str, Union[List[Task], List[TaskId]]]):
SequenceTaskDoesNotExistInScenario^: If a task in the sequence does not exist in the scenario.
"""
_scenario = _Reloader()._reload(self._MANAGER_NAME, self)
_sc_task_ids = set(task.id if isinstance(task, Task) else task for task in _scenario._tasks)
_sc_task_ids = {task.id if isinstance(task, Task) else task for task in _scenario._tasks}
for name, tasks in sequences.items():
_seq_task_ids: Set[TaskId] = set(task.id if isinstance(task, Task) else task for task in tasks)
_seq_task_ids: Set[TaskId] = {task.id if isinstance(task, Task) else task for task in tasks}
self.__check_sequence_tasks_exist_in_scenario_tasks(name, _seq_task_ids, self.id, _sc_task_ids)
# Need to parse twice the sequences to avoid adding some sequences and not others in case of exception
for name, tasks in sequences.items():
Expand Down Expand Up @@ -327,11 +326,15 @@ def rename_sequence(self, old_name, new_name):
self._sequences[new_name] = self._sequences[old_name]
del self._sequences[old_name]
self.sequences = self._sequences # type: ignore
Notifier.publish(Event(EventEntityType.SCENARIO,
EventOperation.UPDATE,
entity_id=self.id,
attribute_name="sequences",
attribute_value=self._sequences))
Notifier.publish(
Event(
EventEntityType.SCENARIO,
EventOperation.UPDATE,
entity_id=self.id,
attribute_name="sequences",
attribute_value=self._sequences,
)
)

@staticmethod
def __check_sequence_tasks_exist_in_scenario_tasks(
Expand Down
2 changes: 2 additions & 0 deletions taipy/gui_core/_GuiCoreLib.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,11 @@ class _GuiCore(ElementLibrary):
"width": ElementProperty(PropertyType.string),
"height": ElementProperty(PropertyType.string),
"class_name": ElementProperty(PropertyType.dynamic_string),
"on_action": ElementProperty(PropertyType.function),
},
inner_properties={
"core_changed": ElementProperty(PropertyType.broadcast, _GuiCoreContext._CORE_CHANGED_NAME),
"on_select": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.on_dag_select}}"),
},
),
"data_node_selector": Element(
Expand Down
23 changes: 21 additions & 2 deletions taipy/gui_core/_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ def process_event(self, event: Event):
if sequence and hasattr(sequence, "parent_ids") and sequence.parent_ids: # type: ignore
self.gui._broadcast(
_GuiCoreContext._CORE_CHANGED_NAME,
{"scenario": [x for x in sequence.parent_ids]}, # type: ignore
{"scenario": list(sequence.parent_ids)}, # type: ignore
)
except Exception as e:
_warn(f"Access to sequence {event.entity_id} failed", e)
Expand Down Expand Up @@ -650,7 +650,7 @@ def __edit_properties(self, entity: t.Union[Scenario, Sequence, DataNode], data:
if isinstance(ent, Scenario):
tags = data.get(_GuiCoreContext.__PROP_SCENARIO_TAGS)
if isinstance(tags, (list, tuple)):
ent.tags = {t for t in tags}
ent.tags = dict(tags)
name = data.get(_GuiCoreContext.__PROP_ENTITY_NAME)
if isinstance(name, str):
if hasattr(ent, _GuiCoreContext.__PROP_ENTITY_NAME):
Expand Down Expand Up @@ -939,3 +939,22 @@ def select_id(self, state: State, id: str, payload: t.Dict[str, str]):
state.assign(_GuiCoreContext._DATANODE_VIZ_DATA_ID_VAR, data_id)
elif chart_id := data.get("chart_id"):
state.assign(_GuiCoreContext._DATANODE_VIZ_DATA_CHART_ID_VAR, chart_id)

def on_dag_select(self, state: State, id: str, payload: t.Dict[str, str]):
args = payload.get("args")
if args is None or not isinstance(args, list) or len(args) < 2:
return
on_action_function = self.gui._get_user_function(args[1]) if args[1] else None
if callable(on_action_function):
try:
entity = core_get(args[0]) if is_readable(args[0]) else f"unredable({args[0]})"
self.gui._call_function_with_state(
on_action_function,
[entity],
)
except Exception as e:
if not self.gui._call_on_exception(args[1], e):
_warn(f"dag.on_action(): Exception raised in '{args[1]}()' with '{args[0]}'", e)
elif args[1]:
_warn(f"dag.on_action(): Invalid function '{args[1]}()'.")

6 changes: 6 additions & 0 deletions taipy/gui_core/viselements.json
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,12 @@
"type": "str",
"default_value": "\"100%\"",
"doc": "The maximum width, in CSS units, of the control."
},
{
"name": "on_action",
"type": "Callback",
"doc": "The name of the function that is triggered when a a node is selected.<br/><br/>All the parameters of that function are optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>entity (DataNode | Task): the entity (DataNode or Task) that was selected.</li>\n</ul>",
"signature": [["state", "State"], ["entity", "Task | DataNode"]]
}
]
}
Expand Down
2 changes: 1 addition & 1 deletion tests/core/data/test_filter_data_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ def test_filter_by_get_item(default_data_frame):
filtered_custom_dn = custom_dn["a"]
assert isinstance(filtered_custom_dn, List)
assert len(filtered_custom_dn) == 10
assert filtered_custom_dn == [i for i in range(10)]
assert filtered_custom_dn == list(range(10))

filtered_custom_dn = custom_dn[0:5]
assert isinstance(filtered_custom_dn, List)
Expand Down
4 changes: 2 additions & 2 deletions tests/core/data/test_generic_data_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,11 @@ def read_fct_modify_data_node_name(data_node_id: DataNodeId, name: str):


def reset_data():
TestGenericDataNode.data = [i for i in range(10)]
TestGenericDataNode.data = list(range(10))


class TestGenericDataNode:
data = [i for i in range(10)]
data = list(range(10))

def test_create(self):
dn = GenericDataNode(
Expand Down
12 changes: 7 additions & 5 deletions tests/core/job/test_job_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -411,10 +411,12 @@ def test_cancel_subsequent_jobs():
assert_true_after_time(job_4.is_canceled)
assert_true_after_time(job_5.is_abandoned)
assert_true_after_time(job_6.is_abandoned)
assert_true_after_time(lambda: all(
not _OrchestratorFactory._orchestrator._is_blocked(job)
for job in [job_1, job_2, job_3, job_4, job_5, job_6]
))
assert_true_after_time(
lambda: all(
not _OrchestratorFactory._orchestrator._is_blocked(job)
for job in [job_1, job_2, job_3, job_4, job_5, job_6]
)
)
assert_true_after_time(lambda: _OrchestratorFactory._orchestrator.jobs_to_run.qsize() == 0)


Expand Down Expand Up @@ -474,7 +476,7 @@ def _create_task(function, nb_outputs=1, name=None):
output_dn_configs = [
Config.configure_data_node(f"output{i}", "pickle", Scope.SCENARIO, default_data=0) for i in range(nb_outputs)
]
_DataManager._bulk_get_or_create({cfg for cfg in output_dn_configs})
_DataManager._bulk_get_or_create(output_dn_configs)
name = name or "".join(random.choice(string.ascii_lowercase) for _ in range(10))
task_config = Config.configure_task(
id=name,
Expand Down
2 changes: 1 addition & 1 deletion tests/core/job/test_job_manager_with_sql_repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ def _create_task(function, nb_outputs=1, name=None):
output_dn_configs = [
Config.configure_data_node(f"output{i}", scope=Scope.SCENARIO, default_data=0) for i in range(nb_outputs)
]
_DataManager._bulk_get_or_create({cfg for cfg in output_dn_configs})
_DataManager._bulk_get_or_create(output_dn_configs)
name = name or "".join(random.choice(string.ascii_lowercase) for _ in range(10))
task_config = Config.configure_task(
id=name,
Expand Down
2 changes: 1 addition & 1 deletion tests/core/scenario/test_scenario_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,7 @@ def test_scenario_manager_only_creates_data_node_once():
scenario_1_sorted_tasks = scenario_1._get_sorted_tasks()
expected = [{task_mult_by_2_config.id, task_mult_by_4_config.id}, {task_mult_by_3_config.id}]
for i, list_tasks_by_level in enumerate(scenario_1_sorted_tasks):
assert set(t.config_id for t in list_tasks_by_level) == expected[i]
assert {t.config_id for t in list_tasks_by_level} == expected[i]
assert scenario_1.cycle.frequency == Frequency.DAILY

_ScenarioManager._create(scenario_config)
Expand Down
2 changes: 1 addition & 1 deletion tests/core/scenario/test_scenario_manager_with_sql_repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ def test_scenario_manager_only_creates_data_node_once(init_sql_repo):
scenario_1_sorted_tasks = scenario_1._get_sorted_tasks()
expected = [{task_mult_by_2_config.id, task_mult_by_4_config.id}, {task_mult_by_3_config.id}]
for i, list_tasks_by_level in enumerate(scenario_1_sorted_tasks):
assert set(t.config_id for t in list_tasks_by_level) == expected[i]
assert {t.config_id for t in list_tasks_by_level} == expected[i]
assert scenario_1.cycle.frequency == Frequency.DAILY

_ScenarioManager._create(scenario_config)
Expand Down

0 comments on commit 1566abf

Please sign in to comment.