From 98957db23d00b426b04609cb1af34b4c20fab246 Mon Sep 17 00:00:00 2001 From: Matteo Ghetta Date: Thu, 16 Jul 2026 09:26:14 +0200 Subject: [PATCH 1/8] adapt for QGIS 4 enums --- DataPlotly/processing/dataplotly_provider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DataPlotly/processing/dataplotly_provider.py b/DataPlotly/processing/dataplotly_provider.py index fa9f8221..4783f9da 100644 --- a/DataPlotly/processing/dataplotly_provider.py +++ b/DataPlotly/processing/dataplotly_provider.py @@ -31,7 +31,7 @@ WITH_PANDAS = False QgsMessageLog.logMessage( "Pandas has not been found. The processing algorithm will not be loaded. " - "Please install qgis-full or qgis standalone", "DataPlotly", Qgis.Warning) + "Please install qgis-full or qgis standalone", "DataPlotly", Qgis.MessageLevel.Warning) class DataPlotlyProvider(QgsProcessingProvider): From a2d030fd2345751645689965bbf02ce0ab235a1b Mon Sep 17 00:00:00 2001 From: Matteo Ghetta Date: Mon, 20 Jul 2026 11:32:13 +0200 Subject: [PATCH 2/8] add 3D scatterplot to Processing interface --- .../core/plot_types/icons/scatter3d.svg | 2727 +++++++++++++++++ .../processing/dataplotly_generic_plot.py | 57 +- DataPlotly/processing/dataplotly_provider.py | 2 + DataPlotly/processing/dataplotly_scatter3d.py | 64 + 4 files changed, 2836 insertions(+), 14 deletions(-) create mode 100644 DataPlotly/core/plot_types/icons/scatter3d.svg create mode 100644 DataPlotly/processing/dataplotly_scatter3d.py diff --git a/DataPlotly/core/plot_types/icons/scatter3d.svg b/DataPlotly/core/plot_types/icons/scatter3d.svg new file mode 100644 index 00000000..b3237c6f --- /dev/null +++ b/DataPlotly/core/plot_types/icons/scatter3d.svg @@ -0,0 +1,2727 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DataPlotly/processing/dataplotly_generic_plot.py b/DataPlotly/processing/dataplotly_generic_plot.py index fbc4ab19..a180f71b 100644 --- a/DataPlotly/processing/dataplotly_generic_plot.py +++ b/DataPlotly/processing/dataplotly_generic_plot.py @@ -52,6 +52,7 @@ class DataPlotlyProcessingPlot(QgsProcessingAlgorithm): INPUT = "INPUT" XEXPRESSION = "XEXPRESSION" YEXPRESSION = "YEXPRESSION" + ZEXPRESSION = "ZEXPRESSION" OFFLINE = "OFFLINE" COLOR = "COLOR" SIZE = "SIZE" @@ -117,6 +118,11 @@ def create_parameter_dictionary(self, plot_type): self.tr("Y Field"), parentLayerParameterName=self.INPUT, ) + z_field = QgsProcessingParameterExpression( + self.ZEXPRESSION, + self.tr("Z Field"), + parentLayerParameterName=self.INPUT, + ) offline_param = QgsProcessingParameterBoolean( self.OFFLINE, self.tr("Complete offline usage"), defaultValue=False @@ -125,7 +131,7 @@ def create_parameter_dictionary(self, plot_type): output_html = QgsProcessingParameterFileDestination( self.OUTPUT_HTML_FILE, - self.tr("Scatter Plot"), + self.tr(f"{self.plot_type.title()} Plot"), self.tr("HTML files (*.html)"), ) @@ -195,17 +201,19 @@ def create_parameter_dictionary(self, plot_type): plot_types_dict[plot_type].append(input_layer) plot_types_dict[plot_type].append(x_field) plot_types_dict[plot_type].append(y_field) + if plot_type in ("scatter_3d",): + plot_types_dict[plot_type].append(z_field) + plot_types_dict[plot_type].append(color_param) + if plot_type in ("scatter",): + plot_types_dict[plot_type].append(size_param) plot_types_dict[plot_type].append(offline_param) plot_types_dict[plot_type].append(output_html) plot_types_dict[plot_type].append(output_json) - plot_types_dict[plot_type].append(color_param) - plot_types_dict[plot_type].append(facet_row) - plot_types_dict[plot_type].append(facet_col) - plot_types_dict[plot_type].append(show_legend) + if self.plot_type in ('scatter', 'bar'): + plot_types_dict[plot_type].append(facet_row) + plot_types_dict[plot_type].append(facet_col) + plot_types_dict[plot_type].append(show_legend) - # add the parameter depending on the plot type - if plot_type in ("scatter"): - plot_types_dict[plot_type].append(size_param) return plot_types_dict[plot_type] @@ -276,6 +284,14 @@ def processAlgorithm(self, parameters, context, feedback): y_expression.prepare(expressionContext) raise QgsProcessingException(y_expression.parserErrorString()) + if parameters.get(self.ZEXPRESSION): + z_expression = self.parameterAsString(parameters, self.ZEXPRESSION, context) + z_expression = QgsExpression(z_expression) + + if z_expression.hasParserError(): + z_expression.prepare(expressionContext) + raise QgsProcessingException(z_expression.parserErrorString()) + size = self.parameterAsDouble(parameters, self.SIZE, context) size_property = None if QgsProcessingParameters.isDynamic(parameters, "SIZE"): @@ -301,7 +317,7 @@ def processAlgorithm(self, parameters, context, feedback): raise QgsProcessingException(facet_col_expression.parserErrorString()) offline = self.parameterAsBool(parameters, self.OFFLINE, context) - if offline is not True: + if not offline: offline = "cdn" show_legend = self.parameterAsBool(parameters, self.SHOW_LEGEND, context) @@ -316,7 +332,10 @@ def processAlgorithm(self, parameters, context, feedback): ) # start building the object to create the pandas dataframe - colnames = ["x", "y", "customdata"] + colnames = ["x", "y"] + if self.plot_type in ('scatter_3d',): + colnames.append('z') + colnames.append("customdata") data = [] request = QgsFeatureRequest() @@ -337,6 +356,11 @@ def processAlgorithm(self, parameters, context, feedback): tl.append(x_val) tl.append(y_val) + + if self.plot_type in ('scatter_3d',): + z_val = z_expression.evaluate(expressionContext) + tl.append(z_val) + tl.append(ids) if facet_row: @@ -380,24 +404,29 @@ def processAlgorithm(self, parameters, context, feedback): plot_params = { "x": "x", "y": "y", - "color": "color" if color_property else None, - "facet_row": "facet_row" if facet_row else None, - "facet_col": "facet_col" if facet_col else None, + "color": "color" if color_property else None } # initialize the updating dictionary (different depending on the plot parameters) fig_update_params = defaultdict(dict) # only if scatter - if self.plot_type in ('scatter'): + if self.plot_type in ('scatter',): if size_property: plot_params["size"] = "size" else: fig_update_params[self.plot_type]["marker_size"] = size + if self.plot_type in ('scatter_3d',): + plot_params['z'] = 'z' + if color_property is None: fig_update_params[self.plot_type]["marker_color"] = color.name() + if self.plot_type in ('scatter', 'bar'): + plot_params["facet_row"] = "facet_row" if facet_row else None + plot_params["facet_col"] = "facet_col" if facet_col else None + # call the methods to create and update the figure fig = self.create_plot(self.plot_type, df, plot_params) fig = self.update_plot(fig, fig_update_params) diff --git a/DataPlotly/processing/dataplotly_provider.py b/DataPlotly/processing/dataplotly_provider.py index 4783f9da..8d6055e2 100644 --- a/DataPlotly/processing/dataplotly_provider.py +++ b/DataPlotly/processing/dataplotly_provider.py @@ -26,6 +26,7 @@ # 🐼 from DataPlotly.processing.dataplotly_scatterplot import DataPlotlyProcessingScatterPlot from DataPlotly.processing.dataplotly_barplot import DataPlotlyProcessingBarPlot + from DataPlotly.processing.dataplotly_scatter3d import DataPlotlyProcessingScatter3D WITH_PANDAS = True except ImportError: WITH_PANDAS = False @@ -89,3 +90,4 @@ def loadAlgorithms(self): if WITH_PANDAS: self.addAlgorithm(DataPlotlyProcessingScatterPlot()) self.addAlgorithm(DataPlotlyProcessingBarPlot()) + self.addAlgorithm(DataPlotlyProcessingScatter3D()) diff --git a/DataPlotly/processing/dataplotly_scatter3d.py b/DataPlotly/processing/dataplotly_scatter3d.py new file mode 100644 index 00000000..6b2e54f0 --- /dev/null +++ b/DataPlotly/processing/dataplotly_scatter3d.py @@ -0,0 +1,64 @@ +""" +/*************************************************************************** + DataPlotly + A QGIS plugin + D3 Plots for QGIS + ------------------- + begin : 2024-10-29 + git sha : $Format:%H$ + copyright : (C) 2024 by matteo ghetta + email : matteo.ghetta@gmail.com + ***************************************************************************/ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +""" + +import os + +from DataPlotly.processing.dataplotly_generic_plot import DataPlotlyProcessingPlot +from qgis.PyQt.QtGui import QIcon + + +class DataPlotlyProcessingScatter3D(DataPlotlyProcessingPlot): + """ + Create a bar with DataPlotly plugin + """ + + def __init__(self): + super().__init__(plot_type="scatter_3d") + + def name(self): + return "scatter3d" + + def displayName(self): + return "Scatter 3D Plot" + + def icon(self): + return QIcon( + os.path.join( + os.path.dirname(__file__), + "..", + "core", + "plot_types", + "icons", + "scatter3d.svg", + ) + ) + + def createInstance(self): + return DataPlotlyProcessingScatter3D() + + def initAlgorithm(self, config=None): + + # create the parameters list + parameters = self.create_parameter_dictionary(self.plot_type) + + # loop and fill the parameters + for param in parameters: + self.addParameter(param) From ff7a84cea508afea461bef34eb4a3e5b7c14c7ab Mon Sep 17 00:00:00 2001 From: Matteo Ghetta Date: Mon, 20 Jul 2026 11:32:37 +0200 Subject: [PATCH 3/8] add pandas to the test requirements --- requirements/tests.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements/tests.txt b/requirements/tests.txt index 24a25c26..96cb4c13 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -11,3 +11,4 @@ pytest-qgis @ git+https://github.com/3liz/pytest-qgis.git@555b184158a58f6922e0df semver==3.0.4 tomli==2.3.0 ; python_full_version < '3.11' typing-extensions==4.15.0 ; python_full_version < '3.11' +pandas \ No newline at end of file From 684dc567dc0fcbcda3da34720d03e79a6748a88f Mon Sep 17 00:00:00 2001 From: Matteo Ghetta Date: Mon, 20 Jul 2026 11:32:53 +0200 Subject: [PATCH 4/8] add some more processing test --- tests/test_processing.py | 51 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/tests/test_processing.py b/tests/test_processing.py index d25e6e44..2c3d900e 100644 --- a/tests/test_processing.py +++ b/tests/test_processing.py @@ -20,6 +20,8 @@ def decode_array_1d(spec: dict) -> array.array: + if isinstance(spec, list): + return spec binary = base64.decodebytes(spec["bdata"].encode()) match spec["dtype"]: case "i2": @@ -47,10 +49,8 @@ def reportError(self, msg: str, fatalError: bool = False): context = QgsProcessingContext() context.setTemporaryFolder(str(output_dir)) - return - result = processing.run( - "DataPlotly:dataplotly_scatterplot", + "DataPlotly:scatterplot", { "INPUT": vl, "XEXPRESSION": '"so4"', @@ -58,8 +58,9 @@ def reportError(self, msg: str, fatalError: bool = False): "SIZE": 10, "COLOR": QColor(142, 186, 217), "FACET_COL": "", - "FACET_ROW": "", "OFFLINE": False, + "SHOW_LEGEND": True, + "FACET_ROW": "", "OUTPUT_HTML_FILE": "TEMPORARY_OUTPUT", "OUTPUT_JSON_FILE": "TEMPORARY_OUTPUT", }, @@ -75,3 +76,45 @@ def reportError(self, msg: str, fatalError: bool = False): y = decode_array_1d(result_dict["data"][0]["y"]) assert y.tolist() == [81.87, 22.26, 74.16, 35.05, 46.64, 126.73, 116.44, 108.25, 110.45] + +def test_barplot_figure(data: Path, output_dir: Path): + """Test for the Processing scatterplot""" + from qgis import processing + + class Feedback(QgsProcessingFeedback): + def reportError(self, msg: str, fatalError: bool = False): + print("\n::test_scatterplot_figure::error", msg) + + layer_path = data.joinpath("test_layer.shp") + + vl = QgsVectorLayer(str(layer_path), "test_layer", "ogr") + + context = QgsProcessingContext() + context.setTemporaryFolder(str(output_dir)) + + result = processing.run( + "DataPlotly:barplot", + { + "INPUT": vl, + "XEXPRESSION": '"profo"', + "YEXPRESSION": '"ca"', + "COLOR": QColor(142, 186, 217), + "FACET_COL": "", + "FACET_ROW": "", + "OFFLINE": False, + "SHOW_LEGEND": True, + "OUTPUT_HTML_FILE": "TEMPORARY_OUTPUT", + "OUTPUT_JSON_FILE": "TEMPORARY_OUTPUT", + }, + context=context, + feedback=Feedback(), + ) + + with open(result["OUTPUT_JSON_FILE"]) as f: + result_dict = json.load(f) + + x = decode_array_1d(result_dict["data"][0]["x"]) + assert x == ['s', 'd', 's', 's', 'd', 's', 'd', 's', 'd'] + + y = decode_array_1d(result_dict["data"][0]["y"]) + assert y.tolist() == [81.87, 22.26, 74.16, 35.05, 46.64, 126.73, 116.44, 108.25, 110.45] From 1c03066611663a039c1dac34433efe482bca73a0 Mon Sep 17 00:00:00 2001 From: Matteo Ghetta Date: Mon, 20 Jul 2026 11:37:23 +0200 Subject: [PATCH 5/8] local vs remote tests --- tests/test_processing.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_processing.py b/tests/test_processing.py index 2c3d900e..e598c5dc 100644 --- a/tests/test_processing.py +++ b/tests/test_processing.py @@ -72,10 +72,10 @@ def reportError(self, msg: str, fatalError: bool = False): result_dict = json.load(f) x = decode_array_1d(result_dict["data"][0]["x"]) - assert x.tolist() == [98, 88, 267, 329, 319, 137, 350, 151, 203] + assert x == [98, 88, 267, 329, 319, 137, 350, 151, 203] y = decode_array_1d(result_dict["data"][0]["y"]) - assert y.tolist() == [81.87, 22.26, 74.16, 35.05, 46.64, 126.73, 116.44, 108.25, 110.45] + assert y == [81.87, 22.26, 74.16, 35.05, 46.64, 126.73, 116.44, 108.25, 110.45] def test_barplot_figure(data: Path, output_dir: Path): """Test for the Processing scatterplot""" @@ -117,4 +117,4 @@ def reportError(self, msg: str, fatalError: bool = False): assert x == ['s', 'd', 's', 's', 'd', 's', 'd', 's', 'd'] y = decode_array_1d(result_dict["data"][0]["y"]) - assert y.tolist() == [81.87, 22.26, 74.16, 35.05, 46.64, 126.73, 116.44, 108.25, 110.45] + assert y == [81.87, 22.26, 74.16, 35.05, 46.64, 126.73, 116.44, 108.25, 110.45] From bd42fbf6bc6b85b7f9ae4001487ffb9ffdc39b88 Mon Sep 17 00:00:00 2001 From: Matteo Ghetta Date: Mon, 20 Jul 2026 11:49:45 +0200 Subject: [PATCH 6/8] typo --- tests/test_processing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_processing.py b/tests/test_processing.py index e598c5dc..f5c9bf63 100644 --- a/tests/test_processing.py +++ b/tests/test_processing.py @@ -83,7 +83,7 @@ def test_barplot_figure(data: Path, output_dir: Path): class Feedback(QgsProcessingFeedback): def reportError(self, msg: str, fatalError: bool = False): - print("\n::test_scatterplot_figure::error", msg) + print("\n::test_barplot_figure::error", msg) layer_path = data.joinpath("test_layer.shp") From b2b2d8b9a72191a1ed599acccc862891c03be157 Mon Sep 17 00:00:00 2001 From: Matteo Ghetta Date: Mon, 20 Jul 2026 14:26:05 +0200 Subject: [PATCH 7/8] installing git for qgis 3.44 --- .docker/run-tests.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.docker/run-tests.sh b/.docker/run-tests.sh index 07e37c58..72df4ce1 100755 --- a/.docker/run-tests.sh +++ b/.docker/run-tests.sh @@ -5,6 +5,8 @@ set -e +apt-get update && apt-get install -y git + cd /src VENV=/src/.docker-venv-$QGIS_VERSION From 2ac45b90f4ac20fdc4214855740e98bcd307418b Mon Sep 17 00:00:00 2001 From: Matteo Ghetta Date: Mon, 20 Jul 2026 14:49:01 +0200 Subject: [PATCH 8/8] revert git installation --- .docker/run-tests.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/.docker/run-tests.sh b/.docker/run-tests.sh index 72df4ce1..07e37c58 100755 --- a/.docker/run-tests.sh +++ b/.docker/run-tests.sh @@ -5,8 +5,6 @@ set -e -apt-get update && apt-get install -y git - cd /src VENV=/src/.docker-venv-$QGIS_VERSION