From c9a13e67be80e9ae98208213af993ff3af8d0b1d Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Wed, 20 Aug 2025 16:12:00 +0700 Subject: [PATCH] Add test coverage for new classes --- src/core/plot/qgsbarchartplot.cpp | 3 +- tests/src/python/CMakeLists.txt | 1 + tests/src/python/test_qgsplot.py | 496 +++++++++++++++++- tests/src/python/test_qgsplotregistry.py | 72 +++ ...xpected_bar_chart_plot_x_axis_category.png | Bin 0 -> 5779 bytes ...ed_bar_chart_plot_x_axis_category_mask.png | Bin 0 -> 3582 bytes .../expected_bar_chart_plot_x_axis_value.png | Bin 0 -> 7351 bytes ...pected_line_chart_plot_x_axis_category.png | Bin 0 -> 7373 bytes .../expected_line_chart_plot_x_axis_value.png | Bin 0 -> 10859 bytes 9 files changed, 553 insertions(+), 19 deletions(-) create mode 100644 tests/src/python/test_qgsplotregistry.py create mode 100644 tests/testdata/control_images/plot/expected_bar_chart_plot_x_axis_category/expected_bar_chart_plot_x_axis_category.png create mode 100644 tests/testdata/control_images/plot/expected_bar_chart_plot_x_axis_category/expected_bar_chart_plot_x_axis_category_mask.png create mode 100644 tests/testdata/control_images/plot/expected_bar_chart_plot_x_axis_value/expected_bar_chart_plot_x_axis_value.png create mode 100644 tests/testdata/control_images/plot/expected_line_chart_plot_x_axis_category/expected_line_chart_plot_x_axis_category.png create mode 100644 tests/testdata/control_images/plot/expected_line_chart_plot_x_axis_value/expected_line_chart_plot_x_axis_value.png diff --git a/src/core/plot/qgsbarchartplot.cpp b/src/core/plot/qgsbarchartplot.cpp index e147863a9df..97442c85256 100644 --- a/src/core/plot/qgsbarchartplot.cpp +++ b/src/core/plot/qgsbarchartplot.cpp @@ -58,7 +58,8 @@ void QgsBarChartPlot::renderContent( QgsRenderContext &context, QgsPlotRenderCon const double xScale = plotArea.width() / ( xMaximum() - xMinimum() ); const double yScale = plotArea.height() / ( yMaximum() - yMinimum() ); const double categoriesWidth = plotArea.width() / categories.size(); - const double barsWidth = categoriesWidth / 2; + const double valuesWidth = plotArea.width() * ( xAxis().gridIntervalMinor() / ( xMaximum() - xMinimum() ) ); + const double barsWidth = xAxis().type() == Qgis::PlotAxisType::CategoryType ? categoriesWidth / 2 : valuesWidth / 2; const double barWidth = barsWidth / seriesList.size(); int seriesIndex = 0; for ( const QgsAbstractPlotSeries *series : seriesList ) diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index 7f776860d76..d68d0969cf7 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -234,6 +234,7 @@ ADD_PYTHON_TEST(PyQgsPalLabelingPlacement test_qgspallabeling_placement.py) ADD_PYTHON_TEST(PyQgsPathResolver test_qgspathresolver.py) ADD_PYTHON_TEST(PyQgsPercentageWidget test_qgspercentagewidget.py) ADD_PYTHON_TEST(PyQgsPlot test_qgsplot.py) +ADD_PYTHON_TEST(PyQgsPlotRegistry test_qgsplotregistry.py) ADD_PYTHON_TEST(PyQgsPoint test_qgspoint.py) ADD_PYTHON_TEST(PyQgsPointCloudAttributeByRampRenderer test_qgspointcloudattributebyramprenderer.py) ADD_PYTHON_TEST(PyQgsPointCloudAttributeModel test_qgspointcloudattributemodel.py) diff --git a/tests/src/python/test_qgsplot.py b/tests/src/python/test_qgsplot.py index 5706c7edb35..68ffc8d18bc 100644 --- a/tests/src/python/test_qgsplot.py +++ b/tests/src/python/test_qgsplot.py @@ -15,16 +15,22 @@ from qgis.PyQt.QtGui import QColor, QImage, QPainter from qgis.PyQt.QtXml import QDomDocument from qgis.core import ( Qgs2DXyPlot, + QgsBarChartPlot, QgsBasicNumericFormat, QgsFillSymbol, QgsFontUtils, + QgsLineChartPlot, QgsLineSymbol, QgsPalLayerSettings, + QgsPlotData, + QgsPlotRenderContext, QgsProperty, + QgsMarkerSymbol, QgsReadWriteContext, QgsRenderContext, QgsSymbolLayer, QgsTextFormat, + QgsXyPlotSeries, Qgis, ) import unittest @@ -108,12 +114,13 @@ class TestQgsPlot(QgisTestCase): painter = QPainter(im) rc = QgsRenderContext.fromQPainter(painter) - plot.render(rc) + prc = QgsPlotRenderContext() + plot.render(rc, prc) painter.end() assert self.image_check("plot_2d_base", "plot_2d_base", im) - plot_rect = plot.interiorPlotArea(rc) + plot_rect = plot.interiorPlotArea(rc, prc) self.assertAlmostEqual(plot_rect.left(), 64.8, 0) self.assertAlmostEqual(plot_rect.right(), 592.44, 0) self.assertAlmostEqual(plot_rect.top(), 7.559, 0) @@ -193,14 +200,15 @@ class TestQgsPlot(QgisTestCase): painter = QPainter(im) rc = QgsRenderContext.fromQPainter(painter) - plot.render(rc) + prc = QgsPlotRenderContext() + plot.render(rc, prc) painter.end() assert self.image_check( "plot_2d_base_suffix_all", "plot_2d_base_suffix_all", im ) - plot_rect = plot.interiorPlotArea(rc) + plot_rect = plot.interiorPlotArea(rc, prc) self.assertAlmostEqual(plot_rect.left(), 80.46, 0) self.assertAlmostEqual(plot_rect.right(), 592.44, 0) self.assertAlmostEqual(plot_rect.top(), 7.559, 0) @@ -280,14 +288,15 @@ class TestQgsPlot(QgisTestCase): painter = QPainter(im) rc = QgsRenderContext.fromQPainter(painter) - plot.render(rc) + prc = QgsPlotRenderContext() + plot.render(rc, prc) painter.end() assert self.image_check( "plot_2d_base_suffix_first", "plot_2d_base_suffix_first", im ) - plot_rect = plot.interiorPlotArea(rc) + plot_rect = plot.interiorPlotArea(rc, prc) self.assertAlmostEqual(plot_rect.left(), 64.82, 0) self.assertAlmostEqual(plot_rect.right(), 592.44, 0) self.assertAlmostEqual(plot_rect.top(), 7.559, 0) @@ -367,14 +376,15 @@ class TestQgsPlot(QgisTestCase): painter = QPainter(im) rc = QgsRenderContext.fromQPainter(painter) - plot.render(rc) + prc = QgsPlotRenderContext() + plot.render(rc, prc) painter.end() assert self.image_check( "plot_2d_base_suffix_last", "plot_2d_base_suffix_last", im ) - plot_rect = plot.interiorPlotArea(rc) + plot_rect = plot.interiorPlotArea(rc, prc) self.assertAlmostEqual(plot_rect.left(), 80.46, 0) self.assertAlmostEqual(plot_rect.right(), 592.44, 0) self.assertAlmostEqual(plot_rect.top(), 7.559, 0) @@ -458,7 +468,8 @@ class TestQgsPlot(QgisTestCase): painter = QPainter(im) rc = QgsRenderContext.fromQPainter(painter) - plot.render(rc) + prc = QgsPlotRenderContext() + plot.render(rc, prc) painter.end() assert self.image_check( @@ -467,7 +478,7 @@ class TestQgsPlot(QgisTestCase): im, ) - plot_rect = plot.interiorPlotArea(rc) + plot_rect = plot.interiorPlotArea(rc, prc) self.assertAlmostEqual(plot_rect.left(), 80.46, 0) self.assertAlmostEqual(plot_rect.right(), 592.44, 0) self.assertAlmostEqual(plot_rect.top(), 7.559, 0) @@ -540,7 +551,8 @@ class TestQgsPlot(QgisTestCase): painter = QPainter(im) rc = QgsRenderContext.fromQPainter(painter) - plot.render(rc) + prc = QgsPlotRenderContext() + plot.render(rc, prc) painter.end() assert self.image_check("plot_2d_intervals", "plot_2d_intervals", im) @@ -639,12 +651,13 @@ class TestQgsPlot(QgisTestCase): painter = QPainter(im) rc = QgsRenderContext.fromQPainter(painter) - plot.render(rc) + prc = QgsPlotRenderContext() + plot.render(rc, prc) painter.end() assert self.image_check("plot_2d_data_defined", "plot_2d_data_defined", im) - plot_rect = plot.interiorPlotArea(rc) + plot_rect = plot.interiorPlotArea(rc, prc) self.assertAlmostEqual(plot_rect.left(), 44.71, 0) self.assertAlmostEqual(plot_rect.right(), 592.44, 0) self.assertAlmostEqual(plot_rect.top(), 7.559, 0) @@ -674,9 +687,10 @@ class TestQgsPlot(QgisTestCase): painter = QPainter(im) rc = QgsRenderContext.fromQPainter(painter) + prc = QgsPlotRenderContext() painter.end() - plot.calculateOptimisedIntervals(rc) + plot.calculateOptimisedIntervals(rc, prc) self.assertEqual(plot.xAxis().labelInterval(), 1) self.assertEqual(plot.yAxis().labelInterval(), 2) self.assertEqual(plot.xAxis().gridIntervalMinor(), 1) @@ -689,7 +703,7 @@ class TestQgsPlot(QgisTestCase): plot.setYMinimum(2) plot.setYMaximum(112) - plot.calculateOptimisedIntervals(rc) + plot.calculateOptimisedIntervals(rc, prc) self.assertEqual(plot.xAxis().labelInterval(), 20) self.assertEqual(plot.yAxis().labelInterval(), 20) self.assertEqual(plot.xAxis().gridIntervalMinor(), 10) @@ -702,7 +716,7 @@ class TestQgsPlot(QgisTestCase): plot.setYMinimum(1.1) plot.setYMaximum(2) - plot.calculateOptimisedIntervals(rc) + plot.calculateOptimisedIntervals(rc, prc) self.assertEqual(plot.xAxis().labelInterval(), 0.05) self.assertEqual(plot.yAxis().labelInterval(), 0.2) self.assertEqual(plot.xAxis().gridIntervalMinor(), 0.025) @@ -715,7 +729,7 @@ class TestQgsPlot(QgisTestCase): plot.setYMinimum(-10000) plot.setYMaximum(-500) - plot.calculateOptimisedIntervals(rc) + plot.calculateOptimisedIntervals(rc, prc) self.assertEqual(plot.xAxis().labelInterval(), 2) self.assertEqual(plot.yAxis().labelInterval(), 2000) self.assertEqual(plot.xAxis().gridIntervalMinor(), 1) @@ -726,7 +740,7 @@ class TestQgsPlot(QgisTestCase): plot.setXMinimum(100000) plot.setXMaximum(200000) - plot.calculateOptimisedIntervals(rc) + plot.calculateOptimisedIntervals(rc, prc) self.assertEqual(plot.xAxis().labelInterval(), 100000) self.assertEqual(plot.xAxis().gridIntervalMinor(), 50000) self.assertEqual(plot.xAxis().gridIntervalMajor(), 200000) @@ -852,6 +866,452 @@ class TestQgsPlot(QgisTestCase): Qgis.PlotAxisSuffixPlacement.FirstAndLastLabels, ) + def testBarChartPlotXAxisCategory(self): + width = 600 + height = 500 + dpi = 96 + + plot = QgsBarChartPlot() + plot.setSize(QSizeF(width, height)) + + sym1 = QgsFillSymbol.createSimple({"color": "#ffffff", "outline_style": "no"}) + plot.setChartBackgroundSymbol(sym1) + + sym2 = QgsFillSymbol.createSimple( + { + "outline_color": "#000000", + "style": "no", + "outline_style": "solid", + "outline_width": 1, + } + ) + plot.setChartBorderSymbol(sym2) + + sym3 = QgsLineSymbol.createSimple( + {"outline_color": "#00ffff", "outline_width": 1, "capstyle": "flat"} + ) + plot.xAxis().setGridMajorSymbol(sym3) + + sym4 = QgsLineSymbol.createSimple( + {"outline_color": "#ff00ff", "outline_width": 0.5, "capstyle": "flat"} + ) + plot.xAxis().setGridMinorSymbol(sym4) + + sym3 = QgsLineSymbol.createSimple( + {"outline_color": "#0066ff", "outline_width": 1, "capstyle": "flat"} + ) + plot.yAxis().setGridMajorSymbol(sym3) + + sym4 = QgsLineSymbol.createSimple( + {"outline_color": "#ff4433", "outline_width": 0.5, "capstyle": "flat"} + ) + plot.yAxis().setGridMinorSymbol(sym4) + + font = QgsFontUtils.getStandardTestFont("Bold", 16) + x_axis_format = QgsTextFormat.fromQFont(font) + plot.xAxis().setTextFormat(x_axis_format) + + font = QgsFontUtils.getStandardTestFont("Bold", 18) + y_axis_format = QgsTextFormat.fromQFont(font) + plot.yAxis().setTextFormat(y_axis_format) + + plot.xAxis().setType(Qgis.PlotAxisType.CategoryType) + plot.setYMinimum(-10) + plot.setYMaximum(10) + + # set symbol for first series + series_symbol = QgsFillSymbol.createSimple( + { + "color": "#00BB00", + "outline_color": "#003300", + "outline_style": "solid", + "outline_width": 1, + } + ) + plot.setFillSymbol(0, series_symbol) + + # set symbol for second series + series_symbol = QgsFillSymbol.createSimple( + { + "color": "#BB0000", + "outline_color": "#330000", + "outline_style": "solid", + "outline_width": 1, + } + ) + plot.setFillSymbol(1, series_symbol) + + data = QgsPlotData() + series = QgsXyPlotSeries() + series.append(0, 1) + series.append(1, 5) + series.append(2, 5) + series.append(3, 9) + data.addSeries(series) + series = QgsXyPlotSeries() + series.append(0, -5) + series.append(1, -2) + series.append(2, 5) + series.append(3, 4) + data.addSeries(series) + data.setCategories(["Q1", "Q2", "Q3", "Q4"]) + + im = QImage(width, height, QImage.Format.Format_ARGB32) + im.fill(Qt.GlobalColor.white) + im.setDotsPerMeterX(int(dpi / 25.4 * 1000)) + im.setDotsPerMeterY(int(dpi / 25.4 * 1000)) + + painter = QPainter(im) + rc = QgsRenderContext.fromQPainter(painter) + rc.setScaleFactor(dpi / 25.4) + prc = QgsPlotRenderContext() + plot.render(rc, prc, data) + painter.end() + + assert self.image_check( + "bar_chart_plot_x_axis_category", "bar_chart_plot_x_axis_category", im + ) + + def testBarChartPlotXAxisValue(self): + width = 600 + height = 500 + dpi = 96 + + plot = QgsBarChartPlot() + plot.setSize(QSizeF(width, height)) + + sym1 = QgsFillSymbol.createSimple({"color": "#ffffff", "outline_style": "no"}) + plot.setChartBackgroundSymbol(sym1) + + sym2 = QgsFillSymbol.createSimple( + { + "outline_color": "#000000", + "style": "no", + "outline_style": "solid", + "outline_width": 1, + } + ) + plot.setChartBorderSymbol(sym2) + + sym3 = QgsLineSymbol.createSimple( + {"outline_color": "#00ffff", "outline_width": 1, "capstyle": "flat"} + ) + plot.xAxis().setGridMajorSymbol(sym3) + + sym4 = QgsLineSymbol.createSimple( + {"outline_color": "#ff00ff", "outline_width": 0.5, "capstyle": "flat"} + ) + plot.xAxis().setGridMinorSymbol(sym4) + + sym3 = QgsLineSymbol.createSimple( + {"outline_color": "#0066ff", "outline_width": 1, "capstyle": "flat"} + ) + plot.yAxis().setGridMajorSymbol(sym3) + + sym4 = QgsLineSymbol.createSimple( + {"outline_color": "#ff4433", "outline_width": 0.5, "capstyle": "flat"} + ) + plot.yAxis().setGridMinorSymbol(sym4) + + font = QgsFontUtils.getStandardTestFont("Bold", 16) + x_axis_format = QgsTextFormat.fromQFont(font) + plot.xAxis().setTextFormat(x_axis_format) + + font = QgsFontUtils.getStandardTestFont("Bold", 18) + y_axis_format = QgsTextFormat.fromQFont(font) + plot.yAxis().setTextFormat(y_axis_format) + + plot.xAxis().setType(Qgis.PlotAxisType.ValueType) + plot.setXMinimum(-10) + plot.setXMaximum(10) + plot.setYMinimum(-10) + plot.setYMaximum(10) + + # set symbol for first series + series_symbol = QgsFillSymbol.createSimple( + { + "color": "#00BB00", + "outline_color": "#003300", + "outline_style": "solid", + "outline_width": 1, + } + ) + plot.setFillSymbol(0, series_symbol) + + # set symbol for second series + series_symbol = QgsFillSymbol.createSimple( + { + "color": "#BB0000", + "outline_color": "#330000", + "outline_style": "solid", + "outline_width": 1, + } + ) + plot.setFillSymbol(1, series_symbol) + + data = QgsPlotData() + series = QgsXyPlotSeries() + series.append(-8, 1) + series.append(0, 5) + series.append(4, 5) + series.append(9, 9) + data.addSeries(series) + series = QgsXyPlotSeries() + series.append(-7, -5) + series.append(1, -2) + series.append(4, 5) + series.append(8, 4) + data.addSeries(series) + + im = QImage(width, height, QImage.Format.Format_ARGB32) + im.fill(Qt.GlobalColor.white) + im.setDotsPerMeterX(int(dpi / 25.4 * 1000)) + im.setDotsPerMeterY(int(dpi / 25.4 * 1000)) + + painter = QPainter(im) + rc = QgsRenderContext.fromQPainter(painter) + rc.setScaleFactor(dpi / 25.4) + prc = QgsPlotRenderContext() + plot.render(rc, prc, data) + painter.end() + + assert self.image_check( + "bar_chart_plot_x_axis_value", "bar_chart_plot_x_axis_value", im + ) + + def testBarChartPlotXAxisCategory(self): + width = 600 + height = 500 + dpi = 96 + + plot = QgsLineChartPlot() + plot.setSize(QSizeF(width, height)) + + sym1 = QgsFillSymbol.createSimple({"color": "#ffffff", "outline_style": "no"}) + plot.setChartBackgroundSymbol(sym1) + + sym2 = QgsFillSymbol.createSimple( + { + "outline_color": "#000000", + "style": "no", + "outline_style": "solid", + "outline_width": 1, + } + ) + plot.setChartBorderSymbol(sym2) + + sym3 = QgsLineSymbol.createSimple( + {"outline_color": "#00ffff", "outline_width": 1, "capstyle": "flat"} + ) + plot.xAxis().setGridMajorSymbol(sym3) + + sym4 = QgsLineSymbol.createSimple( + {"outline_color": "#ff00ff", "outline_width": 0.5, "capstyle": "flat"} + ) + plot.xAxis().setGridMinorSymbol(sym4) + + sym3 = QgsLineSymbol.createSimple( + {"outline_color": "#0066ff", "outline_width": 1, "capstyle": "flat"} + ) + plot.yAxis().setGridMajorSymbol(sym3) + + sym4 = QgsLineSymbol.createSimple( + {"outline_color": "#ff4433", "outline_width": 0.5, "capstyle": "flat"} + ) + plot.yAxis().setGridMinorSymbol(sym4) + + font = QgsFontUtils.getStandardTestFont("Bold", 16) + x_axis_format = QgsTextFormat.fromQFont(font) + plot.xAxis().setTextFormat(x_axis_format) + + font = QgsFontUtils.getStandardTestFont("Bold", 18) + y_axis_format = QgsTextFormat.fromQFont(font) + plot.yAxis().setTextFormat(y_axis_format) + + plot.xAxis().setType(Qgis.PlotAxisType.CategoryType) + plot.setYMinimum(-10) + plot.setYMaximum(10) + + # set symbol for first series + series_symbol = QgsLineSymbol.createSimple( + { + "outline_color": "#00BB00", + "outline_style": "dash", + "outline_width": 1, + } + ) + plot.setLineSymbol(0, series_symbol) + + # set symbols for second series + series_symbol = QgsLineSymbol.createSimple( + { + "outline_color": "#BB0000", + "outline_style": "solid", + "outline_width": 1, + } + ) + plot.setLineSymbol(1, series_symbol) + series_symbol = QgsMarkerSymbol.createSimple( + { + "color": "#BB0000", + "outline_color": "#330000", + "outline_style": "solid", + "outline_width": 1, + "width": 3, + } + ) + plot.setMarkerSymbol(1, series_symbol) + + data = QgsPlotData() + series = QgsXyPlotSeries() + series.append(0, 1) + series.append(1, 2) + series.append(2, 5) + series.append(3, 9) + data.addSeries(series) + series = QgsXyPlotSeries() + series.append(0, -5) + series.append(1, -2) + # skip 3rd category to test disconnected lines + series.append(3, 4) + data.addSeries(series) + data.setCategories(["Q1", "Q2", "Q3", "Q4"]) + + im = QImage(width, height, QImage.Format.Format_ARGB32) + im.fill(Qt.GlobalColor.white) + im.setDotsPerMeterX(int(dpi / 25.4 * 1000)) + im.setDotsPerMeterY(int(dpi / 25.4 * 1000)) + + painter = QPainter(im) + rc = QgsRenderContext.fromQPainter(painter) + rc.setScaleFactor(dpi / 25.4) + prc = QgsPlotRenderContext() + plot.render(rc, prc, data) + painter.end() + + assert self.image_check( + "line_chart_plot_x_axis_category", "line_chart_plot_x_axis_category", im + ) + + def testLineChartPlotXAxisValue(self): + width = 600 + height = 500 + dpi = 96 + + plot = QgsLineChartPlot() + plot.setSize(QSizeF(width, height)) + + sym1 = QgsFillSymbol.createSimple({"color": "#ffffff", "outline_style": "no"}) + plot.setChartBackgroundSymbol(sym1) + + sym2 = QgsFillSymbol.createSimple( + { + "outline_color": "#000000", + "style": "no", + "outline_style": "solid", + "outline_width": 1, + } + ) + plot.setChartBorderSymbol(sym2) + + sym3 = QgsLineSymbol.createSimple( + {"outline_color": "#00ffff", "outline_width": 1, "capstyle": "flat"} + ) + plot.xAxis().setGridMajorSymbol(sym3) + + sym4 = QgsLineSymbol.createSimple( + {"outline_color": "#ff00ff", "outline_width": 0.5, "capstyle": "flat"} + ) + plot.xAxis().setGridMinorSymbol(sym4) + + sym3 = QgsLineSymbol.createSimple( + {"outline_color": "#0066ff", "outline_width": 1, "capstyle": "flat"} + ) + plot.yAxis().setGridMajorSymbol(sym3) + + sym4 = QgsLineSymbol.createSimple( + {"outline_color": "#ff4433", "outline_width": 0.5, "capstyle": "flat"} + ) + plot.yAxis().setGridMinorSymbol(sym4) + + font = QgsFontUtils.getStandardTestFont("Bold", 16) + x_axis_format = QgsTextFormat.fromQFont(font) + plot.xAxis().setTextFormat(x_axis_format) + + font = QgsFontUtils.getStandardTestFont("Bold", 18) + y_axis_format = QgsTextFormat.fromQFont(font) + plot.yAxis().setTextFormat(y_axis_format) + + plot.xAxis().setType(Qgis.PlotAxisType.ValueType) + plot.setXMinimum(-10) + plot.setXMaximum(10) + plot.setYMinimum(-10) + plot.setYMaximum(10) + + # set symbol for first series + series_symbol = QgsLineSymbol.createSimple( + { + "outline_color": "#00BB00", + "outline_style": "dash", + "outline_width": 1, + } + ) + plot.setLineSymbol(0, series_symbol) + + # set symbols for second series + series_symbol = QgsLineSymbol.createSimple( + { + "outline_color": "#BB0000", + "outline_style": "solid", + "outline_width": 1, + } + ) + plot.setLineSymbol(1, series_symbol) + series_symbol = QgsMarkerSymbol.createSimple( + { + "color": "#BB0000", + "outline_color": "#330000", + "outline_style": "solid", + "outline_width": 1, + "width": 3, + } + ) + plot.setMarkerSymbol(1, series_symbol) + + data = QgsPlotData() + series = QgsXyPlotSeries() + series.append(-8, 1) + series.append(0, 5) + series.append(4, 5) + series.append(9, 9) + data.addSeries(series) + series = QgsXyPlotSeries() + series.append(-7, -5) + series.append(1, -2) + series.append(4, 5) + series.append(8, 4) + # Test data() to insure SIP conversion works well + self.assertEqual( + series.data(), [(-7.0, -5.0), (1.0, -2.0), (4.0, 5.0), (8.0, 4.0)] + ) + data.addSeries(series) + + im = QImage(width, height, QImage.Format.Format_ARGB32) + im.fill(Qt.GlobalColor.white) + im.setDotsPerMeterX(int(dpi / 25.4 * 1000)) + im.setDotsPerMeterY(int(dpi / 25.4 * 1000)) + + painter = QPainter(im) + rc = QgsRenderContext.fromQPainter(painter) + rc.setScaleFactor(dpi / 25.4) + prc = QgsPlotRenderContext() + plot.render(rc, prc, data) + painter.end() + + assert self.image_check( + "line_chart_plot_x_axis_value", "line_chart_plot_x_axis_value", im + ) + if __name__ == "__main__": unittest.main() diff --git a/tests/src/python/test_qgsplotregistry.py b/tests/src/python/test_qgsplotregistry.py new file mode 100644 index 00000000000..ccd3fd9e322 --- /dev/null +++ b/tests/src/python/test_qgsplotregistry.py @@ -0,0 +1,72 @@ +"""QGIS Unit tests for QgsPlotRegistry + +.. note:: 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. +""" + +__author__ = "Mathieu Pellerin" +__date__ = "20/08/2025" +__copyright__ = "Copyright 2025, The QGIS Project" + + +from qgis.core import ( + Qgs2DPlot, + QgsPlotRegistry, + QgsPlotAbstractMetadata, +) +import unittest +from qgis.testing import start_app, QgisTestCase + +start_app() + + +class TestPlotAMetadata(QgsPlotAbstractMetadata): + + def __init__(self): + super().__init__("test_plot_a", "test plot a") + + def createPlot(self): + return Qgs2DPlot() + + +class TestPlotBMetadata(QgsPlotAbstractMetadata): + + def __init__(self): + super().__init__("test_plot_b", "test plot b") + + def createPlot(self): + return Qgs2DPlot() + + +class TestQgsPlotRegistry(QgisTestCase): + + def testRegistry(self): + registry = QgsPlotRegistry() + + registry.addPlotType(TestPlotAMetadata()) + registry.addPlotType(TestPlotBMetadata()) + self.assertEqual( + registry.plotTypes(), + { + "test_plot_a": "test plot a", + "test_plot_b": "test plot b", + }, + ) + + plot = registry.createPlot("test_plot_a") + self.assertTrue(plot) + + plot = registry.createPlot("test_plot_b") + self.assertTrue(plot) + + plot = registry.createPlot("invalid_plot") + self.assertFalse(plot) + + registry.removePlotType("test_plot_a") + self.assertEqual(registry.plotTypes(), {"test_plot_b": "test plot b"}) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/testdata/control_images/plot/expected_bar_chart_plot_x_axis_category/expected_bar_chart_plot_x_axis_category.png b/tests/testdata/control_images/plot/expected_bar_chart_plot_x_axis_category/expected_bar_chart_plot_x_axis_category.png new file mode 100644 index 0000000000000000000000000000000000000000..a339c0e7af047640fd365662ec3316e118c6dad9 GIT binary patch literal 5779 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 XPXdme9!#ad6JW5*+Rm!jf*Sij9Y}pI literal 0 HcmV?d00001 diff --git a/tests/testdata/control_images/plot/expected_bar_chart_plot_x_axis_category/expected_bar_chart_plot_x_axis_category_mask.png b/tests/testdata/control_images/plot/expected_bar_chart_plot_x_axis_category/expected_bar_chart_plot_x_axis_category_mask.png new file mode 100644 index 0000000000000000000000000000000000000000..bbcbb2a3491a2de38d9f5c3ed32993eed6a7a8c2 GIT binary patch literal 3582 zcmcgvYfuws6#WvgL=gk(7!?|-jG_)2I)LCvpatZmJXA1%G(g7(7zz=lJOaT~9RWo% zzCbBMtf(ju62Sl?0)e1dkVcFNNPvidK*B>F8zh0yU2vF=fBJ{`V`pdX?w#Fx?>YCJ zFWuMM&0xjG6#xJO4|kVC0H6Z^Kuu!4gCpI(hu^}t&VZ%=K{j z*)Jhyxc#xW+0k_uT6AKL-Q3ZOO2ecj&fU5Ne&_2L-o0jaZ0maH)$1O-i0H^;DvE9f z?nq}VhL!olN#Y=NH~{s*BrE`28?-+7;Xo(~oL_DM7?yu51BP3<0MzNO13%s3qXA|O z{8M3Ccxkvm4**ShVs4>ZIGd+bDvuEBo>Oblj4RM;5*xDFKUy3_F%>%}mq%n5xB-S^ z9KEyu``!fmBTU~@ftjVkl|})8nog@)KmF*F{Ty~32u_nqrPpfI(0r>Mr)s(!%;w0;IEj%*g4`-p9Z*&=5uO@RJFMf7{1I&7?}HEIW23D=W)~BkSt& z{PbBc?dc>i96ixGI<6TQjs!BNuFq%GTG+jZI z-)dx8%~U&+MvamM(+T{e+CUQT=o#@@dAc_H0bJTZwh%)sWkIy`LY7iZudFVvG}_q| zquhXl7XWwDu7Hs3UI7Aoq1Y^^*Xj_bwR52MrTU$M+|kidq_GVN3303%p%b(<$$5~% z^++te4nvsNHY_9B3CBJ6V+fhY_oy@U;jPqWh%4gU*jf~p3(whme8ZzwMbum1CJ2u*!B^2S$dHW~3zAqdy&-WX7jx3D9 zLNoxoL+3Y_)o?4TFDg2k#Om(r>k}z1&aFz8D1^MYJTj-U<=oXN5qX9@n$&%E=|y6J zQjskP>}HzO>Ldum=1EqiIzyN~TKgr5s?8v2%!fGSid9n^U+#S)AU~1xM@?4aoOLAv zfk1v{VtP6es!8;WHod4;tK~+XPzjC=@r?6E_fhCWu0DImSgu`6yz(y3AwDmLfHyT2 zI`o*=MaRU)lZZ}PrfK>4`A8NQ&MNY+Rf2P#aj`3yO_>{nnw!1;HsNqMEb^45avDQ}OF%{5Mj)&$ zVT~ja`5PZZ6yX1=M~2;f@1TsCJ6U0@L|3duRO&<;U)6XfZD;%F6wGdGxw_>#K3+fu zM?aW5X-E8F#sG*M5>n^}+HPz@QxNianlKw+=5B;$2RxXSVFbqh$B1OixL|aiR*xw1 zWKw%D$C+P5&Hwq;JbYX))y|Iz3~gOPQTge{jM460SgqKVMTK`t?->h#B{Z+v6hsQX z64;Krvne589%cx>iPHOiK>QE&&BIYWAmx4~wB_M7u)Zxk?9s-)L0sTa0Z)05SRbF1 zknj+OnMee0Y4c7*SOGXQJT^8qWVIk}6anqoTF7nPq@%8UBJ)$Hp&DTmjg2U zz6^oH<}u})04S^fCg=npvVM^xm8aNrKP(Dxw=A*Q(ZX3on@68EN6_z{;BJ7qS_>cO OfX6;>mtv>Tv;P8o!lzOI literal 0 HcmV?d00001 diff --git a/tests/testdata/control_images/plot/expected_bar_chart_plot_x_axis_value/expected_bar_chart_plot_x_axis_value.png b/tests/testdata/control_images/plot/expected_bar_chart_plot_x_axis_value/expected_bar_chart_plot_x_axis_value.png new file mode 100644 index 0000000000000000000000000000000000000000..6e05c8aa8ae9b950f53d20206403aef4a35e0ada GIT binary patch literal 7351 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 MCyaHAbTHTc2RJPx5&!@I literal 0 HcmV?d00001 diff --git a/tests/testdata/control_images/plot/expected_line_chart_plot_x_axis_category/expected_line_chart_plot_x_axis_category.png b/tests/testdata/control_images/plot/expected_line_chart_plot_x_axis_category/expected_line_chart_plot_x_axis_category.png new file mode 100644 index 0000000000000000000000000000000000000000..5c02937475461d5511748d254c460cf33faf4613 GIT binary patch literal 7373 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