├── .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 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | N0 168 | 169 | 170 | 171 | N1 172 | 173 | 174 | 175 | N2 176 | 177 | 178 | 179 | N3 180 | 181 | 182 | 183 | N7 184 | 185 | 186 | 187 | N8 188 | 189 | 190 | 191 | N9 192 | 193 | 194 | 195 | N10 196 | 197 | 198 | 199 | N11 200 | 201 | 202 | 203 | N12 204 | 205 | 206 | 207 | N13 208 | 209 | 210 | 211 | N14 212 | 213 | 214 | 215 | N15 216 | 217 | 218 | 219 | N16 220 | 221 | 222 | 223 | N17 224 | 225 | 226 | 227 | N18 228 | 229 | 230 | 231 | N19 232 | 233 | 234 | 235 | N20 236 | 237 | 238 | 239 | N21 240 | 241 | 242 | 243 | N22 244 | 245 | 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 | --------------------------------------------------------------------------------