43 |
{context.renderJLOverlayControl()}
44 | {graph}
45 | {overlay}
46 |
47 | );
48 | return element;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/scripts/vale/config/vocabularies/IPyElk/accept.txt:
--------------------------------------------------------------------------------
1 | -compatible
2 | .+_.+
3 | (.+)\(
4 | 0a1
5 | 2FA
6 | activity/flow-chart
7 | additionalProperties
8 | AnyElkEdgeWithProperties
9 | AnyElkLabelWithProperties
10 | AnyElkNode
11 | AnyElkPort
12 | APIs
13 | backport
14 | Backporting
15 | backports
16 | bendPoints
17 | binder-badge
18 | bool
19 | BrokenPipe
20 | BSD-3-Clause
21 | bugfixes
22 | Changelog
23 | CHANGELOG
24 | checks/changes
25 | ci
26 | composable
27 | conda
28 | ControlOverlay
29 | css
30 | dist
31 | do…
32 | doit
33 | DOM
34 | e\.g\.
35 | ElementLoader
36 | ElementLoader
37 | ElkEdgeSection
38 | ElkLabel
39 | ElkPoint
40 | ElkProperties
41 | endmacro
42 | env
43 | EPL-2.0
44 | etc
45 | evented
46 | FontAwesome
47 | forward-port
48 | ground-truth
49 | html
50 | ids
51 | in-browser
52 | incomingSections
53 | incomingShape
54 | inside/outside
55 | ipyelk
56 | isSymbol
57 | js
58 | junctionPoints
59 | jupyrdf
60 | jupyter
61 | Jupyter
62 | JupyterLab
63 | labextension
64 | LayoutOptions
65 | lifecycle
66 | live-reloading
67 | lockfiles
68 | maintenance
69 | Mambaforge
70 | MarkElementWidget
71 | Miniforge
72 | multiline
73 | networkx
74 | NetworkX
75 | npm
76 | NXLoad
77 | outgoingSections
78 | outgoingShape
79 | Pan/Zoom
80 | PipeStatus
81 | PipeStatusView
82 | pixi
83 | predator/prey
84 | PRs
85 | PyData
86 | pypi
87 | PyPI
88 | re-build
89 | ReadTheDocs
90 | REPLite
91 | screencast
92 | selectable
93 | self.status.exception
94 | SPDX-License-Identifier
95 | sprotty
96 | standards-compliant
97 | startPoint
98 | sub-pipes
99 | Subclasses
100 | SVG
101 | TBD
102 | ToolButton
103 | traitlet
104 | two-factor
105 | TypeFox
106 | TypeScript
107 | UI
108 | vertical/horizontal
109 | viewport
110 | WebWorker
111 | width/height
112 | x/y
113 |
--------------------------------------------------------------------------------
/style/pipe_status.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2024 ipyelk contributors.
3 | * Distributed under the terms of the Modified BSD License.
4 | */
5 |
6 | /*
7 | Styling elk pipe status widget
8 | */
9 | .elk-pipe span {
10 | display: inline-block;
11 | margin-left: var(--jp-widgets-margin);
12 | margin-right: var(--jp-widgets-margin);
13 | }
14 |
15 | .elk-pipe-badge > svg {
16 | height: var(--jp-widgets-font-size);
17 | }
18 |
19 | .elk-pipe-elapsed,
20 | .elk-pipe-status {
21 | width: 60px;
22 | }
23 |
24 | .elk-pipe-name {
25 | width: 180px;
26 | }
27 |
28 | .elk-pipe-disposition-waiting .elk-pipe-badge {
29 | stroke: var(--jp-info-color3);
30 | fill: none;
31 | stroke-width: 2px;
32 | color: var(--jp-info-color3);
33 | }
34 |
35 | .elk-pipe-disposition-waiting,
36 | .elk-pipe-accessor {
37 | color: var(--jp-border-color1);
38 | }
39 |
40 | .elk-pipe-disposition-finished .elk-pipe-badge {
41 | fill: var(--jp-success-color1);
42 | }
43 |
44 | .elk-pipe-disposition-running .elk-pipe-badge {
45 | fill: var(--jp-info-color1);
46 | }
47 |
48 | .elk-pipe-disposition-error .elk-pipe-badge {
49 | fill: var(--jp-warn-color0);
50 | }
51 |
52 | .elk-pipe-disposition-error .elk-pipe-error {
53 | display: block;
54 | }
55 |
56 | .elk-pipe-disposition-error .elk-pipe-error > code {
57 | background-color: var(--jp-error-color3);
58 | width: 335px;
59 | display: inline-block;
60 | }
61 |
62 | .widget-button.elk-pipe-toggle-btn,
63 | .elk-pipe-space {
64 | width: 2em;
65 | padding: 0px;
66 | margin: 0px;
67 | margin-bottom: auto;
68 | background-color: unset;
69 | height: var(--jp-code-line-height);
70 | line-height: var(--jp-code-line-height);
71 | }
72 | .widget-button.elk-pipe-toggle-btn i {
73 | transition: all 1s;
74 | }
75 | .elk-pipe-toggle-btn.elk-pipe-closed i {
76 | transform: rotate(-90deg);
77 | }
78 |
--------------------------------------------------------------------------------
/examples/08_Simulation_App.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "id": "7395ec07-9e51-436a-9644-d89f25f83c05",
6 | "metadata": {},
7 | "source": [
8 | "# 🦌 ELK Simulation Demo 🐺🎭\n",
9 | "\n",
10 | "> With [importnb](https://pypi.org/project/importnb), one can interactively build apps,\n",
11 | "> then reuse single parts for presentation. This simulation is built in the\n",
12 | "> [plumbing notebook](./07_Simulation.ipynb)."
13 | ]
14 | },
15 | {
16 | "cell_type": "code",
17 | "execution_count": null,
18 | "id": "b3062f2e-fef0-4aba-a25a-36689d60fab4",
19 | "metadata": {},
20 | "outputs": [],
21 | "source": [
22 | "if __name__ == \"__main__\":\n",
23 | " %pip install -q -r requirements.txt"
24 | ]
25 | },
26 | {
27 | "cell_type": "code",
28 | "execution_count": null,
29 | "id": "e47b5e87-0e13-4738-b4e5-d81a50b514a2",
30 | "metadata": {},
31 | "outputs": [],
32 | "source": [
33 | "with __import__(\"importnb\").Notebook():\n",
34 | " from __07_Simulation import app\n",
35 | "app"
36 | ]
37 | },
38 | {
39 | "cell_type": "markdown",
40 | "id": "e3ff004a-1a91-4a45-b276-803343af5833",
41 | "metadata": {},
42 | "source": [
43 | "## 🦌 Learn More 📖\n",
44 | "\n",
45 | "See the [other examples](./_index.ipynb)."
46 | ]
47 | }
48 | ],
49 | "metadata": {
50 | "kernelspec": {
51 | "display_name": "Python 3 (ipykernel)",
52 | "language": "python",
53 | "name": "python3"
54 | },
55 | "language_info": {
56 | "codemirror_mode": {
57 | "name": "ipython",
58 | "version": 3
59 | },
60 | "file_extension": ".py",
61 | "mimetype": "text/x-python",
62 | "name": "python",
63 | "nbconvert_exporter": "python",
64 | "pygments_lexer": "ipython3",
65 | "version": "3.10.6"
66 | }
67 | },
68 | "nbformat": 4,
69 | "nbformat_minor": 5
70 | }
71 |
--------------------------------------------------------------------------------
/tests/elements/test_nodes.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2024 ipyelk contributors.
2 | # Distributed under the terms of the Modified BSD License.
3 |
4 | from ipyelk.elements import Edge, Label, Node, Port, shapes
5 |
6 |
7 | def test_node_instances():
8 | n1 = Node()
9 | n1.dict()
10 |
11 |
12 | def test_add_child():
13 | key = "child"
14 | n = Node()
15 | p = n.add_child(Node(), key)
16 | assert p.get_parent() is n, "Expect port parent to be the node"
17 | assert n.get_child(key) is p, "Expect node port dict to return same port"
18 | n.dict()
19 |
20 |
21 | def test_add_port():
22 | key = "port"
23 | n = Node()
24 | p = n.add_port(Port(), key)
25 | assert p.get_parent() is n, "Expect port parent to be the node"
26 | assert n.get_port(key) is p, "Expect node port dict to return same port"
27 | n.dict()
28 |
29 |
30 | def test_edge_node_instances():
31 | n1 = Node()
32 | n2 = Node()
33 | e = Edge(
34 | source=n1,
35 | target=n2,
36 | )
37 | assert e.source is n1, "Edge source instance changed"
38 | assert e.target is n2, "Edge target instance changed"
39 | n1.dict()
40 | n2.dict()
41 | e.dict()
42 |
43 |
44 | def test_node_label_instance():
45 | label = Label()
46 | n = Node(labels=[label])
47 | assert n.labels[0] is label, "Expect node label instance to match"
48 | label.dict()
49 |
50 |
51 | def test_edge_port_instances():
52 | n1 = Node()
53 | x = n1.add_port(Port(), "x")
54 | n2 = Node()
55 | e = Edge(
56 | source=x,
57 | target=n2,
58 | )
59 | assert e.source is x, "Edge source instance changed"
60 | assert e.target is n2, "Edge target instance changed"
61 | n1.dict()
62 | x.dict()
63 | n2.dict()
64 | e.dict()
65 |
66 |
67 | def test_node_shape():
68 | shape = shapes.Ellipse()
69 | n = Node(properties={"shape": shape})
70 | data = n.dict()
71 | assert data["properties"]["shape"].get("type") == shape.type
72 |
--------------------------------------------------------------------------------
/scripts/check-dist.py:
--------------------------------------------------------------------------------
1 | """Check for files in dist archives."""
2 |
3 | import sys
4 | import tarfile
5 | import zipfile
6 | from pathlib import Path
7 |
8 | import tomllib
9 |
10 | HERE = Path(__file__).parent
11 | ROOT = HERE.parent
12 | UTF8 = {"encoding": "utf-8"}
13 |
14 | DIST = ROOT / "dist"
15 | PPT = ROOT / "pyproject.toml"
16 | LICENSE = ROOT / "LICENSE.txt"
17 | COPYRIGHT = ROOT / "COPYRIGHT.md"
18 | EPL = ROOT / "third-party/epl-v10.html"
19 | TPL_PATH = (
20 | "share/jupyter/labextensions/@jupyrdf/jupyter-elk/static/third-party-licenses.json"
21 | )
22 | TPL = ROOT / "src/_d" / TPL_PATH
23 |
24 | LICENSE_BYTES = {p: p.read_bytes() for p in [LICENSE, COPYRIGHT, EPL, TPL]}
25 |
26 | PY_VERSION = tomllib.loads(PPT.read_text(**UTF8))["project"]["version"]
27 | PFX = f"ipyelk-{PY_VERSION}"
28 |
29 | WHEEL_FILES = {
30 | f"{PFX}.dist-info/LICENSE.txt": LICENSE_BYTES[LICENSE],
31 | f"{PFX}.data/data/{TPL_PATH}": LICENSE_BYTES[TPL],
32 | }
33 |
34 | SDIST_FILES = {
35 | f"{PFX}/LICENSE.txt": LICENSE_BYTES[LICENSE],
36 | f"{PFX}/COPYRIGHT.md": LICENSE_BYTES[COPYRIGHT],
37 | f"{PFX}/third-party/epl-v10.html": LICENSE_BYTES[EPL],
38 | f"{PFX}/src/_d/{TPL_PATH}": LICENSE_BYTES[TPL],
39 | }
40 |
41 |
42 | def check_whl(path: Path) -> None:
43 | with zipfile.ZipFile(path, "r") as whl:
44 | for fn, fbytes in WHEEL_FILES.items():
45 | assert whl.read(fn) == fbytes, f"!!! wheel {fn} is wrong"
46 | print(f"OK wheel {fn}")
47 |
48 |
49 | def check_sdist(path: Path) -> None:
50 | with tarfile.open(path, "r:gz") as sdist:
51 | for fn, fbytes in SDIST_FILES.items():
52 | assert sdist.extractfile(fn).read() == fbytes, f"!!! sdist {fn} is wrong"
53 | print(f"OK sdist {fn}")
54 |
55 |
56 | def main() -> int:
57 | for path in sorted(DIST.glob("*")):
58 | if path.name.endswith(".whl"):
59 | check_whl(path)
60 | elif path.name.endswith(".tar.gz"):
61 | check_sdist(path)
62 |
63 |
64 | if __name__ == "__main__":
65 | sys.exit(main())
66 |
--------------------------------------------------------------------------------
/scripts/build-ext-cov.py:
--------------------------------------------------------------------------------
1 | """Build instrumented extension."""
2 |
3 | import json
4 | import os
5 | import shutil
6 | import sys
7 | from pathlib import Path
8 | from subprocess import call
9 |
10 | UTF8 = {"encoding": "utf-8"}
11 | HERE = Path(__file__).parent
12 | ROOT = HERE.parent
13 | PKG_JSON = ROOT / "package.json"
14 | PKG_DATA = json.loads(PKG_JSON.read_text(**UTF8))
15 | LIB = ROOT / "lib"
16 |
17 | BUILD = ROOT / "build"
18 |
19 | LIB_TMP = BUILD / "lib-tmp"
20 | TSBUILDINFO = BUILD / "tsc"
21 | COV_EXT = BUILD / "labextensions-cov"
22 | EXT_PKG_JSON = COV_EXT / PKG_DATA["name"] / PKG_JSON.name
23 |
24 |
25 | def main() -> int:
26 | """Work around webpack limitations to get an out-of-tree build with coverage."""
27 | BUILD.mkdir(exist_ok=True, parents=True)
28 |
29 | if not EXT_PKG_JSON.exists():
30 | print("... cleaning", TSBUILDINFO)
31 | [p.unlink() for p in TSBUILDINFO.glob("*.cov")]
32 |
33 | if LIB.exists():
34 | print("... copying", LIB, "to", LIB_TMP)
35 | shutil.rmtree(LIB_TMP, ignore_errors=True)
36 | LIB.rename(LIB_TMP)
37 |
38 | shutil.rmtree(COV_EXT, ignore_errors=True)
39 |
40 | print("... building instrumented lib")
41 | rc = call(["jlpm", "build:ts:cov"])
42 | if rc:
43 | return rc
44 |
45 | env = dict(os.environ)
46 | env["WITH_TOTAL_COVERAGE"] = "1"
47 |
48 | print("... building", COV_EXT)
49 | rc = call(["jlpm", "build:ext"], env=env)
50 |
51 | if rc:
52 | return rc
53 |
54 | print("... patching", EXT_PKG_JSON)
55 | remote = min(COV_EXT.rglob("remoteEntry.*.js"))
56 | print("... found remote", remote)
57 | PKG_DATA["jupyterlab"]["_build"] = {
58 | "load": f"static/{remote.name}",
59 | "extension": "./extension",
60 | }
61 | EXT_PKG_JSON.write_text(json.dumps(PKG_DATA, indent=2), **UTF8)
62 |
63 | if LIB_TMP.exists():
64 | print("... restoring lib")
65 | shutil.rmtree(LIB, ignore_errors=True)
66 | LIB_TMP.rename(LIB)
67 |
68 | return 0
69 |
70 |
71 | if __name__ == "__main__":
72 | sys.exit(main())
73 |
--------------------------------------------------------------------------------
/src/ipyelk/elements/serialization.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2024 ipyelk contributors.
2 | # Distributed under the terms of the Modified BSD License.
3 |
4 | from typing import Dict, Optional
5 |
6 | from ipywidgets import DOMWidget
7 | from pydantic.v1 import BaseModel
8 |
9 | from .elements import Node
10 | from .index import HierarchicalIndex, VisIndex
11 |
12 |
13 | def pop_edges(data: Dict, edges=None):
14 | if edges is None:
15 | edges = {}
16 |
17 | if "edges" in data:
18 | edges[data["id"]] = data.pop("edges")
19 | for child in data.get("children", []):
20 | pop_edges(child, edges)
21 | return edges
22 |
23 |
24 | def apply_edges(data: Dict, edges):
25 | node_id = data["id"]
26 | if node_id in edges:
27 | data["edges"] = edges.get(node_id)
28 | for child in data.get("children", []):
29 | apply_edges(child, edges)
30 | return edges
31 |
32 |
33 | def convert_elkjson(data: Dict, vis_index: VisIndex = None) -> Node:
34 | # pop_edges currently mutates `data` by popping the edge dict
35 | edges_map = pop_edges(data) # dict of node.id to edge list
36 | root = Node(**data) # new element hierarchy without edges
37 | el_map = HierarchicalIndex.from_els(
38 | root, vis_index=vis_index
39 | ) # get mapping of ids to elements
40 | el_map.link_edges(edges_map)
41 | # reapplies edges to `data`
42 | apply_edges(data, edges_map)
43 |
44 | return root
45 |
46 |
47 | def to_json(model: Optional[BaseModel], widget: DOMWidget) -> Optional[Dict]:
48 | """Function to serialize a dictionary of symbols for use in a diagram
49 |
50 | :param defs: dictionary of Symbols
51 | :param diagram: elk diagram widget
52 | :return: json dictionary
53 | """
54 | if model is None:
55 | return None
56 | return model.dict(exclude_none=True)
57 |
58 |
59 | def from_elk_json(js: Optional[Dict], manager) -> Optional[Node]:
60 | if not js:
61 | return None
62 | return convert_elkjson(js)
63 |
64 |
65 | elk_serialization = {"to_json": to_json, "from_json": from_elk_json}
66 | symbol_serialization = {"to_json": to_json}
67 |
--------------------------------------------------------------------------------
/src/ipyelk/util.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2024 ipyelk contributors.
2 | # Distributed under the terms of the Modified BSD License.
3 | from typing import Dict, List, Optional
4 |
5 | from .elements.layout_options.model import strip_none
6 |
7 |
8 | def safely_unobserve(item, handler):
9 | if hasattr(item, "unobserve"):
10 | item.unobserve(handler=handler)
11 |
12 |
13 | def to_dict(obj):
14 | """Shim function to convert obj to a dictionary"""
15 | if obj is None:
16 | data = {}
17 | elif isinstance(obj, dict):
18 | data = obj
19 | elif hasattr(obj, "to_dict"):
20 | data = obj.to_dict()
21 | elif hasattr(obj, "dict"):
22 | data = obj.dict()
23 | else:
24 | raise TypeError("Unable to convert to dictionary")
25 | return data
26 |
27 |
28 | def merge(d1: Optional[Dict], d2: Optional[Dict]) -> Dict:
29 | """Merge two dictionaries while first testing if either are `None`.
30 | The first dictionary's keys take precedence over the second dictionary.
31 | If the final merged dictionary is empty `None` is returned.
32 |
33 | :param d1: primary dictionary
34 | :type d1: Optional[Dict]
35 | :param d2: secondary dictionary
36 | :type d2: Optional[Dict]
37 | :return: merged dictionary
38 | :rtype: Dict
39 | """
40 | d1 = to_dict(d1)
41 | d2 = to_dict(d2)
42 |
43 | cl1 = d1.get("cssClasses") or ""
44 | cl2 = d2.get("cssClasses") or ""
45 | cl = " ".join(sorted(set([*cl1.split(), *cl2.split()]))).strip()
46 |
47 | value = {**strip_none(d2), **strip_none(d1)} # right most wins if duplicated keys
48 |
49 | # if either had cssClasses, update that
50 | if cl:
51 | value["cssClasses"] = cl
52 |
53 | return value
54 |
55 |
56 | def listed(values: Optional[List]) -> List:
57 | """Checks if incoming `values` is None then either returns a new list or
58 | original value.
59 |
60 | :param values: List of values
61 | :type values: Optional[List]
62 | :return: List of values or empty list
63 | :rtype: List
64 | """
65 | if values is None:
66 | return []
67 | return values
68 |
--------------------------------------------------------------------------------
/tests/test_meta.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2024 ipyelk contributors.
2 | # Distributed under the terms of the Modified BSD License.
3 |
4 | from __future__ import annotations
5 |
6 | try:
7 | from importlib.metadata import version
8 | except Exception:
9 | from importlib_metadata import version
10 |
11 | from pathlib import Path
12 | from typing import Any
13 |
14 |
15 | def test_meta() -> None:
16 | """Verify the version is advertised."""
17 | import ipyelk
18 |
19 | assert hasattr(ipyelk, "__version__")
20 | assert ipyelk.__version__ == version("ipyelk")
21 |
22 |
23 | def test_pixi_versions(
24 | the_pixi_version: str,
25 | a_file_with_pixi_versions: Path,
26 | pixi_versions_in_a_file: set[str],
27 | ) -> None:
28 | """Verify the ``pixi`` version is consistent."""
29 | assert len(pixi_versions_in_a_file) == 1, a_file_with_pixi_versions
30 | assert min(pixi_versions_in_a_file) == the_pixi_version, pixi_versions_in_a_file
31 |
32 |
33 | def test_labextension() -> None:
34 | """Verify the labextension path metadata is as expected."""
35 | import ipyelk
36 |
37 | assert len(ipyelk._jupyter_labextension_paths()) == 1
38 |
39 |
40 | def test_changelog_versions(
41 | the_changelog_text: str, the_js_version: str, the_py_version: str
42 | ) -> None:
43 | """Verify ``CHANGELOG.md`` contains the current versions."""
44 | assert f"### `ipyelk {the_py_version}`" in the_changelog_text
45 | assert f"### `@jupyrdf/jupyter-elk {the_js_version}`" in the_changelog_text
46 |
47 |
48 | def test_compatible_versions(the_js_version: str, the_py_version: str) -> None:
49 | """Verify the calculated versions are consistent."""
50 | from ipyelk.constants import EXTENSION_SPEC_VERSION, __version__
51 |
52 | assert __version__ == the_py_version
53 | assert the_js_version == EXTENSION_SPEC_VERSION
54 |
55 |
56 | def test_py_version(the_readme_text: str, the_pyproject_data: dict[str, Any]) -> None:
57 | """Verify the bottom python pin is accurate."""
58 | requires_python = the_pyproject_data["project"]["requires-python"]
59 | assert f"""python {requires_python}""" in the_readme_text
60 |
--------------------------------------------------------------------------------
/src/ipyelk/elements/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2024 ipyelk contributors.
2 | # Distributed under the terms of the Modified BSD License.
3 | from .common import EMPTY_SENTINEL
4 | from .elements import (
5 | BaseElement,
6 | Edge,
7 | EdgeProperties,
8 | ElementMetadata,
9 | HierarchicalElement,
10 | Label,
11 | LabelProperties,
12 | Node,
13 | NodeProperties,
14 | Port,
15 | PortProperties,
16 | exclude_hidden,
17 | exclude_layout,
18 | merge_excluded,
19 | )
20 | from .extended import Compartment, Partition, Record
21 | from .index import (
22 | EdgeReport,
23 | ElementIndex,
24 | HierarchicalIndex,
25 | IDReport,
26 | VisIndex,
27 | iter_edges,
28 | iter_elements,
29 | iter_hierarchy,
30 | iter_labels,
31 | iter_visible,
32 | )
33 | from .mark_factory import Mark, MarkFactory
34 | from .registry import Registry
35 | from .serialization import convert_elkjson, elk_serialization, symbol_serialization
36 | from .shapes import EdgeShape, LabelShape, NodeShape, PortShape
37 | from .symbol import EndpointSymbol, Symbol, SymbolSpec
38 |
39 | __all__ = [
40 | "EMPTY_SENTINEL",
41 | "BaseElement",
42 | "Compartment",
43 | "Edge",
44 | "EdgeProperties",
45 | "EdgeProperties",
46 | "EdgeReport",
47 | "EdgeShape",
48 | "ElementIndex",
49 | "ElementMetadata",
50 | "ElementShape",
51 | "EndpointSymbol",
52 | "HierarchicalElement",
53 | "HierarchicalIndex",
54 | "IDReport",
55 | "Label",
56 | "LabelProperties",
57 | "LabelShape",
58 | "Mark",
59 | "MarkFactory",
60 | "Node",
61 | "NodeProperties",
62 | "NodeShape",
63 | "Partition",
64 | "Port",
65 | "PortProperties",
66 | "PortShape",
67 | "Record",
68 | "Registry",
69 | "Symbol",
70 | "SymbolSpec",
71 | "VisIndex",
72 | "check_ids",
73 | "convert_elkjson",
74 | "elk_serialization",
75 | "exclude_hidden",
76 | "exclude_layout",
77 | "iter_edges",
78 | "iter_elements",
79 | "iter_hierarchy",
80 | "iter_labels",
81 | "iter_visible",
82 | "merge_excluded",
83 | "symbol_serialization",
84 | ]
85 |
--------------------------------------------------------------------------------
/src/ipyelk/pipes/marks.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2024 ipyelk contributors.
2 | # Distributed under the terms of the Modified BSD License.
3 | from typing import Tuple
4 |
5 | import ipywidgets as W
6 | import traitlets as T
7 | from ipywidgets.widgets.trait_types import TypedTuple
8 |
9 | from ..elements import (
10 | BaseElement,
11 | ElementIndex,
12 | HierarchicalElement,
13 | Node,
14 | Registry,
15 | elk_serialization,
16 | )
17 |
18 |
19 | class MarkIndex(W.DOMWidget):
20 | elements: ElementIndex = T.Instance(ElementIndex, allow_none=True)
21 | context: Registry = T.Instance(Registry, kw={})
22 |
23 | _root: Node = None
24 |
25 | def to_id(self, element: BaseElement):
26 | return element.get_id()
27 |
28 | def from_id(self, key) -> HierarchicalElement:
29 | return self.elements.get(key)
30 |
31 | @property
32 | def root(self) -> Node:
33 | if self._root is None:
34 | self._update_root()
35 | return self._root
36 |
37 | @T.observe("elements")
38 | def _update_root(self, change=None):
39 | self._root = None
40 | if self.elements:
41 | self._root = self.elements.root()
42 |
43 |
44 | class MarkElementWidget(W.DOMWidget):
45 | value: Node = T.Instance(Node, allow_none=True).tag(sync=True, **elk_serialization)
46 | index: MarkIndex = T.Instance(MarkIndex, kw={}).tag(
47 | sync=True, **W.widget_serialization
48 | )
49 | flow: Tuple[str] = TypedTuple(T.Unicode(), kw={}).tag(sync=True)
50 |
51 | def persist(self):
52 | if self.index.elements is None:
53 | self.build_index()
54 | else:
55 | self.index.elements.update(ElementIndex.from_els(self.value))
56 | return self
57 |
58 | def build_index(self) -> MarkIndex:
59 | if self.value is None:
60 | index = ElementIndex()
61 | else:
62 | with self.index.context:
63 | index = ElementIndex.from_els(self.value)
64 | self.index.elements = index
65 | return self.index
66 |
67 | def _repr_mimebundle_(self, **kwargs):
68 | from IPython.display import JSON, display
69 |
70 | display(JSON(self.value.dict()))
71 |
--------------------------------------------------------------------------------
/src/ipyelk/tools/toolbar.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2024 ipyelk contributors.
2 | # Distributed under the terms of the Modified BSD License.
3 |
4 | from collections import defaultdict
5 | from itertools import chain
6 | from typing import Dict, List
7 |
8 | import ipywidgets as W
9 | import traitlets as T
10 |
11 | from ..styled_widget import StyledWidget
12 | from .tool import Tool
13 |
14 |
15 | class Toolbar(W.HBox, StyledWidget):
16 | """Toolbar for an Elk App"""
17 |
18 | tools = T.List(T.Instance(Tool), kw={})
19 | close_btn: W.Button = T.Instance(W.Button)
20 | on_close = T.Any(
21 | default_value=None
22 | ) # holds a callable function to execute when close button is pressed
23 |
24 | def __init__(self, *args, **kwargs):
25 | super().__init__(*args, **kwargs)
26 | self.add_class("jp-ElkToolbar")
27 | self._update_children()
28 | self._update_close_callback()
29 |
30 | @T.default("close_btn")
31 | def _default_cls_btn(self) -> W.Button:
32 | btn = W.Button(icon="times-circle").add_class("close-btn")
33 |
34 | def pressed(*args):
35 | if callable(self.on_close):
36 | self.on_close()
37 |
38 | btn.on_click(pressed)
39 | return btn
40 |
41 | @T.observe("on_close")
42 | def _update_close_callback(self, change: T.Bunch = None):
43 | """Toggle visiblity of the close button depending on if the `on_close` trait
44 | is callable
45 | """
46 | shown = "visible" if callable(self.on_close) else "hidden"
47 | self.close_btn.layout.visibility = shown
48 |
49 | @T.observe("tools")
50 | def _update_children(self, change: T.Bunch = None):
51 | self.children = self.tool_order() + [self.close_btn]
52 |
53 | # only have widgets shown if commands are specified or a on_close callback
54 | shown = "visible" if self.tools or callable(self.on_close) else "hidden"
55 | self.layout.visibility = shown
56 |
57 | def tool_order(self) -> List[Tool]:
58 | return list(chain(*[values for k, values in sorted(self.order().items())]))
59 |
60 | def order(self) -> Dict[int, List[Tool]]:
61 | order = defaultdict(list)
62 | for tool in self.tools:
63 | if isinstance(tool.ui, W.DOMWidget):
64 | order[tool.priority].append(tool.ui)
65 | return order
66 |
--------------------------------------------------------------------------------
/js/tools/draw-aware-mouse-listener.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2024 ipyelk contributors.
3 | * Distributed under the terms of the Modified BSD License.
4 | */
5 | import { Action, HoverFeedbackAction } from 'sprotty-protocol';
6 |
7 | import { MouseListener, SModelElementImpl } from 'sprotty';
8 |
9 | import { DiagramTool } from './tool';
10 |
11 | /**
12 | * A mouse listener that is aware of prior mouse dragging.
13 | *
14 | * Therefore, this listener distinguishes between mouse up events after dragging and
15 | * mouse up events without prior dragging. Subclasses may override the methods
16 | * `draggingMouseUp` and/or `nonDraggingMouseUp` to react to only these specific kinds
17 | * of mouse up events.
18 | */
19 | export class DragAwareMouseListener extends MouseListener {
20 | private isMouseDown: boolean = false;
21 | private isMouseDrag: boolean = false;
22 |
23 | mouseDown(target: SModelElementImpl, event: MouseEvent): Action[] {
24 | this.isMouseDown = true;
25 | return [];
26 | }
27 |
28 | mouseMove(target: SModelElementImpl, event: MouseEvent): Action[] {
29 | if (this.isMouseDown) {
30 | this.isMouseDrag = true;
31 | }
32 | return [];
33 | }
34 |
35 | mouseUp(element: SModelElementImpl, event: MouseEvent): Action[] {
36 | this.isMouseDown = false;
37 | if (this.isMouseDrag) {
38 | this.isMouseDrag = false;
39 | return this.draggingMouseUp(element, event);
40 | }
41 |
42 | return this.nonDraggingMouseUp(element, event);
43 | }
44 |
45 | nonDraggingMouseUp(element: SModelElementImpl, event: MouseEvent): Action[] {
46 | return [];
47 | }
48 |
49 | draggingMouseUp(element: SModelElementImpl, event: MouseEvent): Action[] {
50 | return [];
51 | }
52 | }
53 |
54 | export class DragAwareHoverMouseListener extends DragAwareMouseListener {
55 | constructor(
56 | protected elementTypeId: string,
57 | protected tool: DiagramTool,
58 | ) {
59 | super();
60 | }
61 |
62 | mouseOver(target: SModelElementImpl, event: MouseEvent): Action[] {
63 | return [
64 | HoverFeedbackAction.create({ mouseoverElement: target.id, mouseIsOver: true }),
65 | ];
66 | }
67 |
68 | mouseOut(target: SModelElementImpl, event: MouseEvent): (Action | Promise