├── .github
└── workflows
│ ├── test.yml
│ └── update-integration-tests.yml
├── .gitignore
├── Dockerfile
├── README.rst
├── docs
└── examples
│ ├── cpp-interaction.ipynb
│ ├── interactive-bfs.ipynb
│ ├── interactive-dfs-ui.ipynb
│ ├── interactive-dfs.ipynb
│ ├── layouts.ipynb
│ ├── matplotlib.ipynb
│ ├── ogdf-includes.gml
│ ├── ogdf-includes.ipynb
│ ├── ogdf-includes.svg
│ ├── pitfalls.ipynb
│ ├── refresh.sh
│ ├── sugiyama-simple.ipynb
│ └── sugiyama-simple.svg
├── pyproject.toml
├── src
└── ogdf_python
│ ├── __init__.py
│ ├── __main__.py
│ ├── doxygen.json
│ ├── doxygen.py
│ ├── info.py
│ ├── jupyter.py
│ ├── loader.py
│ ├── matplotlib
│ ├── __init__.py
│ ├── gui.py
│ ├── rendering.h
│ ├── util.py
│ └── widget.py
│ ├── pythonize
│ ├── __init__.py
│ ├── container.py
│ ├── graph_attributes.py
│ ├── render.py
│ └── string.py
│ └── utils.py
└── ui-tests
├── .gitignore
├── .yarnrc.yml
├── README.md
├── jupyter_server_test_config.py
├── package.json
├── playwright.config.js
├── tests
├── callbacks.spec.ts
├── callbacks.spec.ts-snapshots
│ ├── callbacks-static-run-4-cell-4-linux.png
│ ├── callbacks-static-run-5-cell-4-linux.png
│ ├── callbacks-static-run-5-cell-5-linux.png
│ ├── callbacks-static-run-6-cell-4-linux.png
│ ├── callbacks-static-run-6-cell-5-linux.png
│ ├── callbacks-static-run-6-cell-6-linux.png
│ ├── callbacks-static-run-7-cell-4-linux.png
│ ├── callbacks-static-run-7-cell-5-linux.png
│ ├── callbacks-static-run-7-cell-6-linux.png
│ ├── callbacks-static-run-7-cell-7-linux.png
│ ├── callbacks-static-run-8-cell-4-linux.png
│ ├── callbacks-static-run-8-cell-5-linux.png
│ ├── callbacks-static-run-8-cell-6-linux.png
│ ├── callbacks-static-run-8-cell-7-linux.png
│ ├── callbacks-static-run-8-cell-8-linux.png
│ ├── callbacks-static-run-9-cell-4-linux.png
│ ├── callbacks-static-run-9-cell-5-linux.png
│ ├── callbacks-static-run-9-cell-6-linux.png
│ ├── callbacks-static-run-9-cell-7-linux.png
│ ├── callbacks-static-run-9-cell-8-linux.png
│ ├── callbacks-static-run-9-cell-9-linux.png
│ ├── callbacks-widget-run-4-cell-4-linux.png
│ ├── callbacks-widget-run-5-cell-4-linux.png
│ ├── callbacks-widget-run-5-cell-5-linux.png
│ ├── callbacks-widget-run-6-cell-4-linux.png
│ ├── callbacks-widget-run-6-cell-5-linux.png
│ ├── callbacks-widget-run-6-cell-6-linux.png
│ ├── callbacks-widget-run-7-cell-4-linux.png
│ ├── callbacks-widget-run-7-cell-5-linux.png
│ ├── callbacks-widget-run-7-cell-6-linux.png
│ ├── callbacks-widget-run-7-cell-7-linux.png
│ ├── callbacks-widget-run-8-cell-4-linux.png
│ ├── callbacks-widget-run-8-cell-5-linux.png
│ ├── callbacks-widget-run-8-cell-6-linux.png
│ ├── callbacks-widget-run-8-cell-7-linux.png
│ ├── callbacks-widget-run-8-cell-8-linux.png
│ ├── callbacks-widget-run-9-cell-4-linux.png
│ ├── callbacks-widget-run-9-cell-5-linux.png
│ ├── callbacks-widget-run-9-cell-6-linux.png
│ ├── callbacks-widget-run-9-cell-7-linux.png
│ ├── callbacks-widget-run-9-cell-8-linux.png
│ └── callbacks-widget-run-9-cell-9-linux.png
├── interactive.spec.ts
├── interactive.spec.ts-snapshots
│ ├── editor-ui-0-linux.png
│ ├── editor-ui-1-linux.png
│ ├── editor-ui-2-linux.png
│ ├── editor-ui-3-linux.png
│ ├── editor-ui-4-linux.png
│ └── editor-ui-5-linux.png
├── notebooks
│ ├── callbacks.ipynb
│ └── sugiyama-simple.ipynb
├── widget.spec.ts
└── widget.spec.ts-snapshots
│ ├── notebook-cell-1-linux.png
│ ├── notebook-cell-2-linux.png
│ ├── notebook-cell-3-linux.png
│ ├── notebook-cell-4-linux.png
│ ├── notebook-cell-5-linux.png
│ ├── notebook-cell-6-linux.png
│ └── notebook-cell-7-linux.png
├── tsconfig.json
└── yarn.lock
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on: [push]
4 |
5 | #on:
6 | # push:
7 | # branches: main
8 | # pull_request:
9 | # branches: '*'
10 |
11 | jobs:
12 | integration-tests:
13 | name: Playwright UI tests
14 | runs-on: ubuntu-latest
15 |
16 | env:
17 | PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/pw-browsers
18 |
19 | steps:
20 | - name: Checkout
21 | uses: actions/checkout@v3
22 |
23 | - name: Base Setup
24 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
25 |
26 | - name: Install ogdf-python
27 | run: |
28 | set -eux
29 | python -m pip install .[quickstart]
30 |
31 | - name: Install test dependencies
32 | working-directory: ui-tests
33 | env:
34 | YARN_ENABLE_IMMUTABLE_INSTALLS: 0
35 | PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
36 | run: jlpm install
37 |
38 | - name: Set up browser cache
39 | uses: actions/cache@v3
40 | with:
41 | path: |
42 | ${{ github.workspace }}/pw-browsers
43 | key: ${{ runner.os }}-${{ hashFiles('ui-tests/yarn.lock') }}
44 |
45 | - name: Install browser
46 | run: jlpm playwright install chromium
47 | working-directory: ui-tests
48 |
49 | - name: Execute playwright tests
50 | working-directory: ui-tests
51 | run: |
52 | jlpm playwright test
53 |
54 | - name: Upload playwright test report
55 | if: always()
56 | uses: actions/upload-artifact@v3
57 | with:
58 | name: playwright-tests
59 | path: |
60 | ui-tests/test-results
61 | ui-tests/playwright-report
62 |
--------------------------------------------------------------------------------
/.github/workflows/update-integration-tests.yml:
--------------------------------------------------------------------------------
1 | name: Update Playwright Snapshots
2 |
3 | on:
4 | issue_comment:
5 | types: [created, edited]
6 |
7 | permissions:
8 | contents: write
9 | pull-requests: write
10 |
11 | jobs:
12 | update-snapshots:
13 | if: ${{ github.event.issue.pull_request && contains(github.event.comment.body, 'please update playwright snapshots') }}
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - name: Checkout
18 | uses: actions/checkout@v3
19 | with:
20 | token: ${{ secrets.GITHUB_TOKEN }}
21 |
22 | - name: Configure git to use https
23 | run: git config --global hub.protocol https
24 |
25 | - name: Checkout the branch from the PR that triggered the job
26 | run: gh pr checkout ${{ github.event.issue.number }}
27 | env:
28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
29 |
30 | - name: Base Setup
31 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
32 |
33 | - name: Install ogdf-python
34 | run: |
35 | set -eux
36 | python -m pip install .[quickstart]
37 |
38 | - uses: jupyterlab/maintainer-tools/.github/actions/update-snapshots@v1
39 | with:
40 | github_token: ${{ secrets.GITHUB_TOKEN }}
41 | # Playwright knows how to start JupyterLab server
42 | start_server_script: 'null'
43 | test_folder: ui-tests
44 | npm_client: "jlpm"
45 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | cmake-build/
2 | src/ogdf_python/stubs/
3 |
4 | # Created by https://www.toptal.com/developers/gitignore/api/python,c++,cmake
5 | # Edit at https://www.toptal.com/developers/gitignore?templates=python,c++,cmake
6 |
7 | ### C++ ###
8 | # Prerequisites
9 | *.d
10 |
11 | # Compiled Object files
12 | *.slo
13 | *.lo
14 | *.o
15 | *.obj
16 |
17 | # Precompiled Headers
18 | *.gch
19 | *.pch
20 |
21 | # Compiled Dynamic libraries
22 | *.so
23 | *.dylib
24 | *.dll
25 |
26 | # Fortran module files
27 | *.mod
28 | *.smod
29 |
30 | # Compiled Static libraries
31 | *.lai
32 | *.la
33 | *.a
34 | *.lib
35 |
36 | # Executables
37 | *.exe
38 | *.out
39 | *.app
40 |
41 | ### CMake ###
42 | CMakeLists.txt.user
43 | CMakeCache.txt
44 | CMakeFiles
45 | CMakeScripts
46 | Testing
47 | Makefile
48 | cmake_install.cmake
49 | install_manifest.txt
50 | compile_commands.json
51 | CTestTestfile.cmake
52 | _deps
53 |
54 | ### CMake Patch ###
55 | # External projects
56 | *-prefix/
57 |
58 | ### Python ###
59 | # Byte-compiled / optimized / DLL files
60 | __pycache__/
61 | *.py[cod]
62 | *$py.class
63 |
64 | # C extensions
65 |
66 | # Distribution / packaging
67 | .Python
68 | build/
69 | develop-eggs/
70 | dist/
71 | downloads/
72 | eggs/
73 | .eggs/
74 | lib/
75 | lib64/
76 | parts/
77 | sdist/
78 | var/
79 | wheels/
80 | share/python-wheels/
81 | *.egg-info/
82 | .installed.cfg
83 | *.egg
84 | MANIFEST
85 |
86 | # PyInstaller
87 | # Usually these files are written by a python script from a template
88 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
89 | *.manifest
90 | *.spec
91 |
92 | # Installer logs
93 | pip-log.txt
94 | pip-delete-this-directory.txt
95 |
96 | # Unit test / coverage reports
97 | htmlcov/
98 | .tox/
99 | .nox/
100 | .coverage
101 | .coverage.*
102 | .cache
103 | nosetests.xml
104 | coverage.xml
105 | *.cover
106 | *.py,cover
107 | .hypothesis/
108 | .pytest_cache/
109 | cover/
110 |
111 | # Translations
112 | *.mo
113 | *.pot
114 |
115 | # Django stuff:
116 | *.log
117 | local_settings.py
118 | db.sqlite3
119 | db.sqlite3-journal
120 |
121 | # Flask stuff:
122 | instance/
123 | .webassets-cache
124 |
125 | # Scrapy stuff:
126 | .scrapy
127 |
128 | # Sphinx documentation
129 | docs/_build/
130 |
131 | # PyBuilder
132 | .pybuilder/
133 | target/
134 |
135 | # Jupyter Notebook
136 | .ipynb_checkpoints
137 |
138 | # IPython
139 | profile_default/
140 | ipython_config.py
141 |
142 | # pyenv
143 | # For a library or package, you might want to ignore these files since the code is
144 | # intended to run in multiple environments; otherwise, check them in:
145 | # .python-version
146 |
147 | # pipenv
148 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
149 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
150 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
151 | # install all needed dependencies.
152 | #Pipfile.lock
153 |
154 | # poetry
155 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
156 | # This is especially recommended for binary packages to ensure reproducibility, and is more
157 | # commonly ignored for libraries.
158 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
159 | #poetry.lock
160 |
161 | # pdm
162 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
163 | #pdm.lock
164 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
165 | # in version control.
166 | # https://pdm.fming.dev/#use-with-ide
167 | .pdm.toml
168 |
169 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
170 | __pypackages__/
171 |
172 | # Celery stuff
173 | celerybeat-schedule
174 | celerybeat.pid
175 |
176 | # SageMath parsed files
177 | *.sage.py
178 |
179 | # Environments
180 | .env
181 | .venv
182 | env/
183 | venv/
184 | ENV/
185 | env.bak/
186 | venv.bak/
187 |
188 | # Spyder project settings
189 | .spyderproject
190 | .spyproject
191 |
192 | # Rope project settings
193 | .ropeproject
194 |
195 | # mkdocs documentation
196 | /site
197 |
198 | # mypy
199 | .mypy_cache/
200 | .dmypy.json
201 | dmypy.json
202 |
203 | # Pyre type checker
204 | .pyre/
205 |
206 | # pytype static type analyzer
207 | .pytype/
208 |
209 | # Cython debug symbols
210 | cython_debug/
211 |
212 | # PyCharm
213 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
214 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
215 | # and can be added to the global gitignore or merged into this file. For a more nuclear
216 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
217 | .idea/
218 |
219 | # End of https://www.toptal.com/developers/gitignore/api/python,c++,cmake
220 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # Docker container for running OGDF Jupyter notebooks on mybinder.org
2 | # based on https://mybinder.readthedocs.io/en/latest/tutorials/dockerfile.html
3 |
4 | FROM python:3.11
5 |
6 | RUN pip install --no-cache-dir ogdf-python[quickstart]
7 |
8 | ARG NB_USER=jovyan
9 | ARG NB_UID=1000
10 | ENV USER ${NB_USER}
11 | ENV NB_UID ${NB_UID}
12 | ENV HOME /home/${NB_USER}
13 |
14 | RUN adduser --disabled-password \
15 | --gecos "Default user" \
16 | --uid ${NB_UID} \
17 | ${NB_USER}
18 |
19 | COPY . ${HOME}
20 | USER root
21 | RUN chown -R ${NB_UID} ${HOME}
22 | USER ${NB_USER}
23 | WORKDIR ${HOME}
24 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | .. |binder| image:: https://mybinder.org/badge_logo.svg
2 | :target: https://mybinder.org/v2/gh/N-Coder/ogdf-python/HEAD?labpath=docs%2Fexamples%2Fsugiyama-simple.ipynb
3 | .. |(TM)| unicode:: U+2122
4 |
5 | ogdf-python 0.3.5-dev: Automagic Python Bindings for the Open Graph Drawing Framework |binder|
6 | ==============================================================================================
7 |
8 | ``ogdf-python`` uses the `black magic `_
9 | of the awesome `cppyy `_ library to automagically generate python bindings
10 | for the C++ `Open Graph Drawing Framework (OGDF) `_.
11 | It is available for Python>=3.6 and is Apache2 licensed.
12 | There are no binding definitions files, no stuff that needs extra compiling, it just works\ |(TM)|, believe me.
13 | Templates, namespaces, cross-language callbacks and inheritance, pythonic iterators and generators, it's all there.
14 | If you want to learn more about the magic behind the curtains, read `this article `_.
15 |
16 | Useful Links
17 | ------------
18 | `Original repository `_ (GitHub) -
19 | `Bugtracker and issues `_ (GitHub) -
20 | `PyPi package `_ (PyPi ``ogdf-python``) -
21 | `Try it out! `_ (mybinder.org).
22 |
23 | `Official OGDF website `_ (ogdf.net) -
24 | `Public OGDF repository `_ (GitHub) -
25 | `OGDF Documentation `_ (GitHub / Doxygen) -
26 | `cppyy Documentation `_ (Read The Docs).
27 |
28 | ..
29 | `Documentation `_ (Read The Docs)
30 | `Internal OGDF repository `_ (GitLab)
31 |
32 |
33 | Quickstart
34 | ----------
35 |
36 | Click here to start an interactive online Jupyter Notebook with an example OGDF graph where you can try out ``ogdf-python``: |binder|
37 |
38 | Simply re-run the code cell to see the graph. You can also find further examples next to that Notebook (i.e. via the folder icon on the left).
39 | To get a similar Jupyter Notebook with a little more compute power running on your local machine, use the following install command and open the link to ``localhost``/``127.0.0.1`` that will be printed in your browser:
40 |
41 | .. code-block:: bash
42 |
43 | pip install 'ogdf-python[quickstart]'
44 | jupyter lab
45 |
46 | The optional ``[quickstart]`` pulls in matplotlib and jupyter lab as well as a ready-to-use binary build of the OGDF via `ogdf-wheel `_.
47 | Please note that downloading and installing all dependencies (especially building ``cppyy``) may take a moment.
48 | Alternatively, see the instructions `below <#manual-installation>`_ for installing ``ogdf-python`` without this if you want to use your own local build of the OGDF.
49 |
50 | Usage
51 | -----
52 | ``ogdf-python`` works very well with Jupyter:
53 |
54 | .. code-block:: python
55 |
56 | # %matplotlib widget
57 | # uncomment the above line if you want the interactive display
58 |
59 | from ogdf_python import *
60 | cppinclude("ogdf/basic/graph_generators/randomized.h")
61 | cppinclude("ogdf/layered/SugiyamaLayout.h")
62 |
63 | G = ogdf.Graph()
64 | ogdf.setSeed(1)
65 | ogdf.randomPlanarTriconnectedGraph(G, 20, 40)
66 | GA = ogdf.GraphAttributes(G, ogdf.GraphAttributes.all)
67 |
68 | for n in G.nodes:
69 | GA.label[n] = "N%s" % n.index()
70 |
71 | SL = ogdf.SugiyamaLayout()
72 | SL.call(GA)
73 | GA
74 |
75 | .. image:: docs/examples/sugiyama-simple.svg
76 | :target: docs/examples/sugiyama-simple.ipynb
77 | :alt: SugiyamaLayouted Graph
78 | :height: 300 px
79 |
80 | Read the `pitfalls section <#pitfalls>`_ and check out `docs/examples/pitfalls.ipynb `_
81 | for the more advanced Sugiyama example from the OGDF docs.
82 | There is also a bigger example in `docs/examples/ogdf-includes.ipynb `_.
83 | If anything is unclear, check out the python help ``help(ogdf.Graph)`` and read the corresponding OGDF documentation.
84 |
85 | Installation without ogdf-wheel
86 | -------------------------------
87 |
88 | Use pip to install the ``ogdf-python`` package locally on your machine.
89 | Please note that building ``cppyy`` from sources may take a while.
90 | Furthermore, you will need a local shared library build (``-DBUILD_SHARED_LIBS=ON``) of the `OGDF `_.
91 | If you didn't install the OGDF globally on your system,
92 | either set the ``OGDF_INSTALL_DIR`` to the prefix you configured in ``cmake``,
93 | or set ``OGDF_BUILD_DIR`` to the subdirectory of your copy of the OGDF repo where your
94 | `out-of-source build `_ lives.
95 |
96 | .. code-block:: bash
97 |
98 | $ pip install ogdf-python
99 | $ OGDF_BUILD_DIR=~/ogdf/build-debug python3
100 |
101 | Pitfalls
102 | --------
103 |
104 | See also `docs/examples/pitfalls.ipynb `_ for full examples.
105 |
106 | OGDF sometimes takes ownership of objects (usually when they are passed as modules),
107 | which may conflict with the automatic cppyy garbage collection.
108 | Set ``__python_owns__ = False`` on those objects to tell cppyy that those objects
109 | don't need to be garbage collected, but will be cleaned up from the C++ side.
110 |
111 | .. code-block:: python
112 |
113 | SL = ogdf.SugiyamaLayout()
114 | ohl = ogdf.OptimalHierarchyLayout()
115 | ohl.__python_owns__ = False
116 | SL.setLayout(ohl)
117 |
118 | When you overwrite a python variable pointing to a C++ object (and it is the only
119 | python variable pointing to that object), the C++ object will usually be immediately deleted.
120 | This might be a problem if another C++ objects depends on that old object, e.g.
121 | a ``GraphAttributes`` instance depending on a ``Graph`` instance.
122 | Now the other C++ object has a pointer to a deleted and now invalid location,
123 | which will usually cause issues down the road (e.g. when the dependant object is
124 | deleted and wants to deregister from its no longer alive parent).
125 | This overwriting might easily happen if you run a Jupyter cell multiple times or some code in a ``for``-loop.
126 | Please ensure that you always overwrite or delete dependent C++ variables in
127 | the reverse order of their initialization.
128 |
129 | .. code-block:: python
130 |
131 | for i in range(5):
132 | # clean-up all variables
133 | CGA = CG = G = None # note that order is different from C++, CGA will be deleted first, G last
134 | # now we can re-use them
135 | G = ogdf.Graph()
136 | CG = ogdf.ClusterGraph(G)
137 | CGA = ogdf.ClusterGraphAttributes(CG, ogdf.ClusterGraphAttributes.all)
138 |
139 | # alternatively manually clean up in the right order
140 | del CGA
141 | del CG
142 | del G
143 |
144 | There seems to be memory leak in the Jupyter Lab server which causes it to use large amounts of memory
145 | over time while working with ogdf-python. On Linux, the following command can be used to limit this memory usage:
146 |
147 | .. code-block:: bash
148 |
149 | systemd-run --scope -p MemoryMax=5G --user -- jupyter notebook
150 |
--------------------------------------------------------------------------------
/docs/examples/cpp-interaction.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": null,
6 | "id": "twelve-loading",
7 | "metadata": {
8 | "execution": {
9 | "iopub.execute_input": "2021-09-14T12:12:14.814903Z",
10 | "iopub.status.busy": "2021-09-14T12:12:14.814057Z",
11 | "iopub.status.idle": "2021-09-14T12:12:16.920874Z",
12 | "shell.execute_reply": "2021-09-14T12:12:16.920524Z"
13 | }
14 | },
15 | "outputs": [],
16 | "source": [
17 | "%matplotlib widget\n",
18 | "from ogdf_python import *\n",
19 | "from cppyy import gbl as cpp"
20 | ]
21 | },
22 | {
23 | "cell_type": "code",
24 | "execution_count": null,
25 | "id": "knowing-characterization",
26 | "metadata": {
27 | "execution": {
28 | "iopub.execute_input": "2021-09-14T12:12:16.930852Z",
29 | "iopub.status.busy": "2021-09-14T12:12:16.930311Z",
30 | "iopub.status.idle": "2021-09-14T12:12:16.969318Z",
31 | "shell.execute_reply": "2021-09-14T12:12:16.969635Z"
32 | }
33 | },
34 | "outputs": [],
35 | "source": [
36 | "%%cpp\n",
37 | "\n",
38 | "std::cout << \"Hello World!\" << std::endl;\n",
39 | "std::cerr << \"Hello Error!\" << std::endl;\n",
40 | "std::cout << \"Hello All!\\n\";\n",
41 | " \n",
42 | "ogdf::Graph G;"
43 | ]
44 | },
45 | {
46 | "cell_type": "code",
47 | "execution_count": null,
48 | "id": "martial-allergy",
49 | "metadata": {
50 | "execution": {
51 | "iopub.execute_input": "2021-09-14T12:12:16.980660Z",
52 | "iopub.status.busy": "2021-09-14T12:12:16.980124Z",
53 | "iopub.status.idle": "2021-09-14T12:12:17.256498Z",
54 | "shell.execute_reply": "2021-09-14T12:12:17.256156Z"
55 | }
56 | },
57 | "outputs": [],
58 | "source": [
59 | "G = cpp.G\n",
60 | "GA = ogdf.GraphAttributes(G, ogdf.GraphAttributes.all)\n",
61 | "GA.destroyAttributes(ogdf.GraphAttributes.nodeId)\n",
62 | "\n",
63 | "cppinclude(\"ogdf/basic/graph_generators/deterministic.h\")\n",
64 | "width = height = 3\n",
65 | "ogdf.gridGraph(G, width, height, True, False)\n",
66 | "\n",
67 | "for n in G.nodes:\n",
68 | " GA.label[n] = str(n.index())\n",
69 | " GA.x[n] = (n.index() % width) * 50 \n",
70 | " GA.y[n] = (n.index() // height) * 50\n",
71 | "\n",
72 | "middle = G.numberOfNodes() // 2\n",
73 | "GA.width[G.nodes[middle]] = 40\n",
74 | "GA.height[G.nodes[middle]] = 40\n",
75 | " \n",
76 | "GA"
77 | ]
78 | },
79 | {
80 | "cell_type": "code",
81 | "execution_count": null,
82 | "id": "varying-frame",
83 | "metadata": {
84 | "execution": {
85 | "iopub.execute_input": "2021-09-14T12:12:17.270634Z",
86 | "iopub.status.busy": "2021-09-14T12:12:17.270248Z",
87 | "iopub.status.idle": "2021-09-14T12:12:17.272313Z",
88 | "shell.execute_reply": "2021-09-14T12:12:17.271951Z"
89 | }
90 | },
91 | "outputs": [],
92 | "source": [
93 | "%%cpp\n",
94 | "\n",
95 | "std::cout << \"The graph has \" << G.numberOfNodes() << \" nodes\\n\";"
96 | ]
97 | },
98 | {
99 | "cell_type": "code",
100 | "execution_count": null,
101 | "id": "sharing-sugar",
102 | "metadata": {
103 | "execution": {
104 | "iopub.execute_input": "2021-09-14T13:18:50.317866Z",
105 | "iopub.status.busy": "2021-09-14T13:18:50.317492Z",
106 | "iopub.status.idle": "2021-09-14T13:18:50.319354Z",
107 | "shell.execute_reply": "2021-09-14T13:18:50.318988Z"
108 | },
109 | "pycharm": {
110 | "name": "#%%\n"
111 | }
112 | },
113 | "outputs": [],
114 | "source": [
115 | "%%cppdef\n",
116 | "\n",
117 | "// implemented in C++ for efficiency\n",
118 | "int avg_width(const ogdf::GraphAttributes &GA) {\n",
119 | " int sum = 0;\n",
120 | " for (auto n : GA.constGraph().nodes) {\n",
121 | " sum += GA.width(n);\n",
122 | " }\n",
123 | " return sum / GA.constGraph().numberOfNodes();\n",
124 | "}"
125 | ]
126 | },
127 | {
128 | "cell_type": "code",
129 | "execution_count": null,
130 | "id": "brutal-egyptian",
131 | "metadata": {
132 | "execution": {
133 | "iopub.execute_input": "2021-09-14T12:12:17.379275Z",
134 | "iopub.status.busy": "2021-09-14T12:12:17.311586Z",
135 | "iopub.status.idle": "2021-09-14T12:12:17.410719Z",
136 | "shell.execute_reply": "2021-09-14T12:12:17.410412Z"
137 | }
138 | },
139 | "outputs": [],
140 | "source": [
141 | "print(\"The node widths are\", GA.width())\n",
142 | "print(\"The average width is\", cpp.avg_width(GA)) # call your own C++ functions from python\n",
143 | "\n",
144 | "dict(zip(G.nodes, GA.width()))"
145 | ]
146 | },
147 | {
148 | "cell_type": "code",
149 | "execution_count": null,
150 | "id": "covered-effect",
151 | "metadata": {
152 | "execution": {
153 | "iopub.execute_input": "2021-09-14T12:12:17.429846Z",
154 | "iopub.status.busy": "2021-09-14T12:12:17.429334Z",
155 | "iopub.status.idle": "2021-09-14T12:12:17.432465Z",
156 | "shell.execute_reply": "2021-09-14T12:12:17.432081Z"
157 | }
158 | },
159 | "outputs": [],
160 | "source": [
161 | "print(\"Deleting node number %s:\" % middle, repr(G.nodes[middle]))\n",
162 | "G.delNode(G.nodes[middle])\n",
163 | "print(\"Node number %s now is:\" % middle, G.nodes[middle])\n",
164 | "print(\"The last node is:\", G.nodes[-1])\n",
165 | "print(\"The node with the biggest ID is:\", G.nodes.byid(G.maxNodeIndex()))\n",
166 | "\n",
167 | "print(\"The line in the middle is edge\", repr(G.searchEdge(\n",
168 | " G.nodes.byid(width * (height // 2)), \n",
169 | " G.nodes.byid(width * (height // 2) + width - 1))))\n",
170 | "\n",
171 | "GA"
172 | ]
173 | },
174 | {
175 | "cell_type": "code",
176 | "execution_count": null,
177 | "id": "wrapped-mexican",
178 | "metadata": {
179 | "execution": {
180 | "iopub.execute_input": "2021-09-14T12:12:17.439841Z",
181 | "iopub.status.busy": "2021-09-14T12:12:17.439298Z",
182 | "iopub.status.idle": "2021-09-14T12:12:17.444310Z",
183 | "shell.execute_reply": "2021-09-14T12:12:17.443968Z"
184 | }
185 | },
186 | "outputs": [],
187 | "source": [
188 | "# we have python docs and also the doxygen docs linked from there\n",
189 | "help(G)"
190 | ]
191 | }
192 | ],
193 | "metadata": {
194 | "kernelspec": {
195 | "display_name": "ogdf-python",
196 | "language": "python",
197 | "name": "ogdf-python"
198 | },
199 | "language_info": {
200 | "codemirror_mode": {
201 | "name": "ipython",
202 | "version": 3
203 | },
204 | "file_extension": ".py",
205 | "mimetype": "text/x-python",
206 | "name": "python",
207 | "nbconvert_exporter": "python",
208 | "pygments_lexer": "ipython3",
209 | "version": "3.11.4"
210 | }
211 | },
212 | "nbformat": 4,
213 | "nbformat_minor": 5
214 | }
215 |
--------------------------------------------------------------------------------
/docs/examples/interactive-bfs.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": null,
6 | "id": "intense-february",
7 | "metadata": {
8 | "execution": {
9 | "iopub.execute_input": "2021-09-01T10:18:35.146158Z",
10 | "iopub.status.busy": "2021-09-01T10:18:35.140634Z",
11 | "iopub.status.idle": "2021-09-01T10:18:36.611404Z",
12 | "shell.execute_reply": "2021-09-01T10:18:36.611034Z"
13 | }
14 | },
15 | "outputs": [],
16 | "source": [
17 | "# %env OGDF_BUILD_DIR=~/ogdf/build-debug\n",
18 | "# uncomment if you didn't set this globally\n",
19 | "from ogdf_python import ogdf, cppinclude\n",
20 | "\n",
21 | "cppinclude(\"ogdf/basic/graph_generators/randomized.h\")\n",
22 | "cppinclude(\"ogdf/layered/SugiyamaLayout.h\")\n",
23 | "\n",
24 | "G = ogdf.Graph()\n",
25 | "ogdf.setSeed(1)\n",
26 | "ogdf.randomPlanarTriconnectedGraph(G, 20, 40)\n",
27 | "GA = ogdf.GraphAttributes(G, ogdf.GraphAttributes.all)\n",
28 | "GA.directed = False\n",
29 | "\n",
30 | "SL = ogdf.SugiyamaLayout()\n",
31 | "SL.call(GA)\n",
32 | "\n",
33 | "FIRST = 0\n",
34 | "LAST = -1"
35 | ]
36 | },
37 | {
38 | "cell_type": "code",
39 | "execution_count": null,
40 | "id": "veterinary-meeting",
41 | "metadata": {
42 | "execution": {
43 | "iopub.execute_input": "2021-09-01T10:18:36.615207Z",
44 | "iopub.status.busy": "2021-09-01T10:18:36.614835Z",
45 | "iopub.status.idle": "2021-09-01T10:18:36.616478Z",
46 | "shell.execute_reply": "2021-09-01T10:18:36.616144Z"
47 | }
48 | },
49 | "outputs": [],
50 | "source": [
51 | "def search(G, node1, GA=None, mode=FIRST):\n",
52 | " todo = [node1]\n",
53 | " order = ogdf.NodeArray[int](G, -1)\n",
54 | " count = 0\n",
55 | " while len(todo) > 0:\n",
56 | " cur = todo.pop(mode)\n",
57 | " if order[cur] >= 0:\n",
58 | " continue\n",
59 | " \n",
60 | " order[cur] = count\n",
61 | " if GA:\n",
62 | " GA.label[cur] = str(count)\n",
63 | " count += 1\n",
64 | " \n",
65 | " for adj in cur.adjEntries:\n",
66 | " if order[adj.twinNode()] < 0:\n",
67 | " todo.append(adj.twinNode())\n",
68 | " \n",
69 | " yield todo\n",
70 | " return count"
71 | ]
72 | },
73 | {
74 | "cell_type": "code",
75 | "execution_count": null,
76 | "id": "cellular-electricity",
77 | "metadata": {
78 | "execution": {
79 | "iopub.execute_input": "2021-09-01T10:18:36.620527Z",
80 | "iopub.status.busy": "2021-09-01T10:18:36.620198Z",
81 | "iopub.status.idle": "2021-09-01T10:18:36.621925Z",
82 | "shell.execute_reply": "2021-09-01T10:18:36.621599Z"
83 | }
84 | },
85 | "outputs": [],
86 | "source": [
87 | "def dfs(G):\n",
88 | " discovery = ogdf.NodeArray[int](G, -1)\n",
89 | " finish = ogdf.NodeArray[int](G, -1)\n",
90 | " predecessor = ogdf.NodeArray[ogdf.node](G, None)\n",
91 | " \n",
92 | " time = 0\n",
93 | " \n",
94 | " def dfs_visit(u):\n",
95 | " nonlocal time\n",
96 | "\n",
97 | " time += 1\n",
98 | " discovery[u] = time\n",
99 | " yield u, discovery[u], finish[u]\n",
100 | " \n",
101 | " for adj in u.adjEntries:\n",
102 | " v = adj.twinNode()\n",
103 | " if discovery[node] < 0:\n",
104 | " predecessor[v] = u\n",
105 | " yield from dfs_visit(v)\n",
106 | "\n",
107 | " time += 1\n",
108 | " finish[u] = time\n",
109 | " yield u, discovery[u], finish[u]\n",
110 | " \n",
111 | " for node in G.nodes:\n",
112 | " if discovery[node] < 0:\n",
113 | " yield from dfs_visit(node)"
114 | ]
115 | },
116 | {
117 | "cell_type": "code",
118 | "execution_count": null,
119 | "id": "polished-position",
120 | "metadata": {
121 | "execution": {
122 | "iopub.execute_input": "2021-09-01T10:18:36.680987Z",
123 | "iopub.status.busy": "2021-09-01T10:18:36.636849Z",
124 | "iopub.status.idle": "2021-09-01T10:18:36.694363Z",
125 | "shell.execute_reply": "2021-09-01T10:18:36.694679Z"
126 | }
127 | },
128 | "outputs": [],
129 | "source": [
130 | "it = search(G, G.nodes[0], GA, LAST)\n",
131 | "GA"
132 | ]
133 | },
134 | {
135 | "cell_type": "code",
136 | "execution_count": null,
137 | "id": "opponent-weight",
138 | "metadata": {
139 | "execution": {
140 | "iopub.execute_input": "2021-09-01T10:18:36.703220Z",
141 | "iopub.status.busy": "2021-09-01T10:18:36.702548Z",
142 | "iopub.status.idle": "2021-09-01T10:18:36.792123Z",
143 | "shell.execute_reply": "2021-09-01T10:18:36.791758Z"
144 | }
145 | },
146 | "outputs": [],
147 | "source": [
148 | "try:\n",
149 | " print([n.index() for n in next(it)])\n",
150 | "except StopIteration as e:\n",
151 | " print(\"done\", e.args)\n",
152 | "GA"
153 | ]
154 | }
155 | ],
156 | "metadata": {
157 | "kernelspec": {
158 | "display_name": "ogdf-python",
159 | "language": "python",
160 | "name": "ogdf-python"
161 | },
162 | "language_info": {
163 | "codemirror_mode": {
164 | "name": "ipython",
165 | "version": 3
166 | },
167 | "file_extension": ".py",
168 | "mimetype": "text/x-python",
169 | "name": "python",
170 | "nbconvert_exporter": "python",
171 | "pygments_lexer": "ipython3",
172 | "version": "3.9.6"
173 | }
174 | },
175 | "nbformat": 4,
176 | "nbformat_minor": 5
177 | }
178 |
--------------------------------------------------------------------------------
/docs/examples/interactive-dfs-ui.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": null,
6 | "id": "stone-mobility",
7 | "metadata": {},
8 | "outputs": [],
9 | "source": [
10 | "%matplotlib widget\n",
11 | "\n",
12 | "from ogdf_python import *\n",
13 | "\n",
14 | "cppinclude(\"ogdf/basic/graph_generators/randomized.h\")\n",
15 | "cppinclude(\"ogdf/layered/SugiyamaLayout.h\")\n",
16 | "null_node = cppyy.bind_object(cppyy.nullptr, 'ogdf::NodeElement')\n",
17 | "\n",
18 | "G = ogdf.Graph()\n",
19 | "ogdf.setSeed(1)\n",
20 | "ogdf.randomPlanarTriconnectedGraph(G, 10, 20)\n",
21 | "GA = ogdf.GraphAttributes(G, ogdf.GraphAttributes.all)\n",
22 | "GA.directed = True\n",
23 | "\n",
24 | "SL = ogdf.SugiyamaLayout()\n",
25 | "SL.call(GA)\n",
26 | "GA.rotateRight90()\n",
27 | "GA"
28 | ]
29 | },
30 | {
31 | "cell_type": "code",
32 | "execution_count": null,
33 | "id": "environmental-fusion",
34 | "metadata": {},
35 | "outputs": [],
36 | "source": [
37 | "# This is the main DFS code. Compare it with the slides from the lecture!\n",
38 | "\n",
39 | "def dfs(G):\n",
40 | " # NodeArrays are used to store information \"labelling\" individual nodes\n",
41 | " discovery = ogdf.NodeArray[int](G, -1)\n",
42 | " finish = ogdf.NodeArray[int](G, -1)\n",
43 | " predecessor = ogdf.NodeArray[\"ogdf::node\"](G, null_node)\n",
44 | " \n",
45 | " time = 0\n",
46 | " \n",
47 | " def dfs_visit(u):\n",
48 | " nonlocal time # (we need to overwrite this variable from the parent function)\n",
49 | "\n",
50 | " time += 1\n",
51 | " discovery[u] = time\n",
52 | " # yield stops the execution of our method and passes the variables to our caller\n",
53 | " yield u, discovery, finish, predecessor\n",
54 | " # the code will continue here the next time `next(it)` is called\n",
55 | "\n",
56 | " for adj in u.adjEntries:\n",
57 | " v = adj.twinNode()\n",
58 | " if adj.isSource() and discovery[v] < 0:\n",
59 | " predecessor[v] = u\n",
60 | " # yield from simply \"copies over\" all yield statements from the called method\n",
61 | " yield from dfs_visit(v)\n",
62 | "\n",
63 | " time += 1\n",
64 | " finish[u] = time\n",
65 | " # yield again to report the state after\n",
66 | " yield u, discovery, finish, predecessor\n",
67 | " \n",
68 | " for node in G.nodes:\n",
69 | " if discovery[node] < 0:\n",
70 | " yield from dfs_visit(node)"
71 | ]
72 | },
73 | {
74 | "cell_type": "code",
75 | "execution_count": null,
76 | "id": "6f9ec0b9-e4da-41bf-bff6-f2443b48c0a9",
77 | "metadata": {},
78 | "outputs": [],
79 | "source": [
80 | "def goto(steps):\n",
81 | " it = dfs(G)\n",
82 | " try:\n",
83 | " for _ in range(steps):\n",
84 | " last, discovery, finish, predecessor = next(it)\n",
85 | " except StopIteration:\n",
86 | " pass\n",
87 | " \n",
88 | " if steps < 1:\n",
89 | " for u in G.nodes:\n",
90 | " GA.label[u] = \"(-1, -1)\"\n",
91 | " GA.width[u] = 40\n",
92 | " GA.fillColor[u] = ogdf.Color(255,255,255)\n",
93 | " GA.strokeColor[u] = ogdf.Color(230, 230, 230)\n",
94 | " for e in G.edges:\n",
95 | " GA.strokeColor[e] = ogdf.Color(150, 150, 150)\n",
96 | " return\n",
97 | " \n",
98 | " for u in G.nodes:\n",
99 | " d = discovery[u]\n",
100 | " f = finish[u]\n",
101 | " GA.label[u] = \"(%s, %s)\" % (d, f)\n",
102 | " GA.width[u] = 40\n",
103 | " GA.fillColor[u] = ogdf.Color(255,255,255)\n",
104 | " \n",
105 | " if d < 0:\n",
106 | " GA.strokeColor[u] = ogdf.Color(230, 230, 230)\n",
107 | " elif f < 0:\n",
108 | " GA.strokeColor[u] = ogdf.Color(150, 150, 150)\n",
109 | " else:\n",
110 | " GA.strokeColor[u] = ogdf.Color(0, 0, 0)\n",
111 | " \n",
112 | " GA.fillColor[last] = ogdf.Color(ogdf.Color.Name.Lightpink)\n",
113 | " \n",
114 | " for e in G.edges:\n",
115 | " ds = discovery[e.source()]\n",
116 | " fs = finish[e.source()]\n",
117 | " dt = discovery[e.target()]\n",
118 | " ft = finish[e.target()]\n",
119 | " \n",
120 | " if ds < 0:\n",
121 | " GA.strokeColor[e] = ogdf.Color(150, 150, 150)\n",
122 | " elif predecessor[e.target()] == e.source():\n",
123 | " GA.strokeColor[e] = ogdf.Color(ogdf.Color.Name.Darkblue)\n",
124 | " elif dt >= 0 and dt < ds:\n",
125 | " if ft >= 0 and fs > ft:\n",
126 | " GA.strokeColor[e] = ogdf.Color(ogdf.Color.Name.Pink) # FIXME\n",
127 | " else:\n",
128 | " GA.strokeColor[e] = ogdf.Color(ogdf.Color.Name.Lightgreen)\n",
129 | " elif ds < dt:\n",
130 | " GA.strokeColor[e] = ogdf.Color(ogdf.Color.Name.Lightblue)\n",
131 | "\n",
132 | " return GA"
133 | ]
134 | },
135 | {
136 | "cell_type": "code",
137 | "execution_count": null,
138 | "id": "dfa2fc95-c640-4dfc-8e25-43e183e3008a",
139 | "metadata": {},
140 | "outputs": [],
141 | "source": [
142 | "from ipywidgets import *\n",
143 | "from ogdf_python.matplotlib import MatplotlibGraph\n",
144 | "\n",
145 | "goto(0)\n",
146 | "w = MatplotlibGraph(GA)\n",
147 | "conf = dict(\n",
148 | " value=0,\n",
149 | " min=0,\n",
150 | " max=G.numberOfNodes() * 2,\n",
151 | " step=1)\n",
152 | "play = widgets.Play(**conf, interval=1000)\n",
153 | "slider = widgets.IntSlider(**conf)\n",
154 | "widgets.jslink((play, 'value'), (slider, 'value'))\n",
155 | "button = widgets.Button(\n",
156 | " description='New Graph'\n",
157 | ")\n",
158 | "\n",
159 | "\n",
160 | "def on_value_change(change):\n",
161 | " goto(slider.value)\n",
162 | " for na in w.nodes.values():\n",
163 | " na.update_attributes(GA)\n",
164 | " for ea in w.edges.values():\n",
165 | " ea.update_attributes(GA)\n",
166 | "\n",
167 | "slider.observe(on_value_change, names='value')\n",
168 | "\n",
169 | "display(VBox([\n",
170 | " HBox([play, slider, button]),\n",
171 | " w.ax.figure.canvas\n",
172 | "]))"
173 | ]
174 | },
175 | {
176 | "cell_type": "code",
177 | "execution_count": null,
178 | "id": "53b285c9-8146-4635-85b1-0f6353f52213",
179 | "metadata": {},
180 | "outputs": [],
181 | "source": [
182 | "display(\n",
183 | "MatplotlibGraph(GA)\\\n",
184 | " .ax.figure.canvas\n",
185 | ")"
186 | ]
187 | },
188 | {
189 | "cell_type": "code",
190 | "execution_count": null,
191 | "id": "cb971f61-0326-4e51-83c4-adc30ae79d9f",
192 | "metadata": {},
193 | "outputs": [],
194 | "source": [
195 | "import os\n",
196 | "os.getpid()"
197 | ]
198 | }
199 | ],
200 | "metadata": {
201 | "kernelspec": {
202 | "display_name": "ogdf-python",
203 | "language": "python",
204 | "name": "ogdf-python"
205 | },
206 | "language_info": {
207 | "codemirror_mode": {
208 | "name": "ipython",
209 | "version": 3
210 | },
211 | "file_extension": ".py",
212 | "mimetype": "text/x-python",
213 | "name": "python",
214 | "nbconvert_exporter": "python",
215 | "pygments_lexer": "ipython3",
216 | "version": "3.11.5"
217 | }
218 | },
219 | "nbformat": 4,
220 | "nbformat_minor": 5
221 | }
222 |
--------------------------------------------------------------------------------
/docs/examples/interactive-dfs.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": null,
6 | "id": "stone-mobility",
7 | "metadata": {},
8 | "outputs": [],
9 | "source": [
10 | "# This cell generates a random graph for the DFS to run on.\n",
11 | "\n",
12 | "import cppyy\n",
13 | "\n",
14 | "# %env OGDF_BUILD_DIR=~/ogdf/build-debug\n",
15 | "# uncomment if you didn't set this globally\n",
16 | "from ogdf_python import ogdf, cppinclude\n",
17 | "\n",
18 | "cppinclude(\"ogdf/basic/graph_generators/randomized.h\")\n",
19 | "cppinclude(\"ogdf/layered/SugiyamaLayout.h\")\n",
20 | "null_node = cppyy.bind_object(cppyy.nullptr, 'ogdf::NodeElement')\n",
21 | "\n",
22 | "G = ogdf.Graph()\n",
23 | "ogdf.setSeed(1)\n",
24 | "ogdf.randomPlanarTriconnectedGraph(G, 10, 20)\n",
25 | "GA = ogdf.GraphAttributes(G, ogdf.GraphAttributes.all)\n",
26 | "GA.directed = True\n",
27 | "\n",
28 | "SL = ogdf.SugiyamaLayout()\n",
29 | "SL.call(GA)\n",
30 | "GA.rotateRight90()"
31 | ]
32 | },
33 | {
34 | "cell_type": "code",
35 | "execution_count": null,
36 | "id": "environmental-fusion",
37 | "metadata": {},
38 | "outputs": [],
39 | "source": [
40 | "# This is the main DFS code. Compare it with the slides from the lecture!\n",
41 | "\n",
42 | "def dfs(G):\n",
43 | " # NodeArrays are used to store information \"labelling\" individual nodes\n",
44 | " discovery = ogdf.NodeArray[int](G, -1)\n",
45 | " finish = ogdf.NodeArray[int](G, -1)\n",
46 | " predecessor = ogdf.NodeArray[\"ogdf::node\"](G, null_node)\n",
47 | " \n",
48 | " time = 0\n",
49 | " \n",
50 | " def dfs_visit(u):\n",
51 | " nonlocal time # (we need to overwrite this variable from the parent function)\n",
52 | "\n",
53 | " time += 1\n",
54 | " discovery[u] = time\n",
55 | " # yield stops the execution of our method and passes the variables to our caller\n",
56 | " yield u, discovery, finish, predecessor\n",
57 | " # the code will continue here the next time `next(it)` is called\n",
58 | "\n",
59 | " for adj in u.adjEntries:\n",
60 | " v = adj.twinNode()\n",
61 | " if adj.isSource() and discovery[v] < 0:\n",
62 | " predecessor[v] = u\n",
63 | " # yield from simply \"copies over\" all yield statements from the called method\n",
64 | " yield from dfs_visit(v)\n",
65 | "\n",
66 | " time += 1\n",
67 | " finish[u] = time\n",
68 | " # yield again to report the state after\n",
69 | " yield u, discovery, finish, predecessor\n",
70 | " \n",
71 | " for node in G.nodes:\n",
72 | " if discovery[node] < 0:\n",
73 | " yield from dfs_visit(node)"
74 | ]
75 | },
76 | {
77 | "cell_type": "code",
78 | "execution_count": null,
79 | "id": "productive-control",
80 | "metadata": {},
81 | "outputs": [],
82 | "source": [
83 | "# This cell (re-)starts the DFS and (re-)initializes the drawing of the graph\n",
84 | "\n",
85 | "last = None\n",
86 | "for u in G.nodes:\n",
87 | " GA.label[u] = \"\"\n",
88 | " GA.strokeColor[u] = ogdf.Color(230, 230, 230)\n",
89 | " GA.width[u] = 40\n",
90 | " GA.fillColor[u] = ogdf.Color(ogdf.Color.Name.White)\n",
91 | "for e in G.edges:\n",
92 | " GA.strokeColor[e] = ogdf.Color(150, 150, 150)\n",
93 | "it = dfs(G)\n",
94 | "GA"
95 | ]
96 | },
97 | {
98 | "cell_type": "code",
99 | "execution_count": null,
100 | "id": "representative-edwards",
101 | "metadata": {
102 | "pycharm": {
103 | "name": "#%%\n"
104 | }
105 | },
106 | "outputs": [],
107 | "source": [
108 | "# This method executes one DFS step and then visualizes the current state\n",
109 | "\n",
110 | "def make_step():\n",
111 | " global it, last\n",
112 | " try:\n",
113 | " # !!! This is the important line:\n",
114 | " u, discovery, finish, predecessor = next(it)\n",
115 | " # All the following code is just for updating the visualisation\n",
116 | " d = discovery[u]\n",
117 | " f = finish[u]\n",
118 | " GA.label[u] = \"(%s, %s)\" % (d, f)\n",
119 | " print(GA.label[u])\n",
120 | "\n",
121 | " if f < 0:\n",
122 | " GA.strokeColor[u] = ogdf.Color(150, 150, 150)\n",
123 | " else:\n",
124 | " GA.strokeColor[u] = ogdf.Color(0, 0, 0)\n",
125 | "\n",
126 | " for adj in u.adjEntries:\n",
127 | " e = adj.theEdge()\n",
128 | " ds = discovery[e.source()]\n",
129 | " fs = finish[e.source()]\n",
130 | " dt = discovery[e.target()]\n",
131 | " ft = finish[e.target()]\n",
132 | "\n",
133 | " if ds < 0:\n",
134 | " continue\n",
135 | " elif predecessor[e.target()] == e.source():\n",
136 | " GA.strokeColor[e] = ogdf.Color(ogdf.Color.Name.Darkblue)\n",
137 | " elif dt >= 0 and dt < ds:\n",
138 | " if ft >= 0 and fs > ft:\n",
139 | " GA.strokeColor[e] = ogdf.Color(ogdf.Color.Name.Pink) # FIXME\n",
140 | " else:\n",
141 | " GA.strokeColor[e] = ogdf.Color(ogdf.Color.Name.Lightgreen)\n",
142 | " elif ds < dt:\n",
143 | " GA.strokeColor[e] = ogdf.Color(ogdf.Color.Name.Lightblue)\n",
144 | "\n",
145 | " if last is not None:\n",
146 | " GA.fillColor[last] = ogdf.Color(255,255,255)\n",
147 | " GA.fillColor[u] = ogdf.Color(ogdf.Color.Name.Lightpink)\n",
148 | " last = u\n",
149 | "\n",
150 | " except StopIteration as e:\n",
151 | " print(\"done\", e.args)\n",
152 | " return GA"
153 | ]
154 | },
155 | {
156 | "cell_type": "code",
157 | "execution_count": null,
158 | "id": "right-scientist",
159 | "metadata": {
160 | "jupyter": {
161 | "outputs_hidden": false
162 | },
163 | "pycharm": {
164 | "name": "#%%\n"
165 | }
166 | },
167 | "outputs": [],
168 | "source": [
169 | "make_step()\n",
170 | "\n",
171 | "# You can also run this cell multiple times to see the graph being updated\n",
172 | "# For the static version of this notebook I included the next steps separately"
173 | ]
174 | },
175 | {
176 | "cell_type": "code",
177 | "execution_count": null,
178 | "id": "taken-topic",
179 | "metadata": {
180 | "jupyter": {
181 | "outputs_hidden": false
182 | },
183 | "pycharm": {
184 | "name": "#%%\n"
185 | }
186 | },
187 | "outputs": [],
188 | "source": [
189 | "make_step()"
190 | ]
191 | },
192 | {
193 | "cell_type": "code",
194 | "execution_count": null,
195 | "id": "clinical-assignment",
196 | "metadata": {
197 | "jupyter": {
198 | "outputs_hidden": false
199 | },
200 | "pycharm": {
201 | "name": "#%%\n"
202 | }
203 | },
204 | "outputs": [],
205 | "source": [
206 | "make_step()"
207 | ]
208 | },
209 | {
210 | "cell_type": "code",
211 | "execution_count": null,
212 | "id": "universal-anaheim",
213 | "metadata": {
214 | "jupyter": {
215 | "outputs_hidden": false
216 | },
217 | "pycharm": {
218 | "name": "#%%\n"
219 | }
220 | },
221 | "outputs": [],
222 | "source": [
223 | "make_step()"
224 | ]
225 | },
226 | {
227 | "cell_type": "code",
228 | "execution_count": null,
229 | "id": "white-klein",
230 | "metadata": {
231 | "jupyter": {
232 | "outputs_hidden": false
233 | },
234 | "pycharm": {
235 | "name": "#%%\n"
236 | }
237 | },
238 | "outputs": [],
239 | "source": [
240 | "make_step()"
241 | ]
242 | },
243 | {
244 | "cell_type": "code",
245 | "execution_count": null,
246 | "id": "balanced-barrel",
247 | "metadata": {
248 | "jupyter": {
249 | "outputs_hidden": false
250 | },
251 | "pycharm": {
252 | "name": "#%%\n"
253 | }
254 | },
255 | "outputs": [],
256 | "source": [
257 | "make_step()"
258 | ]
259 | },
260 | {
261 | "cell_type": "code",
262 | "execution_count": null,
263 | "id": "conditional-disability",
264 | "metadata": {
265 | "jupyter": {
266 | "outputs_hidden": false
267 | },
268 | "pycharm": {
269 | "name": "#%%\n"
270 | }
271 | },
272 | "outputs": [],
273 | "source": [
274 | "make_step()"
275 | ]
276 | },
277 | {
278 | "cell_type": "code",
279 | "execution_count": null,
280 | "id": "greenhouse-cincinnati",
281 | "metadata": {
282 | "jupyter": {
283 | "outputs_hidden": false
284 | },
285 | "pycharm": {
286 | "name": "#%%\n"
287 | }
288 | },
289 | "outputs": [],
290 | "source": [
291 | "make_step()"
292 | ]
293 | },
294 | {
295 | "cell_type": "code",
296 | "execution_count": null,
297 | "id": "8527db6e-3672-47e4-a188-8055af8738f8",
298 | "metadata": {},
299 | "outputs": [],
300 | "source": [
301 | "help(G)\n"
302 | ]
303 | },
304 | {
305 | "cell_type": "code",
306 | "execution_count": null,
307 | "id": "121c3f1f-05fd-4646-bbb9-28a8bb4e68a0",
308 | "metadata": {},
309 | "outputs": [],
310 | "source": [
311 | "from dfs_widget import DFSWidget\n",
312 | "import ipywidgets as widgets\n",
313 | "\n",
314 | "dfs = DFSWidget()\n",
315 | "\n",
316 | "play = widgets.Play(max=G.numberOfNodes() * 2, interval=1000)\n",
317 | "slider = widgets.IntSlider()\n",
318 | "widgets.jslink((play, 'value'), (slider, 'value'))\n",
319 | "slider.observe(dfs.change_step)\n",
320 | "\n",
321 | "button = widgets.Button(description='New Graph')\n",
322 | "button.on_click(dfs.random_graph)\n",
323 | "\n",
324 | "display(widgets.VBox([\n",
325 | " widgets.HBox([play, slider, button]),\n",
326 | " dfs\n",
327 | "])"
328 | ]
329 | }
330 | ],
331 | "metadata": {
332 | "kernelspec": {
333 | "display_name": "ogdf-python",
334 | "language": "python",
335 | "name": "ogdf-python"
336 | },
337 | "language_info": {
338 | "codemirror_mode": {
339 | "name": "ipython",
340 | "version": 3
341 | },
342 | "file_extension": ".py",
343 | "mimetype": "text/x-python",
344 | "name": "python",
345 | "nbconvert_exporter": "python",
346 | "pygments_lexer": "ipython3",
347 | "version": "3.11.4"
348 | }
349 | },
350 | "nbformat": 4,
351 | "nbformat_minor": 5
352 | }
353 |
--------------------------------------------------------------------------------
/docs/examples/layouts.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": null,
6 | "id": "7cda12ee-a55f-498a-8c94-fae556a53778",
7 | "metadata": {},
8 | "outputs": [],
9 | "source": [
10 | "%matplotlib widget\n",
11 | "\n",
12 | "from ogdf_python import *\n",
13 | "\n",
14 | "# from ogdf_python.doxygen import find_include\n",
15 | "# {l: find_include(\"ogdf\", *l.split(\".\")) for l in layouts}\n",
16 | "\n",
17 | "layouts = {\n",
18 | " 'BalloonLayout': 'ogdf/misclayout/BalloonLayout.h',\n",
19 | " 'BertaultLayout': 'ogdf/misclayout/BertaultLayout.h',\n",
20 | " 'CircularLayout': 'ogdf/misclayout/CircularLayout.h',\n",
21 | "# 'ComponentSplitterLayout': 'ogdf/packing/ComponentSplitterLayout.h',\n",
22 | " 'DavidsonHarelLayout': 'ogdf/energybased/DavidsonHarelLayout.h',\n",
23 | " 'DominanceLayout': 'ogdf/upward/DominanceLayout.h', # fails\n",
24 | " 'DTreeMultilevelEmbedder2D': 'ogdf/energybased/DTreeMultilevelEmbedder.h',\n",
25 | " 'DTreeMultilevelEmbedder3D': 'ogdf/energybased/DTreeMultilevelEmbedder.h',\n",
26 | " 'FastMultipoleEmbedder': 'ogdf/energybased/FastMultipoleEmbedder.h',\n",
27 | " 'FastMultipoleMultilevelEmbedder': 'ogdf/energybased/FastMultipoleEmbedder.h',\n",
28 | " 'FMMMLayout': 'ogdf/energybased/FMMMLayout.h',\n",
29 | "# 'ForceLayoutModule': 'ogdf/energybased/ForceLayoutModule.h',\n",
30 | " 'FPPLayout': 'ogdf/planarlayout/FPPLayout.h',\n",
31 | " 'GEMLayout': 'ogdf/energybased/GEMLayout.h',\n",
32 | "# 'GridLayoutModule': 'ogdf/planarlayout/GridLayoutModule.h',\n",
33 | " 'LinearLayout': 'ogdf/misclayout/LinearLayout.h',\n",
34 | " 'MixedModelLayout': 'ogdf/planarlayout/MixedModelLayout.h', # fails\n",
35 | " 'ModularMultilevelMixer': 'ogdf/energybased/multilevel_mixer/ModularMultilevelMixer.h',\n",
36 | " 'MultilevelLayout': 'ogdf/energybased/MultilevelLayout.h',\n",
37 | "# 'MultilevelLayoutModule': 'ogdf/energybased/multilevel_mixer/MultilevelLayoutModule.h',\n",
38 | " 'NodeRespecterLayout': 'ogdf/energybased/NodeRespecterLayout.h',\n",
39 | " 'PivotMDS': 'ogdf/energybased/PivotMDS.h',\n",
40 | " 'PlanarDrawLayout': 'ogdf/planarlayout/PlanarDrawLayout.h',\n",
41 | " 'PlanarizationGridLayout': 'ogdf/planarity/PlanarizationGridLayout.h',\n",
42 | " 'PlanarizationLayout': 'ogdf/planarity/PlanarizationLayout.h',\n",
43 | " 'PlanarStraightLayout': 'ogdf/planarlayout/PlanarStraightLayout.h',\n",
44 | "# 'PreprocessorLayout': 'ogdf/basic/PreprocessorLayout.h',\n",
45 | "# 'ProcrustesSubLayout': 'ogdf/misclayout/ProcrustesSubLayout.h',\n",
46 | " 'RadialTreeLayout': 'ogdf/tree/RadialTreeLayout.h',\n",
47 | "# 'ScalingLayout': 'ogdf/energybased/multilevel_mixer/ScalingLayout.h',\n",
48 | " 'SchnyderLayout': 'ogdf/planarlayout/SchnyderLayout.h',\n",
49 | "# 'SimpleCCPacker': 'ogdf/packing/SimpleCCPacker.h',\n",
50 | "# 'spring_embedder.SpringEmbedderBase': 'ogdf/energybased/spring_embedder/SpringEmbedderBase.h',\n",
51 | " 'SpringEmbedderFRExact': 'ogdf/energybased/SpringEmbedderFRExact.h',\n",
52 | " 'SpringEmbedderGridVariant': 'ogdf/energybased/SpringEmbedderGridVariant.h',\n",
53 | " 'SpringEmbedderKK': 'ogdf/energybased/SpringEmbedderKK.h',\n",
54 | " 'StressMinimization': 'ogdf/energybased/StressMinimization.h',\n",
55 | " 'SugiyamaLayout': 'ogdf/layered/SugiyamaLayout.h',\n",
56 | " 'TreeLayout': 'ogdf/tree/TreeLayout.h',\n",
57 | " 'TutteLayout': 'ogdf/energybased/TutteLayout.h',\n",
58 | " 'UpwardPlanarizationLayout': 'ogdf/upward/UpwardPlanarizationLayout.h', # fails\n",
59 | " 'VisibilityLayout': 'ogdf/upward/VisibilityLayout.h' # fails\n",
60 | "}"
61 | ]
62 | },
63 | {
64 | "cell_type": "code",
65 | "execution_count": null,
66 | "id": "3d266dd0-6c53-4109-9f67-9329edd6bc4c",
67 | "metadata": {},
68 | "outputs": [],
69 | "source": [
70 | "cppinclude(\"ogdf/basic/graph_generators/randomized.h\")\n",
71 | "cppinclude(\"ogdf/layered/SugiyamaLayout.h\")\n",
72 | "\n",
73 | "G = ogdf.Graph()\n",
74 | "H = ogdf.Graph()\n",
75 | "ogdf.setSeed(1)\n",
76 | "ogdf.randomPlanarCNBGraph(H, 20, 40, 3)\n",
77 | "G.insert(H)\n",
78 | "ogdf.randomPlanarCNBGraph(H, 10, 20, 3)\n",
79 | "G.insert(H)\n",
80 | "GA = ogdf.GraphAttributes(G, ogdf.GraphAttributes.all)\n",
81 | "\n",
82 | "for n in G.nodes:\n",
83 | " GA.label[n] = \"N%s\" % n.index()\n",
84 | "\n",
85 | "SL = ogdf.SugiyamaLayout()\n",
86 | "SL.call(GA)\n",
87 | "#GA"
88 | ]
89 | },
90 | {
91 | "cell_type": "code",
92 | "execution_count": null,
93 | "id": "a245863e-55e1-4652-a66c-a46fab330c10",
94 | "metadata": {},
95 | "outputs": [],
96 | "source": [
97 | "import ipywidgets as ipyw\n",
98 | "import textwrap\n",
99 | "select = ipyw.Dropdown(options=layouts.keys(), value=\"SugiyamaLayout\")\n",
100 | "widget = MatplotlibGraph(GA)\n",
101 | "info = ipyw.HTML()\n",
102 | "\n",
103 | "def set_layout(_):\n",
104 | " info.value = f\"Computing...\"\n",
105 | " l = select.value\n",
106 | " print(l, layouts[l])\n",
107 | " print(cppinclude(layouts[l]))\n",
108 | " L = getattr(ogdf, l)()\n",
109 | " GA.clearAllBends()\n",
110 | " try:\n",
111 | " L.call(GA)\n",
112 | " except (ogdf.AssertionFailed, TypeError) as e:\n",
113 | " try:\n",
114 | " if \"OGDF assertion `isConnected(\" in str(e):\n",
115 | " cppinclude(\"ogdf/packing/ComponentSplitterLayout.h\")\n",
116 | " L2 = ogdf.ComponentSplitterLayout()\n",
117 | " L2.setLayoutModule(L)\n",
118 | " L.__python_owns__ = False\n",
119 | " L2.call(GA)\n",
120 | " else:\n",
121 | " raise\n",
122 | " except Exception as e:\n",
123 | " info.value = f\"Failed ({textwrap.shorten(str(e), width=100, placeholder='...')})\"\n",
124 | " raise\n",
125 | " info.value = f\"Success (Docs)\"\n",
126 | " widget.update_all(GA)\n",
127 | " widget.ax.relim()\n",
128 | " widget.ax.autoscale()\n",
129 | " widget.ax.figure.canvas.draw_idle()\n",
130 | " \n",
131 | "\n",
132 | "select.observe(set_layout, names='value')\n",
133 | "\n",
134 | "display(ipyw.VBox([ipyw.HBox([select, info]), widget.ax.figure.canvas]))"
135 | ]
136 | },
137 | {
138 | "cell_type": "code",
139 | "execution_count": null,
140 | "id": "4529ed69-d7d0-49a4-aff4-3dffd176ab61",
141 | "metadata": {},
142 | "outputs": [],
143 | "source": [
144 | "cppinclude(\"ogdf/energybased/TutteLayout.h\")\n",
145 | "cppinclude(\"ogdf/basic/simple_graph_alg.h\")\n",
146 | "cppinclude(\"ogdf/basic/extended_graph_alg.h\")\n",
147 | "L = ogdf.TutteLayout()\n",
148 | "GC = ogdf.GraphCopy(G)\n",
149 | "GCA = ogdf.GraphAttributes(GC, ogdf.GraphAttributes.all)\n",
150 | "ogdf.makeConnected(GC)\n",
151 | "ogdf.planarEmbed(GC)\n",
152 | "ogdf.triangulate(GC)\n",
153 | "L.call(GCA)\n",
154 | "GCA.transferToOriginal(GA)\n",
155 | "GA"
156 | ]
157 | }
158 | ],
159 | "metadata": {
160 | "kernelspec": {
161 | "display_name": "Python 3 (ipykernel)",
162 | "language": "python",
163 | "name": "python3"
164 | },
165 | "language_info": {
166 | "codemirror_mode": {
167 | "name": "ipython",
168 | "version": 3
169 | },
170 | "file_extension": ".py",
171 | "mimetype": "text/x-python",
172 | "name": "python",
173 | "nbconvert_exporter": "python",
174 | "pygments_lexer": "ipython3",
175 | "version": "3.11.5"
176 | }
177 | },
178 | "nbformat": 4,
179 | "nbformat_minor": 5
180 | }
181 |
--------------------------------------------------------------------------------
/docs/examples/matplotlib.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": null,
6 | "id": "0e5593ca-a5ce-48ba-89df-b52dffeffa3b",
7 | "metadata": {
8 | "ExecuteTime": {
9 | "end_time": "2023-09-01T18:15:47.288478779Z",
10 | "start_time": "2023-09-01T18:15:41.848409531Z"
11 | }
12 | },
13 | "outputs": [],
14 | "source": [
15 | "%matplotlib widget\n",
16 | "\n",
17 | "from ogdf_python import *\n",
18 | "\n",
19 | "cppinclude(\"ogdf/basic/graph_generators/randomized.h\")\n",
20 | "cppinclude(\"ogdf/layered/SugiyamaLayout.h\")\n",
21 | "\n",
22 | "G = ogdf.Graph()\n",
23 | "ogdf.setSeed(1)\n",
24 | "ogdf.randomPlanarTriconnectedGraph(G, 20, 40)\n",
25 | "GA = ogdf.GraphAttributes(G, ogdf.GraphAttributes.all)\n",
26 | "\n",
27 | "for n in G.nodes:\n",
28 | " GA.label[n] = \"N%s\" % n.index()\n",
29 | "\n",
30 | "SL = ogdf.SugiyamaLayout()\n",
31 | "SL.call(GA)"
32 | ]
33 | },
34 | {
35 | "cell_type": "code",
36 | "execution_count": null,
37 | "id": "262f5725-0d97-4c41-ab61-c199e6aca3fa",
38 | "metadata": {},
39 | "outputs": [],
40 | "source": [
41 | "# for i in range(10):\n",
42 | "# GA.width[G.nodes[i]] = i * 10\n",
43 | "# GA.height[G.nodes[i]] = i * 10\n",
44 | "# GA.shape[G.nodes[i]] = i\n",
45 | "# GA.strokeColor[G.nodes[i]] = ogdf.Color(ogdf.Color.Name(i))\n",
46 | "# GA.fillColor[G.nodes[i]] = ogdf.Color(ogdf.Color.Name(i+10))\n",
47 | "# GA.fillPattern[G.nodes[i]] = i\n",
48 | "# GA.shape[G.nodes[11]] = ogdf.Shape.RoundedRect\n",
49 | "# GA.width[G.nodes[11]] = 100\n",
50 | "# GA.height[G.nodes[11]] = 100"
51 | ]
52 | },
53 | {
54 | "cell_type": "code",
55 | "execution_count": null,
56 | "id": "f1651592-571c-4c3e-a5e0-93c550fe7031",
57 | "metadata": {},
58 | "outputs": [],
59 | "source": [
60 | "GE = GraphEditorLayout(GA)\n",
61 | "display(GE)\n",
62 | "# click so select a node or edge\n",
63 | "# [del] deletes the selected object\n",
64 | "# [ctrl]+click on a node while another node is selected adds an edge\n",
65 | "# double click on the background adds a node"
66 | ]
67 | },
68 | {
69 | "cell_type": "code",
70 | "execution_count": null,
71 | "id": "1e3024be-8228-4f94-b831-1037b3904b8b",
72 | "metadata": {},
73 | "outputs": [],
74 | "source": [
75 | "# import matplotlib.pyplot as plt\n",
76 | "# oldpoints = None\n",
77 | "\n",
78 | "# def click(event):\n",
79 | "# global oldpoints\n",
80 | "# x = ogdf.DPoint(event.xdata, event.ydata)\n",
81 | "# out = ogdf.DPoint()\n",
82 | "# xs = []\n",
83 | "# ys = []\n",
84 | "# for ea in GE.widget.edges.values():\n",
85 | "# d = ogdf.closestPointOnLine(ea.poly, x, out)\n",
86 | "# xs.append(out.m_x)\n",
87 | "# ys.append(out.m_y)\n",
88 | "# # print(ea.edge.index(), d)\n",
89 | "# if oldpoints:\n",
90 | "# oldpoints[0].remove()\n",
91 | "# oldpoints = plt.plot(xs, ys, 'ro', zorder=500)\n",
92 | "\n",
93 | "# GE.widget.on_background_click = click"
94 | ]
95 | }
96 | ],
97 | "metadata": {
98 | "kernelspec": {
99 | "display_name": "Python 3 (ipykernel)",
100 | "language": "python",
101 | "name": "python3"
102 | },
103 | "language_info": {
104 | "codemirror_mode": {
105 | "name": "ipython",
106 | "version": 3
107 | },
108 | "file_extension": ".py",
109 | "mimetype": "text/x-python",
110 | "name": "python",
111 | "nbconvert_exporter": "python",
112 | "pygments_lexer": "ipython3",
113 | "version": "3.11.5"
114 | }
115 | },
116 | "nbformat": 4,
117 | "nbformat_minor": 5
118 | }
119 |
--------------------------------------------------------------------------------
/docs/examples/ogdf-includes.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": null,
6 | "metadata": {
7 | "ExecuteTime": {
8 | "end_time": "2023-09-21T08:36:00.074116426Z",
9 | "start_time": "2023-09-21T08:36:00.032105573Z"
10 | }
11 | },
12 | "outputs": [],
13 | "source": [
14 | "%matplotlib widget\n",
15 | "\n",
16 | "import os\n",
17 | "import re\n",
18 | "from collections import defaultdict\n",
19 | "\n",
20 | "from ogdf_python import *"
21 | ]
22 | },
23 | {
24 | "cell_type": "code",
25 | "execution_count": null,
26 | "metadata": {
27 | "ExecuteTime": {
28 | "end_time": "2023-09-20T21:37:12.874505060Z",
29 | "start_time": "2023-09-20T21:37:11.330315417Z"
30 | }
31 | },
32 | "outputs": [],
33 | "source": [
34 | "DIR = get_ogdf_include_path()\n",
35 | "\n",
36 | "HEADERS = [\n",
37 | " os.path.relpath(os.path.join(root, file), DIR)\n",
38 | " for root, dirs, files in os.walk(DIR)\n",
39 | " for file in files\n",
40 | "]\n",
41 | "CGA = CG = G = None\n",
42 | "G = ogdf.Graph()\n",
43 | "CG = ogdf.ClusterGraph(G)\n",
44 | "CG.setUpdateDepth(True)\n",
45 | "CGA = ogdf.ClusterGraphAttributes(CG, ogdf.ClusterGraphAttributes.all)\n",
46 | "\n",
47 | "NODES = {h: G.newNode() for h in HEADERS}\n",
48 | "UNKNOWN = defaultdict(list)\n",
49 | "CLUSTERS = {}\n",
50 | "\n",
51 | "for header, node in NODES.items():\n",
52 | " CGA.label[node] = header\n",
53 | " CGA.width[node] = len(header) * 5\n",
54 | "\n",
55 | " parents = os.path.dirname(header).split(os.sep)\n",
56 | " cluster_path = cluster = None\n",
57 | " for i in range(1, len(parents) + 1):\n",
58 | " cluster_path = os.sep.join(parents[:i])\n",
59 | " if cluster_path not in CLUSTERS:\n",
60 | " cluster = CLUSTERS[cluster_path] = CG.createEmptyCluster(cluster or nullptr)\n",
61 | " CGA.label[cluster] = cluster_path\n",
62 | " CGA.strokeType[cluster] = ogdf.StrokeType.Dash\n",
63 | " else:\n",
64 | " cluster = CLUSTERS[cluster_path]\n",
65 | " if cluster is not None:\n",
66 | " CG.reassignNode(node, cluster)\n",
67 | "\n",
68 | " with open(os.path.join(DIR, header)) as f:\n",
69 | " for match in re.finditer(\"#include\\s+([\\\"<])(.*)([\\\">])\", f.read()):\n",
70 | " include = match.group(2)\n",
71 | " include_rel = os.path.join(os.path.dirname(header), include)\n",
72 | " if include in NODES:\n",
73 | " e = G.newEdge(node, NODES[include])\n",
74 | " elif include_rel in NODES and match.group(1) == \"\\\"\":\n",
75 | " e = G.newEdge(node, NODES[include_rel])\n",
76 | " else:\n",
77 | " UNKNOWN[match.group(1) + include + match.group(3)].append(header)\n",
78 | "\n",
79 | "print(G.numberOfNodes(), G.numberOfEdges(), CG.numberOfClusters(), CG.treeDepth())"
80 | ]
81 | },
82 | {
83 | "cell_type": "code",
84 | "execution_count": null,
85 | "metadata": {
86 | "pycharm": {
87 | "name": "#%%\n"
88 | }
89 | },
90 | "outputs": [],
91 | "source": [
92 | "cppinclude(\"ogdf/layered/SugiyamaLayout.h\")\n",
93 | "cppinclude(\"ogdf/layered/MedianHeuristic.h\")\n",
94 | "cppinclude(\"ogdf/layered/OptimalHierarchyClusterLayout.h\")\n",
95 | "cppinclude(\"ogdf/layered/OptimalRanking.h\")\n",
96 | "\n",
97 | "SL = ogdf.SugiyamaLayout()\n",
98 | "r = ogdf.OptimalRanking()\n",
99 | "r.__python_owns__ = False\n",
100 | "SL.setRanking(r)\n",
101 | "h = ogdf.MedianHeuristic()\n",
102 | "h.__python_owns__ = False\n",
103 | "SL.setCrossMin(h)\n",
104 | "\n",
105 | "ohl = ogdf.OptimalHierarchyClusterLayout()\n",
106 | "ohl.__python_owns__ = False\n",
107 | "ohl.layerDistance(30.0)\n",
108 | "ohl.nodeDistance(100.0)\n",
109 | "ohl.weightBalancing(0.8)\n",
110 | "SL.setClusterLayout(ohl)\n",
111 | "\n",
112 | "SL.call(CGA)\n",
113 | "for name, node in NODES.items():\n",
114 | " CGA.width[node] = len(name) * 5\n",
115 | "CGA.updateClusterPositions()\n",
116 | "ogdf.GraphIO.drawSVG(CGA, \"ogdf-includes.svg\")\n",
117 | "ogdf.GraphIO.write(CGA, \"ogdf-includes.gml\")\n",
118 | "CGA"
119 | ]
120 | }
121 | ],
122 | "metadata": {
123 | "kernelspec": {
124 | "display_name": "Python 3 (ipykernel)",
125 | "language": "python",
126 | "name": "python3"
127 | },
128 | "language_info": {
129 | "codemirror_mode": {
130 | "name": "ipython",
131 | "version": 3
132 | },
133 | "file_extension": ".py",
134 | "mimetype": "text/x-python",
135 | "name": "python",
136 | "nbconvert_exporter": "python",
137 | "pygments_lexer": "ipython3",
138 | "version": "3.11.5"
139 | }
140 | },
141 | "nbformat": 4,
142 | "nbformat_minor": 4
143 | }
144 |
--------------------------------------------------------------------------------
/docs/examples/pitfalls.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": null,
6 | "metadata": {
7 | "execution": {
8 | "iopub.execute_input": "2023-09-15T13:35:31.497712Z",
9 | "iopub.status.busy": "2023-09-15T13:35:31.496905Z",
10 | "iopub.status.idle": "2023-09-15T13:35:33.604563Z",
11 | "shell.execute_reply": "2023-09-15T13:35:33.603977Z"
12 | }
13 | },
14 | "outputs": [],
15 | "source": [
16 | "# uncomment if you didn't set this globally:\n",
17 | "# %env OGDF_BUILD_DIR=~/ogdf/build-debug\n",
18 | "from ogdf_python import ogdf, cppinclude"
19 | ]
20 | },
21 | {
22 | "cell_type": "code",
23 | "execution_count": null,
24 | "metadata": {
25 | "execution": {
26 | "iopub.execute_input": "2023-09-15T13:35:33.631833Z",
27 | "iopub.status.busy": "2023-09-15T13:35:33.631616Z",
28 | "iopub.status.idle": "2023-09-15T13:35:33.813857Z",
29 | "shell.execute_reply": "2023-09-15T13:35:33.813245Z"
30 | }
31 | },
32 | "outputs": [],
33 | "source": [
34 | "cppinclude(\"ogdf/layered/SugiyamaLayout.h\")\n",
35 | "cppinclude(\"ogdf/layered/MedianHeuristic.h\")\n",
36 | "cppinclude(\"ogdf/layered/OptimalHierarchyLayout.h\")\n",
37 | "cppinclude(\"ogdf/layered/OptimalRanking.h\")\n",
38 | "\n",
39 | "SL = ogdf.SugiyamaLayout()\n",
40 | "r = ogdf.OptimalRanking()\n",
41 | "r.__python_owns__ = False # ogdf modules take ownership of objects, conflicting with cppyy clean-up\n",
42 | "SL.setRanking(r)\n",
43 | "h = ogdf.MedianHeuristic()\n",
44 | "h.__python_owns__ = False\n",
45 | "SL.setCrossMin(h)\n",
46 | "\n",
47 | "ohl = ogdf.OptimalHierarchyLayout()\n",
48 | "ohl.__python_owns__ = False\n",
49 | "ohl.layerDistance(30.0)\n",
50 | "ohl.nodeDistance(25.0)\n",
51 | "ohl.weightBalancing(0.8)\n",
52 | "SL.setLayout(ohl)"
53 | ]
54 | },
55 | {
56 | "cell_type": "code",
57 | "execution_count": null,
58 | "metadata": {
59 | "execution": {
60 | "iopub.execute_input": "2023-09-15T13:35:33.816229Z",
61 | "iopub.status.busy": "2023-09-15T13:35:33.816038Z",
62 | "iopub.status.idle": "2023-09-15T13:35:33.881487Z",
63 | "shell.execute_reply": "2023-09-15T13:35:33.880970Z"
64 | }
65 | },
66 | "outputs": [],
67 | "source": [
68 | "for i in range(5):\n",
69 | " CGA = CG = G = None # deletion order is important when overwriting parents of dependant objects\n",
70 | " G = ogdf.Graph()\n",
71 | " CG = ogdf.ClusterGraph(G)\n",
72 | " CGA = ogdf.ClusterGraphAttributes(CG, ogdf.ClusterGraphAttributes.all)"
73 | ]
74 | }
75 | ],
76 | "metadata": {
77 | "kernelspec": {
78 | "display_name": "ogdf-python",
79 | "language": "python",
80 | "name": "ogdf-python"
81 | },
82 | "language_info": {
83 | "codemirror_mode": {
84 | "name": "ipython",
85 | "version": 3
86 | },
87 | "file_extension": ".py",
88 | "mimetype": "text/x-python",
89 | "name": "python",
90 | "nbconvert_exporter": "python",
91 | "pygments_lexer": "ipython3",
92 | "version": "3.11.5"
93 | }
94 | },
95 | "nbformat": 4,
96 | "nbformat_minor": 4
97 | }
98 |
--------------------------------------------------------------------------------
/docs/examples/refresh.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | find -maxdepth 1 -name "*.ipynb" -exec jupyter nbconvert --to=notebook --inplace --ExecutePreprocessor.enabled=True {} \;
4 |
--------------------------------------------------------------------------------
/docs/examples/sugiyama-simple.svg:
--------------------------------------------------------------------------------
1 |
2 |
246 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "ogdf-python"
3 | version = "0.3.5-dev"
4 | description = "Automagic Python Bindings for the Open Graph Drawing Framework written in C++"
5 | authors = [
6 | { name = "Simon D. Fink", email = "finksim@fim.uni-passau.de" },
7 | ]
8 | requires-python = ">=3.7"
9 | license = "Apache-2.0"
10 | readme = "README.rst"
11 | homepage = "https://ogdf.github.io"
12 | repository = "https://github.com/N-Coder/ogdf-python"
13 | documentation = "https://ogdf-python.readthedocs.io"
14 | keywords = ["ogdf", "graph", "network", "drawing", "algorithm"]
15 | classifiers = [
16 | 'Development Status :: 3 - Alpha',
17 | 'Intended Audience :: Developers',
18 | 'Intended Audience :: Science/Research',
19 | 'License :: OSI Approved :: Apache Software License',
20 | 'Operating System :: Unix',
21 | 'Programming Language :: C++',
22 | 'Programming Language :: Python :: 3 :: Only',
23 | 'Programming Language :: Python :: 3',
24 | 'Programming Language :: Python :: 3.7',
25 | 'Programming Language :: Python :: 3.8',
26 | 'Programming Language :: Python :: 3.9',
27 | 'Programming Language :: Python :: 3.10',
28 | 'Programming Language :: Python :: 3.11',
29 | 'Programming Language :: Python',
30 | 'Topic :: Scientific/Engineering :: Information Analysis',
31 | 'Topic :: Scientific/Engineering :: Mathematics',
32 | 'Topic :: Scientific/Engineering :: Visualization',
33 | 'Topic :: Software Development :: Libraries :: Python Modules',
34 | ]
35 |
36 | dependencies = [
37 | "cppyy",
38 | "importlib-resources>=1",
39 | ]
40 |
41 | [project.optional-dependencies]
42 | quickstart = [
43 | "jupyterlab",
44 | "ipympl",
45 | "matplotlib",
46 | "ogdf-wheel",
47 | ]
48 |
49 | [build-system]
50 | requires = ["hatchling"]
51 | build-backend = "hatchling.build"
52 |
53 |
54 | [tool.bumpversion]
55 | current_version = "0.3.5-dev"
56 | commit = "True"
57 | tag = "True"
58 | parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)-?(?Pdev)?"
59 | serialize = [
60 | "{major}.{minor}.{patch}-{release}",
61 | "{major}.{minor}.{patch}"
62 | ]
63 |
64 | [[tool.bumpversion.files]]
65 | filename = "pyproject.toml"
66 |
67 | [[tool.bumpversion.files]]
68 | filename = "src/ogdf_python/__init__.py"
69 |
70 | [[tool.bumpversion.files]]
71 | filename = "README.rst"
72 |
73 | [tool.bumpversion.parts.release]
74 | values = [
75 | "dev",
76 | "release"
77 | ]
78 | optional_value = "release"
79 |
--------------------------------------------------------------------------------
/src/ogdf_python/__init__.py:
--------------------------------------------------------------------------------
1 | from ogdf_python.loader import *
2 |
3 | ogdf # keep imports in this order
4 |
5 | from ogdf_python.utils import *
6 | from ogdf_python.info import *
7 |
8 | import ogdf_python.doxygen
9 | import ogdf_python.pythonize
10 | import ogdf_python.jupyter
11 |
12 | __keep_imports = [
13 | ogdf_python.doxygen,
14 | ogdf_python.pythonize,
15 | ogdf_python.jupyter,
16 | ]
17 |
18 | __version__ = "0.3.5-dev"
19 | __all__ = ogdf_python.loader.__all__ + ogdf_python.utils.__all__ + ogdf_python.info.__all__
20 |
21 | try:
22 | import matplotlib
23 | except ImportError:
24 | pass
25 | else:
26 | from ogdf_python.matplotlib import *
27 |
28 | __all__ += ogdf_python.matplotlib.__all__
29 |
--------------------------------------------------------------------------------
/src/ogdf_python/__main__.py:
--------------------------------------------------------------------------------
1 | from pprint import pprint
2 |
3 | from ogdf_python import get_ogdf_info
4 |
5 | pprint(get_ogdf_info(), sort_dicts=False)
6 |
--------------------------------------------------------------------------------
/src/ogdf_python/doxygen.py:
--------------------------------------------------------------------------------
1 | import functools
2 | import os
3 | from collections import defaultdict
4 |
5 |
6 | # doxygen XML parsing #################################################################################################
7 |
8 |
9 | def parse_index_xml():
10 | root = etree.parse(os.path.join(DOXYGEN_XML_DIR, 'index.xml'))
11 | compounds = defaultdict(dict)
12 | for compound in root.iter('compound'):
13 | kind = compound.attrib['kind']
14 | name = compound.find('name').text.strip()
15 | if name in compounds[kind]:
16 | print("duplicate compound", kind, name)
17 | continue
18 | compound_data = compounds[kind][name] = {
19 | "kind": kind,
20 | "name": name,
21 | "refid": compound.attrib['refid'],
22 | "members": defaultdict(dict)
23 | }
24 |
25 | for member in compound.iter('member'):
26 | kind = member.attrib['kind']
27 | name = member.find('name').text.strip()
28 | refid = member.attrib['refid']
29 | if refid in compound_data["members"][name]:
30 | print("duplicate member", kind, name, refid)
31 | continue
32 | compound_data["members"][name][refid] = {
33 | "kind": kind,
34 | "name": name,
35 | "refid": refid
36 | }
37 |
38 | return compounds
39 |
40 |
41 | def find_all_includes():
42 | # set GENERATE_XML = YES in ogdf-doxygen.cfg, delete the doc/html folder and then run make doc
43 | for ctype in ("class", "struct", "namespace"):
44 | for compound in DOXYGEN_DATA[ctype].values():
45 | compound_xml = etree.parse(os.path.join(DOXYGEN_XML_DIR, compound["refid"] + '.xml'))
46 | for location in compound_xml.findall(".//*[@id]/location"):
47 | parent = location.getparent()
48 | if parent.get("id") == compound["refid"]:
49 | member = compound
50 | else:
51 | name = parent.find("name")
52 | if name.text not in compound["members"]:
53 | print("got location for unknown object", compound["refid"], parent.get("id"), name.text)
54 | continue
55 | else:
56 | member = compound["members"][name.text][parent.get("id")]
57 | member["file"] = location.get("declfile", location.get("file"))
58 |
59 |
60 | # doc strings / help messages##########################################################################################
61 |
62 | def pythonize_docstrings(klass, name):
63 | basename = klass.__cpp_name__.partition("<")[0]
64 | if basename not in DOXYGEN_DATA["class"]:
65 | return
66 | data = DOXYGEN_DATA["class"][basename] # TODO do the same for namespace members
67 | url = DOXYGEN_URL % (data["refid"], "")
68 | try:
69 | if klass.__doc__:
70 | klass.__doc__ = "%s\n%s" % (klass.__doc__, url)
71 | else:
72 | klass.__doc__ = url
73 | except AttributeError as e:
74 | pass
75 | # print(klass.__cpp_name__, e) # TODO remove once we can overwrite the __doc__ of CPPOverload etc.
76 |
77 | for mem, val in klass.__dict__.items():
78 | if mem not in data["members"]:
79 | # print(klass.__cpp_name__, "has no member", mem)
80 | continue
81 | try:
82 | val.__doc__ = val.__doc__ or ""
83 | for override in data["members"][mem].values():
84 | val.__doc__ += "\n" + DOXYGEN_URL % (data["refid"], override["refid"][len(data["refid"]) + 2:])
85 | except AttributeError as e:
86 | # print(klass.__cpp_name__, e) # TODO remove once we can overwrite the __doc__ of CPPOverload etc.
87 | # import traceback
88 | # traceback.print_exc()
89 | # print(val.__doc__)
90 | pass
91 |
92 |
93 | # helpful "attribute not found" errors ################################################################################
94 |
95 | def find_include(*names):
96 | if len(names) == 1:
97 | if isinstance(names[0], str):
98 | names = names[0].split("::")
99 | else:
100 | names = names[0]
101 |
102 | name = names[-1]
103 | qualname = "::".join(names)
104 | parentname = "::".join(names[:-1])
105 |
106 | data = DOXYGEN_DATA["class"].get(qualname, None)
107 | filename = None
108 | if not data:
109 | data = DOXYGEN_DATA["struct"].get(qualname, None)
110 | if not data:
111 | namespace_data = DOXYGEN_DATA["namespace"].get(parentname, None)
112 | if namespace_data and name in namespace_data["members"]:
113 | data = next(iter(namespace_data["members"][name].values()))
114 | filename = namespace_data["refid"]
115 | if not data:
116 | return None
117 |
118 | if "file" in data:
119 | return data["file"]
120 |
121 | if not filename:
122 | filename = data["refid"]
123 | namespace_xml = etree.parse(os.path.join(DOXYGEN_XML_DIR, filename + '.xml'))
124 | location = namespace_xml.find(".//*[@id='%s']/location" % data["refid"])
125 | return location.get("declfile", location.get("file"))
126 |
127 |
128 | def wrap_getattribute(ns):
129 | getattrib = type(ns).__getattribute__
130 | if hasattr(getattrib, "__wrapped__"): return
131 |
132 | @functools.wraps(getattrib)
133 | def helpful_getattribute(ns, name):
134 | if name.startswith("__"): # do not modify internals
135 | return getattrib(ns, name)
136 | try:
137 | val = getattrib(ns, name)
138 | if isinstance(type(val), type(type(ns))):
139 | wrap_getattribute(val)
140 | return val
141 | except AttributeError as e:
142 | if hasattr(e, "__helpful__"):
143 | raise e
144 | if not hasattr(ns, "__cpp_name__"):
145 | raise e
146 | msg = e.args[0]
147 | file = find_include(*ns.__cpp_name__.split("::"), name)
148 | if file:
149 | prefix = "include/"
150 | msg += "\nDid you forget to include file %s? Try running\ncppinclude(\"%s\")" % \
151 | (file, file[len(prefix):] if file.startswith(prefix) else file)
152 | else:
153 | msg += "\nThe name %s::%s couldn't be found in the docs." % (ns.__cpp_name__, name)
154 | e.args = (msg, *e.args[1:])
155 | e.__helpful__ = True
156 | raise e
157 |
158 | type(ns).__getattribute__ = helpful_getattribute
159 |
160 |
161 | # __main__ and imported use ###########################################################################################
162 |
163 | if "OGDF_DOC_DIR" in os.environ:
164 | from lxml import etree
165 |
166 | DOXYGEN_XML_DIR = os.path.join(os.environ["OGDF_DOC_DIR"], "xml")
167 | DOXYGEN_DATA = parse_index_xml()
168 |
169 | if __name__ == "__main__":
170 | import json, sys
171 |
172 | find_all_includes()
173 | with open("doxygen.json", "wt") as f:
174 | json.dump(DOXYGEN_DATA, f)
175 | sys.exit(0)
176 |
177 | else:
178 | import json
179 |
180 | DOXYGEN_XML_DIR = None
181 |
182 | try:
183 | with open("doxygen.json", "rt") as f:
184 | DOXYGEN_DATA = json.load(f)
185 | except FileNotFoundError:
186 | import importlib_resources
187 |
188 | with importlib_resources.files("ogdf_python").joinpath("doxygen.json").open("rt") as f:
189 | DOXYGEN_DATA = json.load(f)
190 |
191 | DOXYGEN_URL = os.environ.get("OGDF_DOC_URL", "https://ogdf.github.io/doc/ogdf/%s.html#%s")
192 |
--------------------------------------------------------------------------------
/src/ogdf_python/info.py:
--------------------------------------------------------------------------------
1 | from ogdf_python.loader import *
2 |
3 | __all__ = ["get_ogdf_info", "get_macro", "get_library_path", "get_include_path", "get_ogdf_include_path"]
4 |
5 |
6 | def get_macro(m):
7 | cppdef("""
8 | #define STR(str) #str
9 | #define STRING(str) STR(str)
10 | namespace get_macro {{
11 | #ifdef {m}
12 | std::string var_{m} = STRING({m});
13 | #endif
14 | }};
15 | """.format(m=m))
16 | val = getattr(cppyy.gbl.get_macro, "var_%s" % m, None)
17 | if val is not None:
18 | val = val.decode("ascii")
19 | return val
20 |
21 |
22 | def conf_which(n):
23 | w = "which%s" % n
24 | cppdef("""
25 | namespace conf_which {{
26 | std::string {w}() {{
27 | return ogdf::Configuration::toString(ogdf::Configuration::{w}());
28 | }}
29 | }};
30 | """.format(w=w))
31 | return getattr(cppyy.gbl.conf_which, w)().decode("ascii")
32 |
33 |
34 | def get_library_path(name=None):
35 | if name is None:
36 | return get_library_path("OGDF") or get_library_path("libOGDF")
37 | return cppyy.gbl.gSystem.FindDynamicLibrary(cppyy.gbl.CppyyLegacy.TString(name), True)
38 |
39 |
40 | def get_ogdf_include_path():
41 | include = "ogdf/basic/Graph.h"
42 | path = get_include_path(include)
43 | if path:
44 | path = path.removesuffix(include)
45 | return path
46 |
47 |
48 | def get_include_path(name="ogdf/basic/Graph.h"):
49 | import ctypes
50 | s = ctypes.c_char_p()
51 | if cppyy.gbl.gSystem.IsFileInIncludePath(name, s):
52 | return s.value.decode()
53 | else:
54 | return None
55 |
56 |
57 | def get_ogdf_info():
58 | from ogdf_python import __version__
59 | cppinclude("ogdf/basic/System.h")
60 | data = {
61 | "ogdf_path": get_library_path() or "unknown",
62 | "ogdf_include_path": get_ogdf_include_path() or "unknown",
63 | "ogdf_version": get_macro("OGDF_VERSION").strip('"'),
64 | "ogdf_python_version": __version__,
65 | "ogdf_debug": get_macro("OGDF_DEBUG") is not None,
66 | "ogdf_build_debug": ogdf.debugMode,
67 | "debug": get_macro("NDEBUG") is None,
68 | "numberOfProcessors": ogdf.System.numberOfProcessors(),
69 | "cacheSize": ogdf.System.cacheSizeKBytes() * 1024,
70 | "cacheLineBytes": ogdf.System.cacheLineBytes(),
71 | "pageSize": ogdf.System.pageSize(),
72 | "physicalMemory": ogdf.System.physicalMemory(),
73 | "availablePhysicalMemory": ogdf.System.availablePhysicalMemory(),
74 | "memoryUsedByProcess": ogdf.System.memoryUsedByProcess(),
75 | "memoryAllocatedByMalloc": ogdf.System.memoryAllocatedByMalloc(),
76 | "memoryInFreelistOfMalloc": ogdf.System.memoryInFreelistOfMalloc(),
77 | "memoryAllocatedByMemoryManager": ogdf.System.memoryAllocatedByMemoryManager(),
78 | "memoryInGlobalFreeListOfMemoryManager": ogdf.System.memoryInGlobalFreeListOfMemoryManager(),
79 | "memoryInThreadFreeListOfMemoryManager": ogdf.System.memoryInThreadFreeListOfMemoryManager(),
80 | "cpuFeatures": {
81 | "MMX": ogdf.System.cpuSupports(ogdf.CPUFeature.MMX),
82 | "SSE": ogdf.System.cpuSupports(ogdf.CPUFeature.SSE),
83 | "SSE2": ogdf.System.cpuSupports(ogdf.CPUFeature.SSE2),
84 | "SSE3": ogdf.System.cpuSupports(ogdf.CPUFeature.SSE3),
85 | "SSSE3": ogdf.System.cpuSupports(ogdf.CPUFeature.SSSE3),
86 | "SSE4_1": ogdf.System.cpuSupports(ogdf.CPUFeature.SSE4_1),
87 | "SSE4_2": ogdf.System.cpuSupports(ogdf.CPUFeature.SSE4_2),
88 | "VMX": ogdf.System.cpuSupports(ogdf.CPUFeature.VMX),
89 | "SMX": ogdf.System.cpuSupports(ogdf.CPUFeature.SMX),
90 | "EST": ogdf.System.cpuSupports(ogdf.CPUFeature.EST),
91 | },
92 | "isUnix": get_macro("OGDF_SYSTEM_UNIX") is not None,
93 | "isWindows": get_macro("OGDF_SYSTEM_WINDOWS") is not None,
94 | "isOSX": get_macro("OGDF_SYSTEM_OSX") is not None,
95 | "system": conf_which("System"),
96 | "LPsolver": conf_which("LPSolver"),
97 | "memoryManager": conf_which("MemoryManager"),
98 | }
99 | if hasattr(ogdf.System, "peakMemoryUsedByProcess"):
100 | data["peakMemoryUsedByProcess"] = ogdf.System.peakMemoryUsedByProcess()
101 | return data
102 |
--------------------------------------------------------------------------------
/src/ogdf_python/jupyter.py:
--------------------------------------------------------------------------------
1 | import cppyy
2 | import sys
3 |
4 | ip = None
5 | if "IPython" in sys.modules:
6 | from IPython import get_ipython
7 |
8 | ip = get_ipython()
9 |
10 | if ip is not None:
11 | from IPython.core.magic import register_cell_magic
12 |
13 |
14 | @register_cell_magic
15 | def cpp(line, cell):
16 | """
17 | might yield "function definition is not allowed here" for some multi-part definitions
18 | use `cppdef` instead if required
19 | https://github.com/jupyter-xeus/xeus-cling/issues/40
20 | """
21 | cppyy.gbl.ogdf_pythonization.BeginCaptureStdout()
22 | cppyy.gbl.ogdf_pythonization.BeginCaptureStderr()
23 | try:
24 | cppyy.cppexec(cell)
25 | finally:
26 | print(cppyy.gbl.ogdf_pythonization.EndCaptureStdout(), end="")
27 | print(cppyy.gbl.ogdf_pythonization.EndCaptureStderr(), file=sys.stderr, end="")
28 |
29 |
30 | @register_cell_magic
31 | def cppdef(line, cell):
32 | cppyy.cppdef(cell)
33 |
--------------------------------------------------------------------------------
/src/ogdf_python/loader.py:
--------------------------------------------------------------------------------
1 | import os
2 | import platform
3 |
4 | import importlib_resources
5 | import sys
6 |
7 | try:
8 | import cppyy
9 | import cppyy.ll
10 | except:
11 | print(
12 | "\n##########\n"
13 | "ogdf-python couldn't load cppyy. "
14 | "This is not an ogdf-python error, but probably a problem with your cppyy installation or python environment. "
15 | "To use ogdf-python, check that you can `import cppyy` in a freshly-started python interpreter.\n",
16 | file=sys.stderr
17 | )
18 | if platform.system() == "Windows":
19 | print(
20 | "Note that ogdf-python is not officially supported on Windows."
21 | "Instead, you should use ogdf-python within the Windows Subsystem for Linux (WSL)."
22 | "There, make sure to actually invoke the Linux python(3) binary instead of the Windows python.exe, which"
23 | "can be checked with `python -VV`.\n",
24 | file=sys.stderr
25 | )
26 | print("##########\n\n", file=sys.stderr)
27 | raise
28 |
29 | cppyy.ll.set_signals_as_exception(True)
30 | cppyy.add_include_path(os.path.dirname(os.path.realpath(__file__)))
31 |
32 | if "OGDF_INSTALL_DIR" in os.environ:
33 | INSTALL_DIR = os.path.expanduser(os.getenv("OGDF_INSTALL_DIR"))
34 | cppyy.add_include_path(os.path.join(INSTALL_DIR, "include"))
35 | cppyy.add_library_path(os.path.join(INSTALL_DIR, "lib")) # TODO windows?
36 | if "OGDF_BUILD_DIR" in os.environ:
37 | BUILD_DIR = os.path.expanduser(os.getenv("OGDF_BUILD_DIR"))
38 | cppyy.add_include_path(os.path.join(BUILD_DIR, "include"))
39 | cppyy.add_include_path(os.path.join(BUILD_DIR, "..", "include")) # TODO not canonical
40 | cppyy.add_library_path(BUILD_DIR)
41 | try:
42 | wheel_inst_dir = importlib_resources.files("ogdf_wheel") / "install"
43 | if wheel_inst_dir.is_dir():
44 | cppyy.add_library_path(str(wheel_inst_dir / "lib"))
45 | cppyy.add_library_path(str(wheel_inst_dir / "bin"))
46 | cppyy.add_include_path(str(wheel_inst_dir / "include"))
47 | except ImportError:
48 | pass
49 |
50 | cppyy.cppdef("#undef NDEBUG")
51 | cppyy.cppdef("#define OGDF_INSTALL")
52 | try:
53 | cppyy.include("ogdf/basic/internal/config_autogen.h")
54 | cppyy.include("ogdf/basic/internal/config.h")
55 | cppyy.include("ogdf/basic/Graph.h")
56 | cppyy.include("ogdf/cluster/ClusterGraphObserver.h") # otherwise only pre-declared
57 | cppyy.include("ogdf/fileformats/GraphIO.h")
58 |
59 | cppyy.load_library("OGDF")
60 | except:
61 | print( # TODO check if the issue is really that the file couldn't be found
62 | "ogdf-python couldn't load OGDF. "
63 | "If you haven't installed OGDF globally to your system, "
64 | "please set environment variables OGDF_INSTALL_DIR or OGDF_BUILD_DIR. "
65 | "The current search path is:\n%s\n"
66 | "The current include path is:\n%s\n" %
67 | (cppyy.gbl.gSystem.GetDynamicPath(), cppyy.gbl.gInterpreter.GetIncludePath()),
68 | file=sys.stderr)
69 | raise
70 |
71 | if cppyy.gbl.ogdf.debugMode:
72 | cppyy.cppdef("#undef NDEBUG")
73 | else:
74 | cppyy.cppdef("#define NDEBUG")
75 | cppyy.include("cassert")
76 |
77 | # Load re-exports
78 | from cppyy import include as cppinclude, cppdef, cppexec, nullptr
79 | from cppyy.gbl import ogdf
80 |
81 | __all__ = ["cppyy", "cppinclude", "cppdef", "cppexec", "nullptr", "ogdf"]
82 | __keep_imports = [cppyy, cppinclude, cppdef, cppexec, nullptr, ogdf]
83 |
--------------------------------------------------------------------------------
/src/ogdf_python/matplotlib/__init__.py:
--------------------------------------------------------------------------------
1 | from importlib.resources import *
2 |
3 | from ogdf_python.loader import *
4 | from ogdf_python.matplotlib.util import *
5 | from ogdf_python.matplotlib.widget import MatplotlibGraph, MatplotlibGraphEditor
6 |
7 | __all__ = ["MatplotlibGraph", "MatplotlibGraphEditor"]
8 |
9 | try:
10 | import ipywidgets
11 | except ImportError:
12 | pass
13 | else:
14 | from ogdf_python.matplotlib.gui import GraphEditorLayout
15 |
16 | __all__ += ["GraphEditorLayout"]
17 |
18 | with as_file(files(__package__).joinpath("rendering.h")) as header:
19 | cppinclude(header)
20 |
21 |
22 | def repr_mimebundle_GraphAttributes(self, *args, **kwargs):
23 | fig = new_figure()
24 | ax = fig.subplots()
25 | fig.graph = MatplotlibGraph(self, ax)
26 | return fig.graph._repr_mimebundle_(*args, **kwargs)
27 |
28 |
29 | def repr_mimebundle_Graph(self, *args, **kwargs):
30 | from ogdf_python.pythonize import renderGraph
31 | return repr_mimebundle_GraphAttributes(renderGraph(self), *args, **kwargs)
32 |
33 |
34 | def repr_mimebundle_ClusterGraph(self, *args, **kwargs):
35 | from ogdf_python.pythonize import renderClusterGraph
36 | return repr_mimebundle_GraphAttributes(renderClusterGraph(self), *args, **kwargs)
37 |
38 |
39 | ogdf.GraphAttributes._repr_mimebundle_ = repr_mimebundle_GraphAttributes
40 | ogdf.Graph._repr_mimebundle_ = repr_mimebundle_Graph
41 | ogdf.ClusterGraphAttributes._repr_mimebundle_ = repr_mimebundle_GraphAttributes
42 | ogdf.ClusterGraph._repr_mimebundle_ = repr_mimebundle_ClusterGraph
43 |
--------------------------------------------------------------------------------
/src/ogdf_python/matplotlib/gui.py:
--------------------------------------------------------------------------------
1 | import ipywidgets
2 |
3 | from ogdf_python import ogdf
4 | from ogdf_python.matplotlib.util import new_figure
5 | from ogdf_python.matplotlib.widget import MatplotlibGraphEditor
6 |
7 | __all__ = ["GraphEditorLayout"]
8 |
9 | L = ipywidgets.Label
10 |
11 |
12 | def B(*args, **kwargs):
13 | l = L(*args, **kwargs)
14 | l.style.font_weight = "bold"
15 | return l
16 |
17 |
18 | UI_STYLE = """
19 |
33 | """.strip()
34 |
35 |
36 | class GraphEditorLayout(ipywidgets.GridBox):
37 | def __init__(self, GA):
38 | self.fig = new_figure()
39 | self.widget = MatplotlibGraphEditor(GA, self.fig.subplots())
40 | self.widget.on_selection_changed = self.on_selection_changed
41 | self.widget.on_node_moved = self.on_node_moved
42 |
43 | self.title = B("Selected Vertex")
44 | self.values = {
45 | "id": L("12"),
46 | "label": ipywidgets.Text("Label123", layout=ipywidgets.Layout(width="100px")),
47 | "degree": L("3"),
48 | "position": L("123, 456"),
49 | "width": ipywidgets.IntText("20", layout=ipywidgets.Layout(width="45px")),
50 | "height": ipywidgets.IntText("20", layout=ipywidgets.Layout(width="45px")),
51 | }
52 | self.values["size"] = ipywidgets.HBox([self.values["width"], self.values["height"]])
53 | self.values["width"].continuous_update = True
54 | self.values["width"].observe(self.on_size_changed, names='value')
55 | self.values["height"].continuous_update = True
56 | self.values["height"].observe(self.on_size_changed, names='value')
57 | self.values["label"].continuous_update = True
58 | self.values["label"].observe(self.on_label_changed, names='value')
59 |
60 | self.buttons = {
61 | "del": ipywidgets.Button(description="Delete", button_style="danger", icon="trash")
62 | }
63 | self.buttons["del"].on_click(self.on_delete_clicked)
64 | self.pane = ipywidgets.VBox([
65 | self.title,
66 | ipywidgets.GridBox(children=[
67 | B("ID"), self.values["id"],
68 | B("Label"), self.values["label"],
69 | B("Degree"), self.values["degree"],
70 | B("Position"), self.values["position"],
71 | B("Size"), self.values["size"],
72 | ], layout=ipywidgets.Layout(width="100%", grid_template_columns='1fr 2fr', padding="10px")),
73 | ipywidgets.HBox(list(self.buttons.values()))
74 | ])
75 |
76 | self.fig.canvas.layout.grid_area = "1/1/4/3"
77 | self.pane.layout.grid_area = "3/2/4/3"
78 | self.pane.add_class("ogdf_grapheditor_pane")
79 |
80 | # bar = ipywidgets.HBox([ipywidgets.Button(label="add")])
81 | # bar.layout.grid_area = "1/2/2/3"
82 |
83 | super().__init__(
84 | children=[self.fig.canvas, self.pane, ipywidgets.HTML(UI_STYLE)],
85 | layout=ipywidgets.Layout(
86 | width=f"{self.fig.get_figwidth() * 100}px", height=f"{self.fig.get_figheight() * 100}px",
87 | grid_template_columns='auto 200px',
88 | grid_template_rows='80px auto auto')
89 | )
90 | self.on_selection_changed()
91 |
92 | def on_node_moved(self, node):
93 | if self.widget.selected == node:
94 | GA = self.widget.GA
95 | self.values["position"].value = f"{GA.x[node]:.1f}, {GA.y[node]:.1f}"
96 |
97 | def on_size_changed(self, x):
98 | node = self.widget.selected
99 | self.widget.GA.width[node] = self.values["width"].value
100 | self.widget.GA.height[node] = self.values["height"].value
101 | self.widget.update_node(node)
102 | self.fig.canvas.draw_idle()
103 |
104 | def on_label_changed(self, x):
105 | value = self.values["label"].value
106 | node = self.widget.selected
107 | self.widget.GA.label[node] = value
108 | if node in self.widget.node_labels:
109 | self.widget.node_labels[node].set_text(value)
110 | self.fig.canvas.draw_idle()
111 |
112 | def on_delete_clicked(self, btn):
113 | self.widget.GA.constGraph().delNode(self.widget.selected)
114 |
115 | def on_selection_changed(self):
116 | if isinstance(self.widget.selected, ogdf.NodeElement):
117 | node = self.widget.selected
118 | GA = self.widget.GA
119 | self.pane.layout.visibility = 'visible'
120 | self.title.value = f"Selected Vertex {node.index()}"
121 | self.values["id"].value = f"{node.index()}"
122 | self.values["label"].value = str(GA.label[node])
123 | self.values["degree"].value = f"{node.degree()} ({node.indeg()} + {node.outdeg()})"
124 | self.values["position"].value = f"{GA.x[node]:.1f}, {GA.y[node]:.1f}"
125 | self.values["width"].value, self.values["height"].value = GA.width[node], GA.height[node]
126 | else:
127 | self.pane.layout.visibility = 'hidden'
128 |
--------------------------------------------------------------------------------
/src/ogdf_python/matplotlib/rendering.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 | #include
5 | #include
6 |
7 | namespace ogdf {
8 | namespace python_matplotlib {
9 | void append(std::initializer_list list, DPolyline &out) {
10 | for (auto it = list.begin(); it != list.end();) {
11 | double x = *it, y = *(++it);
12 | out.emplaceBack(x, y);
13 | ++it;
14 | }
15 | }
16 |
17 | void drawPolygonShape(Shape s, double x, double y, double w, double h, DPolyline &out) {
18 | // values are precomputed to save expensive sin/cos calls
19 | const double triangleWidth = 0.43301270189222 * w,
20 | hexagonHalfHeight = 0.43301270189222 * h,
21 | pentagonHalfWidth = 0.475528258147577 * w,
22 | pentagonSmallHeight = 0.154508497187474 * h,
23 | pentagonSmallWidth = 0.293892626146236 * w,
24 | pentagonHalfHeight = 0.404508497187474 * h,
25 | octagonHalfWidth = 0.461939766255643 * w,
26 | octagonSmallWidth = 0.191341716182545 * w,
27 | octagonHalfHeight = 0.461939766255643 * h,
28 | octagonSmallHeight = 0.191341716182545 * h;
29 | switch (s) {
30 | case Shape::Triangle:
31 | append({x, y - h / 2, x + triangleWidth, y + h / 4,
32 | x - triangleWidth, y + h / 4}, out);
33 | break;
34 | case Shape::InvTriangle:
35 | append({x, y + h / 2, x - triangleWidth, y - h / 4,
36 | x + triangleWidth, y - h / 4}, out);
37 | break;
38 | case Shape::Pentagon:
39 | append({x, y - h / 2, x + pentagonHalfWidth, y - pentagonSmallHeight,
40 | x + pentagonSmallWidth, y + pentagonHalfHeight, x - pentagonSmallWidth,
41 | y + pentagonHalfHeight, x - pentagonHalfWidth, y - pentagonSmallHeight}, out);
42 | break;
43 | case Shape::Hexagon:
44 | append({x + w / 4, y + hexagonHalfHeight, x - w / 4,
45 | y + hexagonHalfHeight, x - w / 2, y, x - w / 4,
46 | y - hexagonHalfHeight, x + w / 4, y - hexagonHalfHeight,
47 | x + w / 2, y}, out);
48 | break;
49 | case Shape::Octagon:
50 | append({x + octagonHalfWidth, y + octagonSmallHeight, x + octagonSmallWidth,
51 | y + octagonHalfHeight, x - octagonSmallWidth, y + octagonHalfHeight,
52 | x - octagonHalfWidth, y + octagonSmallHeight, x - octagonHalfWidth,
53 | y - octagonSmallHeight, x - octagonSmallWidth, y - octagonHalfHeight,
54 | x + octagonSmallWidth, y - octagonHalfHeight, x + octagonHalfWidth,
55 | y - octagonSmallHeight}, out);
56 | break;
57 | case Shape::Rhomb:
58 | append({x + w / 2, y, x, y + h / 2, x - w / 2,
59 | y, x, y - h / 2}, out);
60 | break;
61 | case Shape::Trapeze:
62 | append({x - w / 2, y + h / 2, x + w / 2,
63 | y + h / 2, x + w / 4, y - h / 2,
64 | x - w / 4, y - h / 2}, out);
65 | break;
66 | case Shape::InvTrapeze:
67 | append({x - w / 2, y - h / 2, x + w / 2,
68 | y - h / 2, x + w / 4, y + h / 2,
69 | x - w / 4, y + h / 2}, out);
70 | break;
71 | case Shape::Parallelogram:
72 | append({x - w / 2, y + h / 2, x + w / 4,
73 | y + h / 2, x + w / 2, y - h / 2,
74 | x - w / 4, y - h / 2}, out);
75 | break;
76 | case Shape::InvParallelogram:
77 | append({x - w / 2, y - h / 2, x + w / 4,
78 | y - h / 2, x + w / 2, y + h / 2,
79 | x - w / 4, y + h / 2}, out);
80 | break;
81 | default:
82 | OGDF_ASSERT(false); // unsupported shapes are rendered as rectangle
83 | case Shape::Rect:
84 | append({x - w / 2, y - h / 2,
85 | x - w / 2, y + h / 2,
86 | x + w / 2, y + h / 2,
87 | x + w / 2, y - h / 2}, out);
88 | break;
89 | }
90 | }
91 |
92 | bool isPointCoveredByNodeOP(const DPoint& point, const DPoint& v, const DPoint& vSize,
93 | const Shape& shape) {
94 | const double epsilon = 1e-6;
95 | const double trapeziumWidthOffset = vSize.m_x * 0.275;
96 | GenericPolyline polygon;
97 |
98 | auto isInConvexCCWPolygon = [&] {
99 | for (int i = 0; i < polygon.size(); i++) {
100 | DPoint edgePt1 = v + *polygon.get(i);
101 | DPoint edgePt2 = v + *polygon.get((i + 1) % polygon.size());
102 |
103 | if ((edgePt2.m_x - edgePt1.m_x) * (point.m_y - edgePt1.m_y)
104 | - (edgePt2.m_y - edgePt1.m_y) * (point.m_x - edgePt1.m_x)
105 | < -epsilon) {
106 | return false;
107 | }
108 | }
109 | return true;
110 | };
111 |
112 | auto isInRegularPolygon = [&](unsigned int sides) {
113 | polygon.clear();
114 | double radius = (max(vSize.m_x, vSize.m_y) / 2.0);
115 | for (double angle = -(Math::pi / 2) + Math::pi / sides; angle < 1.5 * Math::pi;
116 | angle += 2.0 * Math::pi / sides) {
117 | polygon.pushBack(DPoint(radius * cos(angle), radius * sin(angle)));
118 | }
119 | return isInConvexCCWPolygon();
120 | };
121 |
122 | switch (shape) {
123 | // currently these tikz polygons are only supported as regular polygons, i.e. width=height
124 | case Shape::Pentagon:
125 | return isInRegularPolygon(5);
126 | case Shape::Hexagon:
127 | return isInRegularPolygon(6);
128 | case Shape::Octagon:
129 | return isInRegularPolygon(8);
130 | case Shape::Triangle:
131 | return isInRegularPolygon(3);
132 | // Non-regular polygons
133 | case Shape::InvTriangle:
134 | polygon.pushBack(DPoint(0, -vSize.m_y * 2.0 / 3.0));
135 | polygon.pushBack(DPoint(vSize.m_x / 2.0, vSize.m_y * 1.0 / 3.0));
136 | polygon.pushBack(DPoint(-vSize.m_x / 2.0, vSize.m_y * 1.0 / 3.0));
137 | return isInConvexCCWPolygon();
138 | case Shape::Rhomb:
139 | polygon.pushBack(DPoint(vSize.m_x / 2.0, 0));
140 | polygon.pushBack(DPoint(0, vSize.m_y / 2.0));
141 | polygon.pushBack(DPoint(-vSize.m_x / 2.0, 0));
142 | polygon.pushBack(DPoint(0, -vSize.m_y / 2.0));
143 | return isInConvexCCWPolygon();
144 | case Shape::Trapeze:
145 | polygon.pushBack(DPoint(-vSize.m_x / 2.0, -vSize.m_y / 2.0));
146 | polygon.pushBack(DPoint(vSize.m_x / 2.0, -vSize.m_y / 2.0));
147 | polygon.pushBack(DPoint(vSize.m_x / 2.0 - trapeziumWidthOffset, +vSize.m_y / 2.0));
148 | polygon.pushBack(DPoint(-vSize.m_x / 2.0 + trapeziumWidthOffset, +vSize.m_y / 2.0));
149 | return isInConvexCCWPolygon();
150 | case Shape::InvTrapeze:
151 | polygon.pushBack(DPoint(vSize.m_x / 2.0, vSize.m_y / 2.0));
152 | polygon.pushBack(DPoint(-vSize.m_x / 2.0, vSize.m_y / 2.0));
153 | polygon.pushBack(DPoint(-vSize.m_x / 2.0 + trapeziumWidthOffset, -vSize.m_y / 2.0));
154 | polygon.pushBack(DPoint(vSize.m_x / 2.0 - trapeziumWidthOffset, -vSize.m_y / 2.0));
155 | return isInConvexCCWPolygon();
156 | case Shape::Parallelogram:
157 | polygon.pushBack(DPoint(-vSize.m_x / 2.0, -vSize.m_y / 2.0));
158 | polygon.pushBack(DPoint(vSize.m_x / 2.0 - trapeziumWidthOffset, -vSize.m_y / 2.0));
159 | polygon.pushBack(DPoint(vSize.m_x / 2.0, +vSize.m_y / 2.0));
160 | polygon.pushBack(DPoint(-vSize.m_x / 2.0 + trapeziumWidthOffset, +vSize.m_y / 2.0));
161 | return isInConvexCCWPolygon();
162 | case Shape::InvParallelogram:
163 | polygon.pushBack(DPoint(-vSize.m_x / 2.0 + trapeziumWidthOffset, -vSize.m_y / 2.0));
164 | polygon.pushBack(DPoint(vSize.m_x / 2.0, -vSize.m_y / 2.0));
165 | polygon.pushBack(DPoint(vSize.m_x / 2.0 - trapeziumWidthOffset, vSize.m_y / 2.0));
166 | polygon.pushBack(DPoint(-vSize.m_x / 2.0, vSize.m_y / 2.0));
167 | return isInConvexCCWPolygon();
168 | // Ellipse
169 | case Shape::Ellipse:
170 | return pow((point.m_x - v.m_x) / (vSize.m_x * 0.5), 2)
171 | + pow((point.m_y - v.m_y) / (vSize.m_y * 0.5), 2)
172 | < 1;
173 | // Simple x y comparison
174 | case Shape::Rect:
175 | case Shape::RoundedRect:
176 | default:
177 | return point.m_x + epsilon >= v.m_x - vSize.m_x / 2.0
178 | && point.m_x - epsilon <= v.m_x + vSize.m_x / 2.0
179 | && point.m_y + epsilon >= v.m_y - vSize.m_y / 2.0
180 | && point.m_y - epsilon <= v.m_y + vSize.m_y / 2.0;
181 | }
182 | }
183 |
184 | DPoint contourPointFromAngleOP(double angle, int n, double rotationOffset, const DPoint& center,
185 | const DPoint& vSize) {
186 | // math visualised: https://www.desmos.com/calculator/j6iktd7fs4
187 | double nOffset = floor((angle - rotationOffset) / (2 * Math::pi / n)) * 2 * Math::pi / n;
188 | double polyLineStartAngle = rotationOffset + nOffset;
189 | double polyLineEndAngle = polyLineStartAngle + 2 * Math::pi / n;
190 | DLine polyLine = DLine(-cos(polyLineStartAngle), -sin(polyLineStartAngle),
191 | -cos(polyLineEndAngle), -sin(polyLineEndAngle));
192 |
193 | DLine originLine = DLine(0, 0, cos(angle), sin(angle));
194 |
195 | DPoint intersectionPoint;
196 | originLine.intersection(polyLine, intersectionPoint);
197 | intersectionPoint = DPoint(intersectionPoint.m_x * vSize.m_x, intersectionPoint.m_y * vSize.m_y);
198 | return intersectionPoint + center;
199 | }
200 |
201 | DPoint contourPointFromAngleOP(double angle, Shape shape, const DPoint& center, const DPoint& vSize) {
202 | angle = std::fmod(angle, 2 * Math::pi);
203 | if (angle < 0) {
204 | angle += Math::pi * 2;
205 | }
206 |
207 | switch (shape) {
208 | case Shape::Triangle:
209 | return contourPointFromAngleOP(angle, 3, Math::pi / 2, center, vSize * .5);
210 | case Shape::InvTriangle:
211 | return center - contourPointFromAngleOP(angle + Math::pi, Shape::Triangle, DPoint(), vSize);
212 | case Shape::Image:
213 | case Shape::RoundedRect:
214 | case Shape::Rect:
215 | return contourPointFromAngleOP(angle, 4, Math::pi / 4, center, vSize / sqrt(2));
216 | case Shape::Pentagon:
217 | return contourPointFromAngleOP(angle, 5, Math::pi / 2, center, vSize / 2);
218 | case Shape::Hexagon:
219 | return contourPointFromAngleOP(angle, 6, 0, center, vSize / 2);
220 | case Shape::Octagon:
221 | return contourPointFromAngleOP(angle, 8, Math::pi / 8, center, vSize / 2);
222 | case Shape::Rhomb:
223 | return contourPointFromAngleOP(angle, 4, Math::pi / 2, center, vSize / 2);
224 | case Shape::Trapeze:
225 | if (angle < atan(2) || angle >= Math::pi * 7 / 4) {
226 | DPoint other = contourPointFromAngleOP(Math::pi - angle, Shape::Trapeze, DPoint(), vSize);
227 | other.m_x *= -1;
228 | return other + center;
229 | } else if (angle < Math::pi - atan(2)) {
230 | return contourPointFromAngleOP(angle, Shape::Rect, center, vSize);
231 | } else if (angle < Math::pi * 5 / 4) {
232 | DLine tLine = DLine(.5, -1, 1, 1);
233 | DLine eLine = DLine(0, 0, 2 * cos(angle), 2 * sin(angle));
234 | DPoint iPoint;
235 | tLine.intersection(eLine, iPoint);
236 | iPoint = DPoint(iPoint.m_x * vSize.m_x * .5, iPoint.m_y * vSize.m_y * .5);
237 | return iPoint + center;
238 | } else { // angle < Math::pi * 7 / 4
239 | return contourPointFromAngleOP(angle, Shape::Rect, center, vSize);
240 | }
241 | case Shape::InvTrapeze:
242 | return center - contourPointFromAngleOP(angle + Math::pi, Shape::Trapeze, DPoint(), vSize);
243 | case Shape::Parallelogram:
244 | if (angle < atan(2) || angle > Math::pi * 7 / 4) {
245 | DLine tLine = DLine(-.5, -1, -1, 1);
246 | DLine eLine = DLine(0, 0, 2 * cos(angle), 2 * sin(angle));
247 | DPoint iPoint;
248 | tLine.intersection(eLine, iPoint);
249 | iPoint = DPoint(iPoint.m_x * vSize.m_x * .5, iPoint.m_y * vSize.m_y * .5);
250 | return iPoint + center;
251 | } else if (angle < Math::pi * 3 / 4) {
252 | return contourPointFromAngleOP(angle, Shape::Rect, center, vSize);
253 | } else if (angle < Math::pi + atan(2)) {
254 | DLine tLine = DLine(.5, 1, 1, -1);
255 | DLine eLine = DLine(0, 0, 2 * cos(angle), 2 * sin(angle));
256 | DPoint iPoint;
257 | tLine.intersection(eLine, iPoint);
258 | iPoint = DPoint(iPoint.m_x * vSize.m_x * .5, iPoint.m_y * vSize.m_y * .5);
259 | return iPoint + center;
260 | } else { // angle < Math::pi * 7 / 4
261 | return contourPointFromAngleOP(angle, Shape::Rect, center, vSize);
262 | }
263 | case Shape::InvParallelogram: {
264 | DPoint p = contourPointFromAngleOP(Math::pi - angle, Shape::Parallelogram, DPoint(), vSize);
265 | p.m_x *= -1;
266 | return p + center;
267 | }
268 | case Shape::Ellipse:
269 | default:
270 | return DPoint(-vSize.m_x * .5 * cos(angle), -vSize.m_y * .5 * sin(angle)) + center;
271 | }
272 | }
273 |
274 | bool isArrowEnabled(GraphAttributes &m_attr, adjEntry adj) {
275 | if (m_attr.has(GraphAttributes::edgeArrow)) {
276 | switch (m_attr.arrowType(*adj)) {
277 | case EdgeArrow::Undefined:
278 | return !adj->isSource() && m_attr.directed();
279 | case EdgeArrow::First:
280 | return adj->isSource();
281 | case EdgeArrow::Last:
282 | return !adj->isSource();
283 | case EdgeArrow::Both:
284 | return true;
285 | case EdgeArrow::None:
286 | default:
287 | return false;
288 | }
289 | } else {
290 | return !adj->isSource() && m_attr.directed();
291 | }
292 | }
293 |
294 | double getArrowSize(GraphAttributes &m_attr, adjEntry adj) {
295 | double result = 0;
296 |
297 | if (isArrowEnabled(m_attr, adj)) {
298 | const double minSize =
299 | (m_attr.has(GraphAttributes::edgeStyle) ? m_attr.strokeWidth(adj->theEdge()) : 1) * 3;
300 | node v = adj->theNode();
301 | node w = adj->twinNode();
302 | result = std::max(minSize,
303 | (m_attr.width(v) + m_attr.height(v) + m_attr.width(w) + m_attr.height(w)) / 16.0);
304 | }
305 |
306 | return result;
307 | }
308 |
309 | bool isCoveredBy(GraphAttributes &m_attr, const DPoint &point, adjEntry adj) {
310 | node v = adj->theNode();
311 | DPoint vSize = DPoint(m_attr.width(v), m_attr.height(v));
312 | return isPointCoveredByNodeOP(point, m_attr.point(v), vSize, m_attr.shape(v));
313 | }
314 |
315 | DPolyline drawArrowHead(const DPoint &start, DPoint &end, adjEntry adj, GraphAttributes &m_attr) {
316 | const double dx = end.m_x - start.m_x;
317 | const double dy = end.m_y - start.m_y;
318 | const double size = getArrowSize(m_attr, adj);
319 | node v = adj->theNode();
320 |
321 | DPolyline poly;
322 | if (dx == 0) {
323 | int sign = dy > 0 ? 1 : -1;
324 | double y = m_attr.y(v) - m_attr.height(v) / 2 * sign;
325 | end.m_y = y - sign * size;
326 |
327 | append({end.m_x, y, end.m_x - size / 4, y - size * sign,
328 | end.m_x + size / 4, y - size * sign, end.m_x, y}, poly);
329 | } else {
330 | // identify the position of the tip
331 | double slope = dy / dx;
332 | int sign = dx > 0 ? 1 : -1;
333 |
334 | double x = m_attr.x(v) - m_attr.width(v) / 2 * sign;
335 | double delta = x - start.m_x;
336 | double y = start.m_y + delta * slope;
337 |
338 | if (!isCoveredBy(m_attr, DPoint(x, y), adj)) {
339 | sign = dy > 0 ? 1 : -1;
340 | y = m_attr.y(v) - m_attr.height(v) / 2 * sign;
341 | delta = y - start.m_y;
342 | x = start.m_x + delta / slope;
343 | }
344 |
345 | end.m_x = x;
346 | end.m_y = y;
347 |
348 | // draw the actual arrow head
349 |
350 | double dx2 = end.m_x - start.m_x;
351 | double dy2 = end.m_y - start.m_y;
352 | double length = std::sqrt(dx2 * dx2 + dy2 * dy2);
353 | dx2 /= length;
354 | dy2 /= length;
355 |
356 | double mx = end.m_x - size * dx2;
357 | double my = end.m_y - size * dy2;
358 |
359 | double x2 = mx - size / 4 * dy2;
360 | double y2 = my + size / 4 * dx2;
361 |
362 | double x3 = mx + size / 4 * dy2;
363 | double y3 = my - size / 4 * dx2;
364 |
365 | append({end.m_x, end.m_y, x2, y2, x3, y3, end.m_x, end.m_y}, poly);
366 | }
367 |
368 | return poly;
369 | }
370 |
371 |
372 | DPolyline drawEdge(edge e, GraphAttributes &m_attr, DPoint *label_pos = nullptr,
373 | DPolyline *source_arrow = nullptr, DPolyline *target_arrow = nullptr) {
374 | bool drawSourceArrow = isArrowEnabled(m_attr, e->adjSource());
375 | bool drawTargetArrow = isArrowEnabled(m_attr, e->adjTarget());
376 | bool drawLabel = m_attr.has(GraphAttributes::edgeLabel) && !m_attr.label(e).empty();
377 |
378 | DPolyline path = m_attr.bends(e);
379 | node s = e->source();
380 | node t = e->target();
381 | path.pushFront(m_attr.point(s));
382 | path.pushBack(m_attr.point(t));
383 |
384 | bool drawSegment = false;
385 | bool finished = false;
386 |
387 | DPolyline points;
388 | for (ListConstIterator it = path.begin(); it.succ().valid() && !finished; it++) {
389 | DPoint p1 = *it;
390 | DPoint p2 = *(it.succ());
391 |
392 | // leaving segment at source node ?
393 | if (isCoveredBy(m_attr, p1, e->adjSource()) && !isCoveredBy(m_attr, p2, e->adjSource())) {
394 | if (!drawSegment && drawSourceArrow && source_arrow) {
395 | *source_arrow = drawArrowHead(p2, p1, e->adjSource(), m_attr);
396 | }
397 |
398 | drawSegment = true;
399 | }
400 |
401 | // entering segment at target node ?
402 | if (!isCoveredBy(m_attr, p1, e->adjTarget()) && isCoveredBy(m_attr, p2, e->adjTarget())) {
403 | finished = true;
404 |
405 | if (drawTargetArrow && target_arrow) {
406 | *target_arrow = drawArrowHead(p1, p2, e->adjTarget(), m_attr);
407 | }
408 | }
409 |
410 | if (drawSegment && drawLabel && label_pos) {
411 | label_pos->m_x = (p1.m_x + p2.m_x) / 2;
412 | label_pos->m_y = (p1.m_y + p2.m_y) / 2;
413 |
414 | drawLabel = false;
415 | }
416 |
417 | if (drawSegment) {
418 | points.pushBack(p1);
419 | }
420 |
421 | if (finished) {
422 | points.pushBack(p2);
423 | }
424 | }
425 | if (points.size() < 2) {
426 | return path;
427 | }
428 | return points;
429 | }
430 |
431 | template
432 | DRect getBoundingBox(const GenericPolyline &poly) {
433 | DPoint p1{std::numeric_limits::min(), std::numeric_limits::min()};
434 | DPoint p2{std::numeric_limits::max(), std::numeric_limits::max()};
435 | for (auto p: poly) {
436 | Math::updateMin(p1.m_x, p.m_x);
437 | Math::updateMax(p2.m_x, p.m_x);
438 | Math::updateMin(p1.m_y, p.m_y);
439 | Math::updateMax(p2.m_y, p.m_y);
440 | }
441 | return DRect{p1, p2};
442 | }
443 |
444 | double normSquared(const DPoint &p) {
445 | return p.m_x * p.m_x + p.m_y * p.m_y;
446 | }
447 |
448 | double closestPointOnLine(const DPolyline &line, const DPoint &x, DPoint &out) {
449 | if (line.size() == 0) {
450 | return std::numeric_limits::quiet_NaN();
451 | } else if (line.size() == 1) {
452 | out = line.front();
453 | return out.distance(x);
454 | }
455 | auto it = line.begin();
456 | auto p1 = *it;
457 | it++;
458 | double minDist = std::numeric_limits::infinity();
459 | for (auto p2 = *it; it != line.end(); it++) {
460 | // https://stackoverflow.com/a/10984080/805569
461 | p2 = *it;
462 | auto d = p2 - p1;
463 | auto r = (d * (x - p1)) / normSquared(d);
464 |
465 | if (r < 0) {
466 | auto l = normSquared(x - p1);
467 | if (l < minDist) {
468 | minDist = l;
469 | out = p1;
470 | }
471 | } else if (r > 1) {
472 | auto l = normSquared(x - p2);
473 | if (l < minDist) {
474 | minDist = l;
475 | out = p2;
476 | }
477 | } else {
478 | auto y = p1 + r * d;
479 | auto l = normSquared(x - y);
480 | if (l < minDist) {
481 | minDist = l;
482 | out = y;
483 | }
484 | }
485 | p1 = p2;
486 | }
487 | return minDist;
488 | }
489 |
490 | double closestPointOnEdge(const GraphAttributes &GA, edge e, const DPoint &x, DPoint &out) {
491 | DPoint p1 = GA.point(e->source()), p2;
492 | double minDist = std::numeric_limits::infinity();
493 | auto handle = [&](const DPoint &n) {
494 | // https://stackoverflow.com/a/10984080/805569
495 | p2 = n;
496 | auto d = p2 - p1;
497 | auto r = (d * (x - p1)) / normSquared(d);
498 |
499 | if (r < 0) {
500 | auto l = normSquared(x - p1);
501 | if (l < minDist) {
502 | minDist = l;
503 | out = p1;
504 | }
505 | } else if (r > 1) {
506 | auto l = normSquared(x - p2);
507 | if (l < minDist) {
508 | minDist = l;
509 | out = p2;
510 | }
511 | } else {
512 | auto y = p1 + r * d;
513 | auto l = normSquared(x - y);
514 | if (l < minDist) {
515 | minDist = l;
516 | out = y;
517 | }
518 | }
519 | p1 = p2;
520 | };
521 | for (auto n: GA.bends(e)) {
522 | handle(n);
523 | }
524 | handle(GA.point(e->target()));
525 | return minDist;
526 | }
527 |
528 | edge findClosestEdge(const GraphAttributes &GA, const DPoint &x, DPoint &out) {
529 | DPoint cur;
530 | edge res = nullptr;
531 | double minDist = std::numeric_limits::infinity();
532 | for (edge e: GA.constGraph().edges) {
533 | double d = closestPointOnEdge(GA, e, x, cur);
534 | if (d < minDist) {
535 | res = e;
536 | minDist = d;
537 | out = cur;
538 | }
539 | }
540 | return res;
541 | }
542 |
543 | node findClosestNode(const GraphAttributes &GA, const DPoint &x) {
544 | node res = nullptr;
545 | double minDist = std::numeric_limits::infinity();
546 | for (node n: GA.constGraph().nodes) {
547 | double d = normSquared(x - GA.point(n));
548 | if (d < minDist) {
549 | res = n;
550 | minDist = d;
551 | }
552 | }
553 | return res;
554 | }
555 | }
556 | }
557 |
--------------------------------------------------------------------------------
/src/ogdf_python/matplotlib/util.py:
--------------------------------------------------------------------------------
1 | import warnings
2 |
3 | import numpy as np
4 | import sys
5 | from dataclasses import dataclass, asdict
6 | from typing import List, Optional
7 | import collections
8 | import itertools
9 |
10 | from matplotlib.collections import PathCollection, Collection
11 | from matplotlib.figure import Figure
12 | from matplotlib.path import Path
13 | from matplotlib.text import Text
14 | from matplotlib.transforms import Affine2D
15 |
16 | from ogdf_python.loader import *
17 |
18 | __all__ = ["color", "fillPattern", "strokeType", "dPolylineToPath", "dPolylineToPathVertices", "EdgeStyle", "NodeStyle",
19 | "StyledElementCollection", "get_node_shape", "get_edge_path", "find_closest", "new_figure"]
20 |
21 |
22 | def color(c):
23 | if c.alpha() == 255:
24 | return str(c.toString())
25 | else:
26 | return str(c.toString()), c.alpha() / 255
27 |
28 |
29 | def fillPattern(fp):
30 | if isinstance(fp, str) and len(fp) == 1:
31 | fp = ogdf.FillPattern(ord(fp))
32 | if fp == ogdf.FillPattern.Solid:
33 | return ""
34 | elif fp == ogdf.FillPattern.Dense1:
35 | return "O"
36 | elif fp == ogdf.FillPattern.Dense2:
37 | return "o"
38 | elif fp == ogdf.FillPattern.Dense3:
39 | return "*"
40 | elif fp == ogdf.FillPattern.Dense4:
41 | return "."
42 | elif fp == ogdf.FillPattern.Dense5:
43 | return "OO"
44 | elif fp == ogdf.FillPattern.Dense6:
45 | return "**"
46 | elif fp == ogdf.FillPattern.Dense7:
47 | return ".."
48 | elif fp == ogdf.FillPattern.Horizontal:
49 | return "-"
50 | elif fp == ogdf.FillPattern.Vertical:
51 | return "|"
52 | elif fp == ogdf.FillPattern.Cross:
53 | return "+"
54 | elif fp == ogdf.FillPattern.BackwardDiagonal:
55 | return "\\"
56 | elif fp == ogdf.FillPattern.ForwardDiagonal:
57 | return "/"
58 | elif fp == ogdf.FillPattern.DiagonalCross:
59 | return "x"
60 | else:
61 | warnings.warn(f"Unknown FillPattern {fp!r}")
62 | return ""
63 |
64 |
65 | def strokeType(st):
66 | if isinstance(st, str) and len(st) == 1:
67 | st = ogdf.StrokeType(ord(st))
68 | if st == ogdf.StrokeType.Solid:
69 | return "solid"
70 | elif st == ogdf.StrokeType.Dash:
71 | return "dashed"
72 | elif st == ogdf.StrokeType.Dot:
73 | return "dotted"
74 | elif st == ogdf.StrokeType.Dashdot:
75 | return "dashdot"
76 | elif st == ogdf.StrokeType.Dashdotdot:
77 | return (0, (3, 5, 1, 5, 1, 5))
78 | elif st == getattr(ogdf.StrokeType, "None"):
79 | return (0, (0, 10))
80 | else:
81 | warnings.warn(f"Unknown StrokeType {st!r}")
82 | return ""
83 |
84 |
85 | def dPolylineToPathVertices(poly):
86 | return [(p.m_x, p.m_y) for p in poly]
87 |
88 |
89 | def dPolylineToPath(poly, closed=False):
90 | if closed:
91 | return Path(dPolylineToPathVertices(poly) + [(0, 0)], closed=True)
92 | else:
93 | return Path(dPolylineToPathVertices(poly), closed=False)
94 |
95 |
96 | FROZEN_DATACLASS = dict(frozen=True)
97 | if sys.version_info >= (3, 10):
98 | FROZEN_DATACLASS["slots"] = True
99 |
100 |
101 | @dataclass(**FROZEN_DATACLASS)
102 | class EdgeStyle:
103 | edgecolor: str
104 | linestyle: str
105 | linewidth: float
106 |
107 | def create_text(self, text, x, y):
108 | return Text(
109 | x=x, y=y,
110 | text=text,
111 | color=self.edgecolor,
112 | verticalalignment='center', horizontalalignment='center',
113 | zorder=300,
114 | )
115 |
116 | def create_collection(self, paths):
117 | d = {k + 's': v for k, v in self.asdict().items() if v}
118 | if "edgecolors" in d:
119 | d["facecolors"] = d["edgecolors"]
120 | return PathCollection(paths=paths, zorder=100, **d)
121 |
122 | def asdict(self):
123 | return asdict(self)
124 |
125 | @classmethod
126 | def from_GA(cls, GA, obj):
127 | return cls(**cls.dict_from_GA(GA, obj))
128 |
129 | @staticmethod
130 | def dict_from_GA(GA, obj):
131 | return dict(
132 | edgecolor=color(GA.strokeColor[obj]),
133 | linestyle=strokeType(GA.strokeType[obj]),
134 | linewidth=GA.strokeWidth[obj],
135 | )
136 |
137 |
138 | @dataclass(**FROZEN_DATACLASS)
139 | class NodeStyle(EdgeStyle):
140 | facecolor: str
141 | hatch: str
142 | hatchBgColor: str
143 |
144 | def create_collection(self, paths):
145 | d = {k + 's': v for k, v in self.asdict().items() if v}
146 | if "hatchs" in d:
147 | d["facecolors"] = "none"
148 | del d["hatchs"]
149 | d.pop("hatchBgColors", None)
150 | return PathCollection(paths=paths, zorder=200, **d)
151 |
152 | def create_hatch_collection(self, paths):
153 | if not self.hatch:
154 | return None
155 | return PathCollection(
156 | paths=paths, zorder=199, hatch=self.hatch,
157 | edgecolors=self.facecolor, facecolors=self.hatchBgColor)
158 |
159 | @staticmethod
160 | def dict_from_GA(GA, obj):
161 | d = EdgeStyle.dict_from_GA(GA, obj)
162 | d["facecolor"] = color(GA.fillColor[obj])
163 | d["hatch"] = fillPattern(GA.fillPattern[obj])
164 | d["hatchBgColor"] = color(GA.fillBgColor[obj])
165 | return d
166 |
167 |
168 | @dataclass
169 | class StyledElementCollection:
170 | # style: EdgeStyle
171 | elems: List
172 | paths: List[Path]
173 | coll: Collection
174 | hatch_coll: Optional[Collection] = None
175 |
176 | def add_elem(self, elem, path):
177 | assert len(self.elems) == len(self.paths)
178 | idx = len(self.elems)
179 | self.elems.append(elem)
180 | self.paths.append(path)
181 | self.set_stale()
182 | return idx
183 |
184 | def remove_elem(self, idx):
185 | assert len(self.elems) == len(self.paths)
186 | assert 0 <= idx < len(self.elems)
187 | if idx == len(self.elems) - 1:
188 | self.elems.pop(-1)
189 | self.paths.pop(-1)
190 | self.set_stale()
191 | return None
192 |
193 | last = self.elems[-1]
194 | self.elems[idx] = self.elems.pop(-1)
195 | self.paths[idx] = self.paths.pop(-1)
196 | self.set_stale()
197 | return last
198 |
199 | def set_stale(self):
200 | self.coll.stale = True
201 | if self.hatch_coll:
202 | self.hatch_coll.stale = True
203 |
204 | def remove(self):
205 | self.coll.remove()
206 | if self.hatch_coll:
207 | self.hatch_coll.remove()
208 |
209 |
210 | def get_node_shape(x, y, w, h, s):
211 | if s == ogdf.Shape.Ellipse:
212 | circ = Path.unit_circle()
213 | trans = Affine2D()
214 | trans.scale(w, h)
215 | trans.translate(x, y)
216 | return circ.transformed(trans)
217 | elif s == ogdf.Shape.RoundedRect:
218 | b = min(w, h) * 0.1
219 | return Path(
220 | [(x - w / 2, y - h / 2 + b),
221 | (x - w / 2, y + h / 2 - b),
222 | (x - w / 2, y + h / 2), (x - w / 2 + b, y + h / 2),
223 | (x + w / 2 - b, y + h / 2),
224 | (x + w / 2, y + h / 2), (x + w / 2, y + h / 2 - b),
225 | (x + w / 2, y - h / 2 + b),
226 | (x + w / 2, y - h / 2), (x + w / 2 - b, y - h / 2),
227 | (x - w / 2 + b, y - h / 2),
228 | (x - w / 2, y - h / 2), (x - w / 2, y - h / 2 + b),
229 | (0, 0)],
230 | [Path.MOVETO, Path.LINETO, Path.CURVE3, Path.CURVE3, Path.LINETO, Path.CURVE3, Path.CURVE3, Path.LINETO,
231 | Path.CURVE3, Path.CURVE3, Path.LINETO,
232 | Path.CURVE3, Path.CURVE3, Path.CLOSEPOLY]
233 | )
234 | else:
235 | poly = ogdf.DPolyline()
236 | ogdf.python_matplotlib.drawPolygonShape(s, x, y, w, h, poly)
237 | return dPolylineToPath(poly, closed=True)
238 |
239 |
240 | def sliding_window(iterable, n):
241 | iterator = iter(iterable)
242 | window = collections.deque(itertools.islice(iterator, n - 1), maxlen=n)
243 | for x in iterator:
244 | window.append(x)
245 | yield tuple(window)
246 |
247 |
248 | def interpolate_curves(path, curviness):
249 | if not 0 <= curviness <= 0.5:
250 | raise ValueError(f"invalid curviness {curviness} outside of range [0, 0.5]")
251 | out_path = np.zeros((len(path) * 3 - 4, 2))
252 | out_path[0, :] = path[0]
253 | out_codes = np.full(len(path) * 3 - 4, Path.LINETO)
254 |
255 | for i, (a, b, c) in enumerate(sliding_window(path, 3)):
256 | i = i * 3
257 | out_codes[i + 1] = Path.LINETO
258 | out_path[i + 1, :] = (
259 | a[0] + (b[0] - a[0]) * (1 - curviness),
260 | a[1] + (b[1] - a[1]) * (1 - curviness)
261 | )
262 | out_codes[i + 2] = Path.CURVE3
263 | out_codes[i + 3] = Path.CURVE3
264 | out_path[i + 2, :] = b
265 | out_path[i + 3, :] = (
266 | b[0] + (c[0] - b[0]) * curviness,
267 | b[1] + (c[1] - b[1]) * curviness
268 | )
269 | out_path[-1, :] = path[-1]
270 | out_codes[-1] = Path.LINETO
271 | return out_path, out_codes
272 |
273 |
274 | interpolate_curves([(0, 0), (10, 10), (20, 0)], 0)
275 |
276 |
277 | def get_edge_path(GA, edge, label_pos=ogdf.DPoint(), closed=False, curviness=0):
278 | label_pos.m_x = (GA.x[edge.source()] + GA.x[edge.target()]) / 2
279 | label_pos.m_y = (GA.y[edge.source()] + GA.y[edge.target()]) / 2
280 | src_arr = ogdf.DPolygon() if ogdf.python_matplotlib.isArrowEnabled(GA, edge.adjSource()) else nullptr
281 | tgt_arr = ogdf.DPolygon() if ogdf.python_matplotlib.isArrowEnabled(GA, edge.adjTarget()) else nullptr
282 | poly = ogdf.python_matplotlib.drawEdge(edge, GA, label_pos, src_arr, tgt_arr)
283 |
284 | codes = []
285 | path = []
286 | if src_arr:
287 | src_arr_path = dPolylineToPathVertices(src_arr)
288 | path.append(np.array(src_arr_path))
289 | codes.append(np.full(len(src_arr_path), Path.LINETO))
290 |
291 | edge_path = np.array(dPolylineToPathVertices(poly))
292 | edge_codes = np.full(len(edge_path), Path.LINETO)
293 | if curviness > 0:
294 | edge_path, edge_codes = interpolate_curves(edge_path, curviness)
295 | path.append(edge_path)
296 | codes.append(edge_codes)
297 |
298 | if tgt_arr:
299 | tgt_arr_path = dPolylineToPathVertices(tgt_arr)
300 | path.append(np.array(tgt_arr_path))
301 | codes.append(np.full(len(tgt_arr_path), Path.LINETO))
302 |
303 | codes[0][0] = Path.MOVETO
304 | if closed:
305 | path.append(edge_path[::-1])
306 | codes.append(edge_codes)
307 |
308 | return Path(np.concatenate(path), np.concatenate(codes))
309 |
310 |
311 | def find_closest(GA, x, y, edge_dist=10):
312 | p = ogdf.DPoint(x, y)
313 | n = ogdf.python_matplotlib.findClosestNode(GA, p)
314 | if n:
315 | nd = (p - GA.point(n)).norm()
316 | if (nd <= max(GA.width[n], GA.height[n]) / 2 and
317 | get_node_shape(GA.x[n], GA.y[n], GA.width[n], GA.height[n], GA.shape[n])
318 | .contains_point((x, y), radius=GA.strokeWidth[n] / 2)):
319 | return n, nd, GA.point(n)
320 | out = ogdf.DPoint()
321 | e = ogdf.python_matplotlib.findClosestEdge(GA, p, out)
322 | if e:
323 | ed = (p - out).norm()
324 | if ed <= edge_dist:
325 | return e, ed, out
326 | return None, None, None
327 |
328 |
329 | def new_figure(num=None) -> Figure:
330 | import matplotlib.pyplot as plt
331 | from matplotlib import _pylab_helpers
332 | with plt.ioff():
333 | old_fig = None
334 | manager = _pylab_helpers.Gcf.get_active()
335 | if manager is not None:
336 | old_fig = manager.canvas.figure
337 |
338 | fig = plt.figure(num)
339 |
340 | if old_fig is not None:
341 | plt.figure(old_fig)
342 | return fig
343 |
344 |
345 | # ensure that backend is loaded and doesn't reset any configs (esp. is_interactive)
346 | # when being loaded for the first time
347 | import matplotlib.pyplot as plt
348 |
349 | plt.close(new_figure())
350 |
--------------------------------------------------------------------------------
/src/ogdf_python/matplotlib/widget.py:
--------------------------------------------------------------------------------
1 | import functools
2 | import traceback
3 | from typing import Dict, Tuple, List
4 |
5 | import numpy as np
6 | import sys
7 | from itertools import chain
8 | from matplotlib import patheffects
9 | from matplotlib.axes import Axes
10 | from matplotlib.backend_bases import MouseButton
11 | from matplotlib.patches import PathPatch
12 | from matplotlib.text import Text
13 |
14 | from ogdf_python.loader import *
15 | from ogdf_python.matplotlib.util import *
16 |
17 | __all__ = ["MatplotlibGraph", "MatplotlibGraphEditor"]
18 |
19 |
20 | def catch_exception(wrapped):
21 | @functools.wraps(wrapped)
22 | def fun(*args, **kwargs):
23 | try:
24 | wrapped(*args, **kwargs)
25 | except Exception:
26 | # traceback.print_stack()
27 | traceback.print_exc()
28 | pass
29 |
30 | return fun
31 |
32 |
33 | class MatplotlibGraph(ogdf.GraphObserver):
34 | EDGE_CLICK_WIDTH_PX = 10
35 | MAX_AUTO_NODE_LABELS = 100
36 | curviness = 0.0
37 |
38 | def __init__(self, GA, ax=None, add_nodes=True, add_edges=True,
39 | auto_node_labels=None, auto_edge_labels=None, apply_style=True, hide_spines=True):
40 | super().__init__(GA.constGraph())
41 | self.GA = GA
42 | if ax is None:
43 | ax = new_figure().subplots()
44 | self.ax: Axes = ax
45 | G = GA.constGraph()
46 |
47 | if auto_node_labels is None:
48 | auto_node_labels = G.numberOfNodes() < self.MAX_AUTO_NODE_LABELS
49 | if auto_edge_labels is None:
50 | auto_edge_labels = auto_node_labels
51 | self.node_labels: Dict[ogdf.node, Text] = dict()
52 | self.auto_node_labels: bool = auto_node_labels
53 | self.edge_labels: Dict[ogdf.edge, Text] = dict()
54 | self.auto_edge_labels: bool = auto_edge_labels
55 |
56 | # node -> (style, index)
57 | self.node_styles: Dict[ogdf.node, Tuple[NodeStyle, int]] = dict()
58 | # style -> (nodes, paths, collection, hatch_collection)
59 | self.style_nodes: Dict[NodeStyle, StyledElementCollection] = dict()
60 |
61 | # edge -> (style, index)
62 | self.edge_styles: Dict[ogdf.edge, Tuple[EdgeStyle, int]] = dict()
63 | # style -> (edges, paths, collection)
64 | self.style_edges: Dict[EdgeStyle, StyledElementCollection] = dict()
65 |
66 | self.label_pos = ogdf.EdgeArray[ogdf.DPoint](G)
67 | self.pending_actions: List[Tuple[str, int]] = []
68 | self.addition_timer = ax.figure.canvas.new_timer()
69 | self.addition_timer.add_callback(self.process_actions)
70 | self.addition_timer.interval = 100
71 | self.addition_timer.single_shot = True
72 |
73 | if add_nodes:
74 | for n in G.nodes:
75 | self.add_node(n)
76 | if add_edges:
77 | for e in G.edges:
78 | self.add_edge(e)
79 | if add_nodes or add_edges:
80 | for col in chain((s.coll for s in self.style_nodes.values()), (s.coll for s in self.style_edges.values())):
81 | self.ax.update_datalim(col.get_datalim(self.ax.transData).get_points())
82 |
83 | if apply_style:
84 | self.apply_style()
85 | if hide_spines:
86 | self.hide_spines()
87 |
88 | self._on_click_cid = self.ax.figure.canvas.mpl_connect('button_press_event', self._on_click)
89 | self.ax.figure.canvas.mpl_connect('close_event', lambda e: self.addition_timer.stop())
90 |
91 | def set_graph(self, GA, add_nodes=True, add_edges=True):
92 | self.reregister(GA.constGraph())
93 | self.GA = GA
94 | G = GA.constGraph()
95 | self.label_pos = ogdf.EdgeArray[ogdf.DPoint](G)
96 | self.cleared()
97 | if add_nodes:
98 | for n in G.nodes:
99 | self.add_node(n)
100 | if add_edges:
101 | for e in G.edges:
102 | self.add_edge(e)
103 |
104 | def __del__(self):
105 | self.addition_timer.stop()
106 | self.__destruct__()
107 |
108 | def _on_click(self, event):
109 | # print('%s click: button=%d, x=%d, y=%d, xdata=%f, ydata=%f' %
110 | # ('double' if event.dblclick else 'single', event.button,
111 | # event.x, event.y, event.xdata, event.ydata))
112 | if event.button != MouseButton.LEFT:
113 | return
114 | if self.ax.figure.canvas.toolbar.mode: # PAN / ZOOM are truthy
115 | return
116 | t = self.ax.transData.inverted()
117 | a, _ = t.transform([0, 0])
118 | b, _ = t.transform([self.EDGE_CLICK_WIDTH_PX, self.EDGE_CLICK_WIDTH_PX])
119 | edge_width = abs(a - b)
120 | o, d, p = find_closest(self.GA, event.xdata, event.ydata, edge_width)
121 | # print(edge_width, o, d, p.m_x if p else 0, p.m_y if p else 0)
122 | if not o:
123 | # print("bg")
124 | self.on_background_click(event)
125 | elif isinstance(o, ogdf.NodeElement):
126 | # print("node", o)
127 | self.on_node_click(o, event)
128 | elif isinstance(o, ogdf.EdgeElement):
129 | # print("edge", o)
130 | # self.point = getattr(self, "point", None)
131 | # if self.point:
132 | # self.point.remove()
133 | # self.point = self.ax.scatter([p.m_x], [p.m_y])
134 | self.on_edge_click(o, event)
135 | else:
136 | print(f"Clicked on a weird {type(o)} object {o!r}")
137 |
138 | def on_edge_click(self, edge, event):
139 | pass
140 |
141 | def on_node_click(self, node, event):
142 | pass
143 |
144 | def on_background_click(self, event):
145 | pass
146 |
147 | def update_all(self, autoscale=True):
148 | self.process_actions()
149 | for n in self.GA.constGraph().nodes:
150 | if n in self.node_styles:
151 | self.update_node(n)
152 | for e in self.GA.constGraph().edges:
153 | if e in self.edge_styles:
154 | self.update_edge(e)
155 | if autoscale:
156 | self.ax.ignore_existing_data_limits = True
157 | for col in chain((s.coll for s in self.style_nodes.values()), (s.coll for s in self.style_edges.values())):
158 | self.ax.update_datalim(col.get_datalim(self.ax.transData).get_points())
159 | self.ax.autoscale_view()
160 | self.ax.figure.canvas.draw_idle()
161 |
162 | def apply_style(self):
163 | self.ax.set_aspect(1, anchor="C", adjustable="datalim")
164 | self.ax.update_datalim([(0, 0), (100, 100)])
165 | self.ax.autoscale(enable=True, axis="both")
166 | self.ax.invert_yaxis()
167 | fig = self.ax.figure
168 | fig.canvas.header_visible = False
169 | fig.canvas.footer_visible = False
170 | fig.canvas.capture_scroll = False
171 |
172 | if fig.canvas.toolbar and not hasattr(fig.canvas.toolbar, "update_ogdf_graph"):
173 | def update(*args, **kwargs):
174 | self.update_all()
175 |
176 | def expand(*args, **kwargs):
177 | # autoscale gets disabled by panning
178 | self.ax.autoscale(enable=True, axis="both")
179 | self.update_all(autoscale=True)
180 |
181 | fig.canvas.toolbar.update_ogdf_graph = update
182 | fig.canvas.toolbar.expand_ogdf_graph = expand
183 | fig.canvas.toolbar.toolitems = [*fig.canvas.toolbar.toolitems,
184 | ("Update", "Update the Graph", "refresh", "update_ogdf_graph"),
185 | ("Fit Graph", "Show the full graph", "expand",
186 | "expand_ogdf_graph")] # arrows-alt
187 | fig.canvas.toolbar_visible = 'visible'
188 |
189 | def hide_spines(self):
190 | self.ax.spines['right'].set_visible(False)
191 | self.ax.spines['top'].set_visible(False)
192 | self.ax.spines['left'].set_visible(False)
193 | self.ax.spines['bottom'].set_visible(False)
194 | self.ax.figure.subplots_adjust(left=0, right=1, top=1, bottom=0)
195 | self.ax.figure.canvas.draw_idle()
196 |
197 | def _repr_mimebundle_(self, *args, **kwargs):
198 | from IPython.core.interactiveshell import InteractiveShell
199 |
200 | if not InteractiveShell.initialized():
201 | return {"text/plain": repr(self)}, {}
202 |
203 | fmt = InteractiveShell.instance().display_formatter
204 | display_en = fmt.ipython_display_formatter.enabled
205 | fmt.ipython_display_formatter.enabled = False
206 | try:
207 | data, meta = fmt.format(self.ax.figure.canvas, *args, **kwargs)
208 | # print("canvas", list(data.keys()) if data else "none")
209 | if list(data.keys()) != ["text/plain"] and data:
210 | return data, meta
211 | else:
212 | data, meta = fmt.format(self.ax.figure, *args, **kwargs)
213 | # print("figure", list(data.keys()) if data else "none")
214 | return data, meta
215 | finally:
216 | fmt.ipython_display_formatter.enabled = display_en
217 |
218 | #######################################################
219 |
220 | def process_actions(self):
221 | if not self.pending_actions:
222 | return
223 | pa, self.pending_actions = self.pending_actions, []
224 |
225 | for t, i in pa:
226 | self.process_action(t, i)
227 |
228 | self.ax.figure.canvas.draw_idle()
229 |
230 | def process_action(self, t, i):
231 | if t == "update_node":
232 | self.update_node(i)
233 | return
234 | elif t == "update_edge":
235 | self.update_edge(i)
236 | return
237 | elif t == "add_node":
238 | cont, my_cont, fun = self.GA.constGraph().nodes, self.node_styles, self.add_node
239 | else:
240 | assert t == "add_edge"
241 | cont, my_cont, fun = self.GA.constGraph().edges, self.edge_styles, self.add_edge
242 |
243 | try:
244 | obj = cont.byid(i)
245 | except LookupError:
246 | return
247 |
248 | if not obj or obj in my_cont:
249 | # no longer exists or already added
250 | return
251 |
252 | try:
253 | fun(obj)
254 | except Exception as e:
255 | print(f"Could not add new {t} {i} ({obj}): {e}", sys.stderr)
256 | traceback.print_exc()
257 |
258 | @catch_exception
259 | def cleared(self):
260 | # for r in chain(self.node_labels.values(), self.edge_labels.values(),
261 | # self.style_nodes.values(), self.style_edges.values()):
262 | # r.remove()
263 | inv = self.ax.yaxis.get_inverted() # manually retain this value, all others from apply_style() aren't overwritten
264 | self.ax.clear()
265 | self.ax.yaxis.set_inverted(inv)
266 | # sensible default
267 | self.ax.update_datalim([(0, 0), (100, 100)])
268 | self.ax.autoscale_view()
269 | self.node_labels.clear()
270 | self.edge_labels.clear()
271 | self.node_styles.clear()
272 | self.style_nodes.clear()
273 | self.edge_styles.clear()
274 | self.style_edges.clear()
275 |
276 | @catch_exception
277 | def nodeDeleted(self, node):
278 | if node not in self.node_styles:
279 | return
280 | self.remove_node(node)
281 |
282 | @catch_exception
283 | def edgeDeleted(self, edge):
284 | if edge not in self.edge_styles:
285 | return
286 | self.remove_edge(edge)
287 |
288 | @catch_exception
289 | def nodeAdded(self, node):
290 | self.pending_actions.append(("add_node", node.index()))
291 | self.addition_timer.start()
292 |
293 | @catch_exception
294 | def edgeAdded(self, edge):
295 | self.pending_actions.append(("add_edge", edge.index()))
296 | self.addition_timer.start()
297 |
298 | @catch_exception
299 | def reInit(self):
300 | self.cleared()
301 |
302 | #######################################################
303 |
304 | def add_node(self, n, label=None):
305 | GA = self.GA
306 | style = NodeStyle.from_GA(GA, n)
307 | if style not in self.style_nodes:
308 | paths = []
309 | coll = self.style_nodes[style] = StyledElementCollection(
310 | [], paths, style.create_collection(paths), style.create_hatch_collection(paths)
311 | )
312 | self.ax.add_collection(coll.coll)
313 | if coll.hatch_coll:
314 | self.ax.add_collection(coll.hatch_coll)
315 | else:
316 | coll = self.style_nodes[style]
317 |
318 | path = get_node_shape(GA.x[n], GA.y[n], GA.width[n], GA.height[n], GA.shape[n])
319 | idx = coll.add_elem(n, path)
320 | self.node_styles[n] = (style, idx)
321 | assert coll.elems[idx] == n
322 |
323 | if (label is None and self.auto_node_labels) or label:
324 | self.add_node_label(n)
325 |
326 | def add_node_label(self, n):
327 | GA = self.GA
328 | self.node_labels[n] = t = self.node_styles[n][0].create_text(
329 | GA.label[n], GA.x[n], GA.y[n])
330 | self.ax.add_artist(t)
331 |
332 | def remove_node(self, n):
333 | label = self.node_labels.pop(n, None)
334 | if label:
335 | label.remove()
336 |
337 | if n not in self.node_styles:
338 | return
339 | style, idx = self.node_styles.pop(n)
340 | coll = self.style_nodes[style]
341 | assert coll.elems[idx] == n
342 | chgd = coll.remove_elem(idx)
343 | if chgd:
344 | self.node_styles[chgd] = (style, idx)
345 | assert coll.elems[idx] == chgd
346 | elif not coll.paths:
347 | coll.remove()
348 | del self.style_nodes[style]
349 |
350 | def update_node(self, n):
351 | if n not in self.node_styles:
352 | return # addition probably not yet processed
353 | GA = self.GA
354 | new_style = NodeStyle.from_GA(GA, n)
355 | old_style, idx = self.node_styles[n]
356 | label = self.node_labels.get(n, None)
357 | if new_style == old_style:
358 | if label:
359 | label.set_text(GA.label[n])
360 | coll = self.style_nodes[new_style]
361 | assert coll.elems[idx] == n
362 | path = get_node_shape(GA.x[n], GA.y[n], GA.width[n], GA.height[n], GA.shape[n])
363 | if not np.array_equal(coll.paths[idx].vertices, path.vertices):
364 | coll.paths[idx] = path
365 | coll.set_stale()
366 | if label:
367 | label.set_x(GA.x[n])
368 | label.set_y(GA.y[n])
369 | else:
370 | self.remove_node(n)
371 | self.add_node(n, bool(label))
372 |
373 | #######################################################
374 |
375 | def add_edge(self, e, label=None):
376 | GA = self.GA
377 | style = EdgeStyle.from_GA(GA, e)
378 | if style not in self.style_edges:
379 | paths = []
380 | coll = self.style_edges[style] = StyledElementCollection(
381 | [], paths, style.create_collection(paths), None
382 | )
383 | self.ax.add_collection(coll.coll)
384 | else:
385 | coll = self.style_edges[style]
386 |
387 | path = get_edge_path(GA, e, self.label_pos[e], True, self.curviness)
388 | idx = coll.add_elem(e, path)
389 | self.edge_styles[e] = (style, idx)
390 | assert coll.elems[idx] == e
391 |
392 | if (label is None and self.auto_edge_labels) or label:
393 | self.add_edge_label(e)
394 |
395 | def add_edge_label(self, e):
396 | GA = self.GA
397 | self.edge_labels[e] = t = self.edge_styles[e][0].create_text(
398 | GA.label[e], self.label_pos[e].m_x, self.label_pos[e].m_y)
399 | self.ax.add_artist(t)
400 |
401 | def remove_edge(self, e):
402 | label = self.edge_labels.pop(e, None)
403 | if label:
404 | label.remove()
405 |
406 | if e not in self.edge_styles:
407 | return
408 | style, idx = self.edge_styles.pop(e)
409 | coll = self.style_edges[style]
410 | assert coll.elems[idx] == e
411 | chgd = coll.remove_elem(idx)
412 | if chgd is not None:
413 | self.edge_styles[chgd] = (style, idx)
414 | assert coll.elems[idx] == chgd
415 | elif not coll.paths:
416 | coll.remove()
417 | del self.style_edges[style]
418 |
419 | def update_edge(self, e):
420 | if e not in self.edge_styles:
421 | return # addition probably not yet processed
422 | GA = self.GA
423 | new_style = EdgeStyle.from_GA(GA, e)
424 | old_style, idx = self.edge_styles[e]
425 | label = self.edge_labels.get(e, None)
426 | if new_style == old_style:
427 | if label:
428 | label.set_text(GA.label[e])
429 | coll = self.style_edges[new_style]
430 | assert coll.elems[idx] == e
431 | path = get_edge_path(GA, e, self.label_pos[e], True, self.curviness)
432 | if not np.array_equal(coll.paths[idx].vertices, path.vertices):
433 | coll.paths[idx] = path
434 | coll.set_stale()
435 | if label:
436 | label.set_x(self.label_pos[e].m_x)
437 | label.set_y(self.label_pos[e].m_y)
438 | else:
439 | self.remove_edge(e)
440 | self.add_edge(e, bool(label))
441 |
442 |
443 | class MatplotlibGraphEditor(MatplotlibGraph):
444 | def __init__(self, *args, **kwargs):
445 | self.selected = None
446 | self.selected_artist = None
447 | self.dragging = False
448 |
449 | super().__init__(*args, **kwargs)
450 |
451 | self.ax.figure.canvas.mpl_connect('key_press_event', self._on_key)
452 | self.ax.figure.canvas.mpl_connect('button_release_event', self._on_release)
453 | self.ax.figure.canvas.mpl_connect('motion_notify_event', self._on_motion)
454 |
455 | def unselect(self, notify=True):
456 | if self.selected is None:
457 | return
458 | self.selected = None
459 | if self.selected_artist is not None:
460 | self.selected_artist.remove()
461 | self.selected_artist = None
462 |
463 | if notify:
464 | self.on_selection_changed()
465 | self.ax.figure.canvas.draw_idle()
466 |
467 | def select(self, elem, notify=True):
468 | self.unselect(notify=False)
469 | self.selected = elem
470 | if isinstance(elem, ogdf.NodeElement):
471 | style, idx = self.node_styles[elem]
472 | path = self.style_nodes[style].paths[idx]
473 | else:
474 | assert isinstance(elem, ogdf.EdgeElement)
475 | style, idx = self.edge_styles[elem]
476 | path = self.style_edges[style].paths[idx]
477 | self.selected_artist = PathPatch(
478 | path, fill=False,
479 | edgecolor=style.edgecolor, linestyle=style.linestyle, linewidth=style.linewidth,
480 | path_effects=[patheffects.withStroke(linewidth=5, foreground='blue')])
481 | self.ax.add_artist(self.selected_artist)
482 |
483 | if notify:
484 | self.on_selection_changed()
485 | self.ax.figure.canvas.draw_idle()
486 |
487 | def cleared(self):
488 | self.unselect()
489 | super().cleared()
490 |
491 | def nodeDeleted(self, node):
492 | if self.selected == node:
493 | self.unselect()
494 | super().nodeDeleted(node)
495 |
496 | def edgeDeleted(self, edge):
497 | if self.selected == edge:
498 | self.unselect()
499 | super().edgeDeleted(edge)
500 |
501 | def update_node(self, n):
502 | super().update_node(n)
503 | if n == self.selected:
504 | self.select(n, notify=False)
505 |
506 | def update_edge(self, e):
507 | super().update_edge(e)
508 | if e == self.selected:
509 | self.select(e, notify=False)
510 |
511 | def process_action(self, t, i):
512 | if t == "select":
513 | self.select(i)
514 | else:
515 | super().process_action(t, i)
516 |
517 | def on_background_click(self, event):
518 | super().on_background_click(event)
519 | self.dragging = False
520 | if event.dblclick:
521 | n = self.GA.constGraph().newNode()
522 | self.GA.label[n] = f"N{n.index()}"
523 | self.GA.x[n] = event.xdata
524 | self.GA.y[n] = event.ydata
525 | self.pending_actions.append(("select", n))
526 | self.addition_timer.start()
527 | elif "ctrl" not in event.modifiers:
528 | self.unselect()
529 |
530 | def on_selection_changed(self):
531 | pass
532 |
533 | def on_node_moved(self, node):
534 | pass
535 |
536 | def on_node_click(self, node, event):
537 | super().on_node_click(node, event)
538 | if isinstance(self.selected, ogdf.NodeElement) and self.selected != node and \
539 | "ctrl" in event.modifiers:
540 | self.GA.constGraph().newEdge(self.selected, node)
541 | return
542 |
543 | self.dragging = True
544 | self.select(node)
545 |
546 | def on_edge_click(self, edge, event):
547 | super().on_edge_click(edge, event)
548 | if event.dblclick:
549 | if "ctrl" in event.modifiers:
550 | e2 = self.GA.constGraph().split(edge)
551 | n = e2.source()
552 | self.GA.label[n] = f"N{n.index()}"
553 | self.GA.x[n] = event.xdata
554 | self.GA.y[n] = event.ydata
555 | self.pending_actions.append(("update_edge", edge))
556 | self.pending_actions.append(("select", n))
557 | self.addition_timer.start()
558 | return
559 | else:
560 | self.GA.constGraph().reverseEdge(edge)
561 | self.update_edge(edge)
562 | self.dragging = False
563 | self.select(edge)
564 |
565 | def _on_key(self, event):
566 | if event.key == "delete":
567 | if isinstance(self.selected, ogdf.NodeElement):
568 | self.GA.constGraph().delNode(self.selected)
569 | elif isinstance(self.selected, ogdf.EdgeElement):
570 | self.GA.constGraph().delEdge(self.selected)
571 |
572 | def _on_release(self, event):
573 | self.dragging = False
574 |
575 | def _on_motion(self, event):
576 | if not self.dragging or not self.selected or not isinstance(self.selected, ogdf.NodeElement):
577 | return
578 | node = self.selected
579 | self.GA.x[node] = event.xdata
580 | self.GA.y[node] = event.ydata
581 | self.on_node_moved(node) # allow callback modifications to node style/position
582 |
583 | self.update_node(node)
584 | for adj in node.adjEntries:
585 | self.update_edge(adj.theEdge())
586 |
587 | style, idx = self.node_styles[node]
588 | path = self.style_nodes[style].paths[idx]
589 | self.selected_artist.set_path(path)
590 |
591 | self.ax.figure.canvas.draw_idle()
592 |
--------------------------------------------------------------------------------
/src/ogdf_python/pythonize/__init__.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | from ogdf_python.doxygen import pythonize_docstrings, wrap_getattribute
4 | from ogdf_python.pythonize.container import *
5 | from ogdf_python.pythonize.graph_attributes import *
6 | from ogdf_python.pythonize.render import *
7 | from ogdf_python.pythonize.string import *
8 |
9 |
10 | def pythonize_ogdf(klass, name):
11 | # print(name, klass)
12 | try:
13 | pythonize_docstrings(klass, name)
14 | except Exception:
15 | pass # we ignore if updating the docs fails
16 |
17 | if name == "Graph":
18 | klass._repr_svg_ = Graph_to_svg
19 | elif name == "ClusterGraph":
20 | klass._repr_svg_ = ClusterGraph_to_svg
21 | elif name in ("GraphAttributes", "ClusterGraphAttributes"):
22 | replace_GraphAttributes(klass, name)
23 | klass._repr_svg_ = GraphAttributes_to_svg
24 |
25 | # TODO setitem?
26 | # TODO array slicing
27 | elif name.startswith("GraphObjectContainer"):
28 | klass.byid = GraphObjectContainer_byindex
29 | klass.__getitem__ = iterable_getitem
30 | klass.__str__ = lambda self: "[%s]" % ", ".join(str(i) for i in self)
31 | klass.__repr__ = lambda self: "%s([%s])" % (type(self).__name__, ", ".join(repr(i) for i in self))
32 | elif re.fullmatch("S?List(Pure)?(<.+>)?", name):
33 | klass.__getitem__ = iterable_getitem
34 | klass.__str__ = lambda self: "[%s]" % ", ".join(str(i) for i in self)
35 | klass.__repr__ = lambda self: "%s([%s])" % (type(self).__name__, ", ".join(repr(i) for i in self))
36 | elif re.fullmatch("List(Const)?(Reverse)?Iterator(Base)?(<.+>)?", name):
37 | klass.__next__ = advance_iterator
38 | klass.__iter__ = lambda self: self
39 | elif re.fullmatch("(Node|Edge|AdjEntry|Cluster|Face)Array(<.+>)?", name):
40 | klass.__iter__ = cpp_iterator
41 | klass.keys = ArrayKeys[name.partition("Array")[0]]
42 | klass.asdict = lambda self: {k: self[k] for k in self.keys()}
43 | klass.__str__ = lambda self: str(self.asdict())
44 | klass.__repr__ = lambda self: "%s(%r)" % (type(self).__name__, self.asdict())
45 | # TODO NodeSet et al.
46 |
47 | elif name == "NodeElement":
48 | klass.__str__ = node_str
49 | klass.__repr__ = node_repr
50 | elif name == "EdgeElement":
51 | klass.__str__ = edge_str
52 | klass.__repr__ = edge_repr
53 | elif name == "AdjElement":
54 | klass.__str__ = adjEntry_str
55 | klass.__repr__ = adjEntry_repr
56 |
57 | elif name == "Color":
58 | klass.__str__ = lambda self: self.toString().decode("ascii")
59 | klass.__repr__ = lambda self: "ogdf.Color(%r)" % str(self)
60 |
61 | elif name == "GraphIO":
62 | for key, func in klass.__dict__.items():
63 | if key.startswith(("read", "write", "draw")):
64 | setattr(klass, key, wrap_GraphIO(func))
65 |
66 |
67 | cppyy.py.add_pythonization(pythonize_ogdf, "ogdf")
68 | cppyy.py.add_pythonization(pythonize_ogdf, "ogdf::internal")
69 | cppyy.gbl.ogdf.LayoutStandards.setDefaultEdgeArrow(cppyy.gbl.ogdf.EdgeArrow.Undefined)
70 | generate_GA_setters()
71 | wrap_getattribute(cppyy.gbl.ogdf)
72 |
--------------------------------------------------------------------------------
/src/ogdf_python/pythonize/container.py:
--------------------------------------------------------------------------------
1 | def GraphObjectContainer_byindex(self, idx):
2 | for e in self:
3 | if e.index() == idx:
4 | return e
5 | raise IndexError("Container has no element with index %s." % idx)
6 |
7 |
8 | def advance_iterator(self):
9 | if not self.valid():
10 | raise StopIteration()
11 | val = self.__deref__()
12 | self.__preinc__()
13 | return val
14 |
15 |
16 | def cpp_iterator(self):
17 | it = self.begin()
18 | while it != self.end():
19 | yield it.__deref__()
20 | it.__preinc__()
21 |
22 |
23 | def iterable_getitem(self, key):
24 | if isinstance(key, slice):
25 | indices = range(*key.indices(len(self)))
26 | elems = []
27 | try:
28 | next_ind = next(indices)
29 | for i, e in enumerate(self):
30 | if i == next_ind:
31 | elems.append(e)
32 | next_ind = next(indices)
33 | except StopIteration:
34 | pass
35 | return elems
36 | elif isinstance(key, int):
37 | if key < 0:
38 | key += len(self)
39 | if key < 0 or key >= len(self):
40 | raise IndexError("The index (%d) is out of range." % key)
41 | for i, e in enumerate(self):
42 | if i == key:
43 | return e
44 | else:
45 | raise TypeError("Invalid argument type %s." % type(key))
46 |
47 |
48 | def get_adjentry_array_keys(aea):
49 | for e in aea.graphOf().edges:
50 | yield e.adjSource()
51 | yield e.adjTarget()
52 |
53 |
54 | ArrayKeys = {
55 | "Node": lambda na: na.graphOf().nodes,
56 | "Edge": lambda ea: ea.graphOf().edges,
57 | "AdjEntry": get_adjentry_array_keys,
58 | "Cluster": lambda ca: ca.graphOf().clusters,
59 | "Face": lambda fa: fa.embeddingOf().faces,
60 | }
61 |
--------------------------------------------------------------------------------
/src/ogdf_python/pythonize/graph_attributes.py:
--------------------------------------------------------------------------------
1 | import cppyy
2 |
3 |
4 | class GraphAttributesProxy(object):
5 | def __init__(self, GA, getter, setter):
6 | self.GA = GA
7 | self.getter = getter
8 | self.setter = setter
9 |
10 | def __setitem__(self, key, item):
11 | return self.setter(self.GA, key, item)
12 |
13 | def __getitem__(self, key):
14 | return self.getter(self.GA, key)
15 |
16 | def __call__(self, *args, **kwargs):
17 | return self.getter(self.GA, *args, **kwargs)
18 |
19 |
20 | class GraphAttributesDescriptor(object):
21 | def __init__(self, getter, setter):
22 | self.getter = getter
23 | self.setter = setter
24 |
25 | def __get__(self, obj, type=None):
26 | return GraphAttributesProxy(obj, self.getter, self.setter)
27 |
28 |
29 | GA_FIELDS = [
30 | ("x", "ogdf::node", "double"),
31 | ("y", "ogdf::node", "double"),
32 | ("z", "ogdf::node", "double"),
33 | ("xLabel", "ogdf::node", "double"),
34 | ("yLabel", "ogdf::node", "double"),
35 | ("zLabel", "ogdf::node", "double"),
36 | ("width", "ogdf::node", "double"),
37 | ("height", "ogdf::node", "double"),
38 | ("shape", "ogdf::node", "ogdf::Shape"),
39 | ("strokeType", "ogdf::node", "ogdf::StrokeType"),
40 | ("strokeColor", "ogdf::node", "const ogdf::Color &"),
41 | ("strokeWidth", "ogdf::node", "float"),
42 | ("fillPattern", "ogdf::node", "ogdf::FillPattern"),
43 | ("fillColor", "ogdf::node", "const ogdf::Color &"),
44 | ("fillBgColor", "ogdf::node", "const ogdf::Color &"),
45 | ("label", "ogdf::node", "const std::string &"),
46 | ("templateNode", "ogdf::node", "const std::string &"),
47 | ("weight", "ogdf::node", "int"),
48 | ("type", "ogdf::node", "ogdf::Graph::NodeType"),
49 | ("idNode", "ogdf::node", "int"),
50 | ("bends", "ogdf::edge", "const ogdf::DPolyline &"),
51 | ("arrowType", "ogdf::edge", "ogdf::EdgeArrow"),
52 | ("strokeType", "ogdf::edge", "ogdf::StrokeType"),
53 | ("strokeColor", "ogdf::edge", "const ogdf::Color &"),
54 | ("strokeWidth", "ogdf::edge", "float"),
55 | ("label", "ogdf::edge", "const std::string &"),
56 | ("intWeight", "ogdf::edge", "int"),
57 | ("doubleWeight", "ogdf::edge", "double"),
58 | ("type", "ogdf::edge", "ogdf::Graph::EdgeType"),
59 | ("subGraphBits", "ogdf::edge", "uint32_t"),
60 | ]
61 | CGA_FIELDS = [
62 | ("x", "ogdf::cluster", "double"),
63 | ("y", "ogdf::cluster", "double"),
64 | ("width", "ogdf::cluster", "double"),
65 | ("height", "ogdf::cluster", "double"),
66 | ("label", "ogdf::cluster", "const std::string &"),
67 | ("strokeType", "ogdf::cluster", "ogdf::StrokeType"),
68 | ("strokeColor", "ogdf::cluster", "const ogdf::Color &"),
69 | ("strokeWidth", "ogdf::cluster", "float"),
70 | ("fillPattern", "ogdf::cluster", "ogdf::FillPattern"),
71 | ("fillColor", "ogdf::cluster", "const ogdf::Color &"),
72 | ("fillBgColor", "ogdf::cluster", "const ogdf::Color &"),
73 | ("templateCluster", "ogdf::cluster", "const std::string &"),
74 | ]
75 | GA_FIELD_NAMES = set(t[0] for t in GA_FIELDS)
76 | CGA_FIELD_NAMES = set(t[0] for t in CGA_FIELDS)
77 |
78 |
79 | def generate_GA_setters():
80 | DEFS = """
81 | #include
82 | #include
83 |
84 | namespace ogdf_pythonization {
85 | void GraphAttributes_set_directed(ogdf::GraphAttributes &GA, bool v) { GA.directed() = v; }"""
86 | for name, obj, val in GA_FIELDS:
87 | DEFS += ("\n\tvoid GraphAttributes_set_{name}(ogdf::GraphAttributes &GA, {obj} o, {val} v) "
88 | "{{ GA.{name}(o) = v; }}\n".format(name=name, obj=obj, val=val))
89 | for name, obj, val in CGA_FIELDS:
90 | DEFS += ("\n\tvoid GraphAttributes_set_{name}(ogdf::ClusterGraphAttributes &GA, {obj} o, {val} v) "
91 | "{{ GA.{name}(o) = v; }}\n".format(name=name, obj=obj, val=val))
92 | cppyy.cppdef(DEFS + "};")
93 |
94 |
95 | def replace_GraphAttributes(klass, name):
96 | klass.directed = property(klass.directed, cppyy.gbl.ogdf_pythonization.GraphAttributes_set_directed)
97 | for field in (CGA_FIELD_NAMES if name.startswith("Cluster") else GA_FIELD_NAMES):
98 | setattr(klass, field, GraphAttributesDescriptor(
99 | getattr(klass, field),
100 | getattr(cppyy.gbl.ogdf_pythonization, "GraphAttributes_set_%s" % field)
101 | ))
--------------------------------------------------------------------------------
/src/ogdf_python/pythonize/render.py:
--------------------------------------------------------------------------------
1 | import functools
2 | import tempfile
3 |
4 | import cppyy
5 | import itertools
6 | import sys
7 |
8 | SVGConf = None
9 |
10 |
11 | def wrap_GraphIO(func):
12 | @functools.wraps(func)
13 | def wrapper(*args, **kwargs):
14 | logger = cppyy.gbl.ogdf.GraphIO.logger
15 | levels = {
16 | l: getattr(logger, l)()
17 | for l in [
18 | "globalLogLevel",
19 | "globalInternalLibraryLogLevel",
20 | "globalMinimumLogLevel",
21 | "localLogLevel",
22 | ]
23 | }
24 | logMode = logger.localLogMode()
25 | statMode = logger.globalStatisticMode()
26 | ret = stdout = stderr = None
27 |
28 | try:
29 | logger.localLogMode(type(logger).LogMode.Global)
30 | logger.globalStatisticMode(False)
31 | for l in levels:
32 | getattr(logger, l)(type(logger).Level.Minor)
33 |
34 | cppyy.gbl.ogdf_pythonization.BeginCaptureStdout()
35 | cppyy.gbl.ogdf_pythonization.BeginCaptureStderr()
36 | try:
37 | ret = func(*args, **kwargs)
38 | finally:
39 | stdout = cppyy.gbl.ogdf_pythonization.EndCaptureStdout().decode("utf8", "replace").strip()
40 | stderr = cppyy.gbl.ogdf_pythonization.EndCaptureStderr().decode("utf8", "replace").strip()
41 |
42 | finally:
43 | logger.localLogMode(logMode)
44 | logger.globalStatisticMode(statMode)
45 | for l, v in levels.items():
46 | getattr(logger, l)(v)
47 |
48 | if stdout and logger.is_lout(logger.Level.Medium):
49 | print(stdout)
50 | if stderr:
51 | print(stderr, file=sys.stderr)
52 |
53 | if not ret:
54 | args = ', '.join(itertools.chain(map(repr, args), (f"{k}={v!r}" for k, v in kwargs.items())))
55 | msg = f"GraphIO.{func.__name__}({args}) failed"
56 | if stdout or stderr:
57 | msg += ":"
58 | if stdout:
59 | msg += "\n" + stdout
60 | if stderr:
61 | msg += "\n" + stderr
62 | else:
63 | msg += " for unknown reason. Does the file exist?"
64 | raise IOError(msg)
65 | else:
66 | return ret
67 |
68 | wrapper.__overload__ = func.__overload__
69 | return wrapper
70 |
71 |
72 | def renderGraph(G):
73 | cppyy.include("ogdf/planarity/PlanarizationLayout.h")
74 | ogdf = cppyy.gbl.ogdf
75 | GA = ogdf.GraphAttributes(G, ogdf.GraphAttributes.all)
76 | ogdf.PlanarizationLayout().call(GA)
77 | for n in G.nodes:
78 | GA.label[n] = str(n.index())
79 | return GA
80 |
81 |
82 | def renderClusterGraph(CG):
83 | cppyy.include("ogdf/cluster/ClusterPlanarizationLayout.h")
84 | ogdf = cppyy.gbl.ogdf
85 | CGA = ogdf.ClusterGraphAttributes(CG, ogdf.ClusterGraphAttributes.all)
86 | cppyy.gbl.ogdf_pythonization.BeginCaptureStdout()
87 | try:
88 | ogdf.ClusterPlanarizationLayout().call(CG.getGraph(), CGA, CG)
89 | finally:
90 | stdout = cppyy.gbl.ogdf_pythonization.EndCaptureStdout().decode("utf8", "replace").strip()
91 | for n in CG.getGraph().nodes:
92 | CGA.label[n] = str(n.index())
93 | return CGA
94 |
95 |
96 | def GraphAttributes_to_svg(self):
97 | global SVGConf
98 | if SVGConf is None:
99 | SVGConf = cppyy.gbl.ogdf.GraphIO.SVGSettings()
100 | SVGConf.margin(50)
101 | SVGConf.bezierInterpolation(False)
102 | # SVGConf.curviness(0.1)
103 | with tempfile.NamedTemporaryFile("w+t", suffix=".svg", prefix="ogdf-python-") as f:
104 | cppyy.gbl.ogdf.GraphIO.drawSVG(self, f.name, SVGConf)
105 | return f.read()
106 |
107 |
108 | def Graph_to_svg(self):
109 | return GraphAttributes_to_svg(renderGraph(self))
110 |
111 |
112 | def ClusterGraph_to_svg(self):
113 | return GraphAttributes_to_svg(renderClusterGraph(self))
114 |
--------------------------------------------------------------------------------
/src/ogdf_python/pythonize/string.py:
--------------------------------------------------------------------------------
1 | from ogdf_python.utils import *
2 |
3 |
4 | def node_repr(node):
5 | if is_nullptr(node):
6 | return "typed_nullptr(ogdf.node)"
7 | return "G.nodes.byid(%s)" % node.index()
8 |
9 |
10 | def node_str(node):
11 | if is_nullptr(node):
12 | return "nullptr (of type ogdf::node)"
13 | adjs = ["e%s%sn%s" % (adj.theEdge().index(), "->" if adj.isSource() else "<-", adj.twinNode().index())
14 | for adj in node.adjEntries]
15 | return "n%s °(%si+%so=%s) [%s]" % (node.index(), node.indeg(), node.outdeg(), node.degree(), ", ".join(adjs))
16 |
17 |
18 | def edge_repr(edge):
19 | if is_nullptr(edge):
20 | return "typed_nullptr(ogdf.edge)"
21 | return "G.edges.byid(%s)" % edge.index()
22 |
23 |
24 | def edge_str(edge):
25 | if is_nullptr(edge):
26 | return "nullptr (of type ogdf::edge)"
27 | return "n%s--e%s->n%s" % (edge.source().index(), edge.index(), edge.target().index())
28 |
29 |
30 | def adjEntry_repr(adj):
31 | if is_nullptr(adj):
32 | return "typed_nullptr(ogdf.adjEntry)"
33 | if adj.isSource():
34 | return "G.edges.byid(%s).adjSource()" % adj.theEdge().index()
35 | else:
36 | return "G.edges.byid(%s).adjTarget()" % adj.theEdge().index()
37 |
38 |
39 | def adjEntry_str(adj):
40 | if is_nullptr(adj):
41 | return "nullptr (of type ogdf::adjEntry)"
42 | if adj.isSource():
43 | return "(n%s--a%s)e%s->n%s" % (
44 | adj.theNode().index(), adj.index(), adj.theEdge().index(), adj.twinNode().index())
45 | else:
46 | return "(n%s<-a%s)e%s--n%s" % (
47 | adj.theNode().index(), adj.index(), adj.theEdge().index(), adj.twinNode().index())
48 |
--------------------------------------------------------------------------------
/src/ogdf_python/utils.py:
--------------------------------------------------------------------------------
1 | from ogdf_python.loader import *
2 |
3 | __all__ = ["is_nullptr", "typed_nullptr"]
4 |
5 | _capture_stdout = """
6 | std::ostringstream gCapturedStdout;
7 | std::streambuf* gOldStdoutBuffer = nullptr;
8 |
9 | static void BeginCaptureStdout() {
10 | gOldStdoutBuffer = std::cout.rdbuf();
11 | std::cout.rdbuf(gCapturedStdout.rdbuf());
12 | }
13 |
14 | static std::string EndCaptureStdout() {
15 | std::cout.rdbuf(gOldStdoutBuffer);
16 | gOldStdoutBuffer = nullptr;
17 |
18 | std::string capturedStdout = std::move(gCapturedStdout).str();
19 |
20 | gCapturedStdout.str("");
21 | gCapturedStdout.clear();
22 |
23 | return capturedStdout;
24 | }"""
25 |
26 | cppyy.cppdef("""
27 | #include
28 | #include
29 | #include
30 |
31 | namespace ogdf_pythonization {
32 | %s
33 |
34 | %s
35 | }
36 | """ % (_capture_stdout, _capture_stdout.replace("out", "err")))
37 |
38 |
39 | def is_nullptr(o):
40 | return cppyy.ll.addressof(o) == 0
41 |
42 |
43 | def typed_nullptr(klass):
44 | if isinstance(klass, type(ogdf.node)): # cppyy.TypedefPointerToClass
45 | return klass() # returns typed nullptr
46 | return cppyy.bind_object(cppyy.nullptr, klass)
47 |
--------------------------------------------------------------------------------
/ui-tests/.gitignore:
--------------------------------------------------------------------------------
1 | *.cjs
2 | *.mjs
3 |
4 | *.bundle.*
5 | lib/
6 | node_modules/
7 | *.log
8 | .eslintcache
9 | .stylelintcache
10 | *.egg-info/
11 | .ipynb_checkpoints
12 | *.tsbuildinfo
13 |
14 | # Integration tests
15 | test-results/
16 | playwright-report/
17 |
18 | # OSX files
19 | .DS_Store
20 |
21 | # Yarn cache
22 | .yarn/
23 |
--------------------------------------------------------------------------------
/ui-tests/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | nodeLinker: node-modules
2 |
--------------------------------------------------------------------------------
/ui-tests/README.md:
--------------------------------------------------------------------------------
1 | # Integration Testing
2 |
3 | This folder contains the integration tests of the extension.
4 |
5 | They are defined using [Playwright](https://playwright.dev/docs/intro) test runner
6 | and [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) helper.
7 |
8 | The Playwright configuration is defined in [playwright.config.js](./playwright.config.js).
9 |
10 | The JupyterLab server configuration to use for the integration test is defined
11 | in [jupyter_server_test_config.py](./jupyter_server_test_config.py).
12 |
13 | The default configuration will produce video for failing tests and an HTML report.
14 |
15 | > There is a new experimental UI mode that you may fall in love with; see [that video](https://www.youtube.com/watch?v=jF0yA-JLQW0).
16 |
17 | ## Run the tests
18 |
19 | > All commands are assumed to be executed from the root directory
20 |
21 | To run the tests, you need to:
22 |
23 | 1. Compile the extension:
24 |
25 | ```sh
26 | jlpm install
27 | jlpm build:prod
28 | ```
29 |
30 | > Check the extension is installed in JupyterLab.
31 |
32 | 2. Install test dependencies (needed only once):
33 |
34 | ```sh
35 | cd ./ui-tests
36 | jlpm install
37 | jlpm playwright install
38 | cd ..
39 | ```
40 |
41 | 3. Execute the [Playwright](https://playwright.dev/docs/intro) tests:
42 |
43 | ```sh
44 | cd ./ui-tests
45 | jlpm playwright test
46 | ```
47 |
48 | Test results will be shown in the terminal. In case of any test failures, the test report
49 | will be opened in your browser at the end of the tests execution; see
50 | [Playwright documentation](https://playwright.dev/docs/test-reporters#html-reporter)
51 | for configuring that behavior.
52 |
53 | ## Update the tests snapshots
54 |
55 | > All commands are assumed to be executed from the root directory
56 |
57 | If you are comparing snapshots to validate your tests, you may need to update
58 | the reference snapshots stored in the repository. To do that, you need to:
59 |
60 | 1. Compile the extension:
61 |
62 | ```sh
63 | jlpm install
64 | jlpm build:prod
65 | ```
66 |
67 | > Check the extension is installed in JupyterLab.
68 |
69 | 2. Install test dependencies (needed only once):
70 |
71 | ```sh
72 | cd ./ui-tests
73 | jlpm install
74 | jlpm playwright install
75 | cd ..
76 | ```
77 |
78 | 3. Execute the [Playwright](https://playwright.dev/docs/intro) command:
79 |
80 | ```sh
81 | cd ./ui-tests
82 | jlpm playwright test -u
83 | ```
84 |
85 | > Some discrepancy may occurs between the snapshots generated on your computer and
86 | > the one generated on the CI. To ease updating the snapshots on a PR, you can
87 | > type `please update playwright snapshots` to trigger the update by a bot on the CI.
88 | > Once the bot has computed new snapshots, it will commit them to the PR branch.
89 |
90 | ## Create tests
91 |
92 | > All commands are assumed to be executed from the root directory
93 |
94 | To create tests, the easiest way is to use the code generator tool of playwright:
95 |
96 | 1. Compile the extension:
97 |
98 | ```sh
99 | jlpm install
100 | jlpm build:prod
101 | ```
102 |
103 | > Check the extension is installed in JupyterLab.
104 |
105 | 2. Install test dependencies (needed only once):
106 |
107 | ```sh
108 | cd ./ui-tests
109 | jlpm install
110 | jlpm playwright install
111 | cd ..
112 | ```
113 |
114 | 3. Start the server:
115 |
116 | ```sh
117 | cd ./ui-tests
118 | jlpm start
119 | ```
120 |
121 | 4. Execute the [Playwright code generator](https://playwright.dev/docs/codegen) in **another terminal**:
122 |
123 | ```sh
124 | cd ./ui-tests
125 | jlpm playwright codegen localhost:8888
126 | ```
127 |
128 | ## Debug tests
129 |
130 | > All commands are assumed to be executed from the root directory
131 |
132 | To debug tests, a good way is to use the inspector tool of playwright:
133 |
134 | 1. Compile the extension:
135 |
136 | ```sh
137 | jlpm install
138 | jlpm build:prod
139 | ```
140 |
141 | > Check the extension is installed in JupyterLab.
142 |
143 | 2. Install test dependencies (needed only once):
144 |
145 | ```sh
146 | cd ./ui-tests
147 | jlpm install
148 | jlpm playwright install
149 | cd ..
150 | ```
151 |
152 | 3. Execute the Playwright tests in [debug mode](https://playwright.dev/docs/debug):
153 |
154 | ```sh
155 | cd ./ui-tests
156 | jlpm playwright test --debug
157 | ```
158 |
159 | ## Upgrade Playwright and the browsers
160 |
161 | To update the web browser versions, you must update the package `@playwright/test`:
162 |
163 | ```sh
164 | cd ./ui-tests
165 | jlpm up "@playwright/test"
166 | jlpm playwright install
167 | ```
168 |
--------------------------------------------------------------------------------
/ui-tests/jupyter_server_test_config.py:
--------------------------------------------------------------------------------
1 | """Server configuration for integration tests.
2 |
3 | !! Never use this configuration in production because it
4 | opens the server to the world and provide access to JupyterLab
5 | JavaScript objects through the global window variable.
6 | """
7 | from jupyterlab.galata import configure_jupyter_server
8 |
9 | c.ServerApp.allow_root = True
10 | c.ServerApp.ip = "0.0.0.0"
11 | configure_jupyter_server(c)
12 |
13 | # Uncomment to set server log level to debug level
14 | # c.ServerApp.log_level = "DEBUG"
15 |
--------------------------------------------------------------------------------
/ui-tests/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ogdf-python-ui-tests",
3 | "version": "1.0.0",
4 | "description": "JupyterLab ogdf-python Integration Tests",
5 | "private": true,
6 | "scripts": {
7 | "start": "jupyter lab --config jupyter_server_test_config.py",
8 | "test": "jlpm playwright test",
9 | "test:update": "jlpm playwright test --update-snapshots"
10 | },
11 | "devDependencies": {
12 | "@jupyterlab/galata": "^5.0.5",
13 | "@playwright/test": "^1.37.0"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/ui-tests/playwright.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Configuration for Playwright using default from @jupyterlab/galata
3 | */
4 | const baseConfig = require('@jupyterlab/galata/lib/playwright-config');
5 |
6 | module.exports = {
7 | ...baseConfig,
8 | webServer: {
9 | command: 'jlpm start',
10 | url: 'http://localhost:8888/lab',
11 | timeout: 30000,
12 | reuseExistingServer: !process.env.CI
13 | },
14 | retries: process.env.CI ? 2 : 0,
15 | timeout: 120000,
16 | use: {
17 | viewport: { width: 1600, height: 1200 },
18 | screen: { width: 1600, height: 1200 },
19 | navigationTimeout: 10000,
20 | actionTimeout: 10000,
21 | trace: 'retain-on-failure',
22 | video: 'retain-on-failure'
23 | }
24 | };
25 |
--------------------------------------------------------------------------------
/ui-tests/tests/callbacks.spec.ts:
--------------------------------------------------------------------------------
1 | import {expect, galata, test} from '@jupyterlab/galata';
2 | import * as path from 'path';
3 |
4 | async function testCallbacks(mode, page) {
5 | // 0 widget
6 | // 1 gc disable
7 | // 2 svg formatter disable
8 | // 3 import ogdf_python
9 | // 4 GA create
10 | // 5 newNode
11 | // 6 newEdge
12 | // 7 delEdge
13 | // 8 delNode
14 | // 9 clear+new
15 | // 10 gc run
16 | await page.notebook.runCellByCell({
17 | onAfterCellRun: async (idx) => {
18 | if (idx < 4 || idx == 10) return;
19 | await page.notebook.expandCellOutput(idx, true);
20 | await page.waitForTimeout(500);
21 | for (let c = 4; c <= idx; c++) {
22 | const cell = await (await page.notebook.getCell(c)).$(".jp-OutputArea-output");
23 | await cell.scrollIntoViewIfNeeded();
24 | expect.soft(await cell.screenshot())
25 | .toMatchSnapshot(`callbacks-${mode}-run-${idx}-cell-${c}.png`);
26 | }
27 | }
28 | });
29 | expect((await page.notebook.getCellTextOutput(10))[0]).toEqual("All good!\n")
30 | }
31 |
32 | const fileName = 'callbacks.ipynb';
33 |
34 | test.use({tmpPath: 'widget-callbacks-test'});
35 |
36 | test.describe.serial('Graph change callbacks', () => {
37 | test.beforeAll(async ({request, tmpPath}) => {
38 | const contents = galata.newContentsHelper(request);
39 | await contents.uploadFile(
40 | path.resolve(__dirname, `./notebooks/${fileName}`),
41 | `${tmpPath}/${fileName}`
42 | );
43 | });
44 |
45 | test.beforeEach(async ({page, tmpPath}) => {
46 | await page.filebrowser.openDirectory(tmpPath);
47 | // large enough for biggest figure
48 | // await page.setViewportSize({width: 500, height: 500});
49 | await page.notebook.openByPath(`${tmpPath}/${fileName}`);
50 | await page.notebook.activate(fileName);
51 | await page.getByText('Edit', {exact: true}).click();
52 | await page.locator('#jp-mainmenu-edit').getByText('Clear Outputs of All Cells', {exact: true}).click();
53 | });
54 |
55 | test.afterAll(async ({request, tmpPath}) => {
56 | const contents = galata.newContentsHelper(request);
57 | await contents.deleteDirectory(tmpPath);
58 | });
59 |
60 | test('Works with static inline', async ({page, tmpPath}) => {
61 | await testCallbacks("static", page);
62 | await page.notebook.save();
63 | });
64 |
65 | test('Works with dynamic widget', async ({page, tmpPath}) => {
66 | await page.notebook.setCell(0, "markdown", "%matplotlib widget");
67 | await page.notebook.setCellType(0, "code");
68 | await testCallbacks("widget", page);
69 | await page.notebook.save();
70 | });
71 | });
72 |
--------------------------------------------------------------------------------
/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-static-run-4-cell-4-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-static-run-4-cell-4-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-static-run-5-cell-4-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-static-run-5-cell-4-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-static-run-5-cell-5-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-static-run-5-cell-5-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-static-run-6-cell-4-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-static-run-6-cell-4-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-static-run-6-cell-5-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-static-run-6-cell-5-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-static-run-6-cell-6-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-static-run-6-cell-6-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-static-run-7-cell-4-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-static-run-7-cell-4-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-static-run-7-cell-5-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-static-run-7-cell-5-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-static-run-7-cell-6-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-static-run-7-cell-6-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-static-run-7-cell-7-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-static-run-7-cell-7-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-static-run-8-cell-4-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-static-run-8-cell-4-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-static-run-8-cell-5-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-static-run-8-cell-5-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-static-run-8-cell-6-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-static-run-8-cell-6-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-static-run-8-cell-7-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-static-run-8-cell-7-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-static-run-8-cell-8-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-static-run-8-cell-8-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-static-run-9-cell-4-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-static-run-9-cell-4-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-static-run-9-cell-5-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-static-run-9-cell-5-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-static-run-9-cell-6-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-static-run-9-cell-6-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-static-run-9-cell-7-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-static-run-9-cell-7-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-static-run-9-cell-8-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-static-run-9-cell-8-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-static-run-9-cell-9-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-static-run-9-cell-9-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-widget-run-4-cell-4-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-widget-run-4-cell-4-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-widget-run-5-cell-4-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-widget-run-5-cell-4-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-widget-run-5-cell-5-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-widget-run-5-cell-5-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-widget-run-6-cell-4-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-widget-run-6-cell-4-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-widget-run-6-cell-5-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-widget-run-6-cell-5-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-widget-run-6-cell-6-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-widget-run-6-cell-6-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-widget-run-7-cell-4-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-widget-run-7-cell-4-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-widget-run-7-cell-5-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-widget-run-7-cell-5-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-widget-run-7-cell-6-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-widget-run-7-cell-6-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-widget-run-7-cell-7-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-widget-run-7-cell-7-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-widget-run-8-cell-4-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-widget-run-8-cell-4-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-widget-run-8-cell-5-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-widget-run-8-cell-5-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-widget-run-8-cell-6-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-widget-run-8-cell-6-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-widget-run-8-cell-7-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-widget-run-8-cell-7-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-widget-run-8-cell-8-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-widget-run-8-cell-8-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-widget-run-9-cell-4-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-widget-run-9-cell-4-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-widget-run-9-cell-5-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-widget-run-9-cell-5-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-widget-run-9-cell-6-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-widget-run-9-cell-6-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-widget-run-9-cell-7-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-widget-run-9-cell-7-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-widget-run-9-cell-8-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-widget-run-9-cell-8-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-widget-run-9-cell-9-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/callbacks.spec.ts-snapshots/callbacks-widget-run-9-cell-9-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/interactive.spec.ts:
--------------------------------------------------------------------------------
1 | import {expect, test} from '@jupyterlab/galata';
2 |
3 | async function waitIdle(page) {
4 | await page.waitForTimeout(200);
5 | const idleLocator = page.locator('#jp-main-statusbar >> text=Idle');
6 | await idleLocator.waitFor();
7 | await page.waitForTimeout(200);
8 | }
9 |
10 | test.describe.serial('Editor UI Actions', () => {
11 | test('Run Notebook and capture cell outputs', async ({page}) => {
12 | // large enough for biggest figure
13 | // await page.setViewportSize({width: 1000, height: 1000});
14 | const fileName = "editor-ui.ipynb";
15 | await page.notebook.createNew(fileName);
16 | await page.notebook.activate(fileName);
17 | await page.notebook.openByPath(fileName);
18 | await page.waitForTimeout(200);
19 |
20 | await page.notebook.setCell(0, "markdown", `
21 | %matplotlib widget
22 |
23 | from ogdf_python import *
24 | from ogdf_python.matplotlib import MatplotlibGraphEditor
25 | `);
26 | await page.notebook.setCellType(0, "code");
27 | await page.notebook.runCell(0);
28 | await page.waitForTimeout(200);
29 |
30 | await page.notebook.setCell(1, "markdown", `
31 | G = ogdf.Graph()
32 | GA = ogdf.GraphAttributes(G, ogdf.GraphAttributes.all)
33 |
34 | N1 = G.newNode()
35 | N2 = G.newNode()
36 | N3 = G.newNode()
37 |
38 | G.newEdge(N1, N2)
39 | G.newEdge(N1, N3)
40 | e = G.newEdge(N2, N3)
41 |
42 | GA.x[N1], GA.y[N1] = 0, 0
43 | GA.x[N2], GA.y[N2] = 100, 0
44 | GA.x[N3], GA.y[N3] = 0, 100
45 | GA.bends[e].emplaceBack(40, 40)
46 |
47 | W = MatplotlibGraphEditor(GA)
48 | W
49 | `);
50 | await page.waitForTimeout(200);
51 | await page.notebook.setCellType(1, "code");
52 | await page.notebook.runCell(1);
53 |
54 | await page.notebook.expandCellOutput(1, true);
55 | await page.waitForTimeout(200);
56 | const cell = await (await page.notebook.getCell(1)).$(".jp-OutputArea-output");
57 | expect.soft(await cell.screenshot()).toMatchSnapshot(`editor-ui-0.png`);
58 |
59 | await page.notebook.setCell(2, "markdown", `
60 | print(W.ax.transData.transform([
61 | [100, 100], # dbl-click + click
62 | [0, 100], # ctrl-click + drag
63 | [0, 50], # drag-to
64 | [50, 0], # click + del
65 | [0, 0], # click + del
66 | ]).tolist())
67 | `);
68 | await page.waitForTimeout(200);
69 | await page.notebook.setCellType(2, "code");
70 | await page.notebook.runCell(2);
71 | const posText = await page.notebook.getCellTextOutput(2);
72 | const pos = JSON.parse(posText[0]);
73 | console.log(pos);
74 | expect(pos.length).toEqual(5);
75 |
76 | await cell.scrollIntoViewIfNeeded();
77 | const box = await cell.boundingBox();
78 | console.log(box);
79 | await page.mouse.click(box.x + pos[0][0], box.y + box.height - pos[0][1]);
80 | await page.waitForTimeout(200);
81 |
82 | await page.mouse.dblclick(box.x + pos[0][0], box.y + box.height - pos[0][1]);
83 | await waitIdle(page);
84 | expect.soft(await cell.screenshot()).toMatchSnapshot(`editor-ui-1.png`);
85 |
86 | await page.keyboard.down("Control");
87 | await page.waitForTimeout(100);
88 | await page.mouse.dblclick(box.x + pos[1][0], box.y + box.height - pos[1][1]);
89 | await page.waitForTimeout(100);
90 | await page.keyboard.up("Control");
91 | await waitIdle(page);
92 | expect.soft(await cell.screenshot()).toMatchSnapshot(`editor-ui-2.png`);
93 |
94 | await page.mouse.move(box.x + pos[1][0], box.y + box.height - pos[1][1]);
95 | await page.waitForTimeout(100);
96 | await page.mouse.down();
97 | await page.waitForTimeout(100);
98 | await page.mouse.move(box.x + pos[2][0], box.y + box.height - pos[2][1], {steps: 10});
99 | await page.waitForTimeout(100);
100 | await page.mouse.up();
101 | await waitIdle(page);
102 | expect.soft(await cell.screenshot()).toMatchSnapshot(`editor-ui-3.png`);
103 |
104 | await page.mouse.click(box.x + pos[3][0], box.y + box.height - pos[3][1]);
105 | await waitIdle(page);
106 | await page.keyboard.press("Delete");
107 | await waitIdle(page);
108 | expect.soft(await cell.screenshot()).toMatchSnapshot(`editor-ui-4.png`);
109 |
110 | await page.mouse.click(box.x + pos[4][0], box.y + box.height - pos[4][1]);
111 | await waitIdle(page);
112 | await page.keyboard.press("Delete");
113 | await waitIdle(page);
114 | expect.soft(await cell.screenshot()).toMatchSnapshot(`editor-ui-5.png`);
115 |
116 | // Save outputs for the next tests
117 | await page.notebook.save();
118 | });
119 | });
120 |
--------------------------------------------------------------------------------
/ui-tests/tests/interactive.spec.ts-snapshots/editor-ui-0-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/interactive.spec.ts-snapshots/editor-ui-0-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/interactive.spec.ts-snapshots/editor-ui-1-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/interactive.spec.ts-snapshots/editor-ui-1-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/interactive.spec.ts-snapshots/editor-ui-2-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/interactive.spec.ts-snapshots/editor-ui-2-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/interactive.spec.ts-snapshots/editor-ui-3-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/interactive.spec.ts-snapshots/editor-ui-3-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/interactive.spec.ts-snapshots/editor-ui-4-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/interactive.spec.ts-snapshots/editor-ui-4-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/interactive.spec.ts-snapshots/editor-ui-5-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/interactive.spec.ts-snapshots/editor-ui-5-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/notebooks/callbacks.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": null,
6 | "id": "d199e111-e0c4-43a5-a0e2-671f145e546b",
7 | "metadata": {},
8 | "outputs": [],
9 | "source": [
10 | "#%matplotlib widget"
11 | ]
12 | },
13 | {
14 | "cell_type": "code",
15 | "execution_count": null,
16 | "id": "24b97317-676a-426c-9d24-95f4f5e2ad9c",
17 | "metadata": {},
18 | "outputs": [],
19 | "source": [
20 | "import gc\n",
21 | "gc.disable()"
22 | ]
23 | },
24 | {
25 | "cell_type": "code",
26 | "execution_count": null,
27 | "id": "ed02b9e0-70a3-4b91-8b4c-99fdc2524abe",
28 | "metadata": {},
29 | "outputs": [],
30 | "source": [
31 | "from IPython.core.interactiveshell import InteractiveShell\n",
32 | "\n",
33 | "InteractiveShell.instance().display_formatter.formatters['image/svg+xml'].enabled = False"
34 | ]
35 | },
36 | {
37 | "cell_type": "code",
38 | "execution_count": null,
39 | "id": "3f0a91b05224dfd",
40 | "metadata": {},
41 | "outputs": [],
42 | "source": [
43 | "from ogdf_python import *"
44 | ]
45 | },
46 | {
47 | "cell_type": "code",
48 | "execution_count": null,
49 | "id": "235a4d01-0edf-42f3-bace-980951996531",
50 | "metadata": {},
51 | "outputs": [],
52 | "source": [
53 | "G = ogdf.Graph()\n",
54 | "GA = ogdf.GraphAttributes(G, ogdf.GraphAttributes.all)\n",
55 | "\n",
56 | "N1 = G.newNode()\n",
57 | "N2 = G.newNode()\n",
58 | "N3 = G.newNode()\n",
59 | "\n",
60 | "E1 = G.newEdge(N1, N2)\n",
61 | "E2 = G.newEdge(N1, N3)\n",
62 | "E3 = G.newEdge(N2, N3)\n",
63 | "\n",
64 | "GA.x[N1], GA.y[N1] = 0, 0\n",
65 | "GA.x[N2], GA.y[N2] = 100, 0\n",
66 | "GA.x[N3], GA.y[N3] = 0, 100\n",
67 | "GA.bends[E3].emplaceBack(50, 60)\n",
68 | "\n",
69 | "GA"
70 | ]
71 | },
72 | {
73 | "cell_type": "code",
74 | "execution_count": null,
75 | "id": "338d75d0-1b1f-479b-ab90-a8d80d2fde99",
76 | "metadata": {},
77 | "outputs": [],
78 | "source": [
79 | "N4 = G.newNode()\n",
80 | "GA.x[N4], GA.y[N4] = 100, 100\n",
81 | "GA"
82 | ]
83 | },
84 | {
85 | "cell_type": "code",
86 | "execution_count": null,
87 | "id": "0a703706-649f-48dd-bf35-db3b8d83542e",
88 | "metadata": {},
89 | "outputs": [],
90 | "source": [
91 | "E3 = G.newEdge(N4, N3)\n",
92 | "GA"
93 | ]
94 | },
95 | {
96 | "cell_type": "code",
97 | "execution_count": null,
98 | "id": "479c9bf6-f536-4ea0-ae04-5827bf373e65",
99 | "metadata": {},
100 | "outputs": [],
101 | "source": [
102 | "G.delEdge(E1)\n",
103 | "GA"
104 | ]
105 | },
106 | {
107 | "cell_type": "code",
108 | "execution_count": null,
109 | "id": "3a941233-f81b-415f-b292-7ea246b9a329",
110 | "metadata": {},
111 | "outputs": [],
112 | "source": [
113 | "G.delNode(N3)\n",
114 | "GA"
115 | ]
116 | },
117 | {
118 | "cell_type": "code",
119 | "execution_count": null,
120 | "id": "8dcd5961-262c-4e2b-9a09-043cf9bb53e5",
121 | "metadata": {},
122 | "outputs": [],
123 | "source": [
124 | "G.clear()\n",
125 | "\n",
126 | "n = G.newNode()\n",
127 | "GA.x[n] = GA.y[n] = 15\n",
128 | "GA.label[n] = \"N\"\n",
129 | "\n",
130 | "m = G.newNode()\n",
131 | "GA.x[m], GA.y[m] = 50, 15\n",
132 | "GA.label[m] = \"M\"\n",
133 | "\n",
134 | "G.newEdge(n, m)\n",
135 | "GA"
136 | ]
137 | },
138 | {
139 | "cell_type": "code",
140 | "execution_count": null,
141 | "id": "e5c8135d-c336-4b80-a7ec-05714e2d2a89",
142 | "metadata": {},
143 | "outputs": [],
144 | "source": [
145 | "gc.collect()\n",
146 | "gc.enable()\n",
147 | "gc.collect()\n",
148 | "print(\"All good!\")"
149 | ]
150 | }
151 | ],
152 | "metadata": {
153 | "kernelspec": {
154 | "display_name": "Python 3 (ipykernel)",
155 | "language": "python",
156 | "name": "python3"
157 | },
158 | "language_info": {
159 | "codemirror_mode": {
160 | "name": "ipython",
161 | "version": 3
162 | },
163 | "file_extension": ".py",
164 | "mimetype": "text/x-python",
165 | "name": "python",
166 | "nbconvert_exporter": "python",
167 | "pygments_lexer": "ipython3",
168 | "version": "3.10.12"
169 | }
170 | },
171 | "nbformat": 4,
172 | "nbformat_minor": 5
173 | }
174 |
--------------------------------------------------------------------------------
/ui-tests/tests/widget.spec.ts:
--------------------------------------------------------------------------------
1 | import {expect, galata, test} from '@jupyterlab/galata';
2 | import * as path from 'path';
3 |
4 | const fileName = 'sugiyama-simple.ipynb';
5 |
6 |
7 | // FIXME double output if graph shown in the cell that loaded ogdf-python
8 | `from IPython.core.interactiveshell import InteractiveShell
9 | InteractiveShell.instance().display_formatter.formatters['image/svg+xml'].enabled = False
10 |
11 | from ogdf_python import ogdf
12 | G = ogdf.Graph()
13 | G`
14 |
15 | test.use({tmpPath: 'widget-modes-test'});
16 |
17 | test.describe.serial('Widget Display Modes', () => {
18 | test.beforeAll(async ({request, tmpPath}) => {
19 | const contents = galata.newContentsHelper(request);
20 | await contents.uploadFile(
21 | path.resolve(__dirname, `./notebooks/${fileName}`),
22 | `${tmpPath}/${fileName}`
23 | );
24 | });
25 |
26 | test.beforeEach(async ({page, tmpPath}) => {
27 | await page.filebrowser.openDirectory(tmpPath);
28 | });
29 |
30 | test.afterAll(async ({request, tmpPath}) => {
31 | const contents = galata.newContentsHelper(request);
32 | await contents.deleteDirectory(tmpPath);
33 | });
34 |
35 | test('Run Notebook and capture cell outputs', async ({page, tmpPath}) => {
36 | // large enough for biggest figure
37 | await page.setViewportSize({ width: 1000, height: 2000 });
38 | await page.notebook.openByPath(`${tmpPath}/${fileName}`);
39 | await page.notebook.activate(fileName);
40 | await page.getByText('Edit', {exact: true}).click();
41 | await page.locator('#jp-mainmenu-edit').getByText('Clear Outputs of All Cells', {exact: true}).click();
42 | await page.waitForTimeout(500);
43 |
44 | await page.notebook.runCellByCell({
45 | onAfterCellRun: async (idx) => {
46 | if (idx == 0) return;
47 | await page.notebook.expandCellOutput(idx, true);
48 | const cell = await (await page.notebook.getCell(idx)).$(".jp-OutputArea-output");
49 | await cell.scrollIntoViewIfNeeded();
50 | expect.soft(await cell.screenshot())
51 | .toMatchSnapshot(`notebook-cell-${idx}.png`);
52 | }
53 | });
54 | // Save outputs for the next tests
55 | await page.notebook.save();
56 | });
57 | });
58 |
--------------------------------------------------------------------------------
/ui-tests/tests/widget.spec.ts-snapshots/notebook-cell-1-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/widget.spec.ts-snapshots/notebook-cell-1-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/widget.spec.ts-snapshots/notebook-cell-2-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/widget.spec.ts-snapshots/notebook-cell-2-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/widget.spec.ts-snapshots/notebook-cell-3-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/widget.spec.ts-snapshots/notebook-cell-3-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/widget.spec.ts-snapshots/notebook-cell-4-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/widget.spec.ts-snapshots/notebook-cell-4-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/widget.spec.ts-snapshots/notebook-cell-5-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/widget.spec.ts-snapshots/notebook-cell-5-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/widget.spec.ts-snapshots/notebook-cell-6-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/widget.spec.ts-snapshots/notebook-cell-6-linux.png
--------------------------------------------------------------------------------
/ui-tests/tests/widget.spec.ts-snapshots/notebook-cell-7-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ogdf/ogdf-python/c6213dffe47a35eba348e7c20d50bb6ab4bf51b6/ui-tests/tests/widget.spec.ts-snapshots/notebook-cell-7-linux.png
--------------------------------------------------------------------------------
/ui-tests/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": true,
4 | "composite": true,
5 | "declaration": true,
6 | "esModuleInterop": true,
7 | "incremental": true,
8 | "jsx": "react",
9 | "module": "esnext",
10 | "moduleResolution": "node",
11 | "noEmitOnError": true,
12 | "noImplicitAny": true,
13 | "noUnusedLocals": true,
14 | "preserveWatchOutput": true,
15 | "resolveJsonModule": true,
16 | "outDir": "lib",
17 | "strict": true,
18 | "strictNullChecks": true,
19 | "target": "ES2018"
20 | },
21 | "compilerOptions": {
22 | "types": ["jest"]
23 | }
24 | }
25 |
--------------------------------------------------------------------------------