File indexing completed on 2026-04-19 07:25:00

0001 /*
0002     File                 : StatisticalPlotsTest.cpp
0003     Project              : LabPlot
0004     Description          : Tests for statistical plots like Q-Q plot, KDE plot, etc.
0005     --------------------------------------------------------------------
0006     SPDX-FileCopyrightText: 2023 Alexander Semke <alexander.semke@web.de>
0007 
0008     SPDX-License-Identifier: GPL-2.0-or-later
0009 */
0010 
0011 #include "StatisticalPlotsTest.h"
0012 #include "backend/core/Project.h"
0013 #include "backend/core/column/Column.h"
0014 #include "backend/spreadsheet/Spreadsheet.h"
0015 #include "backend/worksheet/Worksheet.h"
0016 #include "backend/worksheet/plots/cartesian/Histogram.h"
0017 #include "backend/worksheet/plots/cartesian/KDEPlot.h"
0018 #include "backend/worksheet/plots/cartesian/QQPlot.h"
0019 
0020 #include <gsl/gsl_randist.h>
0021 #include <gsl/gsl_rng.h>
0022 
0023 #include <QUndoStack>
0024 
0025 // ##############################################################################
0026 // ############################## Histogram #####################################
0027 // ##############################################################################
0028 /*!
0029  * \brief create and add a new Histogram, undo and redo this step
0030  */
0031 void StatisticalPlotsTest::testHistogramInit() {
0032     Project project;
0033     auto* ws = new Worksheet(QStringLiteral("worksheet"));
0034     project.addChild(ws);
0035 
0036     auto* p = new CartesianPlot(QStringLiteral("plot"));
0037     ws->addChild(p);
0038 
0039     auto* histogram = new Histogram(QStringLiteral("histogram"));
0040     p->addChild(histogram);
0041 
0042     auto children = p->children<Histogram>();
0043 
0044     QCOMPARE(children.size(), 1);
0045 
0046     project.undoStack()->undo();
0047     children = p->children<Histogram>();
0048     QCOMPARE(children.size(), 0);
0049 
0050     project.undoStack()->redo();
0051     children = p->children<Histogram>();
0052     QCOMPARE(children.size(), 1);
0053 }
0054 
0055 /*!
0056  * \brief create and add a new Histogram, duplicate it and check the number of children
0057  */
0058 void StatisticalPlotsTest::testHistogramDuplicate() {
0059     Project project;
0060     auto* ws = new Worksheet(QStringLiteral("worksheet"));
0061     project.addChild(ws);
0062 
0063     auto* p = new CartesianPlot(QStringLiteral("plot"));
0064     ws->addChild(p);
0065 
0066     auto* histogram = new Histogram(QStringLiteral("kdeplot"));
0067     p->addChild(histogram);
0068 
0069     histogram->duplicate();
0070 
0071     auto children = p->children<Histogram>();
0072     QCOMPARE(children.size(), 2);
0073 }
0074 
0075 /*!
0076  * \brief create a Histogram for 3 values check the plot ranges.
0077  */
0078 void StatisticalPlotsTest::testHistogramRangeBinningTypeChanged() {
0079     // prepare the data
0080     Spreadsheet sheet(QStringLiteral("test"), false);
0081     sheet.setColumnCount(1);
0082     sheet.setRowCount(100);
0083     auto* column = sheet.column(0);
0084     column->setValueAt(0, 1.);
0085     column->setValueAt(1, 2.);
0086     column->setValueAt(2, 3.);
0087 
0088     // prepare the worksheet + plot
0089     auto* ws = new Worksheet(QStringLiteral("worksheet"));
0090     auto* p = new CartesianPlot(QStringLiteral("plot"));
0091     ws->addChild(p);
0092 
0093     auto* histogram = new Histogram(QStringLiteral("kdeplot"));
0094     histogram->setBinningMethod(Histogram::BinningMethod::ByNumber);
0095     histogram->setBinCount(3);
0096     histogram->setDataColumn(column);
0097     p->addChild(histogram);
0098 
0099     // the x-range is defined by the min and max values in the data [1, 3]
0100     // because of the bin count 3 we have one value in every bin and the y-range is [0,1]
0101     const auto& rangeX = p->range(Dimension::X);
0102     const auto& rangeY = p->range(Dimension::Y);
0103     QCOMPARE(rangeX.start(), 1);
0104     QCOMPARE(rangeX.end(), 3);
0105     QCOMPARE(rangeY.start(), 0);
0106     QCOMPARE(rangeY.end(), 1);
0107 
0108     // set the bin number to 1, the values 1 and 2 fall into the same bin
0109     histogram->setBinCount(1);
0110     QCOMPARE(rangeX.start(), 1);
0111     QCOMPARE(rangeX.end(), 3);
0112     QCOMPARE(rangeY.start(), 0);
0113     QCOMPARE(rangeY.end(), 2);
0114 }
0115 
0116 /*!
0117  * \brief create a Histogram for 3 values check the plot ranges after a row was removed in the source spreadsheet.
0118  */
0119 void StatisticalPlotsTest::testHistogramRangeRowsChanged() {
0120     Project project;
0121 
0122     // prepare the data
0123     auto* sheet = new Spreadsheet(QStringLiteral("test"), false);
0124     sheet->setColumnCount(1);
0125     sheet->setRowCount(3);
0126     project.addChild(sheet);
0127     auto* column = sheet->column(0);
0128     column->setValueAt(0, 1.);
0129     column->setValueAt(1, 2.);
0130     column->setValueAt(2, 3.);
0131 
0132     // worksheet
0133     auto* ws = new Worksheet(QStringLiteral("worksheet"));
0134     project.addChild(ws);
0135 
0136     auto* p = new CartesianPlot(QStringLiteral("plot"));
0137     ws->addChild(p);
0138 
0139     auto* histogram = new Histogram(QStringLiteral("kdeplot"));
0140     histogram->setBinningMethod(Histogram::BinningMethod::ByNumber);
0141     histogram->setBinCount(3);
0142     histogram->setDataColumn(column);
0143     p->addChild(histogram);
0144 
0145     // remove the last row and check the ranges, the x-range should become [1,2]
0146     sheet->setRowCount(2);
0147     const auto& rangeX = p->range(Dimension::X);
0148     const auto& rangeY = p->range(Dimension::Y);
0149     QCOMPARE(rangeX.start(), 1);
0150     QCOMPARE(rangeX.end(), 2);
0151     QCOMPARE(rangeY.start(), 0);
0152     QCOMPARE(rangeY.end(), 1);
0153 
0154     // undo the row removal and check again, the x-range should become [1,3] again
0155     project.undoStack()->undo();
0156     QCOMPARE(rangeX.start(), 1);
0157     QCOMPARE(rangeX.end(), 3);
0158     QCOMPARE(rangeY.start(), 0);
0159     QCOMPARE(rangeY.end(), 1);
0160 
0161     // add more (empty) rows in the spreadsheet, the ranges should be unchanged
0162     sheet->setRowCount(5);
0163     QCOMPARE(rangeX.start(), 1);
0164     QCOMPARE(rangeX.end(), 3);
0165     QCOMPARE(rangeY.start(), 0);
0166     QCOMPARE(rangeY.end(), 1);
0167 }
0168 
0169 void StatisticalPlotsTest::testHistogramColumnRemoved() {
0170     Project project;
0171     auto* ws = new Worksheet(QStringLiteral("worksheet"));
0172     project.addChild(ws);
0173 
0174     auto* p = new CartesianPlot(QStringLiteral("plot"));
0175     ws->addChild(p);
0176 
0177     auto* histogram = new Histogram(QStringLiteral("histogram"));
0178     p->addChild(histogram);
0179 
0180     auto* c = new Column(QStringLiteral("TestColumn"));
0181     project.addChild(c);
0182 
0183     histogram->setDataColumn(c);
0184     c->setName(QStringLiteral("NewName"));
0185     QCOMPARE(histogram->dataColumnPath(), QStringLiteral("Project/NewName"));
0186 
0187     c->remove();
0188 
0189     QCOMPARE(histogram->dataColumn(), nullptr);
0190     QCOMPARE(histogram->dataColumnPath(), QStringLiteral("Project/NewName"));
0191 
0192     c->setName(QStringLiteral("Another new name")); // Shall not lead to a crash
0193 
0194     QCOMPARE(histogram->dataColumn(), nullptr);
0195     QCOMPARE(histogram->dataColumnPath(), QStringLiteral("Project/NewName"));
0196 }
0197 
0198 // ##############################################################################
0199 // ############################## KDE Plot ######################################
0200 // ##############################################################################
0201 
0202 /*!
0203  * \brief create and add a new KDEPlot, undo and redo this step
0204  */
0205 void StatisticalPlotsTest::testKDEPlotInit() {
0206     Project project;
0207     auto* ws = new Worksheet(QStringLiteral("worksheet"));
0208     project.addChild(ws);
0209 
0210     auto* p = new CartesianPlot(QStringLiteral("plot"));
0211     ws->addChild(p);
0212 
0213     auto* kdePlot = new KDEPlot(QStringLiteral("kdeplot"));
0214     p->addChild(kdePlot);
0215 
0216     auto children = p->children<KDEPlot>();
0217 
0218     QCOMPARE(children.size(), 1);
0219 
0220     project.undoStack()->undo();
0221     children = p->children<KDEPlot>();
0222     QCOMPARE(children.size(), 0);
0223 
0224     // TODO: crash!!!
0225     // project.undoStack()->redo();
0226     // children = p->children<KDEPlot>();
0227     // QCOMPARE(children.size(), 1);
0228 }
0229 
0230 /*!
0231  * \brief create and add a new KDEPlot, duplicate it and check the number of children
0232  */
0233 void StatisticalPlotsTest::testKDEPlotDuplicate() {
0234     Project project;
0235     auto* ws = new Worksheet(QStringLiteral("worksheet"));
0236     project.addChild(ws);
0237 
0238     auto* p = new CartesianPlot(QStringLiteral("plot"));
0239     ws->addChild(p);
0240 
0241     auto* kdePlot = new KDEPlot(QStringLiteral("kdeplot"));
0242     p->addChild(kdePlot);
0243 
0244     kdePlot->duplicate();
0245 
0246     auto children = p->children<KDEPlot>();
0247     QCOMPARE(children.size(), 2);
0248 }
0249 
0250 /*!
0251  * \brief create a KDE plot for 3 values check the plot ranges.
0252  */
0253 void StatisticalPlotsTest::testKDEPlotRange() {
0254     // prepare the data
0255     Spreadsheet sheet(QStringLiteral("test"), false);
0256     sheet.setColumnCount(1);
0257     sheet.setRowCount(100);
0258     auto* column = sheet.column(0);
0259     column->setValueAt(0, 2.);
0260     column->setValueAt(1, 4.);
0261     column->setValueAt(2, 6.);
0262 
0263     // prepare the worksheet + plot
0264     auto* ws = new Worksheet(QStringLiteral("worksheet"));
0265     auto* p = new CartesianPlot(QStringLiteral("plot"));
0266     ws->addChild(p);
0267 
0268     auto* kdePlot = new KDEPlot(QStringLiteral("kdeplot"));
0269     kdePlot->setKernelType(nsl_kernel_gauss);
0270     kdePlot->setBandwidthType(nsl_kde_bandwidth_custom);
0271     kdePlot->setBandwidth(0.3);
0272     p->addChild(kdePlot);
0273     kdePlot->setDataColumn(column);
0274 
0275     // validate with R via:
0276     // data <- c(2,4,6);
0277     // kd <- density(data,kernel="gaussian", bw=0.3)
0278     // plot(kd, col='blue', lwd=2)
0279 
0280     // check the x-range of the plot which should be [1, 7] (subtract/add 3 sigmas from/to min and max, respectively).
0281     const auto& rangeX = p->range(Dimension::X);
0282     QCOMPARE(rangeX.start(), 1);
0283     QCOMPARE(rangeX.end(), 7);
0284 
0285     // check the y-range of the plot which should be [0, 0.45]
0286     const auto& rangeY = p->range(Dimension::Y);
0287     QCOMPARE(rangeY.start(), 0.);
0288     QCOMPARE(rangeY.end(), 0.45);
0289 }
0290 
0291 // ##############################################################################
0292 // ############################## Q-Q Plot ######################################
0293 // ##############################################################################
0294 
0295 /*!
0296  * \brief create and add a new QQPlot, undo and redo this step
0297  */
0298 void StatisticalPlotsTest::testQQPlotInit() {
0299     Project project;
0300     auto* ws = new Worksheet(QStringLiteral("worksheet"));
0301     project.addChild(ws);
0302 
0303     auto* p = new CartesianPlot(QStringLiteral("plot"));
0304     ws->addChild(p);
0305 
0306     auto* qqPlot = new QQPlot(QStringLiteral("qqplot"));
0307     p->addChild(qqPlot);
0308 
0309     auto children = p->children<QQPlot>();
0310 
0311     QCOMPARE(children.size(), 1);
0312 
0313     project.undoStack()->undo();
0314     children = p->children<QQPlot>();
0315     QCOMPARE(children.size(), 0);
0316 
0317     // TODO: crash!!!
0318     // project.undoStack()->redo();
0319     // children = p->children<QQPlot>();
0320     // QCOMPARE(children.size(), 1);
0321 }
0322 
0323 /*!
0324  * \brief create and add a new QQPlot, duplicate it and check the number of children
0325  */
0326 void StatisticalPlotsTest::testQQPlotDuplicate() {
0327     Project project;
0328     auto* ws = new Worksheet(QStringLiteral("worksheet"));
0329     project.addChild(ws);
0330 
0331     auto* p = new CartesianPlot(QStringLiteral("plot"));
0332     ws->addChild(p);
0333 
0334     auto* qqPlot = new QQPlot(QStringLiteral("qqplot"));
0335     p->addChild(qqPlot);
0336 
0337     qqPlot->duplicate();
0338 
0339     auto children = p->children<QQPlot>();
0340     QCOMPARE(children.size(), 2);
0341 }
0342 
0343 /*!
0344  * \brief create QQPlot for 100 normaly distributed values and check the plot ranges.
0345  */
0346 void StatisticalPlotsTest::testQQPlotRange() {
0347     // prepare the data
0348     Spreadsheet sheet(QStringLiteral("test"), false);
0349     sheet.setColumnCount(1);
0350     sheet.setRowCount(100);
0351     auto* column = sheet.column(0);
0352 
0353     // create a generator chosen by the environment variable GSL_RNG_TYPE
0354     gsl_rng_env_setup();
0355     const gsl_rng_type* T = gsl_rng_default;
0356     gsl_rng* r = gsl_rng_alloc(T);
0357     const double sigma = 1.;
0358 
0359     for (int i = 0; i < 100; ++i)
0360         column->setValueAt(i, gsl_ran_gaussian(r, sigma));
0361 
0362     // prepare the worksheet + plot
0363     auto* ws = new Worksheet(QStringLiteral("worksheet"));
0364     auto* p = new CartesianPlot(QStringLiteral("plot"));
0365     ws->addChild(p);
0366 
0367     auto* qqPlot = new QQPlot(QStringLiteral("qqplot"));
0368     p->addChild(qqPlot);
0369     qqPlot->setDataColumn(column);
0370 
0371     // check the x-range of the plot which should be [-2.5, 2.5] for the theoretical quantiles
0372     const auto& range = p->range(Dimension::X);
0373     QCOMPARE(range.start(), -2.5);
0374     QCOMPARE(range.end(), 2.5);
0375 }
0376 
0377 QTEST_MAIN(StatisticalPlotsTest)