├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── workflow-fail-template.md └── workflows │ ├── release.yml │ ├── test_plugin.yaml │ └── transifex.yml ├── .gitignore ├── CHANGELOG.md ├── DataPlotly ├── __init__.py ├── core │ ├── __init__.py │ ├── core_utils.py │ ├── plot_expressions.py │ ├── plot_factory.py │ ├── plot_settings.py │ └── plot_types │ │ ├── __init__.py │ │ ├── bar_plot.py │ │ ├── box.py │ │ ├── contour.py │ │ ├── histogram.py │ │ ├── histogram2d.py │ │ ├── icons │ │ ├── 2dhistogram.svg │ │ ├── barplot.svg │ │ ├── boxplot.svg │ │ ├── contour.svg │ │ ├── histogram.svg │ │ ├── pie.svg │ │ ├── polar.svg │ │ ├── scatterplot.svg │ │ ├── scatterternary.svg │ │ └── violin.svg │ │ ├── pie.py │ │ ├── plot_type.py │ │ ├── polar.py │ │ ├── scatter.py │ │ ├── ternary.py │ │ └── violin.py ├── data_plotly.py ├── gui │ ├── __init__.py │ ├── add_new_dock_dlg.py │ ├── dock.py │ ├── gui_utils.py │ ├── layout_item_gui.py │ ├── plot_settings_widget.py │ └── remove_dock_dlg.py ├── icon.png ├── icons │ ├── circle.svg │ ├── clean.svg │ ├── create_plot.svg │ ├── cross.svg │ ├── dash.png │ ├── dataplotly.svg │ ├── diamond.svg │ ├── dot.png │ ├── dotdash.png │ ├── list_code.svg │ ├── list_custom.svg │ ├── list_help.svg │ ├── list_plot.svg │ ├── list_properties.svg │ ├── longdash.png │ ├── longdashdot.png │ ├── mActionArrowDown.svg │ ├── mActionArrowUp.svg │ ├── mActionDuplicateLayer.svg │ ├── mIconDataDefineExpression.svg │ ├── penta.svg │ ├── refresh.svg │ ├── reload.svg │ ├── save_as_html.svg │ ├── save_as_image.svg │ ├── solid.png │ ├── square.svg │ ├── star.svg │ ├── symbologyAdd.svg │ ├── symbologyRemove.svg │ ├── triangle.svg │ └── x.svg ├── jsscripts │ ├── plotly-1.52.2.min.js │ └── polyfill.min.js ├── layouts │ └── plot_layout_item.py ├── metadata.txt ├── processing │ ├── dataplotly_provider.py │ └── dataplotly_scatterplot.py ├── test │ ├── __init__.py │ ├── processing_scatter.html │ ├── qgis_interface.py │ ├── scatterplot.json │ ├── tenbytenraster.asc │ ├── tenbytenraster.asc.aux.xml │ ├── tenbytenraster.keywords │ ├── tenbytenraster.lic │ ├── tenbytenraster.prj │ ├── tenbytenraster.qml │ ├── test_data_plotly_dialog.py │ ├── test_dock_manager.py │ ├── test_guiutils.py │ ├── test_init.py │ ├── test_layer.cpg │ ├── test_layer.dbf │ ├── test_layer.geojson │ ├── test_layer.prj │ ├── test_layer.qpj │ ├── test_layer.shp │ ├── test_layer.shx │ ├── test_plot_factory.py │ ├── test_plot_settings.py │ ├── test_processing.py │ ├── test_project_with_state.qgs │ ├── test_project_without_state.qgs │ ├── test_qgis_environment.py │ ├── test_resources.py │ └── utilities.py ├── test_suite.py └── ui │ ├── add_dock_dlg.ui │ ├── dataplotly_dockwidget_base.ui │ └── remove_dock_dlg.ui ├── LICENSE ├── Makefile ├── README.md ├── REQUIREMENTS_TESTING.txt ├── img ├── plot_2dhistogram.png ├── plot_bar_stack.png ├── plot_box.png ├── plot_contour.png ├── plot_histogram.png ├── plot_histogram_violin.png ├── plot_interaction_box.gif ├── plot_interaction_scatter.gif ├── plot_interaction_scatter_box.gif ├── plot_pie.png ├── plot_polar.png ├── plot_scatter.png ├── plot_scatter_bar.png ├── plot_scatter_histogram.png ├── plot_ternary.png └── plot_violin.png ├── publiccode.yml ├── pylintrc ├── scripts └── run-env-linux.sh └── setup.cfg /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. If you hit some error please paste the entire Python stacktrace message. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | 21 | **Screenshots** 22 | If applicable, add screenshots to help explain your problem. 23 | 24 | **Desktop (please complete the following information):** 25 | - OS: [e.g. iOS] 26 | - QGIS release [e.g. QGIS 3.10] 27 | - DataPlotly release [e.g. 3.0] 28 | 29 | **Additional context** 30 | Add any other context about the problem here. 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots and URL about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/workflow-fail-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Workflow {{ env.WORKFLOW }} failed for {{ env.DOCKER_TAG }} 3 | labels: bug 4 | --- 5 | 6 | Workflow {{ env.WORKFLOW }} failed for {{ env.DOCKER_TAG }} at: {{ date | date('YYYY-MM-DD HH:mm:ss') }} 7 | 8 | {{ env.TEST_RESULT }} 9 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 🚀 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | jobs: 9 | release: 10 | 11 | runs-on: ubuntu-latest 12 | if: github.repository_owner == 'ghtmtt' && contains(github.ref, 'refs/tags/') 13 | 14 | steps: 15 | - name: Set env 16 | run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV 17 | 18 | - uses: actions/checkout@v2 19 | 20 | - name: Set up Python 3.10 21 | uses: actions/setup-python@v3 22 | with: 23 | python-version: "3.10" 24 | 25 | - name: Install Qt lrelease 26 | run: | 27 | sudo apt-get update 28 | sudo apt-get install qtbase5-dev qt5-qmake qttools5-dev-tools 29 | 30 | - name: Install Python requirements 31 | run: pip install qgis-plugin-ci 32 | 33 | - name : Get current changelog 34 | run: qgis-plugin-ci changelog ${{ env.RELEASE_VERSION }} >> release.md 35 | 36 | - name: Create release on GitHub 37 | uses: ncipollo/release-action@v1.10.0 38 | with: 39 | bodyFile: release.md 40 | token: ${{ secrets.GITHUB_TOKEN }} 41 | 42 | - name: Package and deploy the zip 43 | run: >- 44 | qgis-plugin-ci 45 | release ${{ env.RELEASE_VERSION }} 46 | --github-token ${{ secrets.GITHUB_TOKEN }} 47 | --transifex-token ${{ secrets.TRANSIFEX_TOKEN }} 48 | --osgeo-username ${{ secrets.OSGEO_USERNAME }} 49 | --osgeo-password ${{ secrets.OSGEO_PASSWORD }} 50 | --create-plugin-repo 51 | -------------------------------------------------------------------------------- /.github/workflows/test_plugin.yaml: -------------------------------------------------------------------------------- 1 | name: Test plugin 2 | 3 | on: 4 | push: 5 | paths: 6 | - "DataPlotly/**" 7 | - ".github/workflows/test_plugin.yaml" 8 | pull_request: 9 | paths: 10 | - "DataPlotly/**" 11 | - ".github/workflows/test_plugin.yaml" 12 | 13 | env: 14 | # plugin name/directory where the code for the plugin is stored 15 | PLUGIN_NAME: DataPlotly 16 | # python notation to test running inside plugin 17 | TESTS_RUN_FUNCTION: DataPlotly.test_suite.test_package 18 | # Docker settings 19 | DOCKER_IMAGE: qgis/qgis 20 | 21 | 22 | jobs: 23 | 24 | Test-plugin-DataPlotly: 25 | 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | matrix: 30 | docker_tags: [release-3_28, latest] 31 | 32 | steps: 33 | 34 | - name: Checkout 35 | uses: actions/checkout@v2 36 | 37 | - name: Docker pull and create qgis-testing-environment 38 | run: | 39 | docker pull "$DOCKER_IMAGE":${{ matrix.docker_tags }} 40 | docker run -d --name qgis-testing-environment -v "$GITHUB_WORKSPACE":/tests_directory -e DISPLAY=:99 "$DOCKER_IMAGE":${{ matrix.docker_tags }} 41 | 42 | - name: Docker set up QGIS 43 | run: | 44 | docker exec qgis-testing-environment sh -c "qgis_setup.sh $PLUGIN_NAME" 45 | docker exec qgis-testing-environment sh -c "rm -f /root/.local/share/QGIS/QGIS3/profiles/default/python/plugins/$PLUGIN_NAME" 46 | docker exec qgis-testing-environment sh -c "ln -s /tests_directory/$PLUGIN_NAME /root/.local/share/QGIS/QGIS3/profiles/default/python/plugins/$PLUGIN_NAME" 47 | docker exec qgis-testing-environment sh -c "pip3 install -r /tests_directory/REQUIREMENTS_TESTING.txt" 48 | docker exec qgis-testing-environment sh -c "apt-get update" 49 | docker exec qgis-testing-environment sh -c "apt-get install -y python3-pyqt5.qtwebkit" 50 | 51 | - name: Docker run plugin tests 52 | run: | 53 | docker exec qgis-testing-environment sh -c "qgis_testrunner.sh $TESTS_RUN_FUNCTION" 54 | 55 | Check-code-quality: 56 | runs-on: ubuntu-latest 57 | steps: 58 | 59 | - name: Install Python 60 | uses: actions/setup-python@v1 61 | with: 62 | python-version: '3.8' 63 | architecture: 'x64' 64 | 65 | - name: Checkout 66 | uses: actions/checkout@v2 67 | 68 | - name: Install packages 69 | run: | 70 | pip install -r REQUIREMENTS_TESTING.txt 71 | pip install pylint pycodestyle 72 | 73 | - name: Pylint 74 | run: make pylint 75 | 76 | - name: Pycodestyle 77 | run: make pycodestyle 78 | -------------------------------------------------------------------------------- /.github/workflows/transifex.yml: -------------------------------------------------------------------------------- 1 | name: Transifex 🗺 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - '**.py' 9 | - '**.ui' 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | if: github.repository_owner == 'ghtmtt' 15 | steps: 16 | - uses: actions/checkout@v2 17 | 18 | - name: Set up Python 3.10 19 | uses: actions/setup-python@v3 20 | with: 21 | python-version: "3.10" 22 | 23 | - name: Install Python requirements 24 | run: pip install qgis-plugin-ci 25 | 26 | - name: Push to Transifex 27 | run: qgis-plugin-ci push-translation ${{ secrets.TRANSIFEX_TOKEN }} 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.pyc 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | cover/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | .pybuilder/ 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | # For a library or package, you might want to ignore these files since the code is 88 | # intended to run in multiple environments; otherwise, check them in: 89 | # .python-version 90 | 91 | # pipenv 92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 94 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 95 | # install all needed dependencies. 96 | #Pipfile.lock 97 | 98 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 99 | __pypackages__/ 100 | 101 | # Celery stuff 102 | celerybeat-schedule 103 | celerybeat.pid 104 | 105 | # SageMath parsed files 106 | *.sage.py 107 | 108 | # Environments 109 | .env 110 | .venv 111 | env/ 112 | venv/ 113 | ENV/ 114 | env.bak/ 115 | venv.bak/ 116 | 117 | # Spyder project settings 118 | .spyderproject 119 | .spyproject 120 | 121 | # Rope project settings 122 | .ropeproject 123 | 124 | # mkdocs documentation 125 | /site 126 | 127 | # mypy 128 | .mypy_cache/ 129 | .dmypy.json 130 | dmypy.json 131 | 132 | # Pyre type checker 133 | .pyre/ 134 | 135 | # pytype static type analyzer 136 | .pytype/ 137 | 138 | # Cython debug symbols 139 | cython_debug/ 140 | 141 | # Vscode 142 | .vscode/ 143 | 144 | # Windows thumbs 145 | Thumbs.db 146 | 147 | # zip files 148 | *.zip 149 | 150 | # IDE 151 | .idea/* 152 | 153 | # Vscode 154 | .vscode/* 155 | 156 | # Test 157 | *.qgs~ 158 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Unreleased 4 | 5 | ## 4.2.0 - 2024-10-24 6 | 7 | - Fix loading of the plugin when used with `qgis_process`, contribution from @Gustry 8 | - Fix incomplete plots when used with atlas, thanks to @nyalldawson 9 | - Fix DataType in pie charts, contribution to @jdugge 10 | 11 | ## 4.1.0 - 2023-11-15 12 | 13 | - Fix loading of the plugin when the Pandas library is not found, contribution from @Gustry 14 | 15 | ## 4.0.3 - 2023-06-27 16 | 17 | - fix bug with old projects (second part) thanks to @jdlom 18 | 19 | ## 4.0.2 - 2023-06-24 20 | 21 | - fix bug with old projects thanks to @jdlom 22 | 23 | ## 4.0.1 - 2023-05-12 24 | 25 | - tweak the bar width values, useful with time series 26 | - minimum QGIS version to 3.28 LTR 27 | 28 | ## 4.0.0 - 2023-04-17 29 | 30 | - multiple plot docks thanks to @jdlom 31 | - add a pie hole parameter to have donuts like charts thanks to @jdlom 32 | 33 | ## 3.9.2 - 2022-08-30 34 | 35 | - test added for Processing 36 | - Processing algorithm refactored using plotly.express (scatter plot) 37 | - fix stacktrace when removing all the plots from the layout 38 | - fix signal not updating the fields in the data defined buttons automatically 39 | 40 | ## 3.9.1 - 2022-08-20 41 | 42 | - Add custom function to have plot color the same as map color 43 | - Fix #237 add data-driven color to Polar Plot marker color. Kudos to @jmonticolo 44 | - Fix "Build a generic plot" processing algorithm. Kudos to @agiudiceandrea 45 | 46 | ## 3.9.0 - 2022-04-11 47 | 48 | - Customize font for plot title and plot axis. Kudos to @giliam 49 | - Support for Python 3.10 50 | 51 | ## 3.8.1 - 2021-09-28 52 | 53 | - bugfix 54 | 55 | ## 3.8.0 - 2021-09-28 56 | 57 | - [feature] expose DataPlotly on QGIS Server for a GetPrint request kudos to @Gustry 58 | 59 | ## 3.7.1 - 2020-05-15 60 | 61 | - bugfix 62 | 63 | ## 3.7.0 64 | 65 | - [feature] histogram and pie chart bar and slices with same color of category! kudos to @jdugge 66 | - [feature] plot background transparent in layouts! 67 | 68 | ## 3.6.0 69 | 70 | - [feature] Multi Plot in layout composer! Ultra kudos to @SGroe 71 | - [bugfix] Fix layout composer issue with many plots (ref #207). Thanks to the Italian Community for testing 72 | - [bugfix] Fix categorical bar plot wrong behavior 73 | - [bugfix] code cleaning 74 | 75 | ## 3.5.0 76 | 77 | - [bugfix] Fix loading old projects 78 | 79 | ## 3.4.0 80 | 81 | - [feature] get labels within the plot itself 82 | - [bugfix] Native datetime support! thanks @jdugge 83 | - [bugfix] Fix histogram selection 84 | 85 | ## 3.3.0 86 | 87 | - [bugfix] better loading project part 2 88 | 89 | ## 3.2.0 90 | 91 | - [bugfix] fix violin plot bug 92 | - [bugfix] better loading project handling 93 | 94 | ## 3.1.0 95 | 96 | - [feature] more data defined options available (in layout customization). Thanks @SGroe 97 | - [feature] X and Y axis bounds limits. Thanks @SGroe 98 | - [feature] add box plot within violin plots 99 | - [feature] renaming of plugin metadata to better search. Thanks @Gustry 100 | - [bugfix] Box plot not working when no group is selected 101 | - [bugfix] Data-defined property overrides do not work in layout 102 | 103 | ## 3.0.0 104 | 105 | - [feature] total refactoring of the code 106 | - [feature] plots also in print composer 107 | - [feature] atlas based plots 108 | - [feature] chance to save/load configuration file of plot setting 109 | - [feature] plot settings saved together with the project 110 | - [feature] more datadefined properties 111 | - [feature] show only selected/visible/filtered features 112 | - [feature] unit tests and continuous integration 113 | 114 | ## 2.3.0 115 | 116 | - [feature] tweaks polar plots, thanks @josephholler 117 | 118 | ## 2.2.0 119 | 120 | - [feature] UI tweaks, thanks @nyalldawson 121 | 122 | ## 2.1.0 123 | 124 | - [fix] typos in UI (thanks @leonmvd and @nyalldawson) 125 | - [fix] better python packages imports (thanks @nyalldawson) 126 | 127 | ## 2.0.0 128 | 129 | - [feature] DataPlotly is updated with plotly 3.3 version 130 | 131 | ## 1.6.0 132 | 133 | - [feature] wheel zoom! Give it a try 134 | - [feature] Edit plot title and X/Y labels in place 135 | 136 | ## 1.5.1 137 | 138 | - [feature] Spanish translation. Special thanks to Luca Bellani 139 | - [bugfix] always open English manual if locale not translated 140 | 141 | ## 1.5.0 142 | 143 | - [feature] **new** Violin plots! 144 | - [feature] **new** Polar plot layout! 145 | - [feature] better default color choice 146 | 147 | ## 1.4.3 148 | 149 | - [bugfix] correct interaction with pie plot 150 | - update plotly.js to v 1.34.0 151 | 152 | ## 1.4.2 153 | 154 | - [bugfix] correct saving html plot 155 | 156 | ## 1.4.1 157 | 158 | - [bugfixing] adaptation for new API 159 | 160 | ## 1.4.0 161 | 162 | - [feature] update plotly.js to v 1.33.1 163 | - [feature] multiple selection with Shift + selection tool 164 | - [feature] DataPlotly as Processing provider, thanks to Michaël Douchin of 3Liz 165 | -------------------------------------------------------------------------------- /DataPlotly/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | /*************************************************************************** 3 | DataPlotly 4 | A QGIS plugin 5 | D3 Plots for QGIS 6 | ------------------- 7 | begin : 2017-03-05 8 | copyright : (C) 2017 by matteo ghetta 9 | email : matteo.ghetta@gmail.com 10 | git sha : $Format:%H$ 11 | ***************************************************************************/ 12 | 13 | /*************************************************************************** 14 | * * 15 | * This program is free software; you can redistribute it and/or modify * 16 | * it under the terms of the GNU General Public License as published by * 17 | * the Free Software Foundation; either version 2 of the License, or * 18 | * (at your option) any later version. * 19 | * * 20 | ***************************************************************************/ 21 | This script initializes the plugin, making it known to QGIS. 22 | """ 23 | 24 | 25 | # noinspection PyPep8Naming 26 | def classFactory(iface): # pylint: disable=invalid-name 27 | """Load DataPlotly class from file DataPlotly. 28 | 29 | :param iface: A QGIS interface instance. 30 | :type iface: QgsInterface 31 | """ 32 | # pylint: disable=import-outside-toplevel 33 | from .data_plotly import DataPlotly 34 | return DataPlotly(iface) 35 | 36 | 37 | def serverClassFactory(server_iface): 38 | """Load DataPlotly server. 39 | 40 | :param server_iface: A QGIS Server interface instance. 41 | :type server_iface: QgsServerInterface 42 | """ 43 | _ = server_iface 44 | # pylint: disable=import-outside-toplevel 45 | from qgis.core import QgsApplication, QgsMessageLog, Qgis 46 | from DataPlotly.layouts.plot_layout_item import PlotLayoutItemMetadata 47 | 48 | QgsApplication.layoutItemRegistry().addLayoutItemType(PlotLayoutItemMetadata()) 49 | QgsMessageLog.logMessage("Custom DataPlotly layout item loaded", "DataPlotly", Qgis.Info) 50 | -------------------------------------------------------------------------------- /DataPlotly/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghtmtt/DataPlotly/f57b842703ebb259091b4490a9821518a14b01b9/DataPlotly/core/__init__.py -------------------------------------------------------------------------------- /DataPlotly/core/core_utils.py: -------------------------------------------------------------------------------- 1 | """Core Utilities 2 | 3 | .. note:: This program is free software; you can redistribute it and/or modify 4 | it under the terms of the GNU General Public License as published by 5 | the Free Software Foundation; either version 2 of the License, or 6 | (at your option) any later version. 7 | """ 8 | import uuid 9 | from qgis.PyQt.QtCore import QByteArray 10 | 11 | DOC_URL = "https://dataplotly-docs.readthedocs.io" 12 | 13 | def safe_str_xml(s): 14 | """ replaces spaces by . 15 | """ 16 | return s.replace(" ", ".") 17 | 18 | 19 | def restore_safe_str_xml(s): 20 | """ replaces . by spaces 21 | """ 22 | return s.replace(".", " ") 23 | 24 | 25 | def restore(b_str_64): 26 | """state and geom are stored in str(Base64) in project xml file""" 27 | return QByteArray.fromBase64(QByteArray(b_str_64.encode())) 28 | 29 | 30 | def uuid_suffix(string: str) -> str: 31 | """ uuid4 suffix""" 32 | return f"{string}{uuid.uuid4()}" 33 | -------------------------------------------------------------------------------- /DataPlotly/core/plot_expressions.py: -------------------------------------------------------------------------------- 1 | """ 2 | /*************************************************************************** 3 | DataPlotly 4 | A QGIS plugin 5 | D3 Plots for QGIS 6 | ------------------- 7 | begin : 2022-06-08 8 | git sha : $Format:%H$ 9 | copyright : (C) 2020 by matteo ghetta 10 | email : matteo.ghetta@faunalia.it 11 | ***************************************************************************/ 12 | 13 | /*************************************************************************** 14 | * * 15 | * This program is free software; you can redistribute it and/or modify * 16 | * it under the terms of the GNU General Public License as published by * 17 | * the Free Software Foundation; either version 2 of the License, or * 18 | * (at your option) any later version. * 19 | * * 20 | ***************************************************************************/ 21 | """ 22 | 23 | from qgis.utils import qgsfunction 24 | from qgis.core import QgsRenderContext 25 | 26 | 27 | @qgsfunction(args='auto', group='DataPlotly') 28 | def get_symbol_colors(feature, parent, context): 29 | """ 30 | Retrieve the color of each category as html code. You can use this function 31 | to set the plot items (pie slices, bars, points, etc) to the same color 32 | of the feature visible in the map. 33 |

Syntax

34 |

35 | get_symbol_colors() -> '#da1ddd' 36 |

37 | """ 38 | _ = parent 39 | layer = context.variable('layer') 40 | renderer_context = QgsRenderContext() 41 | renderer = layer.renderer() 42 | renderer.startRender(renderer_context, layer.fields()) 43 | 44 | symbols = renderer.originalSymbolsForFeature(feature, renderer_context) 45 | 46 | if symbols: 47 | color = symbols[0].color().name() 48 | else: 49 | color = '#000000' 50 | 51 | renderer.stopRender(renderer_context) 52 | 53 | return color 54 | -------------------------------------------------------------------------------- /DataPlotly/core/plot_types/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Available plot types 3 | 4 | .. note:: This program is free software; you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation; either version 2 of the License, or 7 | (at your option) any later version. 8 | """ 9 | 10 | from .bar_plot import BarPlotFactory 11 | from .box import BoxPlotFactory 12 | from .contour import ContourFactory 13 | from .histogram2d import Histogram2dFactory 14 | from .histogram import HistogramFactory 15 | from .pie import PieChartFactory 16 | from .polar import PolarChartFactory 17 | from .scatter import ScatterPlotFactory 18 | from .ternary import TernaryFactory 19 | from .violin import ViolinFactory 20 | -------------------------------------------------------------------------------- /DataPlotly/core/plot_types/bar_plot.py: -------------------------------------------------------------------------------- 1 | """ 2 | Bar plot 3 | 4 | .. note:: This program is free software; you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation; either version 2 of the License, or 7 | (at your option) any later version. 8 | """ 9 | 10 | import os 11 | from plotly import graph_objs 12 | from qgis.PyQt.QtGui import QIcon 13 | from DataPlotly.core.plot_types.plot_type import PlotType 14 | 15 | 16 | class BarPlotFactory(PlotType): 17 | """ 18 | Factory for bar plots 19 | """ 20 | 21 | @staticmethod 22 | def type_name(): 23 | return 'bar' 24 | 25 | @staticmethod 26 | def name(): 27 | return PlotType.tr('Bar Plot') 28 | 29 | @staticmethod 30 | def icon(): 31 | return QIcon(os.path.join(os.path.dirname(__file__), 'icons/barplot.svg')) 32 | 33 | @staticmethod 34 | def create_trace(settings): 35 | # flip the variables according to the box orientation 36 | 37 | if settings.properties['box_orientation'] == 'h': 38 | y = settings.x 39 | x = settings.y 40 | else: 41 | x = settings.x 42 | y = settings.y 43 | 44 | # tweak the width value 45 | if settings.data_defined_marker_sizes: 46 | width = settings.data_defined_marker_sizes 47 | # set to None if 0.0 or Auto, useful especially for date/datetime data 48 | elif settings.properties['marker_size'] == 0.0: 49 | width = None 50 | else: 51 | width = settings.properties['marker_size'] 52 | 53 | return [graph_objs.Bar( 54 | x=x, 55 | y=y, 56 | text=settings.additional_hover_text, 57 | textposition=settings.properties.get('hover_label_position'), 58 | name=settings.data_defined_legend_title if settings.data_defined_legend_title != '' else settings.properties['name'], 59 | ids=settings.feature_ids, 60 | customdata=settings.properties['custom'], 61 | orientation=settings.properties['box_orientation'], 62 | marker={'color': settings.data_defined_colors if settings.data_defined_colors else settings.properties['in_color'], 63 | 'colorscale': settings.properties['color_scale'], 64 | 'showscale': settings.properties['show_colorscale_legend'], 65 | 'reversescale': settings.properties['invert_color_scale'], 66 | 'colorbar': { 67 | 'len': 0.8 68 | }, 69 | 'line': { 70 | 'color': settings.data_defined_stroke_colors if settings.data_defined_stroke_colors else settings.properties['out_color'], 71 | 'width': settings.data_defined_stroke_widths if settings.data_defined_stroke_widths else settings.properties['marker_width']} 72 | }, 73 | width=width, 74 | opacity=settings.properties['opacity'] 75 | )] 76 | 77 | @staticmethod 78 | def create_layout(settings): 79 | layout = super(BarPlotFactory, BarPlotFactory).create_layout(settings) 80 | 81 | layout['barmode'] = settings.layout['bar_mode'] 82 | 83 | return layout 84 | -------------------------------------------------------------------------------- /DataPlotly/core/plot_types/box.py: -------------------------------------------------------------------------------- 1 | """ 2 | Box plot 3 | 4 | .. note:: This program is free software; you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation; either version 2 of the License, or 7 | (at your option) any later version. 8 | """ 9 | 10 | import os 11 | from plotly import graph_objs 12 | from qgis.PyQt.QtGui import QIcon 13 | from DataPlotly.core.plot_types.plot_type import PlotType 14 | 15 | 16 | class BoxPlotFactory(PlotType): 17 | """ 18 | Factory for box plots 19 | """ 20 | 21 | @staticmethod 22 | def type_name(): 23 | return 'box' 24 | 25 | @staticmethod 26 | def name(): 27 | return PlotType.tr('Box Plot') 28 | 29 | @staticmethod 30 | def icon(): 31 | return QIcon(os.path.join(os.path.dirname(__file__), 'icons/boxplot.svg')) 32 | 33 | @staticmethod 34 | def create_trace(settings): 35 | # flip the variables according to the box orientation 36 | 37 | if settings.properties['box_orientation'] == 'h': 38 | y = settings.x 39 | x = settings.y 40 | else: 41 | x = settings.x 42 | y = settings.y 43 | 44 | return [graph_objs.Box( 45 | x=x or None, 46 | y=y, 47 | name=settings.data_defined_legend_title if settings.data_defined_legend_title != '' else settings.properties['name'], 48 | customdata=settings.properties['custom'], 49 | boxmean=settings.properties['box_stat'], 50 | orientation=settings.properties['box_orientation'], 51 | boxpoints=settings.properties['box_outliers'], 52 | fillcolor=settings.data_defined_colors[0] if settings.data_defined_colors else settings.properties['in_color'], 53 | line={'color': settings.data_defined_stroke_colors[0] if settings.data_defined_stroke_colors else settings.properties['out_color'], 54 | 'width': settings.data_defined_stroke_widths[0] if settings.data_defined_stroke_widths else settings.properties['marker_width']}, 55 | opacity=settings.properties['opacity'] 56 | )] 57 | -------------------------------------------------------------------------------- /DataPlotly/core/plot_types/contour.py: -------------------------------------------------------------------------------- 1 | """ 2 | Contour chart factory 3 | 4 | .. note:: This program is free software; you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation; either version 2 of the License, or 7 | (at your option) any later version. 8 | """ 9 | 10 | import os 11 | from plotly import graph_objs 12 | from qgis.PyQt.QtGui import QIcon 13 | from DataPlotly.core.plot_types.plot_type import PlotType 14 | 15 | 16 | class ContourFactory(PlotType): 17 | """ 18 | Factory for contour charts 19 | """ 20 | 21 | @staticmethod 22 | def type_name(): 23 | return 'contour' 24 | 25 | @staticmethod 26 | def name(): 27 | return PlotType.tr('Contour Plot') 28 | 29 | @staticmethod 30 | def icon(): 31 | return QIcon(os.path.join(os.path.dirname(__file__), 'icons/contour.svg')) 32 | 33 | @staticmethod 34 | def create_trace(settings): 35 | return [graph_objs.Contour( 36 | z=[settings.x, settings.y], 37 | contours={ 38 | 'coloring': settings.properties['cont_type'], 39 | 'showlines': settings.properties['show_lines'] 40 | }, 41 | colorscale=settings.properties['color_scale'], 42 | opacity=settings.properties['opacity'] 43 | )] 44 | -------------------------------------------------------------------------------- /DataPlotly/core/plot_types/histogram.py: -------------------------------------------------------------------------------- 1 | """ 2 | Histogram factory 3 | 4 | .. note:: This program is free software; you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation; either version 2 of the License, or 7 | (at your option) any later version. 8 | """ 9 | 10 | import os 11 | from plotly import graph_objs 12 | from qgis.PyQt.QtGui import QIcon 13 | from DataPlotly.core.plot_types.plot_type import PlotType 14 | 15 | 16 | class HistogramFactory(PlotType): 17 | """ 18 | Factory for histogram plots 19 | """ 20 | 21 | @staticmethod 22 | def type_name(): 23 | return 'histogram' 24 | 25 | @staticmethod 26 | def name(): 27 | return PlotType.tr('Histogram') 28 | 29 | @staticmethod 30 | def icon(): 31 | return QIcon(os.path.join(os.path.dirname(__file__), 'icons/histogram.svg')) 32 | 33 | @staticmethod 34 | def create_trace(settings): 35 | return [ 36 | graph_objs.Histogram( 37 | x=settings.x, 38 | y=settings.x, 39 | name=settings.data_defined_legend_title if settings.data_defined_legend_title != '' else settings.properties['name'], 40 | orientation=settings.properties['box_orientation'], 41 | nbinsx=settings.properties['bins'], 42 | nbinsy=settings.properties['bins'], 43 | marker={ 44 | 'color': settings.data_defined_colors if settings.data_defined_colors else settings.properties['in_color'], 45 | 'line': { 46 | 'color': settings.data_defined_stroke_colors if settings.data_defined_stroke_colors else settings.properties['out_color'], 47 | 'width': settings.data_defined_stroke_widths if settings.data_defined_stroke_widths else settings.properties['marker_width'] 48 | } 49 | }, 50 | histnorm=settings.properties['normalization'], 51 | opacity=settings.properties['opacity'], 52 | cumulative={ 53 | 'enabled': settings.properties['cumulative'], 54 | 'direction': settings.properties['invert_hist'] 55 | } 56 | ) 57 | ] 58 | 59 | @staticmethod 60 | def create_layout(settings): 61 | layout = super(HistogramFactory, HistogramFactory).create_layout(settings) 62 | 63 | layout['barmode'] = settings.layout['bar_mode'] 64 | layout['bargroupgap'] = settings.layout['bargaps'] 65 | 66 | return layout 67 | -------------------------------------------------------------------------------- /DataPlotly/core/plot_types/histogram2d.py: -------------------------------------------------------------------------------- 1 | """ 2 | 2D histogram factory 3 | 4 | .. note:: This program is free software; you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation; either version 2 of the License, or 7 | (at your option) any later version. 8 | """ 9 | 10 | import os 11 | from plotly import graph_objs 12 | from qgis.PyQt.QtGui import QIcon 13 | from DataPlotly.core.plot_types.plot_type import PlotType 14 | 15 | 16 | class Histogram2dFactory(PlotType): 17 | """ 18 | Factory for 2D histograms 19 | """ 20 | 21 | @staticmethod 22 | def type_name(): 23 | return '2dhistogram' 24 | 25 | @staticmethod 26 | def name(): 27 | return PlotType.tr('2D Histogram') 28 | 29 | @staticmethod 30 | def icon(): 31 | return QIcon(os.path.join(os.path.dirname(__file__), 'icons/2dhistogram.svg')) 32 | 33 | @staticmethod 34 | def create_trace(settings): 35 | return [graph_objs.Histogram2d( 36 | x=settings.x, 37 | y=settings.y, 38 | colorscale=settings.properties['color_scale'] 39 | )] 40 | -------------------------------------------------------------------------------- /DataPlotly/core/plot_types/pie.py: -------------------------------------------------------------------------------- 1 | """ 2 | Pie chart factory 3 | 4 | .. note:: This program is free software; you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation; either version 2 of the License, or 7 | (at your option) any later version. 8 | """ 9 | 10 | import os 11 | from plotly import graph_objs 12 | from qgis.PyQt.QtGui import QIcon 13 | from DataPlotly.core.plot_types.plot_type import PlotType 14 | 15 | 16 | class PieChartFactory(PlotType): 17 | """ 18 | Factory for pie charts 19 | """ 20 | 21 | @staticmethod 22 | def type_name(): 23 | return 'pie' 24 | 25 | @staticmethod 26 | def name(): 27 | return PlotType.tr('Pie Chart') 28 | 29 | @staticmethod 30 | def icon(): 31 | return QIcon(os.path.join(os.path.dirname(__file__), 'icons/pie.svg')) 32 | 33 | @staticmethod 34 | def create_trace(settings): 35 | return [graph_objs.Pie( 36 | labels=settings.x, 37 | values=settings.y, 38 | marker={ 39 | 'colors': settings.data_defined_colors if settings.data_defined_colors else [settings.properties['in_color']] 40 | }, 41 | name=settings.properties['custom'][0], 42 | hole=settings.properties.get('pie_hole', 0) 43 | )] 44 | 45 | @staticmethod 46 | def create_layout(settings): 47 | layout = super(PieChartFactory, PieChartFactory).create_layout(settings) 48 | 49 | layout['xaxis'].update(title='') 50 | layout['xaxis'].update(showgrid=False) 51 | layout['xaxis'].update(zeroline=False) 52 | layout['xaxis'].update(showline=False) 53 | layout['xaxis'].update(showticklabels=False) 54 | layout['yaxis'].update(title='') 55 | layout['yaxis'].update(showgrid=False) 56 | layout['yaxis'].update(zeroline=False) 57 | layout['yaxis'].update(showline=False) 58 | layout['yaxis'].update(showticklabels=False) 59 | 60 | return layout 61 | -------------------------------------------------------------------------------- /DataPlotly/core/plot_types/plot_type.py: -------------------------------------------------------------------------------- 1 | """ 2 | Base class for plot type subclasses 3 | 4 | .. note:: This program is free software; you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation; either version 2 of the License, or 7 | (at your option) any later version. 8 | """ 9 | 10 | from plotly import graph_objs 11 | from qgis.PyQt.QtCore import QCoreApplication 12 | 13 | 14 | def from_qfont_to_plotly(style, color): 15 | """ 16 | Converts a QFont to a Plotly basic font settings dictionary 17 | """ 18 | if style is not None and color is not None: 19 | return { 20 | "size": style.pointSize() or 10, 21 | "color": color.name() or "black", 22 | "family": style.family() or "Arial", 23 | } 24 | return { 25 | "size": 10, 26 | "color": "black", 27 | "family": "Arial", 28 | } 29 | 30 | 31 | class PlotType: 32 | """ 33 | Base class for plot subclasses 34 | """ 35 | 36 | @staticmethod 37 | def type_name(): 38 | """ 39 | Returns the unique type name for this plot type 40 | """ 41 | return '' 42 | 43 | @staticmethod 44 | def name(): 45 | """ 46 | Returns the friendly translated name for the plot type 47 | """ 48 | return '' 49 | 50 | @staticmethod 51 | def icon(): 52 | """ 53 | Returns a path to an icon for this chart type 54 | """ 55 | return '' 56 | 57 | @staticmethod 58 | def create_trace(settings): # pylint: disable=W0613 59 | """ 60 | Returns a new trace using the specified plot settings 61 | """ 62 | return None 63 | 64 | @staticmethod 65 | def create_layout(settings): 66 | """ 67 | Returns a new layout using the specified plot settings 68 | """ 69 | 70 | # flip the variables according to the box orientation 71 | if settings.properties['box_orientation'] == 'h': 72 | y_title = settings.data_defined_y_title if settings.data_defined_y_title != ''\ 73 | else settings.layout['y_title'] 74 | x_title = settings.data_defined_x_title if settings.data_defined_x_title != ''\ 75 | else settings.layout['x_title'] 76 | else: 77 | x_title = settings.data_defined_x_title if settings.data_defined_x_title != ''\ 78 | else settings.layout['x_title'] 79 | y_title = settings.data_defined_y_title if settings.data_defined_y_title != ''\ 80 | else settings.layout['y_title'] 81 | 82 | range_x = None 83 | if settings.layout.get('x_min', None) is not None and settings.layout.get('x_max', None) is not None: 84 | range_x = [ 85 | settings.data_defined_x_min if settings.data_defined_x_min else settings.layout['x_min'], 86 | settings.data_defined_x_max if settings.data_defined_x_max else settings.layout['x_max'] 87 | ] 88 | range_y = None 89 | if settings.layout.get('y_min', None) is not None and settings.layout.get('y_max', None) is not None: 90 | range_y = [ 91 | settings.data_defined_y_min if settings.data_defined_y_min else settings.layout['y_min'], 92 | settings.data_defined_y_max if settings.data_defined_y_max else settings.layout['y_max'] 93 | ] 94 | 95 | bg_color = settings.layout.get('bg_color', 'rgb(255,255,255)') 96 | 97 | # add font size parameter from the title setting 98 | title = settings.data_defined_title if settings.data_defined_title else settings.layout['title'] 99 | if isinstance(title, str): 100 | title = {"text": title} 101 | title["font"] = { 102 | "size": settings.layout.get('font_title_size', 10), 103 | "color": settings.layout.get('font_title_color', "#000"), 104 | "family": settings.layout.get('font_title_family', "Arial"), 105 | } 106 | 107 | layout = graph_objs.Layout( 108 | showlegend=settings.layout['legend'], 109 | legend={'orientation': settings.layout['legend_orientation']}, 110 | title=title, 111 | xaxis={ 112 | 'title': x_title, 113 | 'titlefont': { 114 | "size": settings.layout.get('font_xlabel_size', 10), 115 | "color": settings.layout.get('font_xlabel_color', "#000"), 116 | "family": settings.layout.get('font_xlabel_family', "Arial"), 117 | }, 118 | 'autorange': settings.layout['x_inv'], 119 | 'range': range_x, 120 | 'tickfont': { 121 | "size": settings.layout.get('font_xticks_size', 10), 122 | "color": settings.layout.get('font_xticks_color', "#000"), 123 | "family": settings.layout.get('font_xticks_family', "Arial"), 124 | }, 125 | 'gridcolor': settings.layout.get('gridcolor', '#bdbfc0') 126 | }, 127 | yaxis={ 128 | 'title': y_title, 129 | 'titlefont': { 130 | "size": settings.layout.get('font_ylabel_size', 10), 131 | "color": settings.layout.get('font_ylabel_color', "#000"), 132 | "family": settings.layout.get('font_ylabel_family', "Arial"), 133 | }, 134 | 'autorange': settings.layout['y_inv'], 135 | 'range': range_y, 136 | 'tickfont': { 137 | "size": settings.layout.get('font_yticks_size', 10), 138 | "color": settings.layout.get('font_yticks_color', "#000"), 139 | "family": settings.layout.get('font_yticks_family', "Arial"), 140 | }, 141 | 'gridcolor': settings.layout.get('gridcolor', '#bdbfc0') 142 | }, 143 | paper_bgcolor=bg_color, 144 | plot_bgcolor=bg_color 145 | ) 146 | 147 | # update the x and y axis and add the linear and log only if the data are numeric 148 | # pass if field is empty 149 | try: 150 | if isinstance(settings.x[0], (int, float)): 151 | layout['xaxis'].update(type=settings.layout['x_type']) 152 | except: # pylint:disable=bare-except # noqa: F401 153 | pass 154 | try: 155 | if isinstance(settings.y[0], (int, float)): 156 | layout['yaxis'].update(type=settings.layout['y_type']) 157 | except: # pylint:disable=bare-except # noqa: F401 158 | pass 159 | 160 | return layout 161 | 162 | @staticmethod 163 | def tr(string, context=''): 164 | """ 165 | Translates a string 166 | """ 167 | if context == '': 168 | context = 'Types' 169 | return QCoreApplication.translate(context, string) 170 | -------------------------------------------------------------------------------- /DataPlotly/core/plot_types/polar.py: -------------------------------------------------------------------------------- 1 | """ 2 | Polar chart factory 3 | 4 | .. note:: This program is free software; you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation; either version 2 of the License, or 7 | (at your option) any later version. 8 | """ 9 | 10 | import os 11 | from plotly import graph_objs 12 | from qgis.PyQt.QtGui import QIcon 13 | from DataPlotly.core.plot_types.plot_type import PlotType 14 | 15 | 16 | class PolarChartFactory(PlotType): 17 | """ 18 | Factory for polar charts 19 | """ 20 | 21 | @staticmethod 22 | def type_name(): 23 | return 'polar' 24 | 25 | @staticmethod 26 | def name(): 27 | return PlotType.tr('Polar Plot') 28 | 29 | @staticmethod 30 | def icon(): 31 | return QIcon(os.path.join(os.path.dirname(__file__), 'icons/polar.svg')) 32 | 33 | @staticmethod 34 | def create_trace(settings): 35 | return [graph_objs.Scatterpolar( 36 | r=settings.y, 37 | theta=settings.x, 38 | mode=settings.properties['marker'], 39 | name=settings.properties['y_name'], 40 | marker={ 41 | "color": settings.data_defined_colors if settings.data_defined_colors else settings.properties['in_color'], 42 | "size": settings.data_defined_marker_sizes if settings.data_defined_marker_sizes else settings.properties['marker_size'], 43 | "symbol": settings.properties['marker_symbol'], 44 | "line": { 45 | "color": settings.properties['out_color'], 46 | "width": settings.properties['marker_width'] 47 | } 48 | }, 49 | line={ 50 | "color": settings.properties['in_color'], 51 | "width": settings.data_defined_stroke_widths if settings.data_defined_stroke_widths else settings.properties['marker_width'], 52 | "dash": settings.properties['line_dash'] 53 | }, 54 | opacity=settings.properties['opacity'], 55 | )] 56 | 57 | @staticmethod 58 | def create_layout(settings): 59 | layout = super(PolarChartFactory, PolarChartFactory).create_layout(settings) 60 | 61 | layout['polar'] = settings.layout['polar'] 62 | 63 | return layout 64 | -------------------------------------------------------------------------------- /DataPlotly/core/plot_types/scatter.py: -------------------------------------------------------------------------------- 1 | """ 2 | Base class for trace factories 3 | 4 | .. note:: This program is free software; you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation; either version 2 of the License, or 7 | (at your option) any later version. 8 | """ 9 | 10 | import os 11 | from plotly import graph_objs 12 | from qgis.PyQt.QtGui import QIcon 13 | from DataPlotly.core.plot_types.plot_type import PlotType 14 | 15 | 16 | class ScatterPlotFactory(PlotType): 17 | """ 18 | Factory for scatter plots 19 | """ 20 | 21 | @staticmethod 22 | def type_name(): 23 | return 'scatter' 24 | 25 | @staticmethod 26 | def name(): 27 | return PlotType.tr('Scatter Plot') 28 | 29 | @staticmethod 30 | def icon(): 31 | return QIcon(os.path.join(os.path.dirname(__file__), 'icons/scatterplot.svg')) 32 | 33 | @staticmethod 34 | def create_trace(settings): 35 | 36 | if settings.properties.get('hover_label_text') is not None: 37 | mode = settings.properties.get('marker') + \ 38 | settings.properties.get('hover_label_text') 39 | else: 40 | mode = settings.properties.get('marker') 41 | 42 | return [graph_objs.Scatter( 43 | x=settings.x, 44 | y=settings.y, 45 | mode=mode, 46 | textposition="top center", 47 | name=settings.data_defined_legend_title if settings.data_defined_legend_title != '' else settings.properties['name'], 48 | ids=settings.feature_ids, 49 | customdata=settings.properties['custom'], 50 | text=settings.additional_hover_text, 51 | hoverinfo=settings.properties['hover_text'], 52 | marker={'color': settings.data_defined_colors if settings.data_defined_colors else settings.properties['in_color'], 53 | 'colorscale': settings.properties['color_scale'], 54 | 'showscale': settings.properties['show_colorscale_legend'], 55 | 'reversescale': settings.properties['invert_color_scale'], 56 | 'colorbar': { 57 | 'len': 0.8}, 58 | 'size': settings.data_defined_marker_sizes if settings.data_defined_marker_sizes else settings.properties['marker_size'], 59 | 'symbol': settings.properties['marker_symbol'], 60 | 'line': {'color': settings.data_defined_stroke_colors if settings.data_defined_stroke_colors else settings.properties['out_color'], 61 | 'width': settings.data_defined_stroke_widths if settings.data_defined_stroke_widths else settings.properties['marker_width']} 62 | }, 63 | line={'width': settings.properties['marker_width'], 64 | 'dash': settings.properties['line_dash']}, 65 | opacity=settings.properties['opacity'] 66 | )] 67 | 68 | @staticmethod 69 | def create_layout(settings): 70 | layout = super(ScatterPlotFactory, ScatterPlotFactory).create_layout(settings) 71 | 72 | layout['xaxis'].update(rangeslider=settings.layout['range_slider']) 73 | 74 | return layout 75 | -------------------------------------------------------------------------------- /DataPlotly/core/plot_types/ternary.py: -------------------------------------------------------------------------------- 1 | """ 2 | Ternary plot factory 3 | 4 | .. note:: This program is free software; you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation; either version 2 of the License, or 7 | (at your option) any later version. 8 | """ 9 | 10 | import os 11 | from plotly import graph_objs 12 | from qgis.PyQt.QtGui import QIcon 13 | from DataPlotly.core.plot_types.plot_type import PlotType 14 | 15 | 16 | class TernaryFactory(PlotType): 17 | """ 18 | Factory for ternary plots 19 | """ 20 | 21 | @staticmethod 22 | def type_name(): 23 | return 'ternary' 24 | 25 | @staticmethod 26 | def name(): 27 | return PlotType.tr('Ternary Plot') 28 | 29 | @staticmethod 30 | def icon(): 31 | return QIcon(os.path.join(os.path.dirname(__file__), 'icons/scatterternary.svg')) 32 | 33 | @staticmethod 34 | def create_trace(settings): 35 | # prepare the hover text to display if the additional combobox is empty or not 36 | # this setting is necessary to overwrite the standard hovering labels 37 | if not settings.additional_hover_text: 38 | text = [ 39 | settings.properties['x_name'] + ': {}'.format( 40 | settings.x[k]) + '
{}: {}'.format( 41 | settings.properties['y_name'], 42 | settings.y[k]) + '
{}: {}'.format( 43 | settings.properties['z_name'], settings.z[k]) for k 44 | in 45 | range(len(settings.x))] 46 | else: 47 | text = [ 48 | settings.properties['x_name'] + ': {}'.format( 49 | settings.x[k]) + '
{}: {}'.format( 50 | settings.properties['y_name'], 51 | settings.y[k]) + '
{}: {}'.format( 52 | settings.properties['z_name'], 53 | settings.z[k]) + '
{}'.format( 54 | settings.additional_hover_text[k]) for k in 55 | range(len(settings.x))] 56 | 57 | return [graph_objs.Scatterternary( 58 | a=settings.x, 59 | b=settings.y, 60 | c=settings.z, 61 | name='{} + {} + {}'.format(settings.properties['x_name'], 62 | settings.properties['y_name'], 63 | settings.properties['z_name']), 64 | hoverinfo='text', 65 | text=text, 66 | mode='markers', 67 | marker={ 68 | 'color': settings.data_defined_colors if settings.data_defined_colors else settings.properties['in_color'], 69 | 'colorscale': settings.properties['color_scale'], 70 | 'showscale': settings.properties['show_colorscale_legend'], 71 | 'reversescale':settings.properties['invert_color_scale'], 72 | 'colorbar': { 73 | 'len': 0.8 74 | }, 75 | 'size': settings.data_defined_marker_sizes if settings.data_defined_marker_sizes else settings.properties['marker_size'], 76 | 'symbol': settings.properties['marker_symbol'], 77 | 'line': { 78 | 'color': settings.data_defined_stroke_colors if settings.data_defined_stroke_colors else settings.properties['out_color'], 79 | 'width': settings.data_defined_stroke_widths if settings.data_defined_stroke_widths else settings.properties['marker_width'] 80 | } 81 | }, 82 | opacity=settings.properties['opacity'] 83 | )] 84 | 85 | @staticmethod 86 | def create_layout(settings): 87 | layout = super(TernaryFactory, TernaryFactory).create_layout(settings) 88 | 89 | # flip the variables according to the box orientation 90 | if settings.properties['box_orientation'] == 'h': 91 | x_title = settings.data_defined_y_title if settings.data_defined_y_title != ''\ 92 | else settings.layout['y_title'] 93 | y_title = settings.data_defined_x_title if settings.data_defined_x_title != ''\ 94 | else settings.layout['x_title'] 95 | else: 96 | x_title = settings.data_defined_x_title if settings.data_defined_x_title != ''\ 97 | else settings.layout['x_title'] 98 | y_title = settings.data_defined_y_title if settings.data_defined_y_title != ''\ 99 | else settings.layout['y_title'] 100 | z_title = settings.data_defined_z_title if settings.data_defined_z_title != '' \ 101 | else settings.layout['z_title'] 102 | 103 | layout['xaxis'].update(title='') 104 | layout['xaxis'].update(showgrid=False) 105 | layout['xaxis'].update(zeroline=False) 106 | layout['xaxis'].update(showline=False) 107 | layout['xaxis'].update(showticklabels=False) 108 | layout['yaxis'].update(title='') 109 | layout['yaxis'].update(showgrid=False) 110 | layout['yaxis'].update(zeroline=False) 111 | layout['yaxis'].update(showline=False) 112 | layout['yaxis'].update(showticklabels=False) 113 | layout['ternary'] = { 114 | 'sum': 100, 115 | 'aaxis': { 116 | 'title': x_title, 117 | 'ticksuffix': '%', 118 | }, 119 | 'baxis': { 120 | 'title': y_title, 121 | 'ticksuffix': '%' 122 | }, 123 | 'caxis': { 124 | 'title': z_title, 125 | 'ticksuffix': '%' 126 | }, 127 | } 128 | 129 | return layout 130 | -------------------------------------------------------------------------------- /DataPlotly/core/plot_types/violin.py: -------------------------------------------------------------------------------- 1 | """ 2 | Violin chart factory 3 | 4 | .. note:: This program is free software; you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation; either version 2 of the License, or 7 | (at your option) any later version. 8 | """ 9 | 10 | import os 11 | from plotly import graph_objs 12 | from qgis.PyQt.QtGui import QIcon 13 | from DataPlotly.core.plot_types.plot_type import PlotType 14 | 15 | 16 | class ViolinFactory(PlotType): 17 | """ 18 | Factory for violin charts 19 | """ 20 | 21 | @staticmethod 22 | def type_name(): 23 | return 'violin' 24 | 25 | @staticmethod 26 | def name(): 27 | return PlotType.tr('Violin Plot') 28 | 29 | @staticmethod 30 | def icon(): 31 | return QIcon(os.path.join(os.path.dirname(__file__), 'icons/violin.svg')) 32 | 33 | @staticmethod 34 | def create_trace(settings): 35 | # flip the variables according to the box orientation 36 | if settings.properties['box_orientation'] == 'h': 37 | y = settings.x 38 | x = settings.y 39 | else: 40 | x = settings.x 41 | y = settings.y 42 | 43 | return [graph_objs.Violin( 44 | x=x or None, 45 | y=y, 46 | name=settings.data_defined_legend_title if settings.data_defined_legend_title != '' else settings.properties[ 47 | 'name'], 48 | customdata=settings.properties['custom'], 49 | orientation=settings.properties['box_orientation'], 50 | points=settings.properties['box_outliers'], 51 | fillcolor=settings.properties['in_color'], 52 | line={ 53 | 'color': settings.properties['out_color'], 54 | 'width': settings.properties['marker_width'] 55 | }, 56 | opacity=settings.properties['opacity'], 57 | meanline={ 58 | 'visible': settings.properties['show_mean_line'] 59 | }, 60 | side=settings.properties['violin_side'], 61 | box_visible=settings.properties['violin_box'] 62 | )] 63 | -------------------------------------------------------------------------------- /DataPlotly/gui/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghtmtt/DataPlotly/f57b842703ebb259091b4490a9821518a14b01b9/DataPlotly/gui/__init__.py -------------------------------------------------------------------------------- /DataPlotly/gui/add_new_dock_dlg.py: -------------------------------------------------------------------------------- 1 | """ 2 | Dialog to add new DataPlotlyDock with custom validator 3 | """ 4 | from qgis.PyQt import uic 5 | from qgis.PyQt.QtCore import pyqtSignal 6 | from qgis.PyQt.QtGui import QValidator 7 | from qgis.PyQt.QtWidgets import QDialog, QDialogButtonBox 8 | 9 | from DataPlotly.core.core_utils import uuid_suffix 10 | from DataPlotly.gui.gui_utils import GuiUtils 11 | 12 | WIDGET, _ = uic.loadUiType(GuiUtils.get_ui_file_path('add_dock_dlg.ui')) 13 | 14 | 15 | class DataPlotlyNewDockDialog(QDialog, WIDGET): 16 | """Dialog to add new dock""" 17 | 18 | def __init__(self, dock_widgets=None, parent=None): 19 | super().__init__(parent) 20 | self.setupUi(self) 21 | 22 | self.DockIdInformationLabel.hide() 23 | validator = DataPlotlyNewDockIdValidator(dock_widgets=dock_widgets) 24 | self.DockIdLineEdit.setValidator(validator) 25 | validator.validationChanged.connect(self.update_dlg) 26 | self.DockTitleLineEdit.valueChanged.connect(self.updateDockIdLineEdit) 27 | self.mCustomizeGroupBox.clicked.connect(self.updateDockIdLineEdit) 28 | 29 | def update_dlg(self, state, msg): 30 | """validator slot""" 31 | is_valid = state != QValidator.Intermediate 32 | self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(is_valid) 33 | label = self.DockIdInformationLabel 34 | lineEdit = self.DockIdLineEdit 35 | style_border = "" 36 | if not is_valid: 37 | style_border = "border: 3px solid red" 38 | label.show() 39 | else: 40 | label.hide() 41 | label.setText(msg) 42 | lineEdit.setStyleSheet(style_border) 43 | 44 | def updateDockIdLineEdit(self): 45 | """update the dockid with uuid suffix when 46 | checkbox is unchecked 47 | """ 48 | if not self.mCustomizeGroupBox.isChecked(): 49 | title = self.DockTitleLineEdit.value() 50 | self.DockIdLineEdit.setValue(uuid_suffix(title)) 51 | 52 | def get_params(self): 53 | """greturn dock_title and dock_id""" 54 | dock_title = self.DockTitleLineEdit.value() 55 | dock_id = self.DockIdLineEdit.value() 56 | return dock_title, dock_id 57 | 58 | 59 | # pylint: disable=too-few-public-methods 60 | class DataPlotlyNewDockIdValidator(QValidator): 61 | """Custom validator to prevent some users action""" 62 | validationChanged = pyqtSignal(QValidator.State, str) 63 | 64 | def __init__(self, parent=None, dock_widgets=None): 65 | """Constructor.""" 66 | super().__init__(parent) 67 | self.dock_widgets = dock_widgets 68 | 69 | def validate(self, dock_id, pos): 70 | """Checks if dock_id is not empty and not is already present""" 71 | state = QValidator.Acceptable 72 | msg = None 73 | 74 | if dock_id == "": 75 | state = QValidator.Intermediate 76 | msg = self.tr('DockId can not be empty') 77 | 78 | if dock_id in self.dock_widgets: 79 | state = QValidator.Intermediate 80 | msg = self.tr(f'DockId {dock_id} is already taken') 81 | 82 | if '_' in dock_id: 83 | state = QValidator.Intermediate 84 | msg = self.tr('The underscore _ is not allowed') 85 | 86 | self.validationChanged.emit(state, msg) 87 | 88 | return state, dock_id, pos 89 | -------------------------------------------------------------------------------- /DataPlotly/gui/dock.py: -------------------------------------------------------------------------------- 1 | """Dock widget 2 | 3 | .. note:: This program is free software; you can redistribute it and/or modify 4 | it under the terms of the GNU General Public License as published by 5 | the Free Software Foundation; either version 2 of the License, or 6 | (at your option) any later version. 7 | """ 8 | from qgis.PyQt.QtCore import QCoreApplication, Qt 9 | from qgis.PyQt.QtXml import QDomDocument, QDomElement 10 | 11 | from qgis.core import QgsXmlUtils 12 | from qgis.gui import ( 13 | QgsDockWidget, 14 | QgsPanelWidgetStack 15 | ) 16 | from DataPlotly.core.core_utils import restore, restore_safe_str_xml, safe_str_xml 17 | from DataPlotly.gui.add_new_dock_dlg import DataPlotlyNewDockDialog 18 | from DataPlotly.gui.remove_dock_dlg import DataPlotlyRemoveDockDialog 19 | from DataPlotly.gui.plot_settings_widget import DataPlotlyPanelWidget 20 | 21 | 22 | class DataPlotlyDock(QgsDockWidget): # pylint: disable=too-few-public-methods 23 | """ 24 | Plot settings dock widget 25 | """ 26 | 27 | def __init__(self, parent=None, message_bar=None, # pylint: disable=too-many-arguments 28 | dock_title: str = 'DataPlotly', dock_id: str = 'DataPlotly', 29 | project: QDomDocument = None, override_iface=None): 30 | super().__init__(parent) 31 | self.title = restore_safe_str_xml(dock_title) 32 | self.setWindowTitle(self.title) 33 | self.setObjectName(f'DataPlotly-{dock_id}-Dock') 34 | 35 | self.panel_stack = QgsPanelWidgetStack() 36 | self.setWidget(self.panel_stack) 37 | 38 | self.main_panel = DataPlotlyPanelWidget( 39 | message_bar=message_bar, dock_title=dock_title, dock_id=dock_id, project=project, override_iface=override_iface) 40 | self.panel_stack.setMainPanel(self.main_panel) 41 | self.main_panel.setDockMode(True) 42 | 43 | 44 | class DataPlotlyDockManager(): 45 | """ 46 | Manager to add multiple docks 47 | """ 48 | 49 | def __init__(self, iface, dock_widgets): 50 | self.iface = iface 51 | self.dock_widgets = dock_widgets 52 | self.state = None 53 | self.geometry = None 54 | 55 | @staticmethod 56 | def tr(message): 57 | """ Translate function""" 58 | return QCoreApplication.translate('DataPlotly', message) 59 | 60 | def addNewDockFromDlg(self): 61 | """ Open a dlg and add dock""" 62 | dlg = DataPlotlyNewDockDialog(self.dock_widgets) 63 | if dlg.exec_(): 64 | dock_title, dock_id = dlg.get_params() 65 | self.addNewDock(dock_title, dock_id, False) 66 | 67 | def removeDockFromDlg(self): 68 | """ Open a dlg to remove a dock""" 69 | dlg = DataPlotlyRemoveDockDialog(self.dock_widgets) 70 | if dlg.exec_(): 71 | dock_id = dlg.get_param() 72 | self.removeDock(dock_id) 73 | 74 | def addNewDock(self, dock_title='DataPlotly', dock_id='DataPlotly', # pylint: disable=too-many-arguments 75 | hide=True, message_bar=None, project=None): 76 | """ Add new dock """ 77 | dock_title = safe_str_xml(dock_title) 78 | dock_id = safe_str_xml(dock_id) 79 | if dock_id in self.dock_widgets: 80 | if dock_id == 'DataPlotly': 81 | return self.dock_widgets[dock_id] 82 | self.iface.messageBar().pushWarning(self.tr('Warning'), self.tr( 83 | f'DataPlotlyDock can not be added because {dock_id} is already present')) 84 | return False 85 | message_bar = message_bar or self.iface.messageBar() 86 | dock = DataPlotlyDock( 87 | dock_title=dock_title, message_bar=message_bar, dock_id=dock_id, project=project, override_iface=self.iface) 88 | self.dock_widgets[dock_id] = dock 89 | self.iface.addDockWidget(Qt.RightDockWidgetArea, dock) 90 | if hide: 91 | dock.hide() 92 | return dock 93 | 94 | def removeDock(self, dock_id): 95 | """ Remove dock with id """ 96 | dock = self.dock_widgets.pop(dock_id, None) 97 | if dock: 98 | self.iface.removeDockWidget(dock) 99 | # TODO remove dock_id in project file 100 | 101 | def removeDocks(self): 102 | """ Remove all docks except the main one """ 103 | dock_widgets = self.dock_widgets.copy() 104 | for dock_id in dock_widgets.keys(): 105 | if dock_id == 'DataPlotly': 106 | continue 107 | self.removeDock(dock_id) 108 | 109 | # self.dock_project_empty = True 110 | 111 | def addDocksFromProject(self, document: QDomDocument): 112 | """ Add docks from project instance """ 113 | root_node = document.elementsByTagName("qgis").item(0) 114 | if root_node.isNull(): 115 | return False 116 | # loop to find matching dock 117 | nodes = root_node.childNodes() 118 | for i in range(nodes.length()): 119 | tag_name = nodes.at(i).toElement().tagName() 120 | if tag_name.startswith('DataPlotly_'): 121 | _, dock_title, dock_id = tag_name.split('_') 122 | self.addNewDock(dock_title=restore_safe_str_xml(dock_title), 123 | dock_id=restore_safe_str_xml(dock_id), 124 | hide=False, 125 | message_bar=None, 126 | project=document) 127 | # FIXME : trigger the plot creation (not working) 128 | # main_panel = self.getDock(tag_name).main_panel 129 | # main_panel.create_plot() 130 | if self.iface and self.read_from_project(document): 131 | self.iface.mainWindow().restoreGeometry(self.geometry) 132 | self.iface.mainWindow().restoreState(self.state, version=999) 133 | return True 134 | 135 | def getDock(self, dock_id: str) -> DataPlotlyDock: 136 | """ Return the dock from the dock_id """ 137 | dock = self.dock_widgets.get(dock_id) 138 | if not dock: 139 | self.iface.messageBar().pushWarning( 140 | self.tr('Warning'), 141 | self.tr(f'DataPlotlyDock {dock_id} can not be found')) 142 | return dock 143 | 144 | # TODO: Refactor : this functions are almost the same in plot_settings.py 145 | def write_xml(self, document: QDomDocument): 146 | """ 147 | Writes the docks position settings to an XML element 148 | """ 149 | mw = self.iface.mainWindow() 150 | state = mw.saveState(version=999).toBase64() 151 | geometry = mw.saveGeometry().toBase64() 152 | 153 | element = QgsXmlUtils.writeVariant({ 154 | 'state': str(state, "utf-8"), 155 | 'geometry': str(geometry, "utf-8") 156 | }, document) 157 | return element 158 | 159 | def read_xml(self, element: QDomElement) -> bool: 160 | """ 161 | Reads the docs state settings from an XML element 162 | """ 163 | res = QgsXmlUtils.readVariant(element) 164 | if not isinstance(res, dict) or \ 165 | 'geometry' not in res or \ 166 | 'state' not in res: 167 | return False 168 | 169 | self.state = restore(res['state']) 170 | self.geometry = restore(res['geometry']) 171 | return True 172 | 173 | def write_to_project(self, document: QDomDocument): 174 | """ 175 | Writes the settings to a project (represented by the given DOM document) 176 | """ 177 | elem = self.write_xml(document) 178 | parent_elem = document.createElement('StateDataPlotly') 179 | parent_elem.appendChild(elem) 180 | root_node = document.elementsByTagName("qgis").item(0) 181 | root_node.appendChild(parent_elem) 182 | 183 | def read_from_project(self, document: QDomDocument): 184 | """ 185 | Reads the settings from a project (represented by the given DOM document) 186 | """ 187 | root_node = document.elementsByTagName("qgis").item(0) 188 | if root_node.isNull(): 189 | return False 190 | 191 | node = root_node.toElement().firstChildElement('StateDataPlotly') 192 | if node.isNull(): 193 | return False 194 | 195 | elem = node.toElement() 196 | return self.read_xml(elem.firstChildElement()) 197 | -------------------------------------------------------------------------------- /DataPlotly/gui/gui_utils.py: -------------------------------------------------------------------------------- 1 | """GUI Utilities 2 | 3 | .. note:: This program is free software; you can redistribute it and/or modify 4 | it under the terms of the GNU General Public License as published by 5 | the Free Software Foundation; either version 2 of the License, or 6 | (at your option) any later version. 7 | """ 8 | 9 | import os 10 | from qgis.PyQt.QtGui import QIcon 11 | 12 | 13 | class GuiUtils: 14 | """ 15 | Utilities for GUI plugin components 16 | """ 17 | 18 | @staticmethod 19 | def get_icon(icon: str) -> QIcon: 20 | """ 21 | Returns a plugin icon 22 | :param icon: icon name (svg file name) 23 | :return: QIcon 24 | """ 25 | path = GuiUtils.get_icon_svg(icon) 26 | if not path: 27 | return QIcon() 28 | 29 | return QIcon(path) 30 | 31 | @staticmethod 32 | def get_icon_svg(icon: str) -> str: 33 | """ 34 | Returns a plugin icon's SVG file path 35 | :param icon: icon name (svg file name) 36 | :return: icon svg path 37 | """ 38 | path = os.path.join( 39 | os.path.dirname(__file__), 40 | '..', 41 | 'icons', 42 | icon) 43 | if not os.path.exists(path): 44 | return '' 45 | 46 | return path 47 | 48 | @staticmethod 49 | def get_ui_file_path(file: str) -> str: 50 | """ 51 | Returns a UI file's path 52 | :param file: file name (uifile name) 53 | :return: ui file path 54 | """ 55 | path = os.path.join( 56 | os.path.dirname(__file__), 57 | '..', 58 | 'ui', 59 | file) 60 | if not os.path.exists(path): 61 | return '' 62 | 63 | return path 64 | -------------------------------------------------------------------------------- /DataPlotly/gui/layout_item_gui.py: -------------------------------------------------------------------------------- 1 | """Plot Layout Gui Handling 2 | 3 | .. note:: This program is free software; you can redistribute it and/or modify 4 | it under the terms of the GNU General Public License as published by 5 | the Free Software Foundation; either version 2 of the License, or 6 | (at your option) any later version. 7 | """ 8 | 9 | from qgis.PyQt.QtCore import ( 10 | QCoreApplication, 11 | QItemSelectionModel 12 | ) 13 | from qgis.PyQt.QtWidgets import ( 14 | QListWidget, 15 | QHBoxLayout, 16 | QPushButton, 17 | QVBoxLayout 18 | ) 19 | from qgis.gui import ( 20 | QgsLayoutItemAbstractGuiMetadata, 21 | QgsLayoutItemBaseWidget, 22 | QgsLayoutItemPropertiesWidget 23 | ) 24 | 25 | from DataPlotly.layouts.plot_layout_item import ITEM_TYPE 26 | from DataPlotly.gui.gui_utils import GuiUtils 27 | from DataPlotly.gui.plot_settings_widget import DataPlotlyPanelWidget 28 | 29 | 30 | class PlotLayoutItemWidget(QgsLayoutItemBaseWidget): 31 | """ 32 | Configuration widget for layout plot items 33 | """ 34 | 35 | def __init__(self, parent, layout_object): 36 | super().__init__(parent, layout_object) 37 | self.plot_item = layout_object 38 | self.message_bar = None 39 | 40 | vl = QVBoxLayout() 41 | vl.setContentsMargins(0, 0, 0, 0) 42 | 43 | plot_tools_layout = QHBoxLayout() 44 | 45 | plot_add_button = QPushButton() 46 | plot_add_button.setIcon(GuiUtils.get_icon('symbologyAdd.svg')) 47 | plot_add_button.setToolTip('Add a new plot') 48 | plot_tools_layout.addWidget(plot_add_button) 49 | plot_add_button.clicked.connect(self.add_plot) 50 | 51 | plot_remove_button = QPushButton() 52 | plot_remove_button.setIcon(GuiUtils.get_icon('symbologyRemove.svg')) 53 | plot_remove_button.setToolTip('Remove selected plot') 54 | plot_tools_layout.addWidget(plot_remove_button) 55 | plot_remove_button.clicked.connect(self.remove_plot) 56 | 57 | plot_duplicate_button = QPushButton() 58 | plot_duplicate_button.setIcon(GuiUtils.get_icon('mActionDuplicateLayer.svg')) 59 | plot_duplicate_button.setToolTip('Duplicates the selected plot') 60 | plot_tools_layout.addWidget(plot_duplicate_button) 61 | plot_duplicate_button.clicked.connect(self.duplicate_plot) 62 | 63 | plot_move_up_button = QPushButton() 64 | plot_move_up_button.setIcon(GuiUtils.get_icon('mActionArrowUp.svg')) 65 | plot_move_up_button.setToolTip('Move selected plot up') 66 | plot_tools_layout.addWidget(plot_move_up_button) 67 | plot_move_up_button.clicked.connect(self.move_up_plot) 68 | 69 | plot_move_down_button = QPushButton() 70 | plot_move_down_button.setIcon(GuiUtils.get_icon('mActionArrowDown.svg')) 71 | plot_move_down_button.setToolTip('Move selected plot down') 72 | plot_tools_layout.addWidget(plot_move_down_button) 73 | plot_move_down_button.clicked.connect(self.move_down_plot) 74 | 75 | vl.addLayout(plot_tools_layout) 76 | 77 | self.plot_list = QListWidget() 78 | self.plot_list.setSelectionMode(QListWidget.SingleSelection) 79 | self.plot_list.doubleClicked.connect(self.show_properties) 80 | vl.addWidget(self.plot_list) 81 | self.populate_plot_list() 82 | 83 | plot_properties_button = QPushButton(self.tr('Setup Selected Plot')) 84 | vl.addWidget(plot_properties_button) 85 | plot_properties_button.clicked.connect(self.show_properties) 86 | 87 | self.panel = None 88 | self.setPanelTitle(self.tr('Plot Properties')) 89 | self.item_properties_widget = QgsLayoutItemPropertiesWidget(self, layout_object) 90 | vl.addWidget(self.item_properties_widget) 91 | self.setLayout(vl) 92 | 93 | def populate_plot_list(self): 94 | """ 95 | Clears and re-populates the plot list widget. The currently selection is retained 96 | """ 97 | selected_index = self.plot_list.currentRow() 98 | self.plot_list.clear() 99 | for setting in self.plot_item.plot_settings: 100 | plot_type = setting.plot_type if setting.plot_type is not None else '(not set)' 101 | legend_title = ('[' + setting.properties.get('name') + ']') \ 102 | if setting.properties.get('name', '') != '' else '' 103 | self.plot_list.addItem(plot_type + ' ' + legend_title) 104 | 105 | # select index within range [0, len(plot_settings)-1] 106 | selected_index = max(0, min(len(self.plot_item.plot_settings) - 1, selected_index)) 107 | self.plot_list.setCurrentRow(selected_index, QItemSelectionModel.SelectCurrent) 108 | 109 | def add_plot(self): 110 | """ 111 | Adds a new plot and updates the plot list and the plot item 112 | """ 113 | self.plot_item.add_plot() 114 | self.populate_plot_list() 115 | self.plot_item.refresh() 116 | 117 | def duplicate_plot(self): 118 | """ 119 | Duplicates an existing plot and updates the plot list and the plot item 120 | """ 121 | 122 | selected_plot_index = self.plot_list.currentRow() 123 | if selected_plot_index < 0: 124 | return 125 | 126 | self.plot_item.duplicate_plot(selected_plot_index) 127 | self.populate_plot_list() 128 | self.plot_item.refresh() 129 | 130 | def remove_plot(self): 131 | """ 132 | Removes the selected plot and updates the plot list and the plot item 133 | """ 134 | selected_index = self.plot_list.currentRow() 135 | if selected_index < 0: 136 | return 137 | 138 | self.plot_item.remove_plot(selected_index) 139 | self.populate_plot_list() 140 | self.plot_item.refresh() 141 | 142 | def move_up_plot(self): 143 | """ 144 | Moves the selected plot up and updates the plot list and the plot item 145 | """ 146 | selected_index = self.plot_list.currentRow() 147 | if selected_index <= 0: 148 | return 149 | 150 | item = self.plot_item.plot_settings.pop(selected_index) 151 | self.plot_item.plot_settings.insert(selected_index - 1, item) 152 | self.plot_list.setCurrentRow(selected_index - 1, QItemSelectionModel.SelectCurrent) 153 | self.populate_plot_list() 154 | self.plot_item.refresh() 155 | 156 | def move_down_plot(self): 157 | """ 158 | Moves the selected plot down and updates the plot list and the plot item 159 | """ 160 | selected_index = self.plot_list.currentRow() 161 | if selected_index >= len(self.plot_item.plot_settings) - 1: 162 | return 163 | 164 | item = self.plot_item.plot_settings.pop(selected_index) 165 | self.plot_item.plot_settings.insert(selected_index + 1, item) 166 | self.plot_list.setCurrentRow(selected_index + 1, QItemSelectionModel.SelectCurrent) 167 | self.populate_plot_list() 168 | self.plot_item.refresh() 169 | 170 | def show_properties(self): 171 | """ 172 | Shows the plot properties panel 173 | """ 174 | selected_plot_index = self.plot_list.currentRow() 175 | if selected_plot_index < 0: 176 | return 177 | 178 | self.panel = DataPlotlyPanelWidget(mode=DataPlotlyPanelWidget.MODE_LAYOUT, message_bar=self.message_bar) 179 | 180 | # not quite right -- we ideally want to also add the source layer scope into the context given by plot item, 181 | # but that causes a hard lock in the Python GIL (because PyQt doesn't release the GIL when creating the menu 182 | # for the property override buttons). Nothing much we can do about that here (or in QGIS, 183 | # it's a Python/PyQt limitation) 184 | self.panel.registerExpressionContextGenerator(self.plot_item) 185 | self.panel.set_print_layout(self.plot_item.layout()) 186 | 187 | self.panel.linked_map_combo.blockSignals(True) 188 | self.panel.linked_map_combo.setItem(self.plot_item.linked_map) 189 | self.panel.linked_map_combo.blockSignals(False) 190 | 191 | self.panel.linked_map_combo.itemChanged.connect(self.linked_map_changed) 192 | 193 | self.panel.set_settings(self.plot_item.plot_settings[selected_plot_index]) 194 | # self.panel.set_settings(self.layoutItem().plot_settings) 195 | self.openPanel(self.panel) 196 | self.panel.widgetChanged.connect(self.update_item_settings) 197 | self.panel.panelAccepted.connect(self.set_item_settings) 198 | 199 | def update_item_settings(self): 200 | """ 201 | Updates the plot item without dismissing the properties panel 202 | """ 203 | if not self.panel: 204 | return 205 | 206 | self.plot_item.set_plot_settings(self.plot_list.currentRow(), self.panel.get_settings()) 207 | self.populate_plot_list() 208 | self.plot_item.update() 209 | 210 | def set_item_settings(self): 211 | """ 212 | Updates the plot item based on the settings from the properties panel 213 | """ 214 | if not self.panel: 215 | return 216 | 217 | self.plot_item.set_plot_settings(self.plot_list.currentRow(), self.panel.get_settings()) 218 | self.populate_plot_list() 219 | self.panel = None 220 | self.plot_item.update() 221 | 222 | def linked_map_changed(self, linked_map): 223 | """ 224 | Triggered when the linked map is changed 225 | """ 226 | self.plot_item.set_linked_map(linked_map) 227 | self.plot_item.update() 228 | 229 | def setNewItem(self, item): # pylint: disable=missing-docstring 230 | if item.type() != ITEM_TYPE: 231 | return False 232 | 233 | self.plot_item = item 234 | self.item_properties_widget.setItem(item) 235 | self.populate_plot_list() 236 | 237 | if self.panel is not None: 238 | self.panel.set_settings(self.plot_item.plot_settings[0]) 239 | 240 | self.panel.linked_map_combo.blockSignals(True) 241 | self.panel.linked_map_combo.setItem(self.plot_item.linked_map) 242 | self.panel.linked_map_combo.blockSignals(False) 243 | 244 | return True 245 | 246 | def setDesignerInterface(self, iface): # pylint: disable=missing-docstring 247 | super().setDesignerInterface(iface) 248 | self.message_bar = iface.messageBar() 249 | if self.panel: 250 | self.panel.message_bar = self.message_bar 251 | 252 | 253 | class PlotLayoutItemGuiMetadata(QgsLayoutItemAbstractGuiMetadata): 254 | """ 255 | Metadata for plot item GUI classes 256 | """ 257 | 258 | def __init__(self): 259 | super().__init__(ITEM_TYPE, QCoreApplication.translate('DataPlotly', 'Plot Item')) 260 | 261 | def creationIcon(self): # pylint: disable=missing-docstring, no-self-use 262 | return GuiUtils.get_icon('dataplotly.svg') 263 | 264 | def createItemWidget(self, item): # pylint: disable=missing-docstring, no-self-use 265 | return PlotLayoutItemWidget(None, item) 266 | -------------------------------------------------------------------------------- /DataPlotly/gui/remove_dock_dlg.py: -------------------------------------------------------------------------------- 1 | """ 2 | Minimal dlg with combobox to remove DataPlotlyDialog 3 | """ 4 | from qgis.PyQt import uic 5 | 6 | from qgis.PyQt.QtWidgets import QDialog 7 | from qgis.PyQt.QtCore import Qt 8 | 9 | from DataPlotly.gui.gui_utils import GuiUtils 10 | 11 | 12 | WIDGET, _ = uic.loadUiType(GuiUtils.get_ui_file_path('remove_dock_dlg.ui')) 13 | 14 | 15 | # pylint: disable=too-few-public-methods 16 | class DataPlotlyRemoveDockDialog(QDialog, WIDGET): 17 | """Dialog to remove new dock""" 18 | def __init__(self, dock_widgets=None, parent=None): 19 | super().__init__(parent) 20 | self.setupUi(self) 21 | dock_ids = [dock_id for dock_id in dock_widgets.keys() if dock_id != 'DataPlotly'] 22 | self.DockIdsComboBox.addItems(dock_ids) 23 | for i, dock_id in enumerate(dock_ids): 24 | self.DockIdsComboBox.setItemData(i, dock_widgets[dock_id].title, Qt.ToolTipRole) 25 | 26 | def get_param(self): 27 | """Return the dock_id to delete""" 28 | return self.DockIdsComboBox.currentText() 29 | -------------------------------------------------------------------------------- /DataPlotly/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghtmtt/DataPlotly/f57b842703ebb259091b4490a9821518a14b01b9/DataPlotly/icon.png -------------------------------------------------------------------------------- /DataPlotly/icons/create_plot.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 22 | 23 | 25 | image/svg+xml 26 | 28 | 29 | 30 | 31 | 32 | 56 | 58 | 66 | 70 | 74 | 75 | 83 | 87 | 91 | 92 | 101 | 105 | 109 | 113 | 114 | 124 | 134 | 136 | 140 | 144 | 145 | 155 | 165 | 175 | 185 | 195 | 197 | 201 | 205 | 206 | 216 | 217 | 224 | 230 | 235 | 236 | -------------------------------------------------------------------------------- /DataPlotly/icons/dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghtmtt/DataPlotly/f57b842703ebb259091b4490a9821518a14b01b9/DataPlotly/icons/dash.png -------------------------------------------------------------------------------- /DataPlotly/icons/dataplotly.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 41 | 44 | 45 | 47 | 48 | 50 | image/svg+xml 51 | 53 | 54 | 55 | 56 | 57 | 62 | 69 | 76 | 83 | 90 | 98 | 106 | 114 | 122 | 129 | 134 | 139 | 145 | 151 | 159 | 166 | 173 | 181 | 186 | 191 | 196 | 201 | 206 | 207 | 208 | -------------------------------------------------------------------------------- /DataPlotly/icons/dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghtmtt/DataPlotly/f57b842703ebb259091b4490a9821518a14b01b9/DataPlotly/icons/dot.png -------------------------------------------------------------------------------- /DataPlotly/icons/dotdash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghtmtt/DataPlotly/f57b842703ebb259091b4490a9821518a14b01b9/DataPlotly/icons/dotdash.png -------------------------------------------------------------------------------- /DataPlotly/icons/list_code.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | 18 | 19 | 21 | image/svg+xml 22 | 24 | 25 | 26 | 27 | 28 | 30 | 54 | 59 | 64 | 69 | 74 | 79 | 84 | 89 | 94 | 99 | 104 | 109 | 114 | 115 | -------------------------------------------------------------------------------- /DataPlotly/icons/list_custom.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml -------------------------------------------------------------------------------- /DataPlotly/icons/list_help.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /DataPlotly/icons/list_plot.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 43 | 45 | 46 | 48 | image/svg+xml 49 | 51 | 52 | 53 | 54 | 55 | 60 | 63 | 65 | 67 | 69 | 71 | 73 | 75 | 77 | 79 | 81 | 83 | 85 | 87 | 89 | 91 | 93 | 95 | 100 | 105 | 110 | 114 | 119 | 126 | 131 | 136 | 137 | 145 | 153 | 161 | 169 | 177 | 186 | 187 | 188 | 189 | 190 | -------------------------------------------------------------------------------- /DataPlotly/icons/list_properties.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 | -------------------------------------------------------------------------------- /DataPlotly/icons/longdash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghtmtt/DataPlotly/f57b842703ebb259091b4490a9821518a14b01b9/DataPlotly/icons/longdash.png -------------------------------------------------------------------------------- /DataPlotly/icons/longdashdot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghtmtt/DataPlotly/f57b842703ebb259091b4490a9821518a14b01b9/DataPlotly/icons/longdashdot.png -------------------------------------------------------------------------------- /DataPlotly/icons/mActionArrowDown.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /DataPlotly/icons/mActionArrowUp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /DataPlotly/icons/mActionDuplicateLayer.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /DataPlotly/icons/mIconDataDefineExpression.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /DataPlotly/icons/solid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghtmtt/DataPlotly/f57b842703ebb259091b4490a9821518a14b01b9/DataPlotly/icons/solid.png -------------------------------------------------------------------------------- /DataPlotly/icons/symbologyAdd.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /DataPlotly/icons/symbologyRemove.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /DataPlotly/metadata.txt: -------------------------------------------------------------------------------- 1 | [general] 2 | name=Data Plotly 3 | qgisMinimumVersion=3.28 4 | qgisMaximumVersion=3.98 5 | description=D3 Plots for QGIS 6 | author=Matteo Ghetta (Faunalia) 7 | email=matteo.ghetta@gmail.com 8 | 9 | about=Draw D3 plots in QGIS 10 | 11 | homepage=https://dataplotly-docs.readthedocs.io 12 | tracker=https://github.com/ghtmtt/DataPlotly/issues 13 | repository=https://github.com/ghtmtt/DataPlotly 14 | # End of mandatory metadata 15 | 16 | # Done by qgis-plugin-ci 17 | version=master 18 | changelog= 19 | commitNumber= 20 | commitSha1= 21 | dateTime= 22 | # End of qgis-plugin-ci 23 | 24 | # Recommended items: 25 | 26 | # Tags are comma separated with spaces allowed 27 | tags=python, d3, plots, vector, graphs, datavis, dataviz, dataplotly 28 | 29 | category=Plugins 30 | icon=icon.png 31 | # experimental flag 32 | experimental=False 33 | 34 | # deprecated flag (applies to the whole plugin, not just a single version) 35 | deprecated=False 36 | 37 | hasProcessingProvider=yes 38 | server=True 39 | -------------------------------------------------------------------------------- /DataPlotly/processing/dataplotly_provider.py: -------------------------------------------------------------------------------- 1 | """ 2 | /*************************************************************************** 3 | DataPlotly 4 | A QGIS plugin 5 | D3 Plots for QGIS 6 | ------------------- 7 | begin : 2017-03-05 8 | git sha : $Format:%H$ 9 | copyright : (C) 2017 by matteo ghetta 10 | email : matteo.ghetta@gmail.com 11 | ***************************************************************************/ 12 | 13 | /*************************************************************************** 14 | * * 15 | * This program is free software; you can redistribute it and/or modify * 16 | * it under the terms of the GNU General Public License as published by * 17 | * the Free Software Foundation; either version 2 of the License, or * 18 | * (at your option) any later version. * 19 | * * 20 | ***************************************************************************/ 21 | """ 22 | from qgis.core import Qgis, QgsMessageLog, QgsProcessingProvider 23 | from DataPlotly.gui.gui_utils import GuiUtils 24 | 25 | try: 26 | # 🐼 27 | from DataPlotly.processing.dataplotly_scatterplot import DataPlotlyProcessingScatterPlot 28 | WITH_PANDAS = True 29 | except ImportError: 30 | WITH_PANDAS = False 31 | QgsMessageLog.logMessage( 32 | "Pandas has not been found. The processing algorithm will not be loaded. " 33 | "Please install qgis-full or qgis standalone", "DataPlotly", Qgis.Warning) 34 | 35 | 36 | class DataPlotlyProvider(QgsProcessingProvider): 37 | 38 | def __init__(self, plugin_version): 39 | super().__init__() 40 | self.plugin_version = plugin_version 41 | 42 | def load(self): 43 | """In this method we add settings needed to configure our 44 | provider. 45 | """ 46 | self.refreshAlgorithms() 47 | return True 48 | 49 | def id(self): 50 | """This is the name that will appear on the toolbox group. 51 | 52 | It is also used to create the command line name of all the 53 | algorithms from this provider. 54 | """ 55 | return 'DataPlotly' 56 | 57 | def name(self): 58 | """This is the localised full name. 59 | """ 60 | return 'DataPlotly' 61 | 62 | def longName(self): 63 | return 'DataPlotly' 64 | 65 | def icon(self): 66 | return GuiUtils.get_icon('dataplotly.svg') 67 | 68 | def svgIconPath(self) -> str: 69 | return GuiUtils.get_icon_svg('dataplotly.svg') 70 | 71 | def versionInfo(self) -> str: 72 | return self.plugin_version 73 | 74 | def loadAlgorithms(self): 75 | """Here we fill the list of algorithms in self.algs. 76 | 77 | This method is called whenever the list of algorithms should 78 | be updated. If the list of algorithms can change (for instance, 79 | if it contains algorithms from user-defined scripts and a new 80 | script might have been added), you should create the list again 81 | here. 82 | 83 | In this case, since the list is always the same, we assign from 84 | the pre-made list. This assignment has to be done in this method 85 | even if the list does not change, since the self.algs list is 86 | cleared before calling this method. 87 | """ 88 | if WITH_PANDAS: 89 | self.addAlgorithm(DataPlotlyProcessingScatterPlot()) 90 | -------------------------------------------------------------------------------- /DataPlotly/test/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | DataPlotly test suite 3 | """ 4 | 5 | # import qgis libs so that we set the correct sip api version 6 | import qgis # pylint: disable=W0401,W0614 7 | -------------------------------------------------------------------------------- /DataPlotly/test/processing_scatter.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |
6 | 7 | -------------------------------------------------------------------------------- /DataPlotly/test/qgis_interface.py: -------------------------------------------------------------------------------- 1 | """QGIS plugin implementation. 2 | 3 | .. note:: This program is free software; you can redistribute it and/or modify 4 | it under the terms of the GNU General Public License as published by 5 | the Free Software Foundation; either version 2 of the License, or 6 | (at your option) any later version. 7 | 8 | .. note:: This source code was copied from the 'postgis viewer' application 9 | with original authors: 10 | Copyright (c) 2010 by Ivan Mincik, ivan.mincik@gista.sk 11 | Copyright (c) 2011 German Carrillo, geotux_tuxman@linuxmail.org 12 | Copyright (c) 2014 Tim Sutton, tim@linfiniti.com 13 | 14 | """ 15 | 16 | __author__ = 'tim@linfiniti.com' 17 | __revision__ = '$Format:%H$' 18 | __date__ = '10/01/2011' 19 | __copyright__ = ( 20 | 'Copyright (c) 2010 by Ivan Mincik, ivan.mincik@gista.sk and ' 21 | 'Copyright (c) 2011 German Carrillo, geotux_tuxman@linuxmail.org' 22 | 'Copyright (c) 2014 Tim Sutton, tim@linfiniti.com' 23 | ) 24 | 25 | import logging 26 | from typing import List 27 | from PyQt5.QtCore import QObject, pyqtSlot, pyqtSignal, QSize 28 | from qgis.PyQt.QtWidgets import QDockWidget 29 | from qgis.core import QgsProject, QgsMapLayer 30 | from qgis.gui import (QgsMapCanvas, 31 | QgsMessageBar) 32 | 33 | LOGGER = logging.getLogger('QGIS') 34 | 35 | 36 | # noinspection PyMethodMayBeStatic,PyPep8Naming 37 | # pylint: disable=too-many-public-methods 38 | class QgisInterface(QObject): 39 | """Class to expose QGIS objects and functions to plugins. 40 | 41 | This class is here for enabling us to run unit tests only, 42 | so most methods are simply stubs. 43 | """ 44 | currentLayerChanged = pyqtSignal(QgsMapLayer) 45 | 46 | def __init__(self, canvas: QgsMapCanvas): 47 | """Constructor 48 | :param canvas: 49 | """ 50 | QObject.__init__(self) 51 | self.canvas = canvas 52 | # Set up slots so we can mimic the behaviour of QGIS when layers 53 | # are added. 54 | LOGGER.debug('Initialising canvas...') 55 | # noinspection PyArgumentList 56 | QgsProject.instance().layersAdded.connect(self.addLayers) 57 | # noinspection PyArgumentList 58 | QgsProject.instance().layerWasAdded.connect(self.addLayer) 59 | # noinspection PyArgumentList 60 | QgsProject.instance().removeAll.connect(self.removeAllLayers) 61 | 62 | # For processing module 63 | self.destCrs = None 64 | 65 | self.message_bar = QgsMessageBar() 66 | 67 | def addLayers(self, layers: List[QgsMapLayer]): 68 | """Handle layers being added to the registry so they show up in canvas. 69 | 70 | :param layers: list list of map layers that were added 71 | 72 | .. note:: The QgsInterface api does not include this method, 73 | it is added here as a helper to facilitate testing. 74 | """ 75 | # LOGGER.debug('addLayers called on qgis_interface') 76 | # LOGGER.debug('Number of layers being added: %s' % len(layers)) 77 | # LOGGER.debug('Layer Count Before: %s' % len(self.canvas.layers())) 78 | current_layers = self.canvas.layers() 79 | final_layers = [] 80 | for layer in current_layers: 81 | final_layers.append(layer) 82 | for layer in layers: 83 | final_layers.append(layer) 84 | 85 | self.canvas.setLayers(final_layers) 86 | # LOGGER.debug('Layer Count After: %s' % len(self.canvas.layers())) 87 | 88 | def addLayer(self, layer: QgsMapLayer): 89 | """Handle a layer being added to the registry so it shows up in canvas. 90 | 91 | :param layer: list list of map layers that were added 92 | 93 | .. note: The QgsInterface api does not include this method, it is added 94 | here as a helper to facilitate testing. 95 | 96 | .. note: The addLayer method was deprecated in QGIS 1.8 so you should 97 | not need this method much. 98 | """ 99 | pass # pylint: disable=unnecessary-pass 100 | 101 | @pyqtSlot() 102 | def removeAllLayers(self): # pylint: disable=no-self-use 103 | """Remove layers from the canvas before they get deleted.""" 104 | self.canvas.setLayers([]) 105 | 106 | def newProject(self): # pylint: disable=no-self-use 107 | """Create new project.""" 108 | # noinspection PyArgumentList 109 | QgsProject.instance().clear() 110 | 111 | # ---------------- API Mock for QgsInterface follows ------------------- 112 | 113 | def zoomFull(self): 114 | """Zoom to the map full extent.""" 115 | pass # pylint: disable=unnecessary-pass 116 | 117 | def zoomToPrevious(self): 118 | """Zoom to previous view extent.""" 119 | pass # pylint: disable=unnecessary-pass 120 | 121 | def zoomToNext(self): 122 | """Zoom to next view extent.""" 123 | pass # pylint: disable=unnecessary-pass 124 | 125 | def zoomToActiveLayer(self): 126 | """Zoom to extent of active layer.""" 127 | pass # pylint: disable=unnecessary-pass 128 | 129 | def addVectorLayer(self, path: str, base_name: str, provider_key: str): 130 | """Add a vector layer. 131 | 132 | :param path: Path to layer. 133 | :type path: str 134 | 135 | :param base_name: Base name for layer. 136 | :type base_name: str 137 | 138 | :param provider_key: Provider key e.g. 'ogr' 139 | :type provider_key: str 140 | """ 141 | pass # pylint: disable=unnecessary-pass 142 | 143 | def addRasterLayer(self, path: str, base_name: str): 144 | """Add a raster layer given a raster layer file name 145 | 146 | :param path: Path to layer. 147 | :type path: str 148 | 149 | :param base_name: Base name for layer. 150 | :type base_name: str 151 | """ 152 | pass # pylint: disable=unnecessary-pass 153 | 154 | def activeLayer(self) -> QgsMapLayer: # pylint: disable=no-self-use 155 | """Get pointer to the active layer (layer selected in the legend).""" 156 | # noinspection PyArgumentList 157 | layers = QgsProject.instance().mapLayers() 158 | for item in layers: 159 | return layers[item] 160 | 161 | def addToolBarIcon(self, action): 162 | """Add an icon to the plugins toolbar. 163 | 164 | :param action: Action to add to the toolbar. 165 | :type action: QAction 166 | """ 167 | pass # pylint: disable=unnecessary-pass 168 | 169 | def removeToolBarIcon(self, action): 170 | """Remove an action (icon) from the plugin toolbar. 171 | 172 | :param action: Action to add to the toolbar. 173 | :type action: QAction 174 | """ 175 | pass # pylint: disable=unnecessary-pass 176 | 177 | def addToolBar(self, name): 178 | """Add toolbar with specified name. 179 | 180 | :param name: Name for the toolbar. 181 | :type name: str 182 | """ 183 | pass # pylint: disable=unnecessary-pass 184 | 185 | def mapCanvas(self) -> QgsMapCanvas: 186 | """Return a pointer to the map canvas.""" 187 | return self.canvas 188 | 189 | def mainWindow(self): 190 | """Return a pointer to the main window. 191 | 192 | In case of QGIS it returns an instance of QgisApp. 193 | """ 194 | pass # pylint: disable=unnecessary-pass 195 | 196 | def addDockWidget(self, area, dock_widget: QDockWidget): 197 | """Add a dock widget to the main window. 198 | 199 | :param area: Where in the ui the dock should be placed. 200 | :type area: 201 | 202 | :param dock_widget: A dock widget to add to the UI. 203 | :type dock_widget: QDockWidget 204 | """ 205 | pass # pylint: disable=unnecessary-pass 206 | 207 | def removeDockWidget(self, dock_widget: QDockWidget): 208 | """Remove a dock widget to the main window. 209 | 210 | :param area: Where in the ui the dock should be placed. 211 | :type area: 212 | 213 | :param dock_widget: A dock widget to add to the UI. 214 | :type dock_widget: QDockWidget 215 | """ 216 | pass # pylint: disable=unnecessary-pass 217 | 218 | def legendInterface(self): 219 | """Get the legend.""" 220 | return self.canvas 221 | 222 | def iconSize(self, dockedToolbar) -> int: # pylint: disable=no-self-use 223 | """ 224 | Returns the toolbar icon size. 225 | :param dockedToolbar: If True, the icon size 226 | for toolbars contained within docks is returned. 227 | """ 228 | if dockedToolbar: 229 | return QSize(16, 16) 230 | 231 | return QSize(24, 24) 232 | 233 | def messageBar(self) -> QgsMessageBar: 234 | """ 235 | Return the message bar of the main app 236 | """ 237 | return self.message_bar 238 | -------------------------------------------------------------------------------- /DataPlotly/test/tenbytenraster.asc: -------------------------------------------------------------------------------- 1 | NCOLS 10 2 | NROWS 10 3 | XLLCENTER 1535380.000000 4 | YLLCENTER 5083260.000000 5 | DX 10 6 | DY 10 7 | NODATA_VALUE -9999 8 | 0 1 2 3 4 5 6 7 8 9 9 | 0 1 2 3 4 5 6 7 8 9 10 | 0 1 2 3 4 5 6 7 8 9 11 | 0 1 2 3 4 5 6 7 8 9 12 | 0 1 2 3 4 5 6 7 8 9 13 | 0 1 2 3 4 5 6 7 8 9 14 | 0 1 2 3 4 5 6 7 8 9 15 | 0 1 2 3 4 5 6 7 8 9 16 | 0 1 2 3 4 5 6 7 8 9 17 | 0 1 2 3 4 5 6 7 8 9 18 | CRS 19 | NOTES 20 | -------------------------------------------------------------------------------- /DataPlotly/test/tenbytenraster.asc.aux.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Point 4 | 5 | 6 | 7 | 9 8 | 4.5 9 | 0 10 | 2.872281323269 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /DataPlotly/test/tenbytenraster.keywords: -------------------------------------------------------------------------------- 1 | title: Tenbytenraster 2 | -------------------------------------------------------------------------------- /DataPlotly/test/tenbytenraster.lic: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Tim Sutton, Linfiniti Consulting CC 5 | 6 | 7 | 8 | tenbytenraster.asc 9 | 2700044251 10 | Yes 11 | Tim Sutton 12 | Tim Sutton (QGIS Source Tree) 13 | Tim Sutton 14 | This data is publicly available from QGIS Source Tree. The original 15 | file was created and contributed to QGIS by Tim Sutton. 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /DataPlotly/test/tenbytenraster.prj: -------------------------------------------------------------------------------- 1 | GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]] -------------------------------------------------------------------------------- /DataPlotly/test/tenbytenraster.qml: -------------------------------------------------------------------------------- 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 | 0 26 | 27 | -------------------------------------------------------------------------------- /DataPlotly/test/test_dock_manager.py: -------------------------------------------------------------------------------- 1 | """Plot factory test 2 | 3 | .. note:: This program is free software; you can redistribute it and/or modify 4 | it under the terms of the GNU General Public License as published by 5 | the Free Software Foundation; either version 2 of the License, or 6 | (at your option) any later version. 7 | 8 | """ 9 | 10 | import os.path 11 | import unittest 12 | 13 | from qgis.PyQt.QtCore import QByteArray, QFile, QIODevice 14 | from qgis.PyQt.QtGui import QValidator 15 | from qgis.PyQt.QtXml import QDomDocument 16 | 17 | from DataPlotly.core.core_utils import restore, restore_safe_str_xml, safe_str_xml 18 | from DataPlotly.test.utilities import get_qgis_app 19 | from DataPlotly.gui.dock import (DataPlotlyDock, DataPlotlyDockManager) 20 | from DataPlotly.gui.add_new_dock_dlg import DataPlotlyNewDockIdValidator 21 | 22 | QGIS_APP, CANVAS, IFACE, PARENT = get_qgis_app() 23 | 24 | 25 | def read_project(project_path): 26 | """Retur a document from qgs file 27 | 28 | Args: 29 | project_path (str): path to qgs file 30 | 31 | Returns: 32 | QDocument: document 33 | """ 34 | xml_file = QFile(project_path) 35 | if xml_file.open(QIODevice.ReadOnly): 36 | xml_doc = QDomDocument() 37 | xml_doc.setContent(xml_file) 38 | xml_file.close() 39 | return xml_doc 40 | return None 41 | 42 | 43 | class DataPlotlyDockManagerTest(unittest.TestCase): 44 | """ 45 | Test DataPlotlyDockManager 46 | """ 47 | 48 | def setUp(self): 49 | self.dock_widgets = {} 50 | self.dock_manager = DataPlotlyDockManager( 51 | iface=IFACE, dock_widgets=self.dock_widgets) 52 | 53 | def test_001_constructor(self): 54 | """ 55 | Test the constructor of DataPlotlyDockManager 56 | """ 57 | self.assertIs(self.dock_widgets, self.dock_manager.dock_widgets) 58 | 59 | def test_002_add_new_dock(self): 60 | """ 61 | Test addNewDock of DataPlotlyDockManager 62 | """ 63 | # checks DataPlotly main dock 64 | self.assertNotIn('DataPlotly', self.dock_widgets) 65 | dock_widget = self.dock_manager.addNewDock() 66 | self.assertIsInstance(dock_widget, DataPlotlyDock) 67 | self.assertIn('DataPlotly', self.dock_widgets) 68 | self.assertIs(dock_widget, self.dock_widgets['DataPlotly']) 69 | 70 | # checks it's not possible to add a new dock with DataPlotly as dock_id 71 | dock_widget2 = self.dock_manager.addNewDock(dock_title='NewDataPlotly', 72 | dock_id='DataPlotly') 73 | self.assertIs(dock_widget2, dock_widget) 74 | 75 | # checks we can not add new dock with same dock_id 76 | dock_params = {'dock_title': 'DataPlotly2', 'dock_id': 'DataPlotly2'} 77 | self.dock_manager.addNewDock(**dock_params) 78 | self.assertIn('DataPlotly2', self.dock_widgets) 79 | new_dock_widget = self.dock_manager.addNewDock( 80 | dock_title='DataPlotly2b', dock_id='DataPlotly2') 81 | self.assertFalse(new_dock_widget) 82 | 83 | def test_003_remove_dock(self): 84 | """ 85 | Test removeDock 86 | """ 87 | dock_id = 'dock_to_remove' 88 | self.dock_manager.addNewDock(dock_id=dock_id) 89 | self.assertIn(dock_id, self.dock_widgets) 90 | self.dock_manager.removeDock(dock_id) 91 | self.assertNotIn(dock_id, self.dock_widgets) 92 | 93 | def test_004_remove_docks(self): 94 | """ 95 | Test removeDocks 96 | """ 97 | docks = ['DataPlotly', 'DataPlotly2', 'DataPlotly3'] 98 | for dock in docks: 99 | self.dock_manager.addNewDock(dock_id=dock) 100 | self.dock_manager.removeDocks() 101 | # do not remove DataPlotly main dock 102 | self.assertIn('DataPlotly', self.dock_widgets) 103 | self.assertEqual(len(self.dock_widgets), 1) 104 | 105 | def test_005_get_dock(self): 106 | """ 107 | Test getDock 108 | """ 109 | docks = ['DataPlotly', 'DataPlotly2', 'DataPlotly3'] 110 | for dock in docks: 111 | self.dock_manager.addNewDock(dock_id=dock) 112 | dock = self.dock_manager.getDock('DataPloty4_wrong_id') 113 | self.assertIsNone(dock) 114 | dock = self.dock_manager.getDock('DataPlotly3') 115 | self.assertIsInstance(dock, DataPlotlyDock) 116 | self.assertIs(dock, self.dock_widgets['DataPlotly3']) 117 | 118 | def test_006_read_project(self): 119 | """ 120 | Test read_project with or without StateDataPlotly 121 | """ 122 | # project with StateDataPlotly dom 123 | project_path = os.path.join(os.path.dirname( 124 | __file__), 'test_project_with_state.qgs') 125 | document = read_project(project_path) 126 | ok = self.dock_manager.read_from_project(document) 127 | self.assertTrue(ok) 128 | 129 | # project without StateDataPlotly dom 130 | project_path = os.path.join(os.path.dirname( 131 | __file__), 'test_project_without_state.qgs') 132 | document = read_project(project_path) 133 | ko = self.dock_manager.read_from_project(document) 134 | self.assertFalse(ko) 135 | 136 | def test_007_utils_xml_function(self): 137 | """ 138 | Test restore, restore_safe_str_xml, safe_str_xml 139 | """ 140 | test_string = "My test" 141 | self.assertEqual(test_string, restore_safe_str_xml( 142 | safe_str_xml(test_string))) 143 | test_string = b'test' 144 | str_b64 = str(QByteArray(test_string).toBase64(), 'utf-8') 145 | self.assertEqual(test_string, restore(str_b64)) 146 | 147 | def test_008_add_docks_from_project(self): 148 | """ 149 | Test docks are added, custom project without StateDataPlotly node 150 | """ 151 | project_path = os.path.join(os.path.dirname( 152 | __file__), 'test_project_without_state.qgs') 153 | document = read_project(project_path) 154 | self.dock_manager.addDocksFromProject(document) 155 | # all docks except main DataPlotlyDock are created 156 | dock_id = 'my-test' 157 | dock_title = "My Test" 158 | self.assertIn(dock_id, self.dock_widgets) 159 | # . is replace by space My.Test -> My Test 160 | self.assertEqual(self.dock_widgets[dock_id].title, dock_title) 161 | 162 | def test_009_add_new_dock_validator(self): 163 | """ 164 | Test DockIdValidator 165 | """ 166 | validator = DataPlotlyNewDockIdValidator( 167 | dock_widgets=self.dock_widgets) 168 | docks = ['DataPlotly', 'DataPlotly2', 'DataPlotly3'] 169 | for dock in docks: 170 | self.dock_manager.addNewDock(dock_id=dock) 171 | 172 | # Dockid is valid 173 | state, _, _ = validator.validate('NewDockId', None) 174 | self.assertEqual( 175 | QValidator.Acceptable, 176 | state 177 | ) 178 | # Dockid can not be empty / No underscore / Not already taken 179 | for bad_dock_id in ['', 'with_underscore', 'DataPlotly2']: 180 | state, _, _ = validator.validate(bad_dock_id, None) 181 | self.assertEqual(QValidator.Intermediate, state) 182 | 183 | 184 | if __name__ == "__main__": 185 | suite = unittest.defaultTestLoader.loadTestsFromTestCase(DataPlotlyDockManagerTest) 186 | runner = unittest.TextTestRunner(verbosity=2) 187 | runner.run(suite) 188 | -------------------------------------------------------------------------------- /DataPlotly/test/test_guiutils.py: -------------------------------------------------------------------------------- 1 | """GUI Utils Test. 2 | 3 | .. note:: This program is free software; you can redistribute it and/or modify 4 | it under the terms of the GNU General Public License as published by 5 | the Free Software Foundation; either version 2 of the License, or 6 | (at your option) any later version. 7 | 8 | """ 9 | 10 | __author__ = '(C) 2018 by Nyall Dawson' 11 | __date__ = '20/04/2018' 12 | __copyright__ = 'Copyright 2018, North Road' 13 | # This will get replaced with a git SHA1 when you do a git archive 14 | __revision__ = '$Format:%H$' 15 | 16 | import unittest 17 | from DataPlotly.gui.gui_utils import GuiUtils 18 | from .utilities import get_qgis_app 19 | 20 | QGIS_APP = get_qgis_app() 21 | 22 | 23 | class GuiUtilsTest(unittest.TestCase): 24 | """Test GuiUtils work.""" 25 | 26 | def testGetIcon(self): 27 | """ 28 | Tests get_icon 29 | """ 30 | self.assertFalse( 31 | GuiUtils.get_icon('dataplotly.svg').isNull()) 32 | self.assertTrue(GuiUtils.get_icon('not_an_icon.svg').isNull()) 33 | 34 | def testGetIconSvg(self): 35 | """ 36 | Tests get_icon svg path 37 | """ 38 | self.assertTrue( 39 | GuiUtils.get_icon_svg('dataplotly.svg')) 40 | self.assertIn('dataplotly.svg', 41 | GuiUtils.get_icon_svg('dataplotly.svg')) 42 | self.assertFalse(GuiUtils.get_icon_svg('not_an_icon.svg')) 43 | 44 | 45 | if __name__ == "__main__": 46 | suite = unittest.defaultTestLoader.loadTestsFromTestCase(GuiUtilsTest) 47 | runner = unittest.TextTestRunner(verbosity=2) 48 | runner.run(suite) 49 | -------------------------------------------------------------------------------- /DataPlotly/test/test_init.py: -------------------------------------------------------------------------------- 1 | """Tests QGIS plugin init.""" 2 | 3 | __author__ = 'Tim Sutton ' 4 | __revision__ = '$Format:%H$' 5 | __date__ = '17/10/2010' 6 | __license__ = "GPL" 7 | __copyright__ = 'Copyright 2012, Australia Indonesia Facility for ' 8 | __copyright__ += 'Disaster Reduction' 9 | 10 | import os 11 | import unittest 12 | import logging 13 | import configparser 14 | 15 | LOGGER = logging.getLogger('QGIS') 16 | 17 | 18 | class TestInit(unittest.TestCase): 19 | """Test that the plugin init is usable for QGIS. 20 | 21 | Based heavily on the validator class by Alessandro 22 | Passoti available here: 23 | 24 | http://github.com/qgis/qgis-django/blob/master/qgis-app/ 25 | plugins/validator.py 26 | 27 | """ 28 | 29 | def test_read_init(self): 30 | """Test that the plugin __init__ will validate on plugins.qgis.org.""" 31 | 32 | # You should update this list according to the latest in 33 | # https://github.com/qgis/qgis-django/blob/master/qgis-app/ 34 | # plugins/validator.py 35 | 36 | required_metadata = [ 37 | 'name', 38 | 'description', 39 | 'version', 40 | 'qgisMinimumVersion', 41 | 'email', 42 | 'author'] 43 | 44 | file_path = os.path.abspath(os.path.join( 45 | os.path.dirname(__file__), os.pardir, 46 | 'metadata.txt')) 47 | LOGGER.info(file_path) 48 | metadata = [] 49 | parser = configparser.ConfigParser() 50 | parser.optionxform = str 51 | parser.read(file_path) 52 | message = 'Cannot find a section named "general" in %s' % file_path 53 | assert parser.has_section('general'), message 54 | metadata.extend(parser.items('general')) 55 | 56 | for expectation in required_metadata: 57 | message = ('Cannot find metadata "{}" in metadata source ({}).'.format( 58 | expectation, file_path)) 59 | 60 | self.assertIn(expectation, dict(metadata), message) 61 | 62 | 63 | if __name__ == '__main__': 64 | unittest.main() 65 | -------------------------------------------------------------------------------- /DataPlotly/test/test_layer.cpg: -------------------------------------------------------------------------------- 1 | UTF-8 -------------------------------------------------------------------------------- /DataPlotly/test/test_layer.dbf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghtmtt/DataPlotly/f57b842703ebb259091b4490a9821518a14b01b9/DataPlotly/test/test_layer.dbf -------------------------------------------------------------------------------- /DataPlotly/test/test_layer.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "name": "test_layer", 4 | "crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } }, 5 | "features": [ 6 | { "type": "Feature", "properties": { "id": 27, "profo": "d", "so4": 1322, "ca": 249.28, "mg": 546.89, "profm": -16.504 }, "geometry": { "type": "Point", "coordinates": [ 10.795744304058722, 43.63420441224001 ] } }, 7 | { "type": "Feature", "properties": { "id": 26, "profo": "d", "so4": 1055, "ca": 269.56, "mg": 480.43, "profm": -17.54 }, "geometry": { "type": "Point", "coordinates": [ 10.571051969329464, 43.379752033530515 ] } }, 8 | { "type": "Feature", "properties": { "id": 25, "profo": "d", "so4": 632, "ca": 263.33, "mg": 518.19, "profm": -11.011 }, "geometry": { "type": "Point", "coordinates": [ 10.556071420648362, 43.460472554560745 ] } }, 9 | { "type": "Feature", "properties": { "id": 24, "profo": "d", "so4": 1122, "ca": 270.45, "mg": 513.81, "profm": -15.074 }, "geometry": { "type": "Point", "coordinates": [ 10.549813079578296, 43.461304882390593 ] } }, 10 | { "type": "Feature", "properties": { "id": 23, "profo": "d", "so4": 536, "ca": 53.6, "mg": 556.7, "profm": -13.408 }, "geometry": { "type": "Point", "coordinates": [ 10.623759088971619, 43.695950190510729 ] } }, 11 | { "type": "Feature", "properties": { "id": 22, "profo": "d", "so4": 680, "ca": 81.57, "mg": 237.99, "profm": -10.191 }, "geometry": { "type": "Point", "coordinates": [ 10.884430431231918, 43.403842570916481 ] } }, 12 | { "type": "Feature", "properties": { "id": 21, "profo": "d", "so4": 296, "ca": 83.0, "mg": 222.64, "profm": -11.809 }, "geometry": { "type": "Point", "coordinates": [ 10.754811441622472, 43.363470396557496 ] } }, 13 | { "type": "Feature", "properties": { "id": 20, "profo": "d", "so4": 265, "ca": 62.91, "mg": 442.42, "profm": -14.202 }, "geometry": { "type": "Point", "coordinates": [ 10.586789564057552, 43.348636989224019 ] } }, 14 | { "type": "Feature", "properties": { "id": 19, "profo": "prof", "so4": 788, "ca": 64.94, "mg": 244.03, "profm": -11.628 }, "geometry": { "type": "Point", "coordinates": [ 10.956367033291345, 43.386988748159688 ] } }, 15 | { "type": "Feature", "properties": { "id": 18, "profo": "d", "so4": 791, "ca": 93.39, "mg": 563.42, "profm": -10.434 }, "geometry": { "type": "Point", "coordinates": [ 10.359406317962845, 43.675277001825656 ] } }, 16 | { "type": "Feature", "properties": { "id": 17, "profo": "prof", "so4": 683, "ca": 143.38, "mg": 456.59, "profm": -19.291 }, "geometry": { "type": "Point", "coordinates": [ 10.575430392806258, 43.395006140010288 ] } }, 17 | { "type": "Feature", "properties": { "id": 16, "profo": "prof", "so4": 457, "ca": 85.97, "mg": 460.99, "profm": -16.101 }, "geometry": { "type": "Point", "coordinates": [ 10.695380311244154, 43.784477487882988 ] } }, 18 | { "type": "Feature", "properties": { "id": 15, "profo": "prof", "so4": 267, "ca": 147.26, "mg": 407.14, "profm": -14.022 }, "geometry": { "type": "Point", "coordinates": [ 10.670823816601224, 43.623648723727136 ] } }, 19 | { "type": "Feature", "properties": { "id": 14, "profo": "prof", "so4": 513, "ca": 115.89, "mg": 272.89, "profm": -16.546 }, "geometry": { "type": "Point", "coordinates": [ 10.709919420883274, 43.250832320754803 ] } }, 20 | { "type": "Feature", "properties": { "id": 13, "profo": "prof", "so4": 306, "ca": 149.01, "mg": 538.67, "profm": -15.825 }, "geometry": { "type": "Point", "coordinates": [ 10.348281111366386, 43.664350523807812 ] } }, 21 | { "type": "Feature", "properties": { "id": 12, "profo": "prof", "so4": 627, "ca": 61.46, "mg": 62.17, "profm": -16.911 }, "geometry": { "type": "Point", "coordinates": [ 10.55229480012574, 43.683029492650505 ] } }, 22 | { "type": "Feature", "properties": { "id": 11, "profo": "s", "so4": 100, "ca": 47.43, "mg": 128.51, "profm": -18.632 }, "geometry": { "type": "Point", "coordinates": [ 10.319380041202738, 43.730005294368354 ] } }, 23 | { "type": "Feature", "properties": { "id": 10, "profo": "s", "so4": 84, "ca": 42.94, "mg": 94.87, "profm": -15.048 }, "geometry": { "type": "Point", "coordinates": [ 10.31589787706001, 43.747451729218653 ] } }, 24 | { "type": "Feature", "properties": { "id": 9, "profo": "s", "so4": 98, "ca": 81.87, "mg": 72.31, "profm": -13.488 }, "geometry": { "type": "Point", "coordinates": [ 10.63025252959865, 43.490724296197371 ] } }, 25 | { "type": "Feature", "properties": { "id": 8, "profo": "s", "so4": 88, "ca": 22.26, "mg": 86.03, "profm": -17.352 }, "geometry": { "type": "Point", "coordinates": [ 10.428647512874122, 43.661024534699273 ] } }, 26 | { "type": "Feature", "properties": { "id": 7, "profo": "s", "so4": 267, "ca": 74.16, "mg": 85.26, "profm": -19.026 }, "geometry": { "type": "Point", "coordinates": [ 10.987186384487241, 43.385498650024061 ] } }, 27 | { "type": "Feature", "properties": { "id": 6, "profo": "s", "so4": 329, "ca": 35.05, "mg": 81.11, "profm": -14.719 }, "geometry": { "type": "Point", "coordinates": [ 10.588879752350103, 43.294943020923668 ] } }, 28 | { "type": "Feature", "properties": { "id": 5, "profo": "s", "so4": 319, "ca": 46.64, "mg": 131.59, "profm": -18.841 }, "geometry": { "type": "Point", "coordinates": [ 10.701604657200324, 43.263530250269049 ] } }, 29 | { "type": "Feature", "properties": { "id": 4, "profo": "s", "so4": 137, "ca": 126.73, "mg": 95.36, "profm": -18.183 }, "geometry": { "type": "Point", "coordinates": [ 10.763194062178599, 43.415012407312666 ] } }, 30 | { "type": "Feature", "properties": { "id": 3, "profo": "s", "so4": 350, "ca": 116.44, "mg": 112.88, "profm": -16.874 }, "geometry": { "type": "Point", "coordinates": [ 10.51819290071276, 43.675356405437903 ] } }, 31 | { "type": "Feature", "properties": { "id": 2, "profo": "s", "so4": 151, "ca": 108.25, "mg": 80.55, "profm": -10.111 }, "geometry": { "type": "Point", "coordinates": [ 10.419553200464033, 43.655474054584019 ] } }, 32 | { "type": "Feature", "properties": { "id": 1, "profo": "s", "so4": 203, "ca": 110.45, "mg": 78.34, "profm": -16.597 }, "geometry": { "type": "Point", "coordinates": [ 10.395171539231754, 43.727005546503719 ] } } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /DataPlotly/test/test_layer.prj: -------------------------------------------------------------------------------- 1 | GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]] -------------------------------------------------------------------------------- /DataPlotly/test/test_layer.qpj: -------------------------------------------------------------------------------- 1 | GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]] 2 | -------------------------------------------------------------------------------- /DataPlotly/test/test_layer.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghtmtt/DataPlotly/f57b842703ebb259091b4490a9821518a14b01b9/DataPlotly/test/test_layer.shp -------------------------------------------------------------------------------- /DataPlotly/test/test_layer.shx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghtmtt/DataPlotly/f57b842703ebb259091b4490a9821518a14b01b9/DataPlotly/test/test_layer.shx -------------------------------------------------------------------------------- /DataPlotly/test/test_processing.py: -------------------------------------------------------------------------------- 1 | """Tests for processing algorithms.""" 2 | 3 | import os 4 | import json 5 | 6 | import processing 7 | 8 | from qgis.core import QgsApplication, QgsVectorLayer 9 | from qgis.testing import unittest 10 | from qgis.PyQt.QtGui import QColor 11 | 12 | from DataPlotly.processing.dataplotly_provider import DataPlotlyProvider 13 | 14 | __copyright__ = 'Copyright 2022, Faunalia' 15 | __license__ = 'GPL version 3' 16 | __email__ = 'info@faunalia.eu' 17 | 18 | 19 | class TestProcessing(unittest.TestCase): 20 | """Tests for processing algorithms.""" 21 | 22 | def setUp(self) -> None: 23 | """Set up the processing tests.""" 24 | if not QgsApplication.processingRegistry().providers(): 25 | self.provider = DataPlotlyProvider(plugin_version='2.3') 26 | QgsApplication.processingRegistry().addProvider(self.provider) 27 | self.maxDiff = None 28 | 29 | def test_scatterplot_figure(self): 30 | """Test for the Processing scatterplot""" 31 | 32 | layer_path = os.path.join( 33 | os.path.dirname(__file__), 'test_layer.shp') 34 | 35 | vl = QgsVectorLayer(layer_path, 'test_layer', 'ogr') 36 | 37 | # plot_path = os.path.join( 38 | # os.path.dirname(__file__), 'scatterplot.json') 39 | # with open(plot_path, 'r') as f: 40 | # template_dict = json.load(f) 41 | 42 | plot_param = { 43 | 'INPUT': vl, 44 | 'XEXPRESSION': '"so4"', 45 | 'YEXPRESSION': '"ca"', 46 | 'SIZE': 10, 47 | 'COLOR': QColor(142, 186, 217), 48 | 'FACET_COL': '', 49 | 'FACET_ROW': '', 50 | 'OFFLINE': False, 51 | 'OUTPUT_HTML_FILE': 'TEMPORARY_OUTPUT', 52 | 'OUTPUT_JSON_FILE': 'TEMPORARY_OUTPUT' 53 | } 54 | 55 | result = processing.run("DataPlotly:dataplotly_scatterplot", plot_param) 56 | 57 | with open(result['OUTPUT_JSON_FILE'], encoding='utf8') as f: 58 | result_dict = json.load(f) 59 | 60 | self.assertListEqual( 61 | result_dict['data'][0]['x'], 62 | [98, 88, 267, 329, 319, 137, 350, 151, 203] 63 | ) 64 | self.assertListEqual( 65 | result_dict['data'][0]['y'], 66 | [81.87, 22.26, 74.16, 35.05, 46.64, 126.73, 116.44, 108.25, 110.45] 67 | ) 68 | 69 | 70 | if __name__ == '__main__': 71 | unittest.main() 72 | -------------------------------------------------------------------------------- /DataPlotly/test/test_qgis_environment.py: -------------------------------------------------------------------------------- 1 | """Tests for QGIS functionality. 2 | 3 | .. note:: This program is free software; you can redistribute it and/or modify 4 | it under the terms of the GNU General Public License as published by 5 | the Free Software Foundation; either version 2 of the License, or 6 | (at your option) any later version. 7 | 8 | """ 9 | __author__ = 'tim@linfiniti.com' 10 | __date__ = '20/01/2011' 11 | __copyright__ = ('Copyright 2012, Australia Indonesia Facility for ' 12 | 'Disaster Reduction') 13 | 14 | import unittest 15 | from qgis.core import (QgsProviderRegistry, 16 | QgsCoordinateReferenceSystem) 17 | from .utilities import get_qgis_app 18 | 19 | 20 | QGIS_APP = get_qgis_app() 21 | 22 | 23 | class QGISTest(unittest.TestCase): 24 | """Test the QGIS Environment""" 25 | 26 | def test_qgis_environment(self): 27 | """QGIS environment has the expected providers""" 28 | 29 | r = QgsProviderRegistry.instance() 30 | self.assertIn('gdal', r.providerList()) 31 | self.assertIn('ogr', r.providerList()) 32 | self.assertIn('postgres', r.providerList()) 33 | 34 | def test_projection(self): 35 | """Test that QGIS properly parses a wkt string. 36 | """ 37 | crs = QgsCoordinateReferenceSystem() 38 | wkt = ( 39 | 'GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",' 40 | 'SPHEROID["WGS_1984",6378137.0,298.257223563]],' 41 | 'PRIMEM["Greenwich",0.0],UNIT["Degree",' 42 | '0.0174532925199433]]') 43 | crs.createFromWkt(wkt) 44 | auth_id = crs.authid() 45 | expected_auth_id = 'EPSG:4326' 46 | self.assertEqual(auth_id, expected_auth_id) 47 | 48 | 49 | if __name__ == '__main__': 50 | unittest.main() 51 | -------------------------------------------------------------------------------- /DataPlotly/test/test_resources.py: -------------------------------------------------------------------------------- 1 | """Resources test. 2 | 3 | .. note:: This program is free software; you can redistribute it and/or modify 4 | it under the terms of the GNU General Public License as published by 5 | the Free Software Foundation; either version 2 of the License, or 6 | (at your option) any later version. 7 | 8 | """ 9 | 10 | __author__ = 'matteo.ghetta@gmail.com' 11 | __date__ = '2017-03-05' 12 | __copyright__ = 'Copyright 2017, matteo ghetta' 13 | 14 | import unittest 15 | 16 | from qgis.PyQt.QtGui import QIcon 17 | 18 | 19 | class DataPlotlyResourcesTest(unittest.TestCase): 20 | """Test resources work.""" 21 | 22 | def setUp(self): 23 | """Runs before each test.""" 24 | pass 25 | 26 | def tearDown(self): 27 | """Runs after each test.""" 28 | pass 29 | 30 | def test_icon_png(self): 31 | """Test we can load resources.""" 32 | path = ':/plugins/DataPlotly/icon.png' 33 | icon = QIcon(path) 34 | self.assertFalse(icon.isNull()) 35 | 36 | 37 | if __name__ == "__main__": 38 | suite = unittest.defaultTestLoader.loadTestsFromTestCase(DataPlotlyResourcesTest) 39 | runner = unittest.TextTestRunner(verbosity=2) 40 | runner.run(suite) 41 | -------------------------------------------------------------------------------- /DataPlotly/test/utilities.py: -------------------------------------------------------------------------------- 1 | """Common functionality used by regression tests.""" 2 | 3 | import sys 4 | import logging 5 | import os 6 | import atexit 7 | from qgis.core import QgsApplication 8 | from qgis.gui import QgsMapCanvas 9 | from qgis.PyQt.QtCore import QSize 10 | from qgis.PyQt.QtWidgets import QWidget 11 | from qgis.utils import iface 12 | from .qgis_interface import QgisInterface 13 | 14 | LOGGER = logging.getLogger('QGIS') 15 | QGIS_APP = None # Static variable used to hold hand to running QGIS app 16 | CANVAS = None 17 | PARENT = None 18 | IFACE = None 19 | 20 | 21 | def get_qgis_app(cleanup=True): 22 | """ Start one QGIS application to test against. 23 | 24 | :returns: Handle to QGIS app, canvas, iface and parent. If there are any 25 | errors the tuple members will be returned as None. 26 | :rtype: (QgsApplication, CANVAS, IFACE, PARENT) 27 | 28 | If QGIS is already running the handle to that app will be returned. 29 | """ 30 | 31 | global QGIS_APP, PARENT, IFACE, CANVAS # pylint: disable=W0603 32 | 33 | if iface: 34 | QGIS_APP = QgsApplication 35 | CANVAS = iface.mapCanvas() 36 | PARENT = iface.mainWindow() 37 | IFACE = iface 38 | return QGIS_APP, CANVAS, IFACE, PARENT 39 | 40 | global QGISAPP # pylint: disable=global-variable-undefined 41 | 42 | try: 43 | QGISAPP # pylint: disable=used-before-assignment 44 | except NameError: 45 | myGuiFlag = True # All test will run qgis in gui mode 46 | 47 | # In python3 we need to convert to a bytes object (or should 48 | # QgsApplication accept a QString instead of const char* ?) 49 | try: 50 | argvb = list(map(os.fsencode, sys.argv)) 51 | except AttributeError: 52 | argvb = sys.argv 53 | 54 | # Note: QGIS_PREFIX_PATH is evaluated in QgsApplication - 55 | # no need to mess with it here. 56 | QGISAPP = QgsApplication(argvb, myGuiFlag) 57 | 58 | QGISAPP.initQgis() 59 | s = QGISAPP.showSettings() 60 | LOGGER.debug(s) 61 | 62 | def debug_log_message(message, tag, level): 63 | """ 64 | Prints a debug message to a log 65 | :param message: message to print 66 | :param tag: log tag 67 | :param level: log message level (severity) 68 | :return: 69 | """ 70 | print(f'{tag}({level}): {message}') 71 | 72 | QgsApplication.instance().messageLog().messageReceived.connect( 73 | debug_log_message) 74 | 75 | if cleanup: 76 | @atexit.register 77 | def exitQgis(): # pylint: disable=unused-variable 78 | """ 79 | Gracefully closes the QgsApplication instance 80 | """ 81 | try: 82 | QGISAPP.exitQgis() # pylint: disable=used-before-assignment 83 | QGISAPP = None # pylint: disable=redefined-outer-name 84 | except NameError: 85 | pass 86 | 87 | if PARENT is None: 88 | # noinspection PyPep8Naming 89 | PARENT = QWidget() 90 | 91 | if CANVAS is None: 92 | # noinspection PyPep8Naming 93 | CANVAS = QgsMapCanvas(PARENT) 94 | CANVAS.resize(QSize(400, 400)) 95 | 96 | if IFACE is None: 97 | # QgisInterface is a stub implementation of the QGIS plugin interface 98 | # noinspection PyPep8Naming 99 | IFACE = QgisInterface(CANVAS) 100 | 101 | return QGISAPP, CANVAS, IFACE, PARENT 102 | -------------------------------------------------------------------------------- /DataPlotly/test_suite.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test Suite. 3 | 4 | .. note:: This program is free software; you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation; either version 2 of the License, or 7 | (at your option) any later version. 8 | 9 | """ 10 | 11 | import sys 12 | import os 13 | import unittest 14 | import tempfile 15 | from osgeo import gdal 16 | import qgis # pylint: disable=unused-import 17 | 18 | try: 19 | from pip import main as pipmain 20 | except ImportError: 21 | from pip._internal import main as pipmain 22 | 23 | try: 24 | import coverage 25 | except ImportError: 26 | pipmain(['install', 'coverage']) 27 | import coverage 28 | 29 | __author__ = 'Alessandro Pasotti' 30 | __revision__ = '$Format:%H$' 31 | __date__ = '30/04/2018' 32 | __copyright__ = ( 33 | 'Copyright 2018, North Road') 34 | 35 | 36 | def _run_tests(test_suite, package_name, with_coverage=False): 37 | """Core function to test a test suite.""" 38 | count = test_suite.countTestCases() 39 | print('########') 40 | print('{} tests has been discovered in {}'.format(count, package_name)) 41 | print('Python GDAL : %s' % gdal.VersionInfo('VERSION_NUM')) 42 | print('########') 43 | if with_coverage: 44 | cov = coverage.Coverage( 45 | source=['/DataPlotly'], 46 | omit=['*/test/*'], 47 | ) 48 | cov.start() 49 | 50 | unittest.TextTestRunner(verbosity=3, stream=sys.stdout).run(test_suite) 51 | 52 | if with_coverage: 53 | cov.stop() 54 | cov.save() 55 | with tempfile.NamedTemporaryFile(delete=False) as report: 56 | cov.report(file=report) 57 | # Produce HTML reports in the `htmlcov` folder and open index.html 58 | # cov.html_report() 59 | with open(report.name, encoding='utf8') as fin: 60 | print(fin.read()) 61 | 62 | 63 | def test_package(package='DataPlotly'): 64 | """Test package. 65 | This function is called by travis without arguments. 66 | 67 | :param package: The package to test. 68 | :type package: str 69 | """ 70 | test_loader = unittest.defaultTestLoader 71 | try: 72 | test_suite = test_loader.discover(package) 73 | except ImportError: 74 | test_suite = unittest.TestSuite() 75 | _run_tests(test_suite, package) 76 | 77 | 78 | def test_environment(): 79 | """Test package with an environment variable.""" 80 | package = os.environ.get('TESTING_PACKAGE', 'DataPlotly') 81 | test_loader = unittest.defaultTestLoader 82 | test_suite = test_loader.discover(package) 83 | _run_tests(test_suite, package) 84 | 85 | 86 | if __name__ == '__main__': 87 | test_package() 88 | -------------------------------------------------------------------------------- /DataPlotly/ui/add_dock_dlg.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | DialogAddNewDock 4 | 5 | 6 | 7 | 0 8 | 0 9 | 586 10 | 187 11 | 12 | 13 | 14 | 15 | 586 16 | 0 17 | 18 | 19 | 20 | DataPlotly 21 | 22 | 23 | 24 | 25 | 26 | Add a new dock 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | Dock Title 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; margin-left: 20px; margin-right: 10px; left: 0px; top: 2px;}QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; margin-left: 20px; margin-right: 5px; left: 0px; top: 1px;} QgsCollapsibleGroupBoxBasic { border: none; } 51 | 52 | 53 | Customize Dock Id 54 | 55 | 56 | true 57 | 58 | 59 | false 60 | 61 | 62 | true 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | Dock id must be unique 71 | 72 | 73 | Dock Id 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | true 91 | 92 | 93 | 94 | Qt::LeftToRight 95 | 96 | 97 | *{ margin-right:5px;} 98 | 99 | 100 | Dock id error information 101 | 102 | 103 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 104 | 105 | 106 | 0 107 | 108 | 109 | Qt::NoTextInteraction 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | Qt::Vertical 120 | 121 | 122 | 123 | 20 124 | 40 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | Qt::Horizontal 136 | 137 | 138 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | QgsCollapsibleGroupBox 147 | QGroupBox 148 |
qgscollapsiblegroupbox.h
149 | 1 150 |
151 | 152 | QgsFilterLineEdit 153 | QLineEdit 154 |
qgsfilterlineedit.h
155 |
156 |
157 | 158 | 159 | 160 | buttonBox 161 | accepted() 162 | DialogAddNewDock 163 | accept() 164 | 165 | 166 | 227 167 | 250 168 | 169 | 170 | 157 171 | 186 172 | 173 | 174 | 175 | 176 | buttonBox 177 | rejected() 178 | DialogAddNewDock 179 | reject() 180 | 181 | 182 | 295 183 | 250 184 | 185 | 186 | 286 187 | 186 188 | 189 | 190 | 191 | 192 |
193 | -------------------------------------------------------------------------------- /DataPlotly/ui/remove_dock_dlg.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | DialogAddNewDock 4 | 5 | 6 | 7 | 0 8 | 0 9 | 586 10 | 187 11 | 12 | 13 | 14 | 15 | 586 16 | 0 17 | 18 | 19 | 20 | DataPlotly 21 | 22 | 23 | 24 | 25 | 26 | Remove a dock 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 0 36 | 0 37 | 38 | 39 | 40 | 41 | 100 42 | 16777215 43 | 44 | 45 | 46 | Dock Id 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | Qt::Vertical 63 | 64 | 65 | 66 | 20 67 | 40 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | Qt::Horizontal 79 | 80 | 81 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | buttonBox 91 | accepted() 92 | DialogAddNewDock 93 | accept() 94 | 95 | 96 | 227 97 | 250 98 | 99 | 100 | 157 101 | 186 102 | 103 | 104 | 105 | 106 | buttonBox 107 | rejected() 108 | DialogAddNewDock 109 | reject() 110 | 111 | 112 | 295 113 | 250 114 | 115 | 116 | 286 117 | 186 118 | 119 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #/*************************************************************************** 2 | # DataPlotly 3 | # 4 | # D3 Plots for QGIS 5 | # ------------------- 6 | # begin : 2017-03-05 7 | # git sha : $Format:%H$ 8 | # copyright : (C) 2017 by matteo ghetta 9 | # email : matteo.ghetta@gmail.com 10 | # ***************************************************************************/ 11 | # 12 | #/*************************************************************************** 13 | # * * 14 | # * This program is free software; you can redistribute it and/or modify * 15 | # * it under the terms of the GNU General Public License as published by * 16 | # * the Free Software Foundation; either version 2 of the License, or * 17 | # * (at your option) any later version. * 18 | # * * 19 | # ***************************************************************************/ 20 | 21 | ################################################# 22 | # Edit the following to match your sources lists 23 | ################################################# 24 | 25 | 26 | PEP8EXCLUDE=pydev,conf.py,third_party,ui,.venv,venv, 27 | 28 | 29 | ################################################# 30 | # Normally you would not need to edit below here 31 | ################################################# 32 | 33 | test: 34 | @echo 35 | @echo "----------------------" 36 | @echo "Regression Test Suite" 37 | @echo "----------------------" 38 | 39 | @# Preceding dash means that make will continue in case of errors 40 | @-export PYTHONPATH=`pwd`:$(PYTHONPATH); \ 41 | export QGIS_DEBUG=0; \ 42 | export QGIS_LOG_FILE=/dev/null; \ 43 | nosetests3 -v -s --with-id --with-coverage --cover-package=. DataPlotly.test \ 44 | 3>&1 1>&2 2>&3 3>&- || true 45 | @echo "----------------------" 46 | @echo "If you get a 'no module named qgis.core error, try sourcing" 47 | @echo "the helper script we have provided first then run make test." 48 | @echo "e.g. source run-env-linux.sh ; make test" 49 | @echo "----------------------" 50 | 51 | pylint: 52 | @echo 53 | @echo "-----------------" 54 | @echo "Pylint violations" 55 | @echo "-----------------" 56 | @pylint --reports=n --rcfile=pylintrc DataPlotly 57 | @echo 58 | @echo "----------------------" 59 | @echo "If you get a 'no module named qgis.core' error, try sourcing" 60 | @echo "the helper script we have provided first then run make pylint." 61 | @echo "e.g. source run-env-linux.sh ; make pylint" 62 | @echo "----------------------" 63 | 64 | 65 | # Run pep8/pycodestyle style checking 66 | #http://pypi.python.org/pypi/pep8 67 | pycodestyle: 68 | @echo 69 | @echo "-----------" 70 | @echo "pycodestyle PEP8 issues" 71 | @echo "-----------" 72 | @pycodestyle --repeat --ignore=E203,E121,E122,E123,E124,E125,E126,E127,E128,E402,E501,W504 --exclude $(PEP8EXCLUDE) . 73 | @echo "-----------" 74 | @echo "Ignored in PEP8 check:" 75 | @echo $(PEP8EXCLUDE) 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DataPlotly 2 | 3 | [![QGIS.org](https://img.shields.io/badge/QGIS.org-published-green)](https://plugins.qgis.org/plugins/DataPlotly/) 4 | [![Test plugin](https://github.com/ghtmtt/DataPlotly/actions/workflows/test_plugin.yaml/badge.svg)](https://github.com/ghtmtt/DataPlotly/actions/workflows/test_plugin.yaml) 5 | [![Transifex 🗺](https://github.com/ghtmtt/DataPlotly/actions/workflows/transifex.yml/badge.svg)](https://github.com/ghtmtt/DataPlotly/actions/workflows/transifex.yml) 6 | 7 | **Documentation: https://dataplotly-docs.readthedocs.io/en/latest/intro.html** 8 | 9 | The DataPlotly plugin allows creation of [D3](https://d3js.org/)-like 10 | interactive plots directly within QGIS, thanks to the [Plotly](https://plot.ly/python/) 11 | library and its Python API. 12 | 13 | DataPlotly makes plot creation and customization easy for every needs. 14 | 15 | Besides all the plot and customization available, the plot is **linked** with 16 | the QGIS map canvas: 17 | 18 | ![Plot interactions](img/plot_interaction_scatter.gif) 19 | 20 | ![Plot interactions](img/plot_interaction_box.gif) 21 | 22 | ## Usage 23 | DataPlotly works **only with QGIS 3**. No additional libraries are necessary. 24 | 25 | ## Gallery 26 | 27 | ### Single Plot 28 | 29 | Some examples of single plot type with some options. The list is far away to show all the possibilities. 30 | 31 | #### Scatter Plot 32 | ![Plot interactions](img/plot_scatter.png) 33 | 34 | #### Box Plot with statistics 35 | ![Plot interactions](img/plot_box.png) 36 | 37 | #### Violin Plot 38 | ![Plot interactions](img/plot_violin.png) 39 | 40 | #### Stacked Bar Plot 41 | ![Plot interactions](img/plot_bar_stack.png) 42 | 43 | #### Probability Histogram 44 | ![Plot interactions](img/plot_histogram.png) 45 | 46 | #### Pie Chart 47 | ![Plot interactions](img/plot_pie.png) 48 | 49 | #### 2D Histogram 50 | ![Plot interactions](img/plot_2dhistogram.png) 51 | 52 | #### Polar Plot 53 | ![Plot interactions](img/plot_polar.png) 54 | 55 | #### Ternary Plot 56 | ![Plot interactions](img/plot_ternary.png) 57 | 58 | #### Contour Plot 59 | ![Plot interactions](img/plot_contour.png) 60 | 61 | ### Multi Plots 62 | DataPloty allows creation of different plot type in the same *plot canvas* but also allows the chance to separate each plot in a different canvas. 63 | 64 | 67 | 68 | 69 | ## Overlapped Plots 70 | ![Plot interactions](img/plot_scatter_bar.png) 71 | 72 | ## Subplots in row 73 | ![Plot interactions](img/plot_histogram_violin.png) 74 | 75 | ## Subplots in column 76 | ![Plot interactions](img/plot_scatter_histogram.png) 77 | -------------------------------------------------------------------------------- /REQUIREMENTS_TESTING.txt: -------------------------------------------------------------------------------- 1 | # For tests execution: 2 | deepdiff 3 | mock 4 | flake8 5 | pep257 6 | plotly 7 | pylint 8 | pandas -------------------------------------------------------------------------------- /img/plot_2dhistogram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghtmtt/DataPlotly/f57b842703ebb259091b4490a9821518a14b01b9/img/plot_2dhistogram.png -------------------------------------------------------------------------------- /img/plot_bar_stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghtmtt/DataPlotly/f57b842703ebb259091b4490a9821518a14b01b9/img/plot_bar_stack.png -------------------------------------------------------------------------------- /img/plot_box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghtmtt/DataPlotly/f57b842703ebb259091b4490a9821518a14b01b9/img/plot_box.png -------------------------------------------------------------------------------- /img/plot_contour.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghtmtt/DataPlotly/f57b842703ebb259091b4490a9821518a14b01b9/img/plot_contour.png -------------------------------------------------------------------------------- /img/plot_histogram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghtmtt/DataPlotly/f57b842703ebb259091b4490a9821518a14b01b9/img/plot_histogram.png -------------------------------------------------------------------------------- /img/plot_histogram_violin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghtmtt/DataPlotly/f57b842703ebb259091b4490a9821518a14b01b9/img/plot_histogram_violin.png -------------------------------------------------------------------------------- /img/plot_interaction_box.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghtmtt/DataPlotly/f57b842703ebb259091b4490a9821518a14b01b9/img/plot_interaction_box.gif -------------------------------------------------------------------------------- /img/plot_interaction_scatter.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghtmtt/DataPlotly/f57b842703ebb259091b4490a9821518a14b01b9/img/plot_interaction_scatter.gif -------------------------------------------------------------------------------- /img/plot_interaction_scatter_box.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghtmtt/DataPlotly/f57b842703ebb259091b4490a9821518a14b01b9/img/plot_interaction_scatter_box.gif -------------------------------------------------------------------------------- /img/plot_pie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghtmtt/DataPlotly/f57b842703ebb259091b4490a9821518a14b01b9/img/plot_pie.png -------------------------------------------------------------------------------- /img/plot_polar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghtmtt/DataPlotly/f57b842703ebb259091b4490a9821518a14b01b9/img/plot_polar.png -------------------------------------------------------------------------------- /img/plot_scatter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghtmtt/DataPlotly/f57b842703ebb259091b4490a9821518a14b01b9/img/plot_scatter.png -------------------------------------------------------------------------------- /img/plot_scatter_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghtmtt/DataPlotly/f57b842703ebb259091b4490a9821518a14b01b9/img/plot_scatter_bar.png -------------------------------------------------------------------------------- /img/plot_scatter_histogram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghtmtt/DataPlotly/f57b842703ebb259091b4490a9821518a14b01b9/img/plot_scatter_histogram.png -------------------------------------------------------------------------------- /img/plot_ternary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghtmtt/DataPlotly/f57b842703ebb259091b4490a9821518a14b01b9/img/plot_ternary.png -------------------------------------------------------------------------------- /img/plot_violin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghtmtt/DataPlotly/f57b842703ebb259091b4490a9821518a14b01b9/img/plot_violin.png -------------------------------------------------------------------------------- /publiccode.yml: -------------------------------------------------------------------------------- 1 | # This repository adheres to the publiccode.yml standard by including this 2 | # metadata file that makes public software easily discoverable. 3 | # More info at https://github.com/italia/publiccode.yml 4 | 5 | publiccodeYmlVersion: '0.2' 6 | name: DataPlotly 7 | applicationSuite: QGIS 8 | url: 'https://github.com/ghtmtt/DataPlotly' 9 | landingURL: 'https://www.faunalia.eu/it/dev/dataplotly' 10 | softwareVersion: '3.7' 11 | releaseDate: '2020-05-20' 12 | logo: DataPlotly/icons/dataplotly.svg 13 | platforms: 14 | - linux 15 | - windows 16 | - mac 17 | categories: 18 | - geographic-information-systems 19 | - data-analytics 20 | - data-visualization 21 | developmentStatus: stable 22 | softwareType: addon 23 | dependsOn: 24 | open: 25 | - name: QGIS 26 | versionMin: '3.4' 27 | versionMax: '3.99' 28 | optional: no 29 | maintenance: 30 | type: internal 31 | contacts: 32 | - name: Matteo Ghetta 33 | email: matteo.ghetta@faunalia.eu 34 | affiliation: Faunalia 35 | legal: 36 | license: GPL-2.0 37 | mainCopyrightOwner: Matteo Ghetta 38 | repoOwner: Faunalia 39 | intendedAudience: 40 | scope: 41 | - science-and-technology 42 | - research 43 | - infrastructures 44 | - environment 45 | localisation: 46 | localisationReady: yes 47 | availableLanguages: 48 | - en 49 | - it 50 | - sv 51 | - nl 52 | - es 53 | description: 54 | en: 55 | localisedName: DataPlotly 56 | genericName: DataPlotly 57 | shortDescription: > 58 | DataPlotly is a QGIS plugin that allows the user to create interactive D3 59 | like charts based on vector data. 60 | longDescription: > 61 | With the DataPlotly plugin for QGIS the user can create fully customized 62 | and fully interactive D3 like charts withing QGIS itself. Currently 10 63 | different charts are available (scatter plot, pie chart, bar plot, polar 64 | plot, boxplot, contour plot, density chart, histogram, polar chart, violin 65 | plot, ternary plot). The plugin is also available in the Layout and Report 66 | composer in order to enrich the cartographic export as a dataviz like 67 | tool. Within 68 | the Layout and Report composer it is also available in Atlas mode. 69 | documentation: 'https://dataplotly-docs.readthedocs.io/en/latest/' 70 | features: 71 | - qgis 72 | - plotly 73 | - dataviz 74 | - python 75 | - charts 76 | screenshots: 77 | - img/plot_scatter.png 78 | it: 79 | localisedName: DataPlotly 80 | genericName: DataPlotly 81 | shortDescription: > 82 | DataPlotly è un plugin di QGIS che permette di creare grafici interattivi 83 | in stile D3 di dati vettoriali. 84 | longDescription: > 85 | Con il plugin DatapPlotly per QGIS l'utente può creare grafici 86 | completamente personalizzabili e interettivi all'interno di QGIS. Al 87 | momento sono disponibli 10 grafici diversi (diagramma a dispersione, 88 | grafico a torta, diagramma a barre, diagramma polare, grafici a scatola e 89 | baffi, grafici di densità, istogramma, grafico polare, grafico a violino, 90 | grafico ternario) con molte personalizzazioni disponibili. Il plugin è 91 | abilitato anche nel Compositore di Stampe, ovvero è in grado di arricchire 92 | l'esportazione cartografica con grafici, anche bastati su atlante. 93 | documentation: 'https://dataplotly-docs.readthedocs.io/en/latest/' 94 | features: 95 | - qgis 96 | - plotly 97 | - dataviz 98 | - python 99 | - charts 100 | -------------------------------------------------------------------------------- /pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # Specify a configuration file. 4 | #rcfile= 5 | 6 | # Python code to execute, usually for sys.path manipulation such as 7 | # pygtk.require(). 8 | #init-hook= 9 | 10 | # Profiled execution. 11 | # profile=no 12 | 13 | # Add files or directories to the blacklist. They should be base names, not 14 | # paths. 15 | ignore=CVS 16 | 17 | # Pickle collected data for later comparisons. 18 | persistent=yes 19 | 20 | # List of plugins (as comma separated values of python modules names) to load, 21 | # usually to register additional checkers. 22 | load-plugins=pylint.extensions.no_self_use 23 | 24 | 25 | [MESSAGES CONTROL] 26 | 27 | # Enable the message, report, category or checker with the given id(s). You can 28 | # either give multiple identifier separated by comma (,) or put this option 29 | # multiple time. See also the "--disable" option for examples. 30 | #enable= 31 | 32 | # Disable the message, report, category or checker with the given id(s). You 33 | # can either give multiple identifiers separated by comma (,) or put this 34 | # option multiple times (only on the command line, not in the configuration 35 | # file where it should appear only once).You can also use "--disable=all" to 36 | # disable everything first and then reenable specific checks. For example, if 37 | # you want to run only the similarities checker, you can use "--disable=all 38 | # --enable=similarities". If you want to run only the classes checker, but have 39 | # no Warning level messages displayed, use"--disable=all --enable=classes 40 | # --disable=W" 41 | # see http://stackoverflow.com/questions/21487025/pylint-locally-defined-disables-still-give-warnings-how-to-suppress-them 42 | disable=locally-disabled,C0103,C0301,E0611,W0511,W0107,R0801,import-error, 43 | # Wait for QGIS 3.18 minimum version for f-strings 44 | C0209, 45 | 46 | 47 | [REPORTS] 48 | 49 | # Set the output format. Available formats are text, parseable, colorized, msvs 50 | # (visual studio) and html. You can also give a reporter class, eg 51 | # mypackage.mymodule.MyReporterClass. 52 | output-format=text 53 | 54 | # Put messages in a separate file for each module / package specified on the 55 | # command line instead of printing them on stdout. Reports (if any) will be 56 | # written in a file name "pylint_global.[txt|html]". 57 | # files-output=no 58 | 59 | # Tells whether to display a full report or only the messages 60 | reports=yes 61 | 62 | # Python expression which should return a note less than 10 (10 is the highest 63 | # note). You have access to the variables errors warning, statement which 64 | # respectively contain the number of errors / warnings messages and the total 65 | # number of statements analyzed. This is used by the global evaluation report 66 | # (RP0004). 67 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 68 | 69 | # Add a comment according to your evaluation note. This is used by the global 70 | # evaluation report (RP0004). 71 | # comment=no 72 | 73 | # Template used to display messages. This is a python new-style format string 74 | # used to format the message information. See doc for all details 75 | #msg-template= 76 | 77 | 78 | [BASIC] 79 | 80 | # Required attributes for module, separated by a comma 81 | # required-attributes= 82 | 83 | # List of builtins function names that should not be used, separated by a comma 84 | # bad-functions=map,filter,apply,input 85 | 86 | # Regular expression which should only match correct module names 87 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 88 | 89 | # Regular expression which should only match correct module level names 90 | const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 91 | 92 | # Regular expression which should only match correct class names 93 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 94 | 95 | # Regular expression which should only match correct function names 96 | function-rgx=[a-z_][a-z0-9_]{2,30}$ 97 | 98 | # Regular expression which should only match correct method names 99 | method-rgx=[a-z_][a-z0-9_]{2,30}$ 100 | 101 | # Regular expression which should only match correct instance attribute names 102 | attr-rgx=[a-z_][a-z0-9_]{2,30}$ 103 | 104 | # Regular expression which should only match correct argument names 105 | argument-rgx=[a-z_][a-z0-9_]{2,30}$ 106 | 107 | # Regular expression which should only match correct variable names 108 | variable-rgx=[a-z_][a-z0-9_]{2,30}$ 109 | 110 | # Regular expression which should only match correct attribute names in class 111 | # bodies 112 | class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 113 | 114 | # Regular expression which should only match correct list comprehension / 115 | # generator expression variable names 116 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 117 | 118 | # Good variable names which should always be accepted, separated by a comma 119 | good-names=i,j,k,ex,Run,_ 120 | 121 | # Bad variable names which should always be refused, separated by a comma 122 | bad-names=foo,bar,baz,toto,tutu,tata 123 | 124 | # Regular expression which should only match function or class names that do 125 | # not require a docstring. 126 | no-docstring-rgx=__.*__ 127 | 128 | # Minimum line length for functions/classes that require docstrings, shorter 129 | # ones are exempt. 130 | docstring-min-length=-1 131 | 132 | 133 | [MISCELLANEOUS] 134 | 135 | # List of note tags to take in consideration, separated by a comma. 136 | notes=FIXME,XXX,TODO 137 | 138 | 139 | [TYPECHECK] 140 | 141 | # Tells whether missing members accessed in mixin class should be ignored. A 142 | # mixin class is detected if its name ends with "mixin" (case insensitive). 143 | ignore-mixin-members=yes 144 | 145 | # List of classes names for which member attributes should not be checked 146 | # (useful for classes with attributes dynamically set). 147 | ignored-classes=SQLObject 148 | 149 | # When zope mode is activated, add a predefined set of Zope acquired attributes 150 | # to generated-members. 151 | # zope=no 152 | 153 | # List of members which are set dynamically and missed by pylint inference 154 | # system, and so shouldn't trigger E0201 when accessed. Python regular 155 | # expressions are accepted. 156 | generated-members=REQUEST,acl_users,aq_parent 157 | 158 | 159 | [VARIABLES] 160 | 161 | # Tells whether we should check for unused import in __init__ files. 162 | init-import=no 163 | 164 | # A regular expression matching the beginning of the name of dummy variables 165 | # (i.e. not used). 166 | dummy-variables-rgx=_$|dummy 167 | 168 | # List of additional names supposed to be defined in builtins. Remember that 169 | # you should avoid to define new builtins when possible. 170 | additional-builtins= 171 | 172 | 173 | [FORMAT] 174 | 175 | # Maximum number of characters on a single line. 176 | max-line-length=80 177 | 178 | # Regexp for a line that is allowed to be longer than the limit. 179 | ignore-long-lines=^\s*(# )??$ 180 | 181 | # Allow the body of an if to be on the same line as the test if there is no 182 | # else. 183 | single-line-if-stmt=no 184 | 185 | # List of optional constructs for which whitespace checking is disabled 186 | # no-space-check=trailing-comma,dict-separator 187 | 188 | # Maximum number of lines in a module 189 | max-module-lines=1000 190 | 191 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 192 | # tab). 193 | indent-string=' ' 194 | 195 | 196 | [SIMILARITIES] 197 | 198 | # Minimum lines number of a similarity. 199 | min-similarity-lines=4 200 | 201 | # Ignore comments when computing similarities. 202 | ignore-comments=yes 203 | 204 | # Ignore docstrings when computing similarities. 205 | ignore-docstrings=yes 206 | 207 | # Ignore imports when computing similarities. 208 | ignore-imports=no 209 | 210 | 211 | [IMPORTS] 212 | 213 | # Deprecated modules which should not be used, separated by a comma 214 | deprecated-modules=regsub,TERMIOS,Bastion,rexec 215 | 216 | # Create a graph of every (i.e. internal and external) dependencies in the 217 | # given file (report RP0402 must not be disabled) 218 | import-graph= 219 | 220 | # Create a graph of external dependencies in the given file (report RP0402 must 221 | # not be disabled) 222 | ext-import-graph= 223 | 224 | # Create a graph of internal dependencies in the given file (report RP0402 must 225 | # not be disabled) 226 | int-import-graph= 227 | 228 | 229 | [DESIGN] 230 | 231 | # Maximum number of arguments for function / method 232 | max-args=5 233 | 234 | # Argument names that match this expression will be ignored. Default to name 235 | # with leading underscore 236 | ignored-argument-names=_.* 237 | 238 | # Maximum number of locals for function / method body 239 | max-locals=15 240 | 241 | # Maximum number of return / yield for function / method body 242 | max-returns=6 243 | 244 | # Maximum number of branch for function / method body 245 | max-branches=12 246 | 247 | # Maximum number of statements in function / method body 248 | max-statements=50 249 | 250 | # Maximum number of parents for a class (see R0901). 251 | max-parents=7 252 | 253 | # Maximum number of attributes for a class (see R0902). 254 | max-attributes=7 255 | 256 | # Minimum number of public methods for a class (see R0903). 257 | min-public-methods=2 258 | 259 | # Maximum number of public methods for a class (see R0904). 260 | max-public-methods=20 261 | 262 | 263 | [CLASSES] 264 | 265 | # List of interface methods to ignore, separated by a comma. This is used for 266 | # instance to not check methods defines in Zope's Interface base class. 267 | # ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by 268 | 269 | # List of method names used to declare (i.e. assign) instance attributes. 270 | defining-attr-methods=__init__,__new__,setUp 271 | 272 | # List of valid names for the first argument in a class method. 273 | valid-classmethod-first-arg=cls 274 | 275 | # List of valid names for the first argument in a metaclass class method. 276 | valid-metaclass-classmethod-first-arg=mcs 277 | 278 | 279 | [EXCEPTIONS] 280 | 281 | # Exceptions that will emit a warning when being caught. Defaults to 282 | # "Exception" 283 | overgeneral-exceptions=Exception 284 | -------------------------------------------------------------------------------- /scripts/run-env-linux.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | QGIS_PREFIX_PATH=/usr/local/qgis-3.0 4 | if [ -n "$1" ]; then 5 | QGIS_PREFIX_PATH=$1 6 | fi 7 | 8 | echo ${QGIS_PREFIX_PATH} 9 | 10 | 11 | export QGIS_PREFIX_PATH=${QGIS_PREFIX_PATH} 12 | export QGIS_PATH=${QGIS_PREFIX_PATH} 13 | export LD_LIBRARY_PATH=${QGIS_PREFIX_PATH}/lib 14 | export PYTHONPATH=${QGIS_PREFIX_PATH}/share/qgis/python:${QGIS_PREFIX_PATH}/python:${QGIS_PREFIX_PATH}/python/plugins:${PYTHONPATH} 15 | 16 | echo "QGIS PATH: $QGIS_PREFIX_PATH" 17 | export QGIS_DEBUG=0 18 | export QGIS_LOG_FILE=/tmp/dataplotly/realtime/logs/qgis.log 19 | 20 | export PATH=${QGIS_PREFIX_PATH}/bin:$PATH 21 | 22 | echo "This script is intended to be sourced to set up your shell to" 23 | echo "use a QGIS 3.0 built in $QGIS_PREFIX_PATH" 24 | echo 25 | echo "To use it do:" 26 | echo "source $BASH_SOURCE /your/optional/install/path" 27 | echo 28 | echo "Then use the make file supplied here e.g. make guitest" 29 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [qgis-plugin-ci] 2 | plugin_path = DataPlotly 3 | github_organization_slug = ghtmtt 4 | project_slug = DataPlotly 5 | transifex_organization = dataplotly 6 | transifex_project = dataplotly-ui 7 | transifex_resource = application 8 | transifex_coordinator = ghtmtt 9 | 10 | [flake8] 11 | exclude = 12 | .venv/, 13 | --------------------------------------------------------------------------------