From 2b5a30a6684dabe47fe7d30d62dfbec4f4d70344 Mon Sep 17 00:00:00 2001 From: jkriege2 Date: Thu, 13 Jun 2019 10:27:06 +0200 Subject: [PATCH] statistics library: added functions for grouped statistics and adaptors for them added example for grouped statistics some improvements to documentation --- JKQtPlotterBuildAllExamples.pro | 1 + doc/dox/examples_and_tutorials.dox | 3 + doc/dox/jkqtplotter.dox | 8 + examples/README.md | 1 + examples/simpletest_boxplot/README.md | 7 + examples/simpletest_datastore/README.md | 6 +- .../README.md | 255 ++++++ ...otter_simpletest_datastore_groupedstat.cpp | 265 +++++++ ...otter_simpletest_datastore_groupedstat.pro | 27 + ...mpletest_datastore_groupedstat_and_lib.pro | 8 + .../simpletest_datastore_iterators/README.md | 6 +- .../simpletest_datastore_regression/README.md | 5 +- .../simpletest_datastore_statistics/README.md | 6 +- .../README.md | 6 +- examples/simpletest_errorbarstyles/README.md | 5 + examples/test_distributionplot/README.md | 13 + examples/test_styledboxplot/README.md | 13 + lib/jkqtcommon/jkqtpstatbasics.h | 5 +- ...atisticstools.cpp => jkqtpstatgrouped.cpp} | 20 +- lib/jkqtcommon/jkqtpstatgrouped.h | 237 ++++++ lib/jkqtcommon/jkqtpstatisticstools.h | 1 + lib/jkqtcommon/jkqtpstatpoly.h | 1 - lib/jkqtpcommon.pri | 8 +- lib/jkqtplotter/jkqtpgraphsbarchart.h | 11 +- lib/jkqtplotter/jkqtpgraphsboxplot.h | 9 +- lib/jkqtplotter/jkqtpgraphscontour.h | 2 + .../jkqtpgraphsevaluatedfunction.h | 3 + lib/jkqtplotter/jkqtpgraphsfilledcurve.h | 4 +- lib/jkqtplotter/jkqtpgraphsimage.h | 5 + lib/jkqtplotter/jkqtpgraphsimpulses.h | 8 + lib/jkqtplotter/jkqtpgraphsparsedfunction.h | 5 + lib/jkqtplotter/jkqtpgraphspeakstream.h | 2 + lib/jkqtplotter/jkqtpgraphsscatter.h | 8 +- .../jkqtpgraphsstatisticsadaptors.h | 733 ++++++++++++++++++ ...otter_simpletest_datastore_groupedstat.png | Bin 0 -> 43936 bytes ...pletest_datastore_groupedstat_barchart.png | Bin 0 -> 10118 bytes ..._datastore_groupedstat_barchartrawdata.png | Bin 0 -> 5327 bytes ...mpletest_datastore_groupedstat_boxplot.png | Bin 0 -> 5466 bytes ...mpletest_datastore_groupedstat_scatter.png | Bin 0 -> 26042 bytes ...t_datastore_groupedstat_scatterrawdata.png | Bin 0 -> 14297 bytes ...simpletest_datastore_groupedstat_small.png | Bin 0 -> 12058 bytes 41 files changed, 1669 insertions(+), 28 deletions(-) create mode 100644 examples/simpletest_datastore_groupedstat/README.md create mode 100644 examples/simpletest_datastore_groupedstat/jkqtplotter_simpletest_datastore_groupedstat.cpp create mode 100644 examples/simpletest_datastore_groupedstat/jkqtplotter_simpletest_datastore_groupedstat.pro create mode 100644 examples/simpletest_datastore_groupedstat/jkqtplotter_simpletest_datastore_groupedstat_and_lib.pro rename lib/jkqtcommon/{jkqtpstatisticstools.cpp => jkqtpstatgrouped.cpp} (58%) create mode 100644 lib/jkqtcommon/jkqtpstatgrouped.h create mode 100644 screenshots/jkqtplotter_simpletest_datastore_groupedstat.png create mode 100644 screenshots/jkqtplotter_simpletest_datastore_groupedstat_barchart.png create mode 100644 screenshots/jkqtplotter_simpletest_datastore_groupedstat_barchartrawdata.png create mode 100644 screenshots/jkqtplotter_simpletest_datastore_groupedstat_boxplot.png create mode 100644 screenshots/jkqtplotter_simpletest_datastore_groupedstat_scatter.png create mode 100644 screenshots/jkqtplotter_simpletest_datastore_groupedstat_scatterrawdata.png create mode 100644 screenshots/jkqtplotter_simpletest_datastore_groupedstat_small.png diff --git a/JKQtPlotterBuildAllExamples.pro b/JKQtPlotterBuildAllExamples.pro index f115ba8944..c9dffb0628 100644 --- a/JKQtPlotterBuildAllExamples.pro +++ b/JKQtPlotterBuildAllExamples.pro @@ -84,6 +84,7 @@ addSimpleTest(datastore_iterators) addSimpleTest(datastore_statistics) addSimpleTest(datastore_statistics_2d) addSimpleTest(datastore_regression) +addSimpleTest(datastore_groupedstat) addSimpleTest(contourplot) #addSimpleTest(rgbimageplot_opencv) #addSimpleTest(imageplot_opencv) diff --git a/doc/dox/examples_and_tutorials.dox b/doc/dox/examples_and_tutorials.dox index bcbe5aa0dd..0d2a421b2b 100644 --- a/doc/dox/examples_and_tutorials.dox +++ b/doc/dox/examples_and_tutorials.dox @@ -162,6 +162,9 @@ All test-projects are Qt-projects that use qmake to build. You can load them int \image html jkqtplotter_simpletest_datastore_regression_small.png \subpage JKQTPlotterBasicJKQTPDatastoreRegression Advanced 1-Dimensional Statistical Computation with JKQTPDatastore
using the internal statistics library
Regression Analysis (with the Statistics Library)
robust regression (IRLS)
weighted regression
non-linear regression
polynomial fitting + \image html jkqtplotter_simpletest_datastore_groupedstat_small.png + \subpage JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat + Advanced 1-Dimensional Statistical Computation with JKQTPDatastore
grouped statistics
error indicators from data
boxplots \image html jkqtplotter_simpletest_datastore_statistics_2d_small.png \subpage JKQTPlotterBasicJKQTPDatastoreStatistics2D Advanced 2-Dimensional Statistical Computation with JKQTPDatastore
using the internal statistics library (see \ref jkqtptools_math_statistics )
histograms
kernel density estimates (KDE) diff --git a/doc/dox/jkqtplotter.dox b/doc/dox/jkqtplotter.dox index 1d357326a2..92ed87ccc2 100644 --- a/doc/dox/jkqtplotter.dox +++ b/doc/dox/jkqtplotter.dox @@ -37,7 +37,9 @@ This group assembles a basic set of linear algebra methods, including matrix inv This group contains a statistics library, which offers several basic methods and is based on an iterator interface: - \ref jkqtptools_math_statistics_basic + - \ref jkqtptools_math_statistics_grouped - \ref jkqtptools_math_statistics_regression + - \ref jkqtptools_math_statistics_poly - \ref jkqtptools_math_statistics_1dhist - \ref jkqtptools_math_statistics_2dhist - \ref jkqtptools_math_statistics_1dkde @@ -67,9 +69,15 @@ All statistics functions use all values in the given range and convert each valu \defgroup jkqtptools_math_statistics_basic Basic statistics \ingroup jkqtptools_math_statistics +\defgroup jkqtptools_math_statistics_grouped Grouped statistics +\ingroup jkqtptools_math_statistics + \defgroup jkqtptools_math_statistics_regression Regression Analysis \ingroup jkqtptools_math_statistics +\defgroup jkqtptools_math_statistics_poly Polynomial Fits/Regression +\ingroup jkqtptools_math_statistics + \defgroup jkqtptools_math_statistics_1dhist 1-dimensional Histograms \ingroup jkqtptools_math_statistics diff --git a/examples/README.md b/examples/README.md index 71397b14da..79b9240699 100644 --- a/examples/README.md +++ b/examples/README.md @@ -66,6 +66,7 @@ All test-projects are Qt-projects that use qmake to build. You can load them int | [![](https://raw.githubusercontent.com/jkriege2/JKQtPlotter/master/screenshots/simpletest_datastore_iterators_small.png)](https://github.com/jkriege2/JKQtPlotter/tree/master/examples/simpletest_datastore_iterators) | [Tutorial: Iterator-based access to JKQTPDatastore](https://github.com/jkriege2/JKQtPlotter/tree/master/examples/simpletest_datastore_iterators) | Iterator-based Data Management with JKQTPDatastore | | [![](https://raw.githubusercontent.com/jkriege2/JKQtPlotter/master/screenshots/jkqtplotter_simpletest_datastore_statistics_small.png)](https://github.com/jkriege2/JKQtPlotter/tree/master/examples/simpletest_datastore_statistics) | [Tutorial: Advanced 1-Dimensional Statistics with JKQTPDatastore](https://github.com/jkriege2/JKQtPlotter/tree/master/examples/simpletest_datastore_statistics) | Advanced 1-Dimensional Statistical Computation with JKQTPDatastore
using the internal statistics library
basic statistics (mean, standard deviation, ...)
boxplots
histograms
kernel density estimates (KDE) | | [![](https://raw.githubusercontent.com/jkriege2/JKQtPlotter/master/screenshots/jkqtplotter_simpletest_datastore_regression_small.png)](https://github.com/jkriege2/JKQtPlotter/tree/master/examples/simpletest_datastore_regression) | [Tutorial: Regression Analysis (with the Statistics Library)](https://github.com/jkriege2/JKQtPlotter/tree/master/examples/simpletest_datastore_regression) | Advanced 1-Dimensional Statistical Computation with JKQTPDatastore
using the internal statistics library
Regression Analysis (with the Statistics Library)
robust regression (IRLS)
weighted regression
non-linear regression
polynomial fitting | +| [![](https://raw.githubusercontent.com/jkriege2/JKQtPlotter/master/screenshots/jkqtplotter_simpletest_datastore_groupedstat_small.png)](https://github.com/jkriege2/JKQtPlotter/tree/master/examples/simpletest_datastore_groupedstat) | [Tutorial: 1-Dimensional Group Statistics (with the Statistics Library)](https://github.com/jkriege2/JKQtPlotter/tree/master/examples/simpletest_datastore_groupedstat) | Advanced 1-Dimensional Statistical Computation with JKQTPDatastore
grouped statistics
error indicators from data
boxplots | | [![](https://raw.githubusercontent.com/jkriege2/JKQtPlotter/master/screenshots/jkqtplotter_simpletest_datastore_statistics_2d_small.png)](https://github.com/jkriege2/JKQtPlotter/tree/master/examples/simpletest_datastore_statistics_2d) | [Tutorial: Advanced 2-Dimensional Statistics with JKQTPDatastore](https://github.com/jkriege2/JKQtPlotter/tree/master/examples/simpletest_datastore_statistics_2d) | Advanced 2-Dimensional Statistical Computation with JKQTPDatastore
using the internal statistics library
histograms
kernel density estimates (KDE) | diff --git a/examples/simpletest_boxplot/README.md b/examples/simpletest_boxplot/README.md index 1f49cd5ed6..641e4b4b64 100644 --- a/examples/simpletest_boxplot/README.md +++ b/examples/simpletest_boxplot/README.md @@ -1,6 +1,13 @@ # Example (JKQTPlotter): Boxplots {#JKQTPlotterBoxplotsGraphs} + This project (see [`simpletest_boxplot`](https://github.com/jkriege2/JKQtPlotter/tree/master/examples/simpletest_boxplot) demonstrates how to use JKQTPlotter to draw box plots using the classes `JKQTPBoxplotVerticalGraph` and `JKQTPBoxplotHorizontalGraph`. +[JKQTPlotterBasicJKQTPDatastoreStatistics]: @ref JKQTPlotterBasicJKQTPDatastoreStatistics "Advanced 1-Dimensional Statistics with JKQTPDatastore" +[statisticslibrary]: @ref jkqtptools_math_statistics "JKQTPlotter Statistics Library" +[JKQTPlotterBoxplotStyling]: @ref JKQTPlotterBoxplotStyling "Styling different aspects of boxplots" + +***Note*** that this example explains how to add boxplots to a graph by hand, i.e. by calculating all the statistical properties for the boxplots by hand. The internal [statisticslibrary] offers methods to perform these calculations, which are explained in the tutorial [JKQTPlotterBasicJKQTPDatastoreStatistics] in detail. Additional advanced styling of boxplots is covered by the example [JKQTPlotterBoxplotStyling]. + The source code of the main application is (see [`jkqtplotter_simpletest_boxplot.cpp`](jkqtplotter_simpletest_boxplot.cpp). After adding all necessary data to the JKQTDatastore: diff --git a/examples/simpletest_datastore/README.md b/examples/simpletest_datastore/README.md index b23236a8a1..f862984fab 100644 --- a/examples/simpletest_datastore/README.md +++ b/examples/simpletest_datastore/README.md @@ -1,10 +1,10 @@ # Tutorial (JKQTPDatastore): Basic Usage of JKQTPDatastore {#JKQTPlotterBasicJKQTPDatastore} - [JKQTPlotterBasicJKQTPDatastore]: @ref JKQTPlotterBasicJKQTPDatastore "Basic Usage of JKQTPDatastore" [JKQTPlotterBasicJKQTPDatastoreIterators]: @ref JKQTPlotterBasicJKQTPDatastoreIterators "Iterator-Based usage of JKQTPDatastore" [JKQTPlotterBasicJKQTPDatastoreStatistics]: @ref JKQTPlotterBasicJKQTPDatastoreStatistics "Advanced 1-Dimensional Statistics with JKQTPDatastore" [JKQTPlotterBasicJKQTPDatastoreRegression]: @ref JKQTPlotterBasicJKQTPDatastoreRegression "Regression Analysis (with the Statistics Library)" +[JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat]: @ref JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat "1-Dimensional Group Statistics with JKQTPDatastore" [JKQTPlotterBasicJKQTPDatastoreStatistics2D]: @ref JKQTPlotterBasicJKQTPDatastoreStatistics2D "Advanced 2-Dimensional Statistics with JKQTPDatastore" [statisticslibrary]: @ref jkqtptools_math_statistics "JKQTPlotter Statistics Library" @@ -14,8 +14,10 @@ This tutorial project (see `./examples/simpletest_datastore/`) explains several - [JKQTPlotterBasicJKQTPDatastore] - [JKQTPlotterBasicJKQTPDatastoreIterators] - [JKQTPlotterBasicJKQTPDatastoreStatistics] - - [JKQTPlotterBasicJKQTPDatastoreStatistics2D] - [JKQTPlotterBasicJKQTPDatastoreRegression] + - [JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat] + - [JKQTPlotterBasicJKQTPDatastoreStatistics2D] + [TOC] diff --git a/examples/simpletest_datastore_groupedstat/README.md b/examples/simpletest_datastore_groupedstat/README.md new file mode 100644 index 0000000000..5c7c95c6ee --- /dev/null +++ b/examples/simpletest_datastore_groupedstat/README.md @@ -0,0 +1,255 @@ +# Tutorial (JKQTPDatastore): 1-Dimensional Group Statistics with JKQTPDatastore {#JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat} + +[JKQTPlotterBasicJKQTPDatastore]: @ref JKQTPlotterBasicJKQTPDatastore "Basic Usage of JKQTPDatastore" +[JKQTPlotterBasicJKQTPDatastoreIterators]: @ref JKQTPlotterBasicJKQTPDatastoreIterators "Iterator-Based usage of JKQTPDatastore" +[JKQTPlotterBasicJKQTPDatastoreStatistics]: @ref JKQTPlotterBasicJKQTPDatastoreStatistics "Advanced 1-Dimensional Statistics with JKQTPDatastore" +[JKQTPlotterBasicJKQTPDatastoreRegression]: @ref JKQTPlotterBasicJKQTPDatastoreRegression "Regression Analysis (with the Statistics Library)" +[JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat]: @ref JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat "1-Dimensional Group Statistics with JKQTPDatastore" +[JKQTPlotterBasicJKQTPDatastoreStatistics2D]: @ref JKQTPlotterBasicJKQTPDatastoreStatistics2D "Advanced 2-Dimensional Statistics with JKQTPDatastore" +[statisticslibrary]: @ref jkqtptools_math_statistics "JKQTPlotter Statistics Library" + +This tutorial project (see `./examples/simpletest_datastore_groupedstat/`) explains several advanced functions of JKQTPDatastore in combination with the [[statisticslibrary]] conatined in JKQTPlotter. + +***Note*** that there are additional tutorial explaining other aspects of data mangement in JKQTPDatastore: + - [JKQTPlotterBasicJKQTPDatastore] + - [JKQTPlotterBasicJKQTPDatastoreIterators] + - [JKQTPlotterBasicJKQTPDatastoreStatistics] + - [JKQTPlotterBasicJKQTPDatastoreRegression] + - [JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat] + - [JKQTPlotterBasicJKQTPDatastoreStatistics2D] + +[TOC] + +The source code of the main application can be found in [`jkqtplotter_simpletest_datastore_groupedstat.cpp`](https://github.com/jkriege2/JKQtPlotter/tree/master/examples/simpletest_datastore_groupedstat/jkqtplotter_simpletest_datastore_groupedstat.cpp). +This tutorial cites only parts of this code to demonstrate different ways of working with data for the graphs. + +# Barcharts & Boxplots from categorized data + +## Generating a Dataset for Grouped Barcharts + +To demonstrate the grouped statistics, we first have to generate a dataset. The datapoints consist of pairs ``, where the groups are encoded by the numbers 1,2,3 and in each group, several measurements are taken: +```.cpp + size_t colBarRawGroup=datastore1->addColumn("barchart, rawdata, group"); + size_t colBarRawValue=datastore1->addColumn("barchart, rawdata, value"); + + // data for group 1 + datastore1->appendToColumns(colBarRawGroup, colBarRawValue, 1, 1.1); + datastore1->appendToColumns(colBarRawGroup, colBarRawValue, 1, 1.5); + datastore1->appendToColumns(colBarRawGroup, colBarRawValue, 1, 0.8); + // ... + + // data for group 2 + datastore1->appendToColumns(colBarRawGroup, colBarRawValue, 2, 2.2); + // ... + + // data for group 3 + datastore1->appendToColumns(colBarRawGroup, colBarRawValue, 3, 4.1); + // ... + +``` + +Note that the data does not have to be sorted. You can add the dataset in any order! + +This dataset can be visualized with a simple scatter plot: +```.cpp + JKQTPXYLineGraph* gScatterForBar; + plotbarchart->addGraph(gScatterForBar=new JKQTPXYLineGraph(plotbarchart)); + gScatterForBar->setXYColumns(colBarRawGroup, colBarRawValue); + gScatterForBar->setDrawLine(false); + gScatterForBar->setSymbolType(JKQTPCross); + gScatterForBar->setSymbolSize(5); + gScatterForBar->setSymbolColor(QColorWithAlphaF(QColor("red"), 0.5)); +``` + +![jkqtplotter_simpletest_datastore_groupedstat_barchartrawdata](https://raw.githubusercontent.com/jkriege2/JKQtPlotter/master/screenshots/jkqtplotter_simpletest_datastore_groupedstat_barchartrawdata.png) + +## Calculating Grouped Statistics for a Barchart + +Now we want to draw a barchart for every group, which indicates the average in each group. This is done using methods from the statistics library. +First we need to group the data using `jkqtpstatGroupData()`, which assembles the data points in each group groupeddataBar +```.cpp + std::map > groupeddataBar; + jkqtpstatGroupData(datastore1->begin(colBarRawGroup), datastore1->end(colBarRawGroup), + datastore1->begin(colBarRawValue), datastore1->end(colBarRawValue), + groupeddataBar); +``` +Now we can calculate the statistics for each group separately: Data is collected in new columns `colBarGroup`, `colBarAverage` and `colBarStdDev`. The statistics is then calculated by simply iterating over `groupeddataBar` and calling functions like `jkqtpstatAverage()` for each group: +```.cpp + size_t colBarGroup=datastore1->addColumn("barchart, group"); + size_t colBarAverage=datastore1->addColumn("barchart, group-average"); + size_t colBarStdDev=datastore1->addColumn("barchart, group-stddev"); + + for (auto it=groupeddataBar.begin(); it!=groupeddataBar.end(); ++it) { + datastore1->appendToColumn(colBarGroup, it->first); + datastore1->appendToColumn(colBarAverage, jkqtpstatAverage(it->second.begin(), it->second.end())); + datastore1->appendToColumn(colBarStdDev, jkqtpstatStdDev(it->second.begin(), it->second.end())); + } +``` + +Finally the calculated groups are drawn: +```.cpp + JKQTPBarVerticalErrorGraph* gBar; + plotbarchart->addGraph(gBar=new JKQTPBarVerticalErrorGraph(plotbarchart)); + gBar->setXYColumns(colBarGroup, colBarAverage); + gBar->setYErrorColumn(static_cast(colBarStdDev)); +``` + +![jkqtplotter_simpletest_datastore_groupedstat_barchart](https://raw.githubusercontent.com/jkriege2/JKQtPlotter/master/screenshots/jkqtplotter_simpletest_datastore_groupedstat_barchart.png) + +In order to safe yo the typing of the code above, shortcuts in the form of adaptors exist: +```.cpp + jkqtpstatAddYErrorBarGraph(plotbarchart->getPlotter(), + datastore1->begin(colBarRawGroup), datastore1->end(colBarRawGroup), + datastore1->begin(colBarRawValue), datastore1->end(colBarRawValue)); +``` +Also other flavors exist that generate different graphs (see the JKQTPlotter documentation): + - `jkqtpstatAddYErrorLineGraph()` / `jkqtpstatAddXErrorLineGraph()` + - `jkqtpstatAddYErrorBarGraph()` / `jkqtpstatAddXErrorBarGraph()` + - `jkqtpstatAddYErrorImpulsesGraph()` / `jkqtpstatAddXErrorImpulsesGraph()` + - `jkqtpstatAddYErrorParametrizedScatterGraph()` / `jkqtpstatAddXErrorParametrizedScatterGraph()` + - `jkqtpstatAddYErrorFilledCurveGraph()` / `jkqtpstatAddXErrorFilledCurveGraph()` + - `jkqtpstatAddYErrorGraph()` / `jkqtpstatAddXErrorGraph()` + + +## Calculating Grouped Statistics for a Boxplot + +With the methods above we can also calculate more advanced statistics, like e.g. boxplots: +```.cpp + size_t colBarMedian=datastore1->addColumn("barchart, group-median"); + size_t colBarMin=datastore1->addColumn("barchart, group-min"); + size_t colBarMax=datastore1->addColumn("barchart, group-max"); + size_t colBarQ25=datastore1->addColumn("barchart, group-Q25"); + size_t colBarQ75=datastore1->addColumn("barchart, group-Q75"); + for (auto it=groupeddataBar.begin(); it!=groupeddataBar.end(); ++it) { + datastore1->appendToColumn(colBarMedian, jkqtpstatMedian(it->second.begin(), it->second.end())); + datastore1->appendToColumn(colBarMin, jkqtpstatMinimum(it->second.begin(), it->second.end())); + datastore1->appendToColumn(colBarMax, jkqtpstatMaximum(it->second.begin(), it->second.end())); + datastore1->appendToColumn(colBarQ25, jkqtpstatQuantile(it->second.begin(), it->second.end(), 0.25)); + datastore1->appendToColumn(colBarQ75, jkqtpstatQuantile(it->second.begin(), it->second.end(), 0.75)); + } +``` +The result can be plotted using JKQTPBoxplotVerticalGraph, which receives a column for each value class of the final plot: +```.cpp + JKQTPBoxplotVerticalGraph* gBoxplot; + plotboxplot->addGraph(gBoxplot=new JKQTPBoxplotVerticalGraph(plotboxplot)); + gBoxplot->setPositionColumn(colBarGroup); + gBoxplot->setMinColumn(colBarMin); + gBoxplot->setMaxColumn(colBarMax); + gBoxplot->setMedianColumn(colBarMedian); + gBoxplot->setPercentile25Column(colBarQ25); + gBoxplot->setPercentile75Column(colBarQ75); +``` + +![jkqtplotter_simpletest_datastore_groupedstat_boxplot](https://raw.githubusercontent.com/jkriege2/JKQtPlotter/master/screenshots/jkqtplotter_simpletest_datastore_groupedstat_boxplot.png) + +In order to safe yo the typing of the code above, shortcuts in the form of adaptors exist: +```.cpp + jkqtpstatAddHBoxplotsAndOutliers(plotboxplot->getPlotter(), + datastore1->begin(colBarRawGroup), datastore1->end(colBarRawGroup), + datastore1->begin(colBarRawValue), datastore1->end(colBarRawValue)); +``` +Also other flavors exist that generate different graphs (see the JKQTPlotter documentation): + - `jkqtpstatAddVBoxplotsAndOutliers()` / `jkqtpstatAddHBoxplotsAndOutliers()` + - `jkqtpstatVAddBoxplots()` / `jkqtpstatHAddBoxplots()` + - `jkqtpstatAddBoxplots()` + + +# (Scatter-)Graphs with X/Y-errors from Categorized Data + +## Dataset for XY Scatter Graphs + +First we generate a second dataset, which is going to be used for a scaterplot. The datapoints consist of pairs ``, that are based on a parabula with random deviations, both in x- and y-direction: +```.cpp + size_t colScatterRawX=datastore1->addColumn("scatterplot, rawdata, x"); + size_t colScatterRawY=datastore1->addColumn("scatterplot, rawdata, y"); + std::random_device rd; // random number generators: + std::mt19937 gen{rd()}; + std::normal_distribution<> d1{0,0.5}; + const size_t N=100; + const double xmax=3.5; + for (size_t i=0; i(i)-static_cast(N)/2.0)*xmax/(static_cast(N)/2.0); + const double y=jkqtp_sqr(x)+2.0; + datastore1->appendToColumns(colScatterRawX, colScatterRawY, x+d1(gen), y+d1(gen)); + } +``` + +This dataset can be visualized: +```.cpp + JKQTPXYParametrizedScatterGraph* gScatterRaw; + plotscattererrors->addGraph(gScatterRaw=new JKQTPXYParametrizedScatterGraph(plotscattererrors)); + gScatterRaw->setXYColumns(colScatterRawX, colScatterRawY); + gScatterRaw->setDrawLine(false); + gScatterRaw->setSymbolType(JKQTPCross); + gScatterRaw->setSymbolSize(5); +``` + +![jkqtplotter_simpletest_datastore_groupedstat_scatterrawdata](https://raw.githubusercontent.com/jkriege2/JKQtPlotter/master/screenshots/jkqtplotter_simpletest_datastore_groupedstat_scatterrawdata.png) + +## Calculating x- and y-Errors from Categorized Data + +Now we want to draw a scatterchart of the data, where data-points should be grouped together, in x-intervals of width 0.5. From all the points in each interval, we calculate the in both x- and y-direction the average and standard deviation. First we need to group the data using `jkqtpstatGroupData()`, which assembles the data points in each group groupeddataScatter. For the custom grouping of the datapoints we use the optional functor provided to `jkqtpstatGroupData()`: We use `jkqtpstatGroupingCustomRound1D()` with given parameters 0.25 for the (center) location of the first bin and bin width 0.5. The functor is not built by hand (which would be possible using std::bind), but with the generator function `jkqtpstatMakeGroupingCustomRound1D()`. In addition we use a variant of `jkqtpstatGroupData()`, which outputs a column with the category assigned to every data pair in the input data range: +```.cpp + std::map,std::vector > > groupeddataScatter; + size_t colScatterRawGroup=datastore1->addColumn("scatterplot, rawdata, assigned-group"); + jkqtpstatGroupData(datastore1->begin(colScatterRawX), datastore1->end(colScatterRawX), + datastore1->begin(colScatterRawY), datastore1->end(colScatterRawY), + datastore1->backInserter(colScatterRawGroup), + groupeddataScatter, + jkqtpstatMakeGroupingCustomRound1D(0.25, 0.5)); +``` + +The column colScatterRawGroup can now be used to color the scatter graph: +```.cpp + gScatterRaw->setColorColumn(colScatterRawGroup); +``` + +Now we can calculate the statistics for each group separately: Data is collected in two new columns. Then the statistics is calculated by simply iterating over `groupeddataScatter` and calling functions like `jkqtpstatAverage()` for each group: +```.cpp + size_t colScatterXAvg=datastore1->addColumn("scatter, x, average"); + size_t colScatterXStd=datastore1->addColumn("scatter, x, stddev"); + size_t colScatterYAvg=datastore1->addColumn("scatter, y, average"); + size_t colScatterYStd=datastore1->addColumn("scatter, y, stddev"); + + for (auto it=groupeddataScatter.begin(); it!=groupeddataScatter.end(); ++it) { + datastore1->appendToColumn(colScatterXAvg, jkqtpstatAverage(it->second.first.begin(), it->second.first.end())); + datastore1->appendToColumn(colScatterXStd, jkqtpstatStdDev(it->second.first.begin(), it->second.first.end())); + datastore1->appendToColumn(colScatterYAvg, jkqtpstatAverage(it->second.second.begin(), it->second.second.end())); + datastore1->appendToColumn(colScatterYStd, jkqtpstatStdDev(it->second.second.begin(), it->second.second.end())); + } +``` + +Finally the calculated groups are drawn +```.cpp + JKQTPXYLineErrorGraph* gScatterErr; + plotscattererrors->addGraph(gScatterErr=new JKQTPXYLineErrorGraph(plotscattererrors)); + gScatterErr->setXYColumns(colScatterXAvg, colScatterYAvg); + gScatterErr->setXErrorColumn(static_cast(colScatterXStd)); + gScatterErr->setYErrorColumn(static_cast(colScatterYStd)); + gScatterErr->setSymbolType(JKQTPFilledTriangle); + gScatterErr->setDrawLine(false); +``` + +![jkqtplotter_simpletest_datastore_groupedstat_scatter](https://raw.githubusercontent.com/jkriege2/JKQtPlotter/master/screenshots/jkqtplotter_simpletest_datastore_groupedstat_scatter.png) + + +In order to safe yo the typing of the code above, shortcuts in the form of adaptors exist: +```.cpp + jkqtpstatAddXYErrorLineGraph(plotscattererrors->getPlotter(), + datastore1->begin(colScatterRawX), datastore1->end(colScatterRawX), + datastore1->begin(colScatterRawY), datastore1->end(colScatterRawY), + jkqtpstatMakeGroupingCustomRound1D(0.25, 0.5)); +``` +Also other flavors exist that generate different graphs (see the JKQTPlotter documentation): + - `jkqtpstatAddXYErrorLineGraph()` + - `jkqtpstatAddXYErrorParametrizedScatterGraph()` + - `jkqtpstatAddXYErrorGraph()` + + +# Screenshot of the full Program + +The output of the full test program [`jkqtplotter_simpletest_datastore_groupedstat.cpp`](https://github.com/jkriege2/JKQtPlotter/tree/master/examples/simpletest_datastore_groupedstat/jkqtplotter_simpletest_datastore_groupedstat.cpp) looks like this: + +![jkqtplotter_simpletest_datastore_groupedstat](https://raw.githubusercontent.com/jkriege2/JKQtPlotter/master/screenshots/jkqtplotter_simpletest_datastore_groupedstat.png) + + diff --git a/examples/simpletest_datastore_groupedstat/jkqtplotter_simpletest_datastore_groupedstat.cpp b/examples/simpletest_datastore_groupedstat/jkqtplotter_simpletest_datastore_groupedstat.cpp new file mode 100644 index 0000000000..7fa8cedf6e --- /dev/null +++ b/examples/simpletest_datastore_groupedstat/jkqtplotter_simpletest_datastore_groupedstat.cpp @@ -0,0 +1,265 @@ +/** \example jkqtplotter_simpletest_datastore_groupedstat.cpp + * Explains how to use the internal statistics library (see \ref jkqtptools_statistics ) together with JKQTPDatastore to generate grouped statistics (i.e. calculates errorbars or boxplots from groups of datapoints in a x/y-dataset). + * + * \ref JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat + */ + +#include +#include "jkqtplotter/jkqtplotter.h" +#include "jkqtplotter/jkqtpgraphsscatter.h" +#include "jkqtplotter/jkqtpgraphsbarchart.h" +#include "jkqtplotter/jkqtpgraphsstatisticsadaptors.h" +#include "jkqtcommon/jkqtpstatisticstools.h" +#include "jkqtcommon/jkqtpstringtools.h" +#include +#include +#include + + +int main(int argc, char* argv[]) +{ + QApplication app(argc, argv); + + + // 1. create a window with several plotters and get a pointer to the internal datastores (for convenience) + QWidget mainWidget; + QGridLayout* lay; + mainWidget.setLayout(lay=new QGridLayout); + JKQTPlotter* plotbarchart=new JKQTPlotter(&mainWidget); + plotbarchart->getPlotter()->setPlotLabel("Barcharts"); + JKQTPDatastore* datastore1=plotbarchart->getDatastore(); + lay->addWidget(plotbarchart,0,0); + JKQTPlotter* plotboxplot=new JKQTPlotter(datastore1, &mainWidget); + plotboxplot->getPlotter()->setPlotLabel("Boxplots"); + lay->addWidget(plotboxplot,0,1); + JKQTPlotter* plotscattererrors=new JKQTPlotter(datastore1, &mainWidget); + plotscattererrors->getPlotter()->setPlotLabel("Scatter Plot with Error Indicators"); + lay->addWidget(plotscattererrors,0,2); + lay->setColumnStretch(0,1); + lay->setColumnStretch(1,1); + lay->setColumnStretch(2,2); + + + + // 2. Barcharts from categorized data: + // 2.1. First we generate a dataset, which is going to be used for a barchart + // The datapoints consist of pairs , where the groups are encoded + // by the numbers 1,2,3 and in each group, several measurements are taken + size_t colBarRawGroup=datastore1->addColumn("barchart, rawdata, group"); + size_t colBarRawValue=datastore1->addColumn("barchart, rawdata, value"); + + // data for group 1 + datastore1->appendToColumns(colBarRawGroup, colBarRawValue, 1, 1.1); + datastore1->appendToColumns(colBarRawGroup, colBarRawValue, 1, 1.5); + datastore1->appendToColumns(colBarRawGroup, colBarRawValue, 1, 0.8); + datastore1->appendToColumns(colBarRawGroup, colBarRawValue, 1, 1.2); + datastore1->appendToColumns(colBarRawGroup, colBarRawValue, 1, 1.4); + // data for group 2 + datastore1->appendToColumns(colBarRawGroup, colBarRawValue, 2, 2.2); + datastore1->appendToColumns(colBarRawGroup, colBarRawValue, 2, 2.4); + datastore1->appendToColumns(colBarRawGroup, colBarRawValue, 2, 1.9); + datastore1->appendToColumns(colBarRawGroup, colBarRawValue, 2, 2.6); + datastore1->appendToColumns(colBarRawGroup, colBarRawValue, 2, 2.1); + // data for group 3 + datastore1->appendToColumns(colBarRawGroup, colBarRawValue, 3, 4.1); + datastore1->appendToColumns(colBarRawGroup, colBarRawValue, 3, 4.4); + datastore1->appendToColumns(colBarRawGroup, colBarRawValue, 3, 3.8); + datastore1->appendToColumns(colBarRawGroup, colBarRawValue, 3, 4.5); + datastore1->appendToColumns(colBarRawGroup, colBarRawValue, 3, 3.7); + datastore1->appendToColumns(colBarRawGroup, colBarRawValue, 3, 4.0); + // now some more datapoint, in mixed order + datastore1->appendToColumns(colBarRawGroup, colBarRawValue, 1, 0.9); + datastore1->appendToColumns(colBarRawGroup, colBarRawValue, 2, 2.3); + datastore1->appendToColumns(colBarRawGroup, colBarRawValue, 2, 2.0); + datastore1->appendToColumns(colBarRawGroup, colBarRawValue, 1, 1.0); + datastore1->appendToColumns(colBarRawGroup, colBarRawValue, 3, 4.2); + datastore1->appendToColumns(colBarRawGroup, colBarRawValue, 1, 1.25); + datastore1->appendToColumns(colBarRawGroup, colBarRawValue, 2, 2.35); + datastore1->appendToColumns(colBarRawGroup, colBarRawValue, 3, 3.7); + datastore1->appendToColumns(colBarRawGroup, colBarRawValue, 1, 0.75); + datastore1->appendToColumns(colBarRawGroup, colBarRawValue, 2, 1.85); + datastore1->appendToColumns(colBarRawGroup, colBarRawValue, 3, 4.5); + datastore1->appendToColumns(colBarRawGroup, colBarRawValue, 1, 0.95); + datastore1->appendToColumns(colBarRawGroup, colBarRawValue, 2, 1.65); + datastore1->appendToColumns(colBarRawGroup, colBarRawValue, 3, 4.1); + datastore1->appendToColumns(colBarRawGroup, colBarRawValue, 1, 1.15); + datastore1->appendToColumns(colBarRawGroup, colBarRawValue, 2, 2.15); + + + // 2.2. This dataset can be visualized with a simple scatter plot: + JKQTPXYLineGraph* gScatterForBar; + plotbarchart->addGraph(gScatterForBar=new JKQTPXYLineGraph(plotbarchart)); + gScatterForBar->setXYColumns(colBarRawGroup, colBarRawValue); + gScatterForBar->setDrawLine(false); + gScatterForBar->setSymbolType(JKQTPCross); + gScatterForBar->setSymbolSize(5); + gScatterForBar->setSymbolColor(QColorWithAlphaF(QColor("red"), 0.5)); + + // 2.3. Now we want to draw a barchart for every group, which indicates the + // average in each group. This is done using methods from the statistics + // library. + // First we need to group the data using jkqtpstatGroupData(), which assembles + // the data points in each group groupeddataBar + std::map > groupeddataBar; + jkqtpstatGroupData(datastore1->begin(colBarRawGroup), datastore1->end(colBarRawGroup), + datastore1->begin(colBarRawValue), datastore1->end(colBarRawValue), + groupeddataBar); + // now we can calculate the statistics for each group separately: + // Data is collected in two new columns + size_t colBarGroup=datastore1->addColumn("barchart, group"); + size_t colBarAverage=datastore1->addColumn("barchart, group-average"); + size_t colBarStdDev=datastore1->addColumn("barchart, group-stddev"); + // Statistics is calculated by simply iterating over groupeddataBar + // and calling functions like jkqtpstatAverage() for each group + for (auto it=groupeddataBar.begin(); it!=groupeddataBar.end(); ++it) { + datastore1->appendToColumn(colBarGroup, it->first); + datastore1->appendToColumn(colBarAverage, jkqtpstatAverage(it->second.begin(), it->second.end())); + datastore1->appendToColumn(colBarStdDev, jkqtpstatStdDev(it->second.begin(), it->second.end())); + } + + // 2.4. Finally the calculated groups are drawn + JKQTPBarVerticalErrorGraph* gBar; + plotbarchart->addGraph(gBar=new JKQTPBarVerticalErrorGraph(plotbarchart)); + gBar->setXYColumns(colBarGroup, colBarAverage); + gBar->setYErrorColumn(static_cast(colBarStdDev)); + + // 2.5. With the methods above we can also calculate more advanced statistics, like e.g. boxplots: + size_t colBarMedian=datastore1->addColumn("barchart, group-median"); + size_t colBarMin=datastore1->addColumn("barchart, group-min"); + size_t colBarMax=datastore1->addColumn("barchart, group-max"); + size_t colBarQ25=datastore1->addColumn("barchart, group-Q25"); + size_t colBarQ75=datastore1->addColumn("barchart, group-Q75"); + for (auto it=groupeddataBar.begin(); it!=groupeddataBar.end(); ++it) { + datastore1->appendToColumn(colBarMedian, jkqtpstatMedian(it->second.begin(), it->second.end())); + datastore1->appendToColumn(colBarMin, jkqtpstatMinimum(it->second.begin(), it->second.end())); + datastore1->appendToColumn(colBarMax, jkqtpstatMaximum(it->second.begin(), it->second.end())); + datastore1->appendToColumn(colBarQ25, jkqtpstatQuantile(it->second.begin(), it->second.end(), 0.25)); + datastore1->appendToColumn(colBarQ75, jkqtpstatQuantile(it->second.begin(), it->second.end(), 0.75)); + } + // 2.6. The result can be plotted using JKQTPBoxplotVerticalGraph, which receives a column for each value class of the final plot: + JKQTPBoxplotVerticalGraph* gBoxplot; + plotboxplot->addGraph(gBoxplot=new JKQTPBoxplotVerticalGraph(plotboxplot)); + gBoxplot->setPositionColumn(colBarGroup); + gBoxplot->setMinColumn(colBarMin); + gBoxplot->setMaxColumn(colBarMax); + gBoxplot->setMedianColumn(colBarMedian); + gBoxplot->setPercentile25Column(colBarQ25); + gBoxplot->setPercentile75Column(colBarQ75); + // 2.7. In order to safe yo the typing of the code above, shortcuts in the form of adaptors exist: + /* + jkqtpstatAddYErrorBarGraph(plotbarchart->getPlotter(), + datastore1->begin(colBarRawGroup), datastore1->end(colBarRawGroup), + datastore1->begin(colBarRawValue), datastore1->end(colBarRawValue)); + jkqtpstatAddHBoxplotsAndOutliers(plotboxplot->getPlotter(), + datastore1->begin(colBarRawGroup), datastore1->end(colBarRawGroup), + datastore1->begin(colBarRawValue), datastore1->end(colBarRawValue)); + */ + + + + + + // 3. Scatterplots from categorized data: + // 3.1. First we generate a second dataset, which is going to be used for a scaterplot + // The datapoints consist of pairs , that are based on a parabula with random + // deviations, both in x- and y-direction + size_t colScatterRawX=datastore1->addColumn("scatterplot, rawdata, x"); + size_t colScatterRawY=datastore1->addColumn("scatterplot, rawdata, y"); + std::random_device rd; // random number generators: + std::mt19937 gen{rd()}; + std::normal_distribution<> d1{0,0.5}; + const size_t N=100; + const double xmax=3.5; + for (size_t i=0; i(i)-static_cast(N)/2.0)*xmax/(static_cast(N)/2.0); + const double y=jkqtp_sqr(x)+2.0; + datastore1->appendToColumns(colScatterRawX, colScatterRawY, x+d1(gen), y+d1(gen)); + } + // 3.2. Now we can also add the raw dataset to the plot for visualization: + JKQTPXYParametrizedScatterGraph* gScatterRaw; + plotscattererrors->addGraph(gScatterRaw=new JKQTPXYParametrizedScatterGraph(plotscattererrors)); + gScatterRaw->setXYColumns(colScatterRawX, colScatterRawY); + gScatterRaw->setDrawLine(false); + gScatterRaw->setSymbolType(JKQTPCross); + gScatterRaw->setSymbolSize(5); + + // 3.3. Now we want to draw a scatterchart of the data, where data-points should be grouped + // together, in x-intervals of width 0.5. From all the points in each interval, we calculate the + // in both x- and y-direction the average and standard deviation. + // First we need to group the data using jkqtpstatGroupData(), which assembles + // the data points in each group groupeddataScatter. For the custom grouping of the datapoints + // we use the optional functor provided to jkqtpstatGroupData(): We use jkqtpstatGroupingCustomRound1D() + // with given parameters 0.25 for the (center) location of the first bin and bin width 0.5. The functor + // is not built by hand (which would be possible using std::bind), but with the generator function + // jkqtpstatMakeGroupingCustomRound1D(). + // in addition we use a variant of jkqtpstatGroupData(), which outputs a column with the category + // assigned to every data pair in the input data range + std::map,std::vector > > groupeddataScatter; + size_t colScatterRawGroup=datastore1->addColumn("scatterplot, rawdata, assigned-group"); + jkqtpstatGroupData(datastore1->begin(colScatterRawX), datastore1->end(colScatterRawX), + datastore1->begin(colScatterRawY), datastore1->end(colScatterRawY), + datastore1->backInserter(colScatterRawGroup), + groupeddataScatter, + jkqtpstatMakeGroupingCustomRound1D(0.25, 0.5)); + // The column colScatterRawGroup can now be used to color the scatter graph: + gScatterRaw->setColorColumn(colScatterRawGroup); + // now we can calculate the statistics for each group separately: + // Data is collected in two new columns + size_t colScatterXAvg=datastore1->addColumn("scatter, x, average"); + size_t colScatterXStd=datastore1->addColumn("scatter, x, stddev"); + size_t colScatterYAvg=datastore1->addColumn("scatter, y, average"); + size_t colScatterYStd=datastore1->addColumn("scatter, y, stddev"); + // Statistics is calculated by simply iterating over groupeddataScatter + // and calling functions like jkqtpstatAverage() for each group + for (auto it=groupeddataScatter.begin(); it!=groupeddataScatter.end(); ++it) { + datastore1->appendToColumn(colScatterXAvg, jkqtpstatAverage(it->second.first.begin(), it->second.first.end())); + datastore1->appendToColumn(colScatterXStd, jkqtpstatStdDev(it->second.first.begin(), it->second.first.end())); + datastore1->appendToColumn(colScatterYAvg, jkqtpstatAverage(it->second.second.begin(), it->second.second.end())); + datastore1->appendToColumn(colScatterYStd, jkqtpstatStdDev(it->second.second.begin(), it->second.second.end())); + } + + // 3.4. Finally the calculated groups are drawn + JKQTPXYLineErrorGraph* gScatterErr; + plotscattererrors->addGraph(gScatterErr=new JKQTPXYLineErrorGraph(plotscattererrors)); + gScatterErr->setXYColumns(colScatterXAvg, colScatterYAvg); + gScatterErr->setXErrorColumn(static_cast(colScatterXStd)); + gScatterErr->setYErrorColumn(static_cast(colScatterYStd)); + gScatterErr->setSymbolType(JKQTPFilledTriangle); + gScatterErr->setDrawLine(false); + + // 3.5. also here an adaptor exists, which makes the task easier: + /* + jkqtpstatAddXYErrorLineGraph(plotscattererrors->getPlotter(), + datastore1->begin(colScatterRawX), datastore1->end(colScatterRawX), + datastore1->begin(colScatterRawY), datastore1->end(colScatterRawY), + jkqtpstatMakeGroupingCustomRound1D(0.25, 0.5)); + */ + + + + + // autoscale the plot so the graph is contained + plotboxplot->synchronizeToMaster(plotbarchart, JKQTBasePlotter::sdXYAxes); + plotboxplot->zoomToFit(); + plotboxplot->setGrid(false); + plotboxplot->setShowZeroAxes(false); + plotboxplot->getPlotter()->setKeyBackgroundColor(QColorWithAlphaF("white", 0.25), Qt::SolidPattern); + plotbarchart->setAbsoluteY(0,5); + plotboxplot->setAbsoluteY(0,5); + plotbarchart->zoomToFit(); + plotbarchart->setGrid(false); + plotbarchart->setShowZeroAxes(false); + plotbarchart->getPlotter()->setKeyBackgroundColor(QColorWithAlphaF("white", 0.25), Qt::SolidPattern); + plotbarchart->moveGraphTop(gScatterForBar); + plotscattererrors->zoomToFit(); + plotscattererrors->setGrid(false); + plotscattererrors->setShowZeroAxes(false); + plotscattererrors->getPlotter()->setKeyBackgroundColor(QColorWithAlphaF("white", 0.25), Qt::SolidPattern); + + + // show plotter and make it a decent size + mainWidget.show(); + mainWidget.resize(1200,400); + + return app.exec(); +} diff --git a/examples/simpletest_datastore_groupedstat/jkqtplotter_simpletest_datastore_groupedstat.pro b/examples/simpletest_datastore_groupedstat/jkqtplotter_simpletest_datastore_groupedstat.pro new file mode 100644 index 0000000000..817ebf4752 --- /dev/null +++ b/examples/simpletest_datastore_groupedstat/jkqtplotter_simpletest_datastore_groupedstat.pro @@ -0,0 +1,27 @@ +# source code for this simple demo +SOURCES = jkqtplotter_simpletest_datastore_groupedstat.cpp + +# configure Qt +CONFIG += link_prl qt +QT += core gui xml svg +greaterThan(QT_MAJOR_VERSION, 4): QT += widgets printsupport + +# output executable name +TARGET = jkqtplotter_simpletest_datastore_groupedstat + +# include JKQTPlotter source headers and link against library +DEPENDPATH += ../../lib ../../staticlib/jkqtplotterlib +INCLUDEPATH += ../../lib +CONFIG (debug, debug|release) { + LIBS += -L../../staticlib/jkqtplotterlib/debug -ljkqtplotterlib_debug +} else { + LIBS += -L../../staticlib/jkqtplotterlib/release -ljkqtplotterlib +} +message("LIBS = $$LIBS") + +win32-msvc*: DEFINES += _USE_MATH_DEFINES +win32-msvc*: DEFINES += NOMINMAX + + + + diff --git a/examples/simpletest_datastore_groupedstat/jkqtplotter_simpletest_datastore_groupedstat_and_lib.pro b/examples/simpletest_datastore_groupedstat/jkqtplotter_simpletest_datastore_groupedstat_and_lib.pro new file mode 100644 index 0000000000..f3d7e69339 --- /dev/null +++ b/examples/simpletest_datastore_groupedstat/jkqtplotter_simpletest_datastore_groupedstat_and_lib.pro @@ -0,0 +1,8 @@ +TEMPLATE = subdirs + +SUBDIRS += jkqtplotterlib jkqtplotter_simpletest_datastore_groupedstat + +jkqtplotterlib.file = ../../staticlib/jkqtplotterlib/jkqtplotterlib.pro + +jkqtplotter_simpletest_datastore_groupedstat.file=$$PWD/jkqtplotter_simpletest_datastore_groupedstat.pro +jkqtplotter_simpletest_datastore_groupedstat.depends = jkqtplotterlib diff --git a/examples/simpletest_datastore_iterators/README.md b/examples/simpletest_datastore_iterators/README.md index e9d7f6537e..407f5e0e25 100644 --- a/examples/simpletest_datastore_iterators/README.md +++ b/examples/simpletest_datastore_iterators/README.md @@ -1,10 +1,10 @@ # Tutorial (JKQTPDatastore): Iterator-Based usage of JKQTPDatastore {#JKQTPlotterBasicJKQTPDatastoreIterators} - [JKQTPlotterBasicJKQTPDatastore]: @ref JKQTPlotterBasicJKQTPDatastore "Basic Usage of JKQTPDatastore" [JKQTPlotterBasicJKQTPDatastoreIterators]: @ref JKQTPlotterBasicJKQTPDatastoreIterators "Iterator-Based usage of JKQTPDatastore" [JKQTPlotterBasicJKQTPDatastoreStatistics]: @ref JKQTPlotterBasicJKQTPDatastoreStatistics "Advanced 1-Dimensional Statistics with JKQTPDatastore" [JKQTPlotterBasicJKQTPDatastoreRegression]: @ref JKQTPlotterBasicJKQTPDatastoreRegression "Regression Analysis (with the Statistics Library)" +[JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat]: @ref JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat "1-Dimensional Group Statistics with JKQTPDatastore" [JKQTPlotterBasicJKQTPDatastoreStatistics2D]: @ref JKQTPlotterBasicJKQTPDatastoreStatistics2D "Advanced 2-Dimensional Statistics with JKQTPDatastore" [statisticslibrary]: @ref jkqtptools_math_statistics "JKQTPlotter Statistics Library" @@ -14,8 +14,10 @@ This tutorial project (see `./examples/simpletest_datastore_iterators/`) explain - [JKQTPlotterBasicJKQTPDatastore] - [JKQTPlotterBasicJKQTPDatastoreIterators] - [JKQTPlotterBasicJKQTPDatastoreStatistics] - - [JKQTPlotterBasicJKQTPDatastoreStatistics2D] - [JKQTPlotterBasicJKQTPDatastoreRegression] + - [JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat] + - [JKQTPlotterBasicJKQTPDatastoreStatistics2D] + diff --git a/examples/simpletest_datastore_regression/README.md b/examples/simpletest_datastore_regression/README.md index 4a98a9a1b3..d7d2aab14c 100644 --- a/examples/simpletest_datastore_regression/README.md +++ b/examples/simpletest_datastore_regression/README.md @@ -4,18 +4,19 @@ [JKQTPlotterBasicJKQTPDatastoreIterators]: @ref JKQTPlotterBasicJKQTPDatastoreIterators "Iterator-Based usage of JKQTPDatastore" [JKQTPlotterBasicJKQTPDatastoreStatistics]: @ref JKQTPlotterBasicJKQTPDatastoreStatistics "Advanced 1-Dimensional Statistics with JKQTPDatastore" [JKQTPlotterBasicJKQTPDatastoreRegression]: @ref JKQTPlotterBasicJKQTPDatastoreRegression "Regression Analysis (with the Statistics Library)" +[JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat]: @ref JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat "1-Dimensional Group Statistics with JKQTPDatastore" [JKQTPlotterBasicJKQTPDatastoreStatistics2D]: @ref JKQTPlotterBasicJKQTPDatastoreStatistics2D "Advanced 2-Dimensional Statistics with JKQTPDatastore" [statisticslibrary]: @ref jkqtptools_math_statistics "JKQTPlotter Statistics Library" - This tutorial project (see `./examples/simpletest_datastore_statistics/`) explains several advanced functions of JKQTPDatastore in combination with the [[statisticslibrary]] conatined in JKQTPlotter. ***Note*** that there are additional tutorial explaining other aspects of data mangement in JKQTPDatastore: - [JKQTPlotterBasicJKQTPDatastore] - [JKQTPlotterBasicJKQTPDatastoreIterators] - [JKQTPlotterBasicJKQTPDatastoreStatistics] - - [JKQTPlotterBasicJKQTPDatastoreStatistics2D] - [JKQTPlotterBasicJKQTPDatastoreRegression] + - [JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat] + - [JKQTPlotterBasicJKQTPDatastoreStatistics2D] [TOC] diff --git a/examples/simpletest_datastore_statistics/README.md b/examples/simpletest_datastore_statistics/README.md index 1addbfbb48..1bacdbb887 100644 --- a/examples/simpletest_datastore_statistics/README.md +++ b/examples/simpletest_datastore_statistics/README.md @@ -1,22 +1,22 @@ # Tutorial (JKQTPDatastore): Advanced 1-Dimensional Statistics with JKQTPDatastore {#JKQTPlotterBasicJKQTPDatastoreStatistics} - [JKQTPlotterBasicJKQTPDatastore]: @ref JKQTPlotterBasicJKQTPDatastore "Basic Usage of JKQTPDatastore" [JKQTPlotterBasicJKQTPDatastoreIterators]: @ref JKQTPlotterBasicJKQTPDatastoreIterators "Iterator-Based usage of JKQTPDatastore" [JKQTPlotterBasicJKQTPDatastoreStatistics]: @ref JKQTPlotterBasicJKQTPDatastoreStatistics "Advanced 1-Dimensional Statistics with JKQTPDatastore" [JKQTPlotterBasicJKQTPDatastoreRegression]: @ref JKQTPlotterBasicJKQTPDatastoreRegression "Regression Analysis (with the Statistics Library)" +[JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat]: @ref JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat "1-Dimensional Group Statistics with JKQTPDatastore" [JKQTPlotterBasicJKQTPDatastoreStatistics2D]: @ref JKQTPlotterBasicJKQTPDatastoreStatistics2D "Advanced 2-Dimensional Statistics with JKQTPDatastore" [statisticslibrary]: @ref jkqtptools_math_statistics "JKQTPlotter Statistics Library" - This tutorial project (see `./examples/simpletest_datastore_statistics/`) explains several advanced functions of JKQTPDatastore in combination with the [[statisticslibrary]] conatined in JKQTPlotter. ***Note*** that there are additional tutorial explaining other aspects of data mangement in JKQTPDatastore: - [JKQTPlotterBasicJKQTPDatastore] - [JKQTPlotterBasicJKQTPDatastoreIterators] - [JKQTPlotterBasicJKQTPDatastoreStatistics] - - [JKQTPlotterBasicJKQTPDatastoreStatistics2D] - [JKQTPlotterBasicJKQTPDatastoreRegression] + - [JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat] + - [JKQTPlotterBasicJKQTPDatastoreStatistics2D] [TOC] diff --git a/examples/simpletest_datastore_statistics_2d/README.md b/examples/simpletest_datastore_statistics_2d/README.md index 5007a28edd..124ba0d450 100644 --- a/examples/simpletest_datastore_statistics_2d/README.md +++ b/examples/simpletest_datastore_statistics_2d/README.md @@ -1,22 +1,22 @@ # Tutorial (JKQTPDatastore): Advanced 2-Dimensional Statistics with JKQTPDatastore {#JKQTPlotterBasicJKQTPDatastoreStatistics2D} - [JKQTPlotterBasicJKQTPDatastore]: @ref JKQTPlotterBasicJKQTPDatastore "Basic Usage of JKQTPDatastore" [JKQTPlotterBasicJKQTPDatastoreIterators]: @ref JKQTPlotterBasicJKQTPDatastoreIterators "Iterator-Based usage of JKQTPDatastore" [JKQTPlotterBasicJKQTPDatastoreStatistics]: @ref JKQTPlotterBasicJKQTPDatastoreStatistics "Advanced 1-Dimensional Statistics with JKQTPDatastore" [JKQTPlotterBasicJKQTPDatastoreRegression]: @ref JKQTPlotterBasicJKQTPDatastoreRegression "Regression Analysis (with the Statistics Library)" +[JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat]: @ref JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat "1-Dimensional Group Statistics with JKQTPDatastore" [JKQTPlotterBasicJKQTPDatastoreStatistics2D]: @ref JKQTPlotterBasicJKQTPDatastoreStatistics2D "Advanced 2-Dimensional Statistics with JKQTPDatastore" [statisticslibrary]: @ref jkqtptools_math_statistics "JKQTPlotter Statistics Library" - This tutorial project (see `./examples/simpletest_datastore_statistics_2d/`) explains several advanced functions of JKQTPDatastore in combination with the [[statisticslibrary]] conatined in JKQTPlotter. ***Note*** that there are additional tutorial explaining other aspects of data mangement in JKQTPDatastore: - [JKQTPlotterBasicJKQTPDatastore] - [JKQTPlotterBasicJKQTPDatastoreIterators] - [JKQTPlotterBasicJKQTPDatastoreStatistics] - - [JKQTPlotterBasicJKQTPDatastoreStatistics2D] - [JKQTPlotterBasicJKQTPDatastoreRegression] + - [JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat] + - [JKQTPlotterBasicJKQTPDatastoreStatistics2D] [TOC] diff --git a/examples/simpletest_errorbarstyles/README.md b/examples/simpletest_errorbarstyles/README.md index e8d28528a1..5ab4aa8ba7 100644 --- a/examples/simpletest_errorbarstyles/README.md +++ b/examples/simpletest_errorbarstyles/README.md @@ -1,6 +1,11 @@ # Example (JKQTPlotter): Different Types of Errorindicators {#JKQTPlotterErrorBarStyles} This project (see `./examples/simpletest_errorbarstyles/`) simply creates a JKQTPlotter widget (as a new window) and adds several curves show-casing different styles of error indicators. Data is initialized from two QVector objects. +[JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat]: @ref JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat "1-Dimensional Group Statistics with JKQTPDatastore" +[statisticslibrary]: @ref jkqtptools_math_statistics "JKQTPlotter Statistics Library" + +***Note:*** This examples explains how to plot graphs with error indicators, when the data has already been calculated. The tutorial [JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat] explains one way how to use the [statisticslibrary] in order to calculate the errors from data. + The source code of the main application can be found in [`jkqtplotter_simpletest_errorbarstyles.cpp`](https://github.com/jkriege2/JKQtPlotter/tree/master/examples/simpletest_errorbarstyles/jkqtplotter_simpletest_errorbarstyles.cpp). First some data is added to the internal datastore (mostly, like explained in several other examples, like e.g. [Line Graph with Different Symbols and Line Styles](https://github.com/jkriege2/JKQtPlotter/tree/master/examples/simpletest_symbols_and_styles)). The (in a loop) several graphs are added, each with a distinct style for its error indicators: diff --git a/examples/test_distributionplot/README.md b/examples/test_distributionplot/README.md index ffaca77491..c833b9b974 100644 --- a/examples/test_distributionplot/README.md +++ b/examples/test_distributionplot/README.md @@ -1,6 +1,19 @@ # Example (JKQTPlotter): Plotting a Statistical Distribution of Data {#JKQTPlotterDistributionPlot} This project (see [`test_distributionplot`](https://github.com/jkriege2/JKQtPlotter/tree/master/examples/test_distributionplot) demonstrates how to combine several different graphs and geometric elements to show a set of random values and their statistics. + +[JKQTPlotterBasicJKQTPDatastore]: @ref JKQTPlotterBasicJKQTPDatastore "Basic Usage of JKQTPDatastore" +[JKQTPlotterBasicJKQTPDatastoreIterators]: @ref JKQTPlotterBasicJKQTPDatastoreIterators "Iterator-Based usage of JKQTPDatastore" +[JKQTPlotterBasicJKQTPDatastoreStatistics]: @ref JKQTPlotterBasicJKQTPDatastoreStatistics "Advanced 1-Dimensional Statistics with JKQTPDatastore" +[JKQTPlotterBasicJKQTPDatastoreRegression]: @ref JKQTPlotterBasicJKQTPDatastoreRegression "Regression Analysis (with the Statistics Library)" +[JKQTPlotterBasicJKQTPDatastoreStatistics2D]: @ref JKQTPlotterBasicJKQTPDatastoreStatistics2D "Advanced 2-Dimensional Statistics with JKQTPDatastore" +[statisticslibrary]: @ref jkqtptools_math_statistics "JKQTPlotter Statistics Library" +[JKQTPlotterBoxplotStyling]: @ref JKQTPlotterBoxplotStyling "Styling different aspects of boxplots" +[JKQTPlotterBoxplotsGraphs]: @ref JKQTPlotterBoxplotsGraphs "Boxplots" + +***Note*** that this example explains how to generate the statistics plots by hand, i.e. by calculating all the statistical properties for the boxplots and then adding the necessary graphs. The internal [statisticslibrary] offers methods to perform these calculations, which are explained in the tutorial [JKQTPlotterBasicJKQTPDatastoreStatistics] in detail. Several examples give more details on boxplots: [JKQTPlotterBoxplotsGraphs], [JKQTPlotterBoxplotStyling]. + + The source code of the main application is (see [`test_distributionplot.cpp`](test_distributionplot.cpp). After adding all necessary data to the JKQTDatastore: diff --git a/examples/test_styledboxplot/README.md b/examples/test_styledboxplot/README.md index 6b967df6fe..0c8a782e64 100644 --- a/examples/test_styledboxplot/README.md +++ b/examples/test_styledboxplot/README.md @@ -1,6 +1,19 @@ # Example (JKQTPlotter): Styling different aspects of boxplots {#JKQTPlotterBoxplotStyling} This project (see [`test_styledboxplot`](https://github.com/jkriege2/JKQtPlotter/tree/master/examples/test_styledboxplot) demonstrates how to style different aspects of boxplots and how to draw different types and styles of boxplots. For a simple introduction into how to use boxplots, see [Plotting Box Plots](https://github.com/jkriege2/JKQtPlotter/tree/master/examples/simpletest_boxplot) and [Plotting a Statistical Distribution of Data](https://github.com/jkriege2/JKQtPlotter/tree/master/examples/test_distributionplot). +[JKQTPlotterBasicJKQTPDatastore]: @ref JKQTPlotterBasicJKQTPDatastore "Basic Usage of JKQTPDatastore" +[JKQTPlotterBasicJKQTPDatastoreIterators]: @ref JKQTPlotterBasicJKQTPDatastoreIterators "Iterator-Based usage of JKQTPDatastore" +[JKQTPlotterBasicJKQTPDatastoreStatistics]: @ref JKQTPlotterBasicJKQTPDatastoreStatistics "Advanced 1-Dimensional Statistics with JKQTPDatastore" +[JKQTPlotterBasicJKQTPDatastoreRegression]: @ref JKQTPlotterBasicJKQTPDatastoreRegression "Regression Analysis (with the Statistics Library)" +[JKQTPlotterBasicJKQTPDatastoreStatistics2D]: @ref JKQTPlotterBasicJKQTPDatastoreStatistics2D "Advanced 2-Dimensional Statistics with JKQTPDatastore" +[statisticslibrary]: @ref jkqtptools_math_statistics "JKQTPlotter Statistics Library" +[JKQTPlotterBoxplotStyling]: @ref JKQTPlotterBoxplotStyling "Styling different aspects of boxplots" +[JKQTPlotterBoxplotsGraphs]: @ref JKQTPlotterBoxplotsGraphs "Boxplots" + +***Note*** that this example explains how to style boxplots. The example [JKQTPlotterBoxplotsGraphs] explains basics of how to create boxplots. The internal [statisticslibrary] offers methods to calculate the statistical properties necessary for boxplots, which is explained in the tutorial [JKQTPlotterBasicJKQTPDatastoreStatistics] in detail. + + + The link http://vita.had.co.nz/papers/boxplots.pdf leads to a paper that described the history and different types of boxplots. The source code of the main application can be found in [`test_styledboxplot.cpp`](test_styledboxplot.cpp). diff --git a/lib/jkqtcommon/jkqtpstatbasics.h b/lib/jkqtcommon/jkqtpstatbasics.h index 4021f38220..9ecf319ccd 100644 --- a/lib/jkqtcommon/jkqtpstatbasics.h +++ b/lib/jkqtcommon/jkqtpstatbasics.h @@ -40,9 +40,6 @@ #include "jkqtcommon/jkqtpdebuggingtools.h" - - - /*! \brief calculates the average of a given data range \a first ... \a last \ingroup jkqtptools_math_statistics_basic @@ -78,6 +75,8 @@ inline double jkqtpstatAverage(InputIt first, InputIt last, size_t* Noutput=null + + /*! \brief calculates the weighted average of a given data range \a first ... \a last \ingroup jkqtptools_math_statistics_basic diff --git a/lib/jkqtcommon/jkqtpstatisticstools.cpp b/lib/jkqtcommon/jkqtpstatgrouped.cpp similarity index 58% rename from lib/jkqtcommon/jkqtpstatisticstools.cpp rename to lib/jkqtcommon/jkqtpstatgrouped.cpp index d9fccf079c..422ad0eadf 100644 --- a/lib/jkqtcommon/jkqtpstatisticstools.cpp +++ b/lib/jkqtcommon/jkqtpstatgrouped.cpp @@ -19,4 +19,22 @@ -#include "jkqtpstatisticstools.h" +#include "jkqtpstatgrouped.h" + +double jkqtpstatGroupingIdentity1D(double v) { + return v; +} + +double jkqtpstatGroupingRound1D(double v) { + return round(v); +} + +double jkqtpstatGroupingCustomRound1D(double v, double firstGroupCenter, double groupWidth) { + return round((v-firstGroupCenter)/(2.0*groupWidth)); +} + + +JKQTPStatGroupDefinitionFunctor1D jkqtpstatMakeGroupingCustomRound1D(double firstGroupCenter, double groupWidth) +{ + return std::bind(&jkqtpstatGroupingCustomRound1D, std::placeholders::_1, firstGroupCenter, groupWidth); +} diff --git a/lib/jkqtcommon/jkqtpstatgrouped.h b/lib/jkqtcommon/jkqtpstatgrouped.h new file mode 100644 index 0000000000..4009a15e40 --- /dev/null +++ b/lib/jkqtcommon/jkqtpstatgrouped.h @@ -0,0 +1,237 @@ +/* + Copyright (c) 2008-2019 Jan W. Krieger () + + last modification: $LastChangedDate$ (revision $Rev$) + + This software is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License (LGPL) as published by + the Free Software Foundation, either version 2.1 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License (LGPL) for more details. + + You should have received a copy of the GNU Lesser General Public License (LGPL) + along with this program. If not, see . +*/ + + +#ifndef JKQTPSTATGROUPED_H_INCLUDED +#define JKQTPSTATGROUPED_H_INCLUDED + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "jkqtcommon/jkqtp_imexport.h" +#include "jkqtcommon/jkqtplinalgtools.h" +#include "jkqtcommon/jkqtparraytools.h" +#include "jkqtcommon/jkqtpdebuggingtools.h" +#include "jkqtcommon/jkqtpstatbasics.h" + +/*! \brief a functor \f$ f(x): \mathbb{R}\rightarrow\mathbb{R} \f$ which assignes a value \f$ x \f$ to a group center \f$ f(x) \f$ + \ingroup jkqtptools_math_statistics_grouped + + The simplest version are e.g. + - jkqtpstatGroupingIdentity1D() + - jkqtpstatGroupingRound1D() + - jkqtpstatGroupingCustomRound1D() + . + + \see jkqtpstatGroupData +*/ +typedef std::function JKQTPStatGroupDefinitionFunctor1D; + + + +/*! \brief use a column value as group ID directly + \ingroup jkqtptools_math_statistics_grouped + + \see JKQTPStatGroupDefinitionFunctor1D +*/ +double jkqtpstatGroupingIdentity1D(double v); +/*! \brief use a rounded column value as group ID directly \f$ f(x)=\mbox{round}(x) \f$ + \ingroup jkqtptools_math_statistics_grouped + + \see JKQTPStatGroupDefinitionFunctor1D +*/ +double jkqtpstatGroupingRound1D(double v); +/*! \brief assign each value to groups \f$ \mbox{firstGroupCenter}, \mbox{firstGroupCenter}\pm\mbox{groupWidth}/2, \mbox{firstGroupCenter}\pm2\cdot\mbox{groupWidth}/2, , \mbox{firstGroupCenter}\pm3\cdot\mbox{groupWidth}/2, ... \f$ + \ingroup jkqtptools_math_statistics_grouped + + This is equivalent to \f$ \mbox{round}\left(\frac{x-\mbox{firstGroupCenter}}{\mbox{groupWidth}/2}\right) \f$ + + \see JKQTPStatGroupDefinitionFunctor1D, jkqtpstatMakeGroupingCustomRound1D() for a factory-function that returns a functor of this function bound to specific arguments. +*/ +double jkqtpstatGroupingCustomRound1D(double v, double firstGroupCenter, double groupWidth); +/*! \brief generates a functor of jkqtpstatGroupingCustomRound1D() with the two paramaters \a firstGroupCenter and \a groupWidth fixed to the given values + \ingroup jkqtptools_math_statistics_grouped + + This is equivalent to \c std::bind(&jkqtpstatGroupingCustomRound1D,std::placeholders::_1,firstGroupCenter,groupWidth); + + \see JKQTPStatGroupDefinitionFunctor1D, jkqtpstatGroupingCustomRound1D() +*/ +JKQTPStatGroupDefinitionFunctor1D jkqtpstatMakeGroupingCustomRound1D(double firstGroupCenter, double groupWidth); + + + +/*! \brief groups data from an input range \a inFirstCat / \a inFirstValue ... \a inLastCat / \a outFirstCat representing pairs \f$ (c_i,v_i) \f$ of a + category value \f$ c_i \f$ and a group value \f$ v_i \f$ into groups \f$ V_j=\{v_{i}|c_i\equiv c_{\text{out},j}\} \f$ of data that were assigned + to the same group, i.e. \f$ c_i\equiv c_{\text{out},j} \f$ . A functor \a groupDefFunc assigns a group \f$ c_{\text{out},j} \f$ to each category value \f$ c_i \f$ . + \ingroup jkqtptools_math_statistics_grouped + + \tparam InputCatIt standard iterator type of \a inFirstCat and \a inLastCat + \tparam InputValueIt standard iterator type of \a inFirstValue and \a inLastValue + \param inFirstCat iterator pointing to the first item in the category dataset to use \f$ c_1 \f$ + \param inLastCat iterator pointing behind the last item in the category dataset to use \f$ c_N \f$ + \param inFirstValue iterator pointing to the first item in the category dataset to use \f$ v_1 \f$ + \param inLastValue iterator pointing behind the last item in the category dataset to use \f$ v_N \f$ + \param[out] groupeddata receives the grouped data, each key in the map represents one group, each map-value contains a vector with the value data + \param groupDefFunc assigns a group \f$ c_{\text{out},j} \f$ to each category value \f$ c_i \f$ . + + \note the contents of \a groupeddata is not cleared before usage, so you can also use this fucntion to append to a group! + + \see JKQTPStatGroupDefinitionFunctor1D, \ref JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat +*/ +template +inline void jkqtpstatGroupData(InputCatIt inFirstCat, InputCatIt inLastCat, InputValueIt inFirstValue, InputValueIt inLastValue, std::map >& groupeddata, JKQTPStatGroupDefinitionFunctor1D groupDefFunc=&jkqtpstatGroupingIdentity1D) { + auto inCat=inFirstCat; + auto inVal=inFirstValue; + for (; inCat!=inLastCat && inVal!=inLastValue; ++inCat, ++inVal) { + const double c=jkqtp_todouble(*inCat); + const double v=jkqtp_todouble(*inVal); + if (JKQTPIsOKFloat(c) && JKQTPIsOKFloat(v)) { + const double g=groupDefFunc(c); + groupeddata[g].push_back(v); + } + } +} + +/*! \brief groups data from an input range \a inFirstCat / \a inFirstValue ... \a inLastCat / \a outFirstCat representing pairs \f$ (c_i,v_i) \f$ of a + category value \f$ c_i \f$ and a group value \f$ v_i \f$ into groups \f$ V_j=\{v_{i}|c_i\equiv c_{\text{out},j}\} \f$ of data that were assigned + to the same group, i.e. \f$ c_i\equiv c_{\text{out},j} \f$ . A functor \a groupDefFunc assigns a group \f$ c_{\text{out},j} \f$ to each category value \f$ c_i \f$ . + \ingroup jkqtptools_math_statistics_grouped + + \tparam InputCatIt standard iterator type of \a inFirstCat and \a inLastCat + \tparam InputValueIt standard iterator type of \a inFirstValue and \a inLastValue + \tparam OutputGroupIt standard output iterator type of \a outFirstCategory + \param inFirstCat iterator pointing to the first item in the category dataset to use \f$ c_1 \f$ + \param inLastCat iterator pointing behind the last item in the category dataset to use \f$ c_N \f$ + \param inFirstValue iterator pointing to the first item in the category dataset to use \f$ v_1 \f$ + \param inLastValue iterator pointing behind the last item in the category dataset to use \f$ v_N \f$ + \param[out] outFirstCategory for each element in the range \a inFirstCat / \a inFirstValue ... \a inLastCat / \a outFirstCat this receives the calculated category + \param[out] groupeddata receives the grouped data, each key in the map represents one group, each map-value contains a vector with the value data + \param groupDefFunc assigns a group \f$ c_{\text{out},j} \f$ to each category value \f$ c_i \f$ . + + \note the contents of \a groupeddata is not cleared before usage, so you can also use this fucntion to append to a group! + + \see JKQTPStatGroupDefinitionFunctor1D, \ref JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat +*/ +template +inline void jkqtpstatGroupData(InputCatIt inFirstCat, InputCatIt inLastCat, InputValueIt inFirstValue, InputValueIt inLastValue, OutputGroupIt outFirstCategory, std::map >& groupeddata, JKQTPStatGroupDefinitionFunctor1D groupDefFunc=&jkqtpstatGroupingIdentity1D) { + auto inCat=inFirstCat; + auto inVal=inFirstValue; + auto outCat=outFirstCategory; + for (; inCat!=inLastCat && inVal!=inLastValue; ++inCat, ++inVal) { + const double c=jkqtp_todouble(*inCat); + const double v=jkqtp_todouble(*inVal); + if (JKQTPIsOKFloat(c) && JKQTPIsOKFloat(v)) { + const double g=groupDefFunc(c); + groupeddata[g].push_back(v); + *outCat=g; + } else { + *outCat=JKQTP_DOUBLE_NAN; + } + ++outCat; + } +} + + +/*! \brief groups data from an input range \a inFirstCat / \a inFirstValue ... \a inLastCat / \a outFirstCat representing pairs \f$ (c_i,v_i) \f$ of a + category value \f$ c_i \f$ and a group value \f$ v_i \f$ into groups \f$ V_j=\{v_{i}|c_i\equiv c_{\text{out},j}\} \f$ of data that were assigned + to the same group, i.e. \f$ c_i\equiv c_{\text{out},j} \f$ . A functor \a groupDefFunc assigns a group \f$ c_{\text{out},j} \f$ to each category value \f$ c_i \f$ . + \ingroup jkqtptools_math_statistics_grouped + + \tparam InputCatIt standard iterator type of \a inFirstCat and \a inLastCat + \tparam InputValueIt standard iterator type of \a inFirstValue and \a inLastValue + \param inFirstCat iterator pointing to the first item in the category dataset to use \f$ c_1 \f$ + \param inLastCat iterator pointing behind the last item in the category dataset to use \f$ c_N \f$ + \param inFirstValue iterator pointing to the first item in the category dataset to use \f$ v_1 \f$ + \param inLastValue iterator pointing behind the last item in the category dataset to use \f$ v_N \f$ + \param[out] groupeddata receives the grouped data, each key in the map represents one group, each map-value contains two vecors with the category and value data respectively + \param groupDefFunc assigns a group \f$ c_{\text{out},j} \f$ to each category value \f$ c_i \f$ . + + \note the contents of \a groupeddata is not cleared before usage, so you can also use this fucntion to append to a group! + + \see JKQTPStatGroupDefinitionFunctor1D, \ref JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat +*/ +template +inline void jkqtpstatGroupData(InputCatIt inFirstCat, InputCatIt inLastCat, InputValueIt inFirstValue, InputValueIt inLastValue, std::map,std::vector > >& groupeddata, JKQTPStatGroupDefinitionFunctor1D groupDefFunc=&jkqtpstatGroupingIdentity1D) { + auto inCat=inFirstCat; + auto inVal=inFirstValue; + for (; inCat!=inLastCat && inVal!=inLastValue; ++inCat, ++inVal) { + const double c=jkqtp_todouble(*inCat); + const double v=jkqtp_todouble(*inVal); + if (JKQTPIsOKFloat(c) && JKQTPIsOKFloat(v)) { + const double g=groupDefFunc(c); + groupeddata[g].first.push_back(c); + groupeddata[g].second.push_back(v); + } + } +} + + +/*! \brief groups data from an input range \a inFirstCat / \a inFirstValue ... \a inLastCat / \a outFirstCat representing pairs \f$ (c_i,v_i) \f$ of a + category value \f$ c_i \f$ and a group value \f$ v_i \f$ into groups \f$ V_j=\{v_{i}|c_i\equiv c_{\text{out},j}\} \f$ of data that were assigned + to the same group, i.e. \f$ c_i\equiv c_{\text{out},j} \f$ . A functor \a groupDefFunc assigns a group \f$ c_{\text{out},j} \f$ to each category value \f$ c_i \f$ . + \ingroup jkqtptools_math_statistics_grouped + + \tparam InputCatIt standard iterator type of \a inFirstCat and \a inLastCat + \tparam InputValueIt standard iterator type of \a inFirstValue and \a inLastValue + \tparam OutputGroupIt standard output iterator type of \a outFirstCategory + \param inFirstCat iterator pointing to the first item in the category dataset to use \f$ c_1 \f$ + \param inLastCat iterator pointing behind the last item in the category dataset to use \f$ c_N \f$ + \param inFirstValue iterator pointing to the first item in the category dataset to use \f$ v_1 \f$ + \param inLastValue iterator pointing behind the last item in the category dataset to use \f$ v_N \f$ + \param[out] outFirstCategory for each element in the range \a inFirstCat / \a inFirstValue ... \a inLastCat / \a outFirstCat this receives the calculated category + \param[out] groupeddata receives the grouped data, each key in the map represents one group, each map-value contains two vecors with the category and value data respectively + \param groupDefFunc assigns a group \f$ c_{\text{out},j} \f$ to each category value \f$ c_i \f$ . + + \note the contents of \a groupeddata is not cleared before usage, so you can also use this fucntion to append to a group! + + \see JKQTPStatGroupDefinitionFunctor1D, \ref JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat +*/ +template +inline void jkqtpstatGroupData(InputCatIt inFirstCat, InputCatIt inLastCat, InputValueIt inFirstValue, InputValueIt inLastValue, OutputGroupIt outFirstCategory, std::map,std::vector > >& groupeddata, JKQTPStatGroupDefinitionFunctor1D groupDefFunc=&jkqtpstatGroupingIdentity1D) { + auto inCat=inFirstCat; + auto inVal=inFirstValue; + auto outCat=outFirstCategory; + for (; inCat!=inLastCat && inVal!=inLastValue; ++inCat, ++inVal) { + const double c=jkqtp_todouble(*inCat); + const double v=jkqtp_todouble(*inVal); + if (JKQTPIsOKFloat(c) && JKQTPIsOKFloat(v)) { + const double g=groupDefFunc(c); + groupeddata[g].first.push_back(c); + groupeddata[g].second.push_back(v); + *outCat=g; + } else { + *outCat=JKQTP_DOUBLE_NAN; + } + ++outCat; + } +} + +#endif // JKQTPSTATGROUPED_H_INCLUDED + + diff --git a/lib/jkqtcommon/jkqtpstatisticstools.h b/lib/jkqtcommon/jkqtpstatisticstools.h index 3469177546..bc2f4527c0 100644 --- a/lib/jkqtcommon/jkqtpstatisticstools.h +++ b/lib/jkqtcommon/jkqtpstatisticstools.h @@ -27,6 +27,7 @@ #include "jkqtcommon/jkqtpstatkde.h" #include "jkqtcommon/jkqtpstatpoly.h" #include "jkqtcommon/jkqtpstatregression.h" +#include "jkqtcommon/jkqtpstatgrouped.h" #endif // JKQTPSTATISTICSTOOLS_H_INCLUDED diff --git a/lib/jkqtcommon/jkqtpstatpoly.h b/lib/jkqtcommon/jkqtpstatpoly.h index c2186918d1..d5f0774793 100644 --- a/lib/jkqtcommon/jkqtpstatpoly.h +++ b/lib/jkqtcommon/jkqtpstatpoly.h @@ -46,7 +46,6 @@ /*! \brief fits (in a least-squares sense) a polynomial \f$ f(x)=\sum\limits_{i=0}^Pp_ix^i \f$ of order P to a set of N data pairs \f$ (x_i,y_i) \f$ \ingroup jkqtptools_math_statistics_poly - \ingroup jkqtptools_math_statistics_regression \tparam InputItX standard iterator type of \a firstX and \a lastX. \tparam InputItY standard iterator type of \a firstY and \a lastY. diff --git a/lib/jkqtpcommon.pri b/lib/jkqtpcommon.pri index 694a89ba68..e13646c465 100644 --- a/lib/jkqtpcommon.pri +++ b/lib/jkqtpcommon.pri @@ -35,7 +35,9 @@ isEmpty(JKQTP_COMMON_PRI_INCLUDED) { $$PWD/jkqtcommon/jkqtpstathistogram.h \ $$PWD/jkqtcommon/jkqtpstatkde.h \ $$PWD/jkqtcommon/jkqtpstatregression.h \ - $$PWD/jkqtcommon/jkqtpstatpoly.h + $$PWD/jkqtcommon/jkqtpstatpoly.h \ + $$PWD/jkqtcommon/jkqtpstatgrouped.h + SOURCES += $$PWD/jkqtcommon/jkqtpdebuggingtools.cpp \ @@ -51,12 +53,12 @@ isEmpty(JKQTP_COMMON_PRI_INCLUDED) { $$PWD/jkqtcommon/jkqtpmathparser.cpp \ $$PWD/jkqtcommon/jkqttools.cpp \ $$PWD/jkqtcommon/jkqtparraytools.cpp \ - $$PWD/jkqtcommon/jkqtpstatisticstools.cpp \ $$PWD/jkqtcommon/jkqtpstatbasics.cpp \ $$PWD/jkqtcommon/jkqtpstathistogram.cpp \ $$PWD/jkqtcommon/jkqtpstatkde.cpp \ $$PWD/jkqtcommon/jkqtpstatregression.cpp \ - $$PWD/jkqtcommon/jkqtpstatpoly.cpp + $$PWD/jkqtcommon/jkqtpstatpoly.cpp \ + $$PWD/jkqtcommon/jkqtpstatgrouped.cpp INCLUDEPATH += $$PWD diff --git a/lib/jkqtplotter/jkqtpgraphsbarchart.h b/lib/jkqtplotter/jkqtpgraphsbarchart.h index ae64ac2daa..dd439061f0 100644 --- a/lib/jkqtplotter/jkqtpgraphsbarchart.h +++ b/lib/jkqtplotter/jkqtpgraphsbarchart.h @@ -54,7 +54,7 @@ You can use JKQTPlotter::addHorizontalBargraph() to add a series of bargraphs, where the width and shift are determined automatically. The y-columns are given as a QVector to this function. - + \see JKQTPBarHorizontalGraph, \ref JKQTPlotterBarcharts, jkqtpstatAddHHistogram1D(), jkqtpstatAddHHistogram1DAutoranged() */ class JKQTP_LIB_EXPORT JKQTPBarVerticalGraph: public JKQTPXYGraph, public JKQTPGraphLineStyleMixin, public JKQTPGraphFillStyleMixin { Q_OBJECT @@ -173,6 +173,7 @@ class JKQTP_LIB_EXPORT JKQTPBarVerticalGraph: public JKQTPXYGraph, public JKQTPG * Draw stacked barcharts by connecting several plots by calling \c setStackedParent(belowPlot) for each plot * \image html JKQTPBarVerticalGraphStacked.png * + * \see JKQTPBarVerticalGraph, \ref JKQTPlotterStackedBarChart */ class JKQTP_LIB_EXPORT JKQTPBarVerticalStackableGraph: public JKQTPBarVerticalGraph { Q_OBJECT @@ -212,6 +213,7 @@ class JKQTP_LIB_EXPORT JKQTPBarVerticalStackableGraph: public JKQTPBarVerticalGr * This works much the same as JKQTPBarHorizontalGraph. Here is an example output: * \image html plot_bargraphverploterr.png * + * \see jkqtpstatAddYErrorBarGraph(), JKQTPBarVerticalGraph, \ref JKQTPlotterBarcharts */ class JKQTP_LIB_EXPORT JKQTPBarVerticalErrorGraph: public JKQTPBarVerticalGraph, public JKQTPYGraphErrors { Q_OBJECT @@ -241,8 +243,11 @@ class JKQTP_LIB_EXPORT JKQTPBarVerticalErrorGraph: public JKQTPBarVerticalGraph, \ingroup jkqtplotter_barssticks This works much the same as JKQTPBarHorizontalGraph. Here is an example output: + \image html plot_bargraphhorplot.png + + \see \ref JKQTPlotterBarcharts, jkqtpstatAddVHistogram1D(), jkqtpstatAddVHistogram1DAutoranged() */ class JKQTP_LIB_EXPORT JKQTPBarHorizontalGraph: public JKQTPBarVerticalGraph { Q_OBJECT @@ -279,6 +284,8 @@ class JKQTP_LIB_EXPORT JKQTPBarHorizontalGraph: public JKQTPBarVerticalGraph { * Draw stacked barcharts by connecting several plots by calling \c setStackedParent(belowPlot) for each plot * \image html JKQTPBarHorizontalGraphStacked.png * + * + * \see JKQTPBarHorizontalGraph, \ref JKQTPlotterStackedBarChart */ class JKQTP_LIB_EXPORT JKQTPBarHorizontalStackableGraph: public JKQTPBarHorizontalGraph { Q_OBJECT @@ -318,6 +325,8 @@ class JKQTP_LIB_EXPORT JKQTPBarHorizontalStackableGraph: public JKQTPBarHorizont * This works much the same as JKQTPBarHorizontalGraph. Here is an example output: * \image html plot_bargraphhorploterr.png * + * \see jkqtpstatAddXErrorBarGraph(), JKQTPBarHorizontalGraph, \ref JKQTPlotterBarcharts + * */ class JKQTP_LIB_EXPORT JKQTPBarHorizontalErrorGraph: public JKQTPBarHorizontalGraph, public JKQTPXGraphErrors { Q_OBJECT diff --git a/lib/jkqtplotter/jkqtpgraphsboxplot.h b/lib/jkqtplotter/jkqtpgraphsboxplot.h index 0c2e14e052..386575201a 100644 --- a/lib/jkqtplotter/jkqtpgraphsboxplot.h +++ b/lib/jkqtplotter/jkqtpgraphsboxplot.h @@ -95,7 +95,7 @@ graphOutliers->setSymbolSize(7); \endcode - \see \ref JKQTPlotterBoxplotsGraphs + \see \ref JKQTPlotterBoxplotsGraphs, jkqtpstatVAddBoxplots(),\ref JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat, \ref JKQTPlotterBasicJKQTPDatastoreStatistics, \ref JKQTPlotterBoxplotStyling */ class JKQTP_LIB_EXPORT JKQTPBoxplotVerticalGraph: public JKQTPGraph, public JKQTPGraphBoxplotStyleMixin { @@ -252,7 +252,7 @@ class JKQTP_LIB_EXPORT JKQTPBoxplotVerticalGraph: public JKQTPGraph, public JKQT \note See the documentation of JKQTPBoxplotVerticalGraph for details on the properties of this class! - \see JKQTPBoxplotVerticalGraph \ref JKQTPlotterBoxplotsGraphs + \see JKQTPBoxplotVerticalGraph \ref JKQTPlotterBoxplotsGraphs, jkqtpstatHAddBoxplots(), \ref JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat, \ref JKQTPlotterBasicJKQTPDatastoreStatistics, \ref JKQTPlotterBoxplotStyling */ class JKQTP_LIB_EXPORT JKQTPBoxplotHorizontalGraph: public JKQTPBoxplotVerticalGraph { @@ -306,6 +306,8 @@ class JKQTP_LIB_EXPORT JKQTPBoxplotHorizontalGraph: public JKQTPBoxplotVerticalG \image html test_styledboxplot.png + \see jkqtpstatVAddBoxplot(), \ref JKQTPlotterBasicJKQTPDatastoreStatistics, \ref JKQTPlotterBoxplotsGraphs, \ref JKQTPlotterBoxplotStyling, jkqtpstatAddVBoxplotAndOutliers() + */ class JKQTP_LIB_EXPORT JKQTPBoxplotVerticalElement: public JKQTPPlotObject, public JKQTPGraphBoxplotStyleMixin { Q_OBJECT @@ -428,7 +430,8 @@ class JKQTP_LIB_EXPORT JKQTPBoxplotVerticalElement: public JKQTPPlotObject, publ \note See JKQTPBoxplotVerticalElement for a detailed documentation - \see JKQTPBoxplotVerticalElement + \see JKQTPBoxplotVerticalElement, jkqtpstatHAddBoxplot(), \ref JKQTPlotterBasicJKQTPDatastoreStatistics, \ref JKQTPlotterBoxplotsGraphs, \ref JKQTPlotterBoxplotStyling, jkqtpstatAddHBoxplotAndOutliers() + */ class JKQTP_LIB_EXPORT JKQTPBoxplotHorizontalElement: public JKQTPBoxplotVerticalElement { Q_OBJECT diff --git a/lib/jkqtplotter/jkqtpgraphscontour.h b/lib/jkqtplotter/jkqtpgraphscontour.h index 9a4432a283..f415cfdc3c 100644 --- a/lib/jkqtplotter/jkqtpgraphscontour.h +++ b/lib/jkqtplotter/jkqtpgraphscontour.h @@ -247,6 +247,8 @@ class JKQTP_LIB_EXPORT JKQTPContourPlot: public JKQTPMathImage, public JKQTPGrap * \ingroup jkqtplotter_imagelots_contour * * \copydetails JKQTPContourPlot + * + * \see jkqtpstatAddKDE2DContour(), jkqtpstatAddHistogram2DContour(), \ref JKQTPlotterContourPlot, \ref JKQTPlotterBasicJKQTPDatastoreStatistics2D */ class JKQTP_LIB_EXPORT JKQTPColumnContourPlot: public JKQTPContourPlot { Q_OBJECT diff --git a/lib/jkqtplotter/jkqtpgraphsevaluatedfunction.h b/lib/jkqtplotter/jkqtpgraphsevaluatedfunction.h index 4a35becef0..beb07a4129 100644 --- a/lib/jkqtplotter/jkqtpgraphsevaluatedfunction.h +++ b/lib/jkqtplotter/jkqtpgraphsevaluatedfunction.h @@ -65,6 +65,8 @@ typedef std::function jkqtpSimplePlotFunctionType; the following image \image html plot_functionplots.png + + \see \ref JKQTPlotterFunctionPlots, jkqtpstatAddPolyFit(), jkqtpstatAddWeightedRegression(), jkqtpstatAddRobustIRLSRegression(), jkqtpstatAddRegression(), jkqtpstatAddLinearWeightedRegression(), jkqtpstatAddRobustIRLSLinearRegression(), jkqtpstatAddLinearRegression() */ class JKQTP_LIB_EXPORT JKQTPXFunctionLineGraph: public JKQTPGraph, public JKQTPGraphLineStyleMixin, public JKQTPGraphFillStyleMixin { Q_OBJECT @@ -392,6 +394,7 @@ class JKQTP_LIB_EXPORT JKQTPXFunctionLineGraph: public JKQTPGraph, public JKQTPG /*! \brief This implements line plots where the data is taken from a user supplied function \f$ x=f(y) \f$ \ingroup jkqtplotter_functiongraphs + \see \ref JKQTPlotterFunctionPlots */ class JKQTP_LIB_EXPORT JKQTPYFunctionLineGraph: public JKQTPXFunctionLineGraph { Q_OBJECT diff --git a/lib/jkqtplotter/jkqtpgraphsfilledcurve.h b/lib/jkqtplotter/jkqtpgraphsfilledcurve.h index 95b4e71346..c43b3dbbb1 100644 --- a/lib/jkqtplotter/jkqtpgraphsfilledcurve.h +++ b/lib/jkqtplotter/jkqtpgraphsfilledcurve.h @@ -54,7 +54,7 @@ class JKQTP_LIB_EXPORT JKQTPFilledCurveXGraph: public JKQTPSpecialLineHorizontal \image html plot_filledcurvexerrorplots.png - \see \ref JKQTPlotterFilledGraphs + \see \ref JKQTPlotterFilledGraphs, jkqtpstatAddXErrorFilledCurveGraph() */ class JKQTP_LIB_EXPORT JKQTPFilledCurveXErrorGraph: public JKQTPFilledCurveXGraph, public JKQTPYGraphErrors { Q_OBJECT @@ -103,7 +103,7 @@ class JKQTP_LIB_EXPORT JKQTPFilledCurveYGraph: public JKQTPSpecialLineVerticalGr \image html plot_filledcurveyerrorplots.png - \see \ref JKQTPlotterFilledGraphs + \see \ref JKQTPlotterFilledGraphs, jkqtpstatAddYErrorFilledCurveGraph() */ class JKQTP_LIB_EXPORT JKQTPFilledCurveYErrorGraph: public JKQTPFilledCurveYGraph, public JKQTPXGraphErrors { Q_OBJECT diff --git a/lib/jkqtplotter/jkqtpgraphsimage.h b/lib/jkqtplotter/jkqtpgraphsimage.h index 05bf86ab0d..57d8a5655e 100644 --- a/lib/jkqtplotter/jkqtpgraphsimage.h +++ b/lib/jkqtplotter/jkqtpgraphsimage.h @@ -334,6 +334,8 @@ class JKQTP_LIB_EXPORT JKQTPMathImageBase: public JKQTPImageBase { \ingroup jkqtplotter_imagelots_elements \image html jkqtplotter_simpletest_rgbimageplot_qt.png + + \see \ref JKQTPlotterImagePlotQImageRGB */ class JKQTP_LIB_EXPORT JKQTPImage: public JKQTPImageBase { Q_OBJECT @@ -467,6 +469,8 @@ class JKQTP_LIB_EXPORT JKQTPImage: public JKQTPImageBase { \image html jkqtplotter_simpletest_imageplot.png \image html jkqtplotter_simpletest_imageplot_modifier.png \image html jkqtplotter_simpletest_imageplot__smallscaletransparent.png + + \see \ref JKQTPlotterImagePlotNoDatastore */ class JKQTP_LIB_EXPORT JKQTPMathImage: public JKQTPMathImageBase { Q_OBJECT @@ -994,6 +998,7 @@ int JKQTPMathImage::getModifierSampleSize() const { \image html jkqtplotter_simpletest_imageplot_modifier.png \image html jkqtplotter_simpletest_imageplot__smallscaletransparent.png + \see jkqtpstatAddKDE2DImage(), jkqtpstatAddHistogram2DImage(), \ref JKQTPlotterImagePlot, \ref JKQTPlotterImagePlotModifier, \ref JKQTPlotterImagePlotOpenCV */ class JKQTP_LIB_EXPORT JKQTPColumnMathImage: public JKQTPMathImage { Q_OBJECT diff --git a/lib/jkqtplotter/jkqtpgraphsimpulses.h b/lib/jkqtplotter/jkqtpgraphsimpulses.h index 1016a9798a..d9861f069c 100644 --- a/lib/jkqtplotter/jkqtpgraphsimpulses.h +++ b/lib/jkqtplotter/jkqtpgraphsimpulses.h @@ -31,6 +31,8 @@ \ingroup jkqtplotter_barssticks \image html plot_impulsesxplots.png + + \see JKQTPImpulsesVerticalGraph, \ref JKQTPlotterImpulsePlots */ class JKQTP_LIB_EXPORT JKQTPImpulsesHorizontalGraph: public JKQTPXYGraph, public JKQTPGraphLineStyleMixin, public JKQTPGraphSymbolStyleMixin{ Q_OBJECT @@ -80,6 +82,8 @@ class JKQTP_LIB_EXPORT JKQTPImpulsesHorizontalGraph: public JKQTPXYGraph, public \ingroup jkqtplotter_barssticks \image html plot_impulsesxerrorsplots.png + + \see jkqtpstatAddXErrorImpulsesGraph(), JKQTPImpulsesHorizontalGraph, \ref JKQTPlotterImpulsePlots */ class JKQTP_LIB_EXPORT JKQTPImpulsesHorizontalErrorGraph: public JKQTPImpulsesHorizontalGraph, public JKQTPXGraphErrors { Q_OBJECT @@ -103,6 +107,8 @@ class JKQTP_LIB_EXPORT JKQTPImpulsesHorizontalErrorGraph: public JKQTPImpulsesHo \ingroup jkqtplotter_barssticks \image html plot_impulsesyplots.png + + \see \ref JKQTPlotterImpulsePlots */ class JKQTP_LIB_EXPORT JKQTPImpulsesVerticalGraph: public JKQTPImpulsesHorizontalGraph { Q_OBJECT @@ -122,6 +128,8 @@ class JKQTP_LIB_EXPORT JKQTPImpulsesVerticalGraph: public JKQTPImpulsesHorizonta \ingroup jkqtplotter_barssticks \image html plot_impulsesyerrorsplots.png + + \see JKQTPImpulsesVerticalGraph, jkqtpstatAddYErrorImpulsesGraph(), \ref JKQTPlotterImpulsePlots */ class JKQTP_LIB_EXPORT JKQTPImpulsesVerticalErrorGraph: public JKQTPImpulsesVerticalGraph, public JKQTPYGraphErrors { Q_OBJECT diff --git a/lib/jkqtplotter/jkqtpgraphsparsedfunction.h b/lib/jkqtplotter/jkqtpgraphsparsedfunction.h index c7409b98c6..4993087eea 100644 --- a/lib/jkqtplotter/jkqtpgraphsparsedfunction.h +++ b/lib/jkqtplotter/jkqtpgraphsparsedfunction.h @@ -42,6 +42,8 @@ class JKQTPlotter; Parameters may also be given from a data column. Then first the params from the column and the the parameters from the vector are numbered. Use the variable \c x in an equation to refer to the free parameter of the curve. + + \see \ref JKQTPlotterParsedFunctionPlot, JKQTPMathParser */ class JKQTP_LIB_EXPORT JKQTPXParsedFunctionLineGraph: public JKQTPXFunctionLineGraph { Q_OBJECT @@ -108,6 +110,9 @@ class JKQTP_LIB_EXPORT JKQTPXParsedFunctionLineGraph: public JKQTPXFunctionLineG Parameters may also be given from a data column. Then first the params from the column and the the parameters from the vector are numbered. Use the variable \c y in an equation to refer to the free parameter of the curve (\c is also understood for convenience). + + \see \ref JKQTPlotterParsedFunctionPlot, JKQTPMathParser + */ class JKQTP_LIB_EXPORT JKQTPYParsedFunctionLineGraph: public JKQTPYFunctionLineGraph { Q_OBJECT diff --git a/lib/jkqtplotter/jkqtpgraphspeakstream.h b/lib/jkqtplotter/jkqtpgraphspeakstream.h index c9a2c58329..c3bcf27252 100644 --- a/lib/jkqtplotter/jkqtpgraphspeakstream.h +++ b/lib/jkqtplotter/jkqtpgraphspeakstream.h @@ -42,6 +42,8 @@ class JKQTPDatastore; \image html JKQTPPeakStreamGraphY.png "yPeaks=true" \image html JKQTPPeakStreamGraphX.png "yPeaks=false" + + \see \ref JKQTPlotterBasicJKQTPDatastoreStatistics */ class JKQTP_LIB_EXPORT JKQTPPeakStreamGraph: public JKQTPSingleColumnGraph, public JKQTPGraphLineStyleMixin { Q_OBJECT diff --git a/lib/jkqtplotter/jkqtpgraphsscatter.h b/lib/jkqtplotter/jkqtpgraphsscatter.h index 626d10a56d..d1193d09b6 100644 --- a/lib/jkqtplotter/jkqtpgraphsscatter.h +++ b/lib/jkqtplotter/jkqtpgraphsscatter.h @@ -50,6 +50,8 @@ class JKQTPDatastore; \ingroup jkqtplotter_linesymbolgraphs_simple \image html plot_lineplots.png + + \see \ref JKQTPlotterAdvancedLineAndFillStyling, \ref JKQTPlotterSimpleTest, \ref JKQTPlotterSymbolsAndStyles, jkqtpstatAddVKDE1D(), jkqtpstatAddVKDE1DAutoranged(), jkqtpstatAddHKDE1D(), jkqtpstatAddHKDE1DAutoranged() */ class JKQTP_LIB_EXPORT JKQTPXYLineGraph: public JKQTPXYGraph, public JKQTPGraphLineStyleMixin, public JKQTPGraphSymbolStyleMixin { Q_OBJECT @@ -109,7 +111,7 @@ class JKQTP_LIB_EXPORT JKQTPXYLineGraph: public JKQTPXYGraph, public JKQTPGraphL \image html JKQTPXYParametrizedScatterGraph_SymbolFunctor.png - \see JKQTPXYParametrizedErrorScatterGraph, \ref JKQTPlotterParamScatter , \ref JKQTPlotterParamScatterImage + \see JKQTPXYParametrizedErrorScatterGraph, \ref JKQTPlotterParamScatter , \ref JKQTPlotterParamScatterImage, \ref JKQTPlotterParametricCurves */ class JKQTP_LIB_EXPORT JKQTPXYParametrizedScatterGraph: public JKQTPXYLineGraph, public JKQTPColorPaletteStyleAndToolsMixin { Q_OBJECT @@ -367,6 +369,8 @@ class JKQTP_LIB_EXPORT JKQTPXYParametrizedScatterGraph: public JKQTPXYLineGraph, \image html plot_errorbarlineplots.png \image html plot_errorlinelineplots.png \image html plot_errorpolygonlineplots.png + + \see jkqtpstatAddXYErrorLineGraph(), jkqtpstatAddXErrorLineGraph(), jkqtpstatAddYErrorLineGraph(), \ref JKQTPlotterErrorBarStyles, \ref JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat */ class JKQTP_LIB_EXPORT JKQTPXYLineErrorGraph: public JKQTPXYLineGraph, public JKQTPXYGraphErrors { Q_OBJECT @@ -403,7 +407,7 @@ class JKQTP_LIB_EXPORT JKQTPXYLineErrorGraph: public JKQTPXYLineGraph, public JK \image html screen_parmetrizedplots_datatable.png - \see JKQTPXYParametrizedScatterGraph, \ref JKQTPlotterParamScatter + \see JKQTPXYParametrizedScatterGraph, \ref JKQTPlotterParamScatter, jkqtpstatAddXErrorParametrizedScatterGraph(), jkqtpstatAddYErrorParametrizedScatterGraph() */ class JKQTP_LIB_EXPORT JKQTPXYParametrizedErrorScatterGraph: public JKQTPXYParametrizedScatterGraph, public JKQTPXYGraphErrors { Q_OBJECT diff --git a/lib/jkqtplotter/jkqtpgraphsstatisticsadaptors.h b/lib/jkqtplotter/jkqtpgraphsstatisticsadaptors.h index 0a5035afb2..04d1ff0536 100644 --- a/lib/jkqtplotter/jkqtpgraphsstatisticsadaptors.h +++ b/lib/jkqtplotter/jkqtpgraphsstatisticsadaptors.h @@ -30,6 +30,8 @@ #include "jkqtplotter/jkqtpgraphsevaluatedfunction.h" #include "jkqtplotter/jkqtpgraphsimage.h" #include "jkqtplotter/jkqtpgraphscontour.h" +#include "jkqtplotter/jkqtpgraphsimpulses.h" +#include "jkqtplotter/jkqtpgraphsfilledcurve.h" #ifndef JKQTPGRAPHSSTATISTICSADAPTORS_H_INCLUDED @@ -1788,4 +1790,735 @@ JKQTP_LIB_EXPORT JKQTPXFunctionLineGraph* jkqtpstatAddPolyFit(JKQTPXYGraph *data + + + + + + + +/*! \brief create a plot with y-direction error bars, calculated from average +/- stddev of groups in the input data + \ingroup jkqtptools_math_statistics_adaptors + + \tparam TGraph type of graph that should be added to the plot + \tparam InputCatIt standard iterator type of \a inFirstCat_X and \a inLastCat_X + \tparam InputValueIt standard iterator type of \a inFirstValue_Y and \a inLastValue_Y + \param plotter the plotter to which to add the resulting graph + \param inFirstCat_X iterator pointing to the first item in the category dataset to use \f$ c_1 \f$ (used for x-coordinates) + \param inLastCat_X iterator pointing behind the last item in the category dataset to use \f$ c_N \f$ (used for x-coordinates) + \param inFirstValue_Y iterator pointing to the first item in the category dataset to use \f$ v_1 \f$ (used for y-coordinates) + \param inLastValue_Y iterator pointing behind the last item in the category dataset to use \f$ v_N \f$ (used for y-coordinates) + \param groupDefFunc assigns a group \f$ c_{\text{out},j} \f$ to each category value \f$ c_i \f$ . + \param columnBaseName string component used to build the names of the columns generated by this function + \return the graph showing \f$ c_{\text{out},j} \f$ and average +/- stddev for each group \f$ j \f$ + + + + \see jkqtpstatGroupData(), \ref JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat +*/ +template +inline TGraph* jkqtpstatAddYErrorGraph(JKQTBasePlotter* plotter, InputCatIt inFirstCat_X, InputCatIt inLastCat_X, InputValueIt inFirstValue_Y, InputValueIt inLastValue_Y, JKQTPStatGroupDefinitionFunctor1D groupDefFunc=&jkqtpstatGroupingIdentity1D, const QString& columnBaseName=QString("grouped data")) { + std::map > groupeddataBar; + jkqtpstatGroupData(inFirstCat_X, inLastCat_X, inFirstValue_Y, inLastValue_Y, groupeddataBar, groupDefFunc); + + size_t colGroup=plotter->getDatastore()->addColumn(columnBaseName+", group"); + size_t colAverage=plotter->getDatastore()->addColumn(columnBaseName+", average"); + size_t colStdDev=plotter->getDatastore()->addColumn(columnBaseName+", stddev"); + + for (auto it=groupeddataBar.begin(); it!=groupeddataBar.end(); ++it) { + plotter->getDatastore()->appendToColumn(colGroup, it->first); + plotter->getDatastore()->appendToColumn(colAverage, jkqtpstatAverage(it->second.begin(), it->second.end())); + plotter->getDatastore()->appendToColumn(colStdDev, jkqtpstatStdDev(it->second.begin(), it->second.end())); + } + + // 2.4. Finally the calculated groups are drawn + TGraph* graph; + plotter->addGraph(graph=new TGraph(plotter)); + graph->setXColumn(colGroup); + graph->setYColumn(colAverage); + graph->setYErrorColumn(static_cast(colStdDev)); + + return graph; +} + + +/*! \brief create a JKQTPXYLineErrorGraph with y-direction error bars, calculated from average +/- stddev of groups in the input data + \ingroup jkqtptools_math_statistics_adaptors + + \tparam InputCatIt standard iterator type of \a inFirstCat_X and \a inLastCat_X + \tparam InputValueIt standard iterator type of \a inFirstValue_Y and \a inLastValue_Y + \param plotter the plotter to which to add the resulting graph + \param inFirstCat_X iterator pointing to the first item in the category dataset to use \f$ c_1 \f$ (used for x-coordinates) + \param inLastCat_X iterator pointing behind the last item in the category dataset to use \f$ c_N \f$ (used for x-coordinates) + \param inFirstValue_Y iterator pointing to the first item in the category dataset to use \f$ v_1 \f$ (used for y-coordinates) + \param inLastValue_Y iterator pointing behind the last item in the category dataset to use \f$ v_N \f$ (used for y-coordinates) + \param groupDefFunc assigns a group \f$ c_{\text{out},j} \f$ to each category value \f$ c_i \f$ . + \param columnBaseName string component used to build the names of the columns generated by this function + \return the graph showing \f$ c_{\text{out},j} \f$ and average +/- stddev for each group \f$ j \f$ + + + + \see jkqtpstatGroupData(), jkqtpstatAddYErrorGraph(), JKQTPXYLineErrorGraph, \ref JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat +*/ +template +inline JKQTPXYLineErrorGraph* jkqtpstatAddYErrorLineGraph(JKQTBasePlotter* plotter, InputCatIt inFirstCat_X, InputCatIt inLastCat_X, InputValueIt inFirstValue_Y, InputValueIt inLastValue_Y, JKQTPStatGroupDefinitionFunctor1D groupDefFunc=&jkqtpstatGroupingIdentity1D, const QString& columnBaseName=QString("grouped data")) { + return jkqtpstatAddYErrorGraph(plotter,inFirstCat_X, inLastCat_X, inFirstValue_Y, inLastValue_Y, groupDefFunc, columnBaseName); +} + + +/*! \brief create a JKQTPBarVerticalErrorGraph with y-direction error bars, calculated from average +/- stddev of groups in the input data + \ingroup jkqtptools_math_statistics_adaptors + + \tparam InputCatIt standard iterator type of \a inFirstCat_X and \a inLastCat_X + \tparam InputValueIt standard iterator type of \a inFirstValue_Y and \a inLastValue_Y + \param plotter the plotter to which to add the resulting graph + \param inFirstCat_X iterator pointing to the first item in the category dataset to use \f$ c_1 \f$ (used for x-coordinates) + \param inLastCat_X iterator pointing behind the last item in the category dataset to use \f$ c_N \f$ (used for x-coordinates) + \param inFirstValue_Y iterator pointing to the first item in the category dataset to use \f$ v_1 \f$ (used for y-coordinates) + \param inLastValue_Y iterator pointing behind the last item in the category dataset to use \f$ v_N \f$ (used for y-coordinates) + \param groupDefFunc assigns a group \f$ c_{\text{out},j} \f$ to each category value \f$ c_i \f$ . + \param columnBaseName string component used to build the names of the columns generated by this function + \return the graph showing \f$ c_{\text{out},j} \f$ and average +/- stddev for each group \f$ j \f$ + + + + \see jkqtpstatGroupData(), jkqtpstatAddYErrorGraph(), JKQTPBarVerticalErrorGraph, \ref JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat +*/ +template +inline JKQTPBarVerticalErrorGraph* jkqtpstatAddYErrorBarGraph(JKQTBasePlotter* plotter, InputCatIt inFirstCat_X, InputCatIt inLastCat_X, InputValueIt inFirstValue_Y, InputValueIt inLastValue_Y, JKQTPStatGroupDefinitionFunctor1D groupDefFunc=&jkqtpstatGroupingIdentity1D, const QString& columnBaseName=QString("grouped data")) { + return jkqtpstatAddYErrorGraph(plotter,inFirstCat_X, inLastCat_X, inFirstValue_Y, inLastValue_Y, groupDefFunc, columnBaseName); +} + + +/*! \brief create a JKQTPImpulsesVerticalErrorGraph with y-direction error bars, calculated from average +/- stddev of groups in the input data + \ingroup jkqtptools_math_statistics_adaptors + + \tparam InputCatIt standard iterator type of \a inFirstCat_X and \a inLastCat_X + \tparam InputValueIt standard iterator type of \a inFirstValue_Y and \a inLastValue_Y + \param plotter the plotter to which to add the resulting graph + \param inFirstCat_X iterator pointing to the first item in the category dataset to use \f$ c_1 \f$ (used for x-coordinates) + \param inLastCat_X iterator pointing behind the last item in the category dataset to use \f$ c_N \f$ (used for x-coordinates) + \param inFirstValue_Y iterator pointing to the first item in the category dataset to use \f$ v_1 \f$ (used for y-coordinates) + \param inLastValue_Y iterator pointing behind the last item in the category dataset to use \f$ v_N \f$ (used for y-coordinates) + \param groupDefFunc assigns a group \f$ c_{\text{out},j} \f$ to each category value \f$ c_i \f$ . + \param columnBaseName string component used to build the names of the columns generated by this function + \return the graph showing \f$ c_{\text{out},j} \f$ and average +/- stddev for each group \f$ j \f$ + + + + \see jkqtpstatGroupData(), jkqtpstatAddYErrorGraph(), JKQTPImpulsesVerticalErrorGraph, \ref JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat +*/ +template +inline JKQTPImpulsesVerticalErrorGraph* jkqtpstatAddYErrorImpulsesGraph(JKQTBasePlotter* plotter, InputCatIt inFirstCat_X, InputCatIt inLastCat_X, InputValueIt inFirstValue_Y, InputValueIt inLastValue_Y, JKQTPStatGroupDefinitionFunctor1D groupDefFunc=&jkqtpstatGroupingIdentity1D, const QString& columnBaseName=QString("grouped data")) { + return jkqtpstatAddYErrorGraph(plotter,inFirstCat_X, inLastCat_X, inFirstValue_Y, inLastValue_Y, groupDefFunc, columnBaseName); +} + + +/*! \brief create a JKQTPXYParametrizedErrorScatterGraph with y-direction error bars, calculated from average +/- stddev of groups in the input data + \ingroup jkqtptools_math_statistics_adaptors + + \tparam InputCatIt standard iterator type of \a inFirstCat_X and \a inLastCat_X + \tparam InputValueIt standard iterator type of \a inFirstValue_Y and \a inLastValue_Y + \param plotter the plotter to which to add the resulting graph + \param inFirstCat_X iterator pointing to the first item in the category dataset to use \f$ c_1 \f$ (used for x-coordinates) + \param inLastCat_X iterator pointing behind the last item in the category dataset to use \f$ c_N \f$ (used for x-coordinates) + \param inFirstValue_Y iterator pointing to the first item in the category dataset to use \f$ v_1 \f$ (used for y-coordinates) + \param inLastValue_Y iterator pointing behind the last item in the category dataset to use \f$ v_N \f$ (used for y-coordinates) + \param groupDefFunc assigns a group \f$ c_{\text{out},j} \f$ to each category value \f$ c_i \f$ . + \param columnBaseName string component used to build the names of the columns generated by this function + \return the graph showing \f$ c_{\text{out},j} \f$ and average +/- stddev for each group \f$ j \f$ + + + + \see jkqtpstatGroupData(), jkqtpstatAddYErrorGraph(), JKQTPXYParametrizedErrorScatterGraph, \ref JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat +*/ +template +inline JKQTPXYParametrizedErrorScatterGraph* jkqtpstatAddYErrorParametrizedScatterGraph(JKQTBasePlotter* plotter, InputCatIt inFirstCat_X, InputCatIt inLastCat_X, InputValueIt inFirstValue_Y, InputValueIt inLastValue_Y, JKQTPStatGroupDefinitionFunctor1D groupDefFunc=&jkqtpstatGroupingIdentity1D, const QString& columnBaseName=QString("grouped data")) { + return jkqtpstatAddYErrorGraph(plotter,inFirstCat_X, inLastCat_X, inFirstValue_Y, inLastValue_Y, groupDefFunc, columnBaseName); +} + + +/*! \brief create a JKQTPFilledCurveYErrorGraph with y-direction error bars, calculated from average +/- stddev of groups in the input data + \ingroup jkqtptools_math_statistics_adaptors + + \tparam InputCatIt standard iterator type of \a inFirstCat_X and \a inLastCat_X + \tparam InputValueIt standard iterator type of \a inFirstValue_Y and \a inLastValue_Y + \param plotter the plotter to which to add the resulting graph + \param inFirstCat_X iterator pointing to the first item in the category dataset to use \f$ c_1 \f$ (used for x-coordinates) + \param inLastCat_X iterator pointing behind the last item in the category dataset to use \f$ c_N \f$ (used for x-coordinates) + \param inFirstValue_Y iterator pointing to the first item in the category dataset to use \f$ v_1 \f$ (used for y-coordinates) + \param inLastValue_Y iterator pointing behind the last item in the category dataset to use \f$ v_N \f$ (used for y-coordinates) + \param groupDefFunc assigns a group \f$ c_{\text{out},j} \f$ to each category value \f$ c_i \f$ . + \param columnBaseName string component used to build the names of the columns generated by this function + \return the graph showing \f$ c_{\text{out},j} \f$ and average +/- stddev for each group \f$ j \f$ + + + + \see jkqtpstatGroupData(), jkqtpstatAddYErrorGraph(), JKQTPFilledCurveYErrorGraph, \ref JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat +*/ +template +inline JKQTPFilledCurveYErrorGraph* jkqtpstatAddYErrorFilledCurveGraph(JKQTBasePlotter* plotter, InputCatIt inFirstCat_X, InputCatIt inLastCat_X, InputValueIt inFirstValue_Y, InputValueIt inLastValue_Y, JKQTPStatGroupDefinitionFunctor1D groupDefFunc=&jkqtpstatGroupingIdentity1D, const QString& columnBaseName=QString("grouped data")) { + return jkqtpstatAddYErrorGraph(plotter,inFirstCat_X, inLastCat_X, inFirstValue_Y, inLastValue_Y, groupDefFunc, columnBaseName); +} + + + + + + + +/*! \brief create a plot with x-direction error bars, calculated from average +/- stddev of groups in the input data + \ingroup jkqtptools_math_statistics_adaptors + + \tparam TGraph type of graph that should be added to the plot + \tparam InputCatIt standard iterator type of \a inFirstCat_Y and \a inLastCat_Y + \tparam InputValueIt standard iterator type of \a inFirstValue_Y and \a inLastValue_Y + \param plotter the plotter to which to add the resulting graph + \param inFirstCat_Y iterator pointing to the first item in the category dataset to use \f$ c_1 \f$ (used for y-coordinates) + \param inLastCat_Y iterator pointing behind the last item in the category dataset to use \f$ c_N \f$ (used for y-coordinates) + \param inFirstValue_Y iterator pointing to the first item in the category dataset to use \f$ v_1 \f$ (used for x-coordinates) + \param inLastValue_Y iterator pointing behind the last item in the category dataset to use \f$ v_N \f$ (used for x-coordinates) + \param groupDefFunc assigns a group \f$ c_{\text{out},j} \f$ to each category value \f$ c_i \f$ . + \param columnBaseName string component used to build the names of the columns generated by this function + \return the graph showing \f$ c_{\text{out},j} \f$ and average +/- stddev for each group \f$ j \f$ + + + + \see jkqtpstatGroupData(), \ref JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat +*/ +template +inline TGraph* jkqtpstatAddXErrorGraph(JKQTBasePlotter* plotter, InputCatIt inFirstCat_Y, InputCatIt inLastCat_Y, InputValueIt inFirstValue_Y, InputValueIt inLastValue_Y, JKQTPStatGroupDefinitionFunctor1D groupDefFunc=&jkqtpstatGroupingIdentity1D, const QString& columnBaseName=QString("grouped data")) { + std::map > groupeddataBar; + jkqtpstatGroupData(inFirstCat_Y, inLastCat_Y, inFirstValue_Y, inLastValue_Y, groupeddataBar, groupDefFunc); + + size_t colGroup=plotter->getDatastore()->addColumn(columnBaseName+", group"); + size_t colAverage=plotter->getDatastore()->addColumn(columnBaseName+", average"); + size_t colStdDev=plotter->getDatastore()->addColumn(columnBaseName+", stddev"); + + for (auto it=groupeddataBar.begin(); it!=groupeddataBar.end(); ++it) { + plotter->getDatastore()->appendToColumn(colGroup, it->first); + plotter->getDatastore()->appendToColumn(colAverage, jkqtpstatAverage(it->second.begin(), it->second.end())); + plotter->getDatastore()->appendToColumn(colStdDev, jkqtpstatStdDev(it->second.begin(), it->second.end())); + } + + // 2.4. Finally the calculated groups are drawn + TGraph* graph; + plotter->addGraph(graph=new TGraph(plotter)); + graph->setXColumn(colAverage); + graph->setYColumn(colGroup); + graph->setXErrorColumn(static_cast(colStdDev)); + + return graph; +} + + +/*! \brief create a JKQTPXYLineErrorGraph with x-direction error bars, calculated from average +/- stddev of groups in the input data + \ingroup jkqtptools_math_statistics_adaptors + + \tparam InputCatIt standard iterator type of \a inFirstCat_Y and \a inLastCat_Y + \tparam InputValueIt standard iterator type of \a inFirstValue_Y and \a inLastValue_Y + \param plotter the plotter to which to add the resulting graph + \param inFirstCat_Y iterator pointing to the first item in the category dataset to use \f$ c_1 \f$ (used for y-coordinates) + \param inLastCat_Y iterator pointing behind the last item in the category dataset to use \f$ c_N \f$ (used for y-coordinates) + \param inFirstValue_Y iterator pointing to the first item in the category dataset to use \f$ v_1 \f$ (used for x-coordinates) + \param inLastValue_Y iterator pointing behind the last item in the category dataset to use \f$ v_N \f$ (used for x-coordinates) + \param groupDefFunc assigns a group \f$ c_{\text{out},j} \f$ to each category value \f$ c_i \f$ . + \param columnBaseName string component used to build the names of the columns generated by this function + \return the graph showing \f$ c_{\text{out},j} \f$ and average +/- stddev for each group \f$ j \f$ + + + + \see jkqtpstatGroupData(), jkqtpstatAddXErrorGraph(), JKQTPXYLineErrorGraph, \ref JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat +*/ +template +inline JKQTPXYLineErrorGraph* jkqtpstatAddXErrorLineGraph(JKQTBasePlotter* plotter, InputCatIt inFirstCat_Y, InputCatIt inLastCat_Y, InputValueIt inFirstValue_Y, InputValueIt inLastValue_Y, JKQTPStatGroupDefinitionFunctor1D groupDefFunc=&jkqtpstatGroupingIdentity1D, const QString& columnBaseName=QString("grouped data")) { + return jkqtpstatAddXErrorGraph(plotter,inFirstCat_Y, inLastCat_Y, inFirstValue_Y, inLastValue_Y, groupDefFunc, columnBaseName); +} + + +/*! \brief create a JKQTPBarHorizontalErrorGraph with x-direction error bars, calculated from average +/- stddev of groups in the input data + \ingroup jkqtptools_math_statistics_adaptors + + \tparam InputCatIt standard iterator type of \a inFirstCat_Y and \a inLastCat_Y + \tparam InputValueIt standard iterator type of \a inFirstValue_Y and \a inLastValue_Y + \param plotter the plotter to which to add the resulting graph + \param inFirstCat_Y iterator pointing to the first item in the category dataset to use \f$ c_1 \f$ (used for y-coordinates) + \param inLastCat_Y iterator pointing behind the last item in the category dataset to use \f$ c_N \f$ (used for y-coordinates) + \param inFirstValue_Y iterator pointing to the first item in the category dataset to use \f$ v_1 \f$ (used for x-coordinates) + \param inLastValue_Y iterator pointing behind the last item in the category dataset to use \f$ v_N \f$ (used for x-coordinates) + \param groupDefFunc assigns a group \f$ c_{\text{out},j} \f$ to each category value \f$ c_i \f$ . + \param columnBaseName string component used to build the names of the columns generated by this function + \return the graph showing \f$ c_{\text{out},j} \f$ and average +/- stddev for each group \f$ j \f$ + + + + \see jkqtpstatGroupData(), jkqtpstatAddXErrorGraph(), JKQTPBarHorizontalErrorGraph, \ref JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat +*/ +template +inline JKQTPBarHorizontalErrorGraph* jkqtpstatAddXErrorBarGraph(JKQTBasePlotter* plotter, InputCatIt inFirstCat_Y, InputCatIt inLastCat_Y, InputValueIt inFirstValue_Y, InputValueIt inLastValue_Y, JKQTPStatGroupDefinitionFunctor1D groupDefFunc=&jkqtpstatGroupingIdentity1D, const QString& columnBaseName=QString("grouped data")) { + return jkqtpstatAddXErrorGraph(plotter,inFirstCat_Y, inLastCat_Y, inFirstValue_Y, inLastValue_Y, groupDefFunc, columnBaseName); +} + + +/*! \brief create a JKQTPImpulsesHorizontalErrorGraph with x-direction error bars, calculated from average +/- stddev of groups in the input data + \ingroup jkqtptools_math_statistics_adaptors + + \tparam InputCatIt standard iterator type of \a inFirstCat_Y and \a inLastCat_Y + \tparam InputValueIt standard iterator type of \a inFirstValue_Y and \a inLastValue_Y + \param plotter the plotter to which to add the resulting graph + \param inFirstCat_Y iterator pointing to the first item in the category dataset to use \f$ c_1 \f$ (used for y-coordinates) + \param inLastCat_Y iterator pointing behind the last item in the category dataset to use \f$ c_N \f$ (used for y-coordinates) + \param inFirstValue_Y iterator pointing to the first item in the category dataset to use \f$ v_1 \f$ (used for x-coordinates) + \param inLastValue_Y iterator pointing behind the last item in the category dataset to use \f$ v_N \f$ (used for x-coordinates) + \param groupDefFunc assigns a group \f$ c_{\text{out},j} \f$ to each category value \f$ c_i \f$ . + \param columnBaseName string component used to build the names of the columns generated by this function + \return the graph showing \f$ c_{\text{out},j} \f$ and average +/- stddev for each group \f$ j \f$ + + + + \see jkqtpstatGroupData(), jkqtpstatAddXErrorGraph(), JKQTPImpulsesHorizontalErrorGraph, \ref JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat +*/ +template +inline JKQTPImpulsesHorizontalErrorGraph* jkqtpstatAddXErrorImpulsesGraph(JKQTBasePlotter* plotter, InputCatIt inFirstCat_Y, InputCatIt inLastCat_Y, InputValueIt inFirstValue_Y, InputValueIt inLastValue_Y, JKQTPStatGroupDefinitionFunctor1D groupDefFunc=&jkqtpstatGroupingIdentity1D, const QString& columnBaseName=QString("grouped data")) { + return jkqtpstatAddXErrorGraph(plotter,inFirstCat_Y, inLastCat_Y, inFirstValue_Y, inLastValue_Y, groupDefFunc, columnBaseName); +} + + +/*! \brief create a JKQTPXYParametrizedErrorScatterGraph with x-direction error bars, calculated from average +/- stddev of groups in the input data + \ingroup jkqtptools_math_statistics_adaptors + + \tparam InputCatIt standard iterator type of \a inFirstCat_Y and \a inLastCat_Y + \tparam InputValueIt standard iterator type of \a inFirstValue_Y and \a inLastValue_Y + \param plotter the plotter to which to add the resulting graph + \param inFirstCat_Y iterator pointing to the first item in the category dataset to use \f$ c_1 \f$ (used for y-coordinates) + \param inLastCat_Y iterator pointing behind the last item in the category dataset to use \f$ c_N \f$ (used for y-coordinates) + \param inFirstValue_Y iterator pointing to the first item in the category dataset to use \f$ v_1 \f$ (used for x-coordinates) + \param inLastValue_Y iterator pointing behind the last item in the category dataset to use \f$ v_N \f$ (used for x-coordinates) + \param groupDefFunc assigns a group \f$ c_{\text{out},j} \f$ to each category value \f$ c_i \f$ . + \param columnBaseName string component used to build the names of the columns generated by this function + \return the graph showing \f$ c_{\text{out},j} \f$ and average +/- stddev for each group \f$ j \f$ + + + + \see jkqtpstatGroupData(), jkqtpstatAddXErrorGraph(), JKQTPXYParametrizedErrorScatterGraph, \ref JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat +*/ +template +inline JKQTPXYParametrizedErrorScatterGraph* jkqtpstatAddXErrorParametrizedScatterGraph(JKQTBasePlotter* plotter, InputCatIt inFirstCat_Y, InputCatIt inLastCat_Y, InputValueIt inFirstValue_Y, InputValueIt inLastValue_Y, JKQTPStatGroupDefinitionFunctor1D groupDefFunc=&jkqtpstatGroupingIdentity1D, const QString& columnBaseName=QString("grouped data")) { + return jkqtpstatAddXErrorGraph(plotter,inFirstCat_Y, inLastCat_Y, inFirstValue_Y, inLastValue_Y, groupDefFunc, columnBaseName); +} + + +/*! \brief create a JKQTPFilledCurveXErrorGraph with x-direction error bars, calculated from average +/- stddev of groups in the input data + \ingroup jkqtptools_math_statistics_adaptors + + \tparam InputCatIt standard iterator type of \a inFirstCat_Y and \a inLastCat_Y + \tparam InputValueIt standard iterator type of \a inFirstValue_Y and \a inLastValue_Y + \param plotter the plotter to which to add the resulting graph + \param inFirstCat_Y iterator pointing to the first item in the category dataset to use \f$ c_1 \f$ (used for y-coordinates) + \param inLastCat_Y iterator pointing behind the last item in the category dataset to use \f$ c_N \f$ (used for y-coordinates) + \param inFirstValue_Y iterator pointing to the first item in the category dataset to use \f$ v_1 \f$ (used for x-coordinates) + \param inLastValue_Y iterator pointing behind the last item in the category dataset to use \f$ v_N \f$ (used for x-coordinates) + \param groupDefFunc assigns a group \f$ c_{\text{out},j} \f$ to each category value \f$ c_i \f$ . + \param columnBaseName string component used to build the names of the columns generated by this function + \return the graph showing \f$ c_{\text{out},j} \f$ and average +/- stddev for each group \f$ j \f$ + + + + \see jkqtpstatGroupData(), jkqtpstatAddXErrorGraph(), JKQTPFilledCurveXErrorGraph, \ref JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat +*/ +template +inline JKQTPFilledCurveXErrorGraph* jkqtpstatAddXErrorFilledCurveGraph(JKQTBasePlotter* plotter, InputCatIt inFirstCat_Y, InputCatIt inLastCat_Y, InputValueIt inFirstValue_Y, InputValueIt inLastValue_Y, JKQTPStatGroupDefinitionFunctor1D groupDefFunc=&jkqtpstatGroupingIdentity1D, const QString& columnBaseName=QString("grouped data")) { + return jkqtpstatAddXErrorGraph(plotter,inFirstCat_Y, inLastCat_Y, inFirstValue_Y, inLastValue_Y, groupDefFunc, columnBaseName); +} + + + + + + + + +/*! \brief create a plot with x- and y-direction error bars, calculated from directional average +/- stddev of groups in the input data + \ingroup jkqtptools_math_statistics_adaptors + + \tparam TGraph type of graph that should be added to the plot + \tparam InputCatIt standard iterator type of \a inFirstCat_X and \a inLastCat_X + \tparam InputValueIt standard iterator type of \a inFirstValue_Y and \a inLastValue_Y + \param plotter the plotter to which to add the resulting graph + \param inFirstCat_X iterator pointing to the first item in the category dataset to use \f$ c_1 \f$ (used for x-coordinates) + \param inLastCat_X iterator pointing behind the last item in the category dataset to use \f$ c_N \f$ (used for x-coordinates) + \param inFirstValue_Y iterator pointing to the first item in the category dataset to use \f$ v_1 \f$ (used for y-coordinates) + \param inLastValue_Y iterator pointing behind the last item in the category dataset to use \f$ v_N \f$ (used for y-coordinates) + \param groupDefFunc assigns a group \f$ c_{\text{out},j} \f$ to each category value \f$ c_i \f$ . + \param columnBaseName string component used to build the names of the columns generated by this function + \return the graph showing \f$ c_{\text{out},j} \f$ and average +/- stddev for each group \f$ j \f$ + + + + \see jkqtpstatGroupData(), \ref JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat +*/ +template +inline TGraph* jkqtpstatAddXYErrorGraph(JKQTBasePlotter* plotter, InputCatIt inFirstCat_X, InputCatIt inLastCat_X, InputValueIt inFirstValue_Y, InputValueIt inLastValue_Y, JKQTPStatGroupDefinitionFunctor1D groupDefFunc=&jkqtpstatGroupingIdentity1D, const QString& columnBaseName=QString("grouped data")) { + std::map, std::vector > > groupeddataBar; + jkqtpstatGroupData(inFirstCat_X, inLastCat_X, inFirstValue_Y, inLastValue_Y, groupeddataBar, groupDefFunc); + + size_t colGroup=plotter->getDatastore()->addColumn(columnBaseName+", group"); + size_t colAverageX=plotter->getDatastore()->addColumn(columnBaseName+", category-average"); + size_t colStdDevX=plotter->getDatastore()->addColumn(columnBaseName+", category-stddev"); + size_t colAverageY=plotter->getDatastore()->addColumn(columnBaseName+", value-average"); + size_t colStdDevY=plotter->getDatastore()->addColumn(columnBaseName+", value-stddev"); + + for (auto it=groupeddataBar.begin(); it!=groupeddataBar.end(); ++it) { + plotter->getDatastore()->appendToColumn(colGroup, it->first); + plotter->getDatastore()->appendToColumn(colAverageX, jkqtpstatAverage(it->second.first.begin(), it->second.first.end())); + plotter->getDatastore()->appendToColumn(colStdDevX, jkqtpstatStdDev(it->second.first.begin(), it->second.first.end())); + plotter->getDatastore()->appendToColumn(colAverageY, jkqtpstatAverage(it->second.second.begin(), it->second.second.end())); + plotter->getDatastore()->appendToColumn(colStdDevY, jkqtpstatStdDev(it->second.second.begin(), it->second.second.end())); + } + + // 2.4. Finally the calculated groups are drawn + TGraph* graph; + plotter->addGraph(graph=new TGraph(plotter)); + graph->setXColumn(colAverageX); + graph->setYColumn(colAverageY); + graph->setXErrorColumn(static_cast(colStdDevX)); + graph->setYErrorColumn(static_cast(colStdDevY)); + + return graph; +} + + +/*! \brief create a JKQTPXYLineErrorGraph with y-direction error bars, calculated from average +/- stddev of groups in the input data + \ingroup jkqtptools_math_statistics_adaptors + + \tparam InputCatIt standard iterator type of \a inFirstCat_X and \a inLastCat_X + \tparam InputValueIt standard iterator type of \a inFirstValue_Y and \a inLastValue_Y + \param plotter the plotter to which to add the resulting graph + \param inFirstCat_X iterator pointing to the first item in the category dataset to use \f$ c_1 \f$ (used for x-coordinates) + \param inLastCat_X iterator pointing behind the last item in the category dataset to use \f$ c_N \f$ (used for x-coordinates) + \param inFirstValue_Y iterator pointing to the first item in the category dataset to use \f$ v_1 \f$ (used for y-coordinates) + \param inLastValue_Y iterator pointing behind the last item in the category dataset to use \f$ v_N \f$ (used for y-coordinates) + \param groupDefFunc assigns a group \f$ c_{\text{out},j} \f$ to each category value \f$ c_i \f$ . + \param columnBaseName string component used to build the names of the columns generated by this function + \return the graph showing \f$ c_{\text{out},j} \f$ and average +/- stddev for each group \f$ j \f$ + + + + \see jkqtpstatGroupData(), jkqtpstatAddXYErrorGraph(), JKQTPXYLineErrorGraph, \ref JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat +*/ +template +inline JKQTPXYLineErrorGraph* jkqtpstatAddXYErrorLineGraph(JKQTBasePlotter* plotter, InputCatIt inFirstCat_X, InputCatIt inLastCat_X, InputValueIt inFirstValue_Y, InputValueIt inLastValue_Y, JKQTPStatGroupDefinitionFunctor1D groupDefFunc=&jkqtpstatGroupingIdentity1D, const QString& columnBaseName=QString("grouped data")) { + return jkqtpstatAddXYErrorGraph(plotter,inFirstCat_X, inLastCat_X, inFirstValue_Y, inLastValue_Y, groupDefFunc, columnBaseName); +} + + + +/*! \brief create a JKQTPXYParametrizedErrorScatterGraph with y-direction error bars, calculated from average +/- stddev of groups in the input data + \ingroup jkqtptools_math_statistics_adaptors + + \tparam InputCatIt standard iterator type of \a inFirstCat_X and \a inLastCat_X + \tparam InputValueIt standard iterator type of \a inFirstValue_Y and \a inLastValue_Y + \param plotter the plotter to which to add the resulting graph + \param inFirstCat_X iterator pointing to the first item in the category dataset to use \f$ c_1 \f$ (used for x-coordinates) + \param inLastCat_X iterator pointing behind the last item in the category dataset to use \f$ c_N \f$ (used for x-coordinates) + \param inFirstValue_Y iterator pointing to the first item in the category dataset to use \f$ v_1 \f$ (used for y-coordinates) + \param inLastValue_Y iterator pointing behind the last item in the category dataset to use \f$ v_N \f$ (used for y-coordinates) + \param groupDefFunc assigns a group \f$ c_{\text{out},j} \f$ to each category value \f$ c_i \f$ . + \param columnBaseName string component used to build the names of the columns generated by this function + \return the graph showing \f$ c_{\text{out},j} \f$ and average +/- stddev for each group \f$ j \f$ + + + + \see jkqtpstatGroupData(), jkqtpstatAddXYErrorGraph(), JKQTPXYParametrizedErrorScatterGraph, \ref JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat +*/ +template +inline JKQTPXYParametrizedErrorScatterGraph* jkqtpstatAddXYErrorParametrizedScatterGraph(JKQTBasePlotter* plotter, InputCatIt inFirstCat_X, InputCatIt inLastCat_X, InputValueIt inFirstValue_Y, InputValueIt inLastValue_Y, JKQTPStatGroupDefinitionFunctor1D groupDefFunc=&jkqtpstatGroupingIdentity1D, const QString& columnBaseName=QString("grouped data")) { + return jkqtpstatAddXYErrorGraph(plotter,inFirstCat_X, inLastCat_X, inFirstValue_Y, inLastValue_Y, groupDefFunc, columnBaseName); +} + + + + + +/*! \brief create horizontal boxplots of type \­a TGraph, from the 5-value-summary of groups in the input data + \ingroup jkqtptools_math_statistics_adaptors + \internal + + \tparam TGraph type of graph that should be added to the plot, has to offer the same interface as JKQTPBoxplotVerticalGraph or JKQTPBoxplotHorizontalGraph + \tparam InputCatIt standard iterator type of \a inFirstCat_Y and \a inLastCat_Y + \tparam InputValueIt standard iterator type of \a inFirstValue_Y and \a inLastValue_Y + \param plotter the plotter to which to add the resulting graph + \param inFirstCat_Y iterator pointing to the first item in the category dataset to use \f$ c_1 \f$ (used for y-coordinates) + \param inLastCat_Y iterator pointing behind the last item in the category dataset to use \f$ c_N \f$ (used for y-coordinates) + \param inFirstValue_Y iterator pointing to the first item in the category dataset to use \f$ v_1 \f$ (used for x-coordinates) + \param inLastValue_Y iterator pointing behind the last item in the category dataset to use \f$ v_N \f$ (used for x-coordinates) + \param quantile1Spec specifies which quantile to calculate for \a qantile1 (range: 0..1) + \param quantile2Spec specifies which quantile to calculate for \a qantile2 (range: 0..1) + \param minimumQuantile specifies a quantile for the return value minimum (default is 0 for the real minimum, but you could e.g. use 0.05 for the 5% quantile!) + \param maximumQuantile specifies a quantile for the return value maximum (default is 1 for the real maximum, but you could e.g. use 0.95 for the 95% quantile!) + \param groupDefFunc assigns a group \f$ c_{\text{out},j} \f$ to each category value \f$ c_i \f$ . + \param columnBaseName string component used to build the names of the columns generated by this function + \return the boxplot graph + + + + \see jkqtpstatGroupData(), \ref JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat +*/ +template +inline TGraph* jkqtpstatAddBoxplots(JKQTBasePlotter* plotter, InputCatIt inFirstCat_Y, InputCatIt inLastCat_Y, InputValueIt inFirstValue_Y, InputValueIt inLastValue_Y, double quantile1Spec=0.25, double quantile2Spec=0.75, double minimumQuantile=0, double maximumQuantile=1.0, JKQTPStatGroupDefinitionFunctor1D groupDefFunc=&jkqtpstatGroupingIdentity1D, const QString& columnBaseName=QString("grouped boxplot data")) { + std::map > groupeddataBar; + jkqtpstatGroupData(inFirstCat_Y, inLastCat_Y, inFirstValue_Y, inLastValue_Y, groupeddataBar, groupDefFunc); + + size_t colGroup=plotter->getDatastore()->addColumn(columnBaseName+", group"); + size_t colMin=plotter->getDatastore()->addColumn(columnBaseName+", minimum"); + size_t colQ25=plotter->getDatastore()->addColumn(columnBaseName+", quartile25"); + size_t colMedian=plotter->getDatastore()->addColumn(columnBaseName+", median"); + size_t colIQRSig=plotter->getDatastore()->addColumn(columnBaseName+", MedianSignificance"); + size_t colAverage=plotter->getDatastore()->addColumn(columnBaseName+", average"); + size_t colQ75=plotter->getDatastore()->addColumn(columnBaseName+", quartile75"); + size_t colMax=plotter->getDatastore()->addColumn(columnBaseName+", maximum"); + + for (auto it=groupeddataBar.begin(); it!=groupeddataBar.end(); ++it) { + + auto stat5=jkqtpstat5NumberStatistics(it->second.begin(), it->second.end(), quantile1Spec, quantile2Spec, minimumQuantile, maximumQuantile); + + plotter->getDatastore()->appendToColumn(colGroup, it->first); + plotter->getDatastore()->appendToColumn(colAverage, jkqtpstatAverage(it->second.begin(), it->second.end())); + plotter->getDatastore()->appendToColumn(colMin, stat5.minimum); + plotter->getDatastore()->appendToColumn(colQ25, stat5.quantile1); + plotter->getDatastore()->appendToColumn(colMedian, stat5.median); + plotter->getDatastore()->appendToColumn(colQ75, stat5.quantile2); + plotter->getDatastore()->appendToColumn(colMax, stat5.maximum); + plotter->getDatastore()->appendToColumn(colIQRSig, stat5.IQRSignificanceEstimate()); + } + + // 2.4. Finally the calculated groups are drawn + TGraph* graph; + plotter->addGraph(graph=new TGraph(plotter)); + graph->setPositionColumn(colGroup); + graph->setMinColumn(colMin); + graph->setMaxColumn(colMax); + graph->setMedianColumn(colMedian); + graph->setMeanColumn(colAverage); + graph->setPercentile25Column(colQ25); + graph->setPercentile75Column(colQ75); + graph->setMedianConfidenceColumn(colIQRSig); + + return graph; +} + +/*! \brief create vertical boxplots of type \­a JKQTPBoxplotVerticalGraph, from the 5-value-summary of groups in the input data + \ingroup jkqtptools_math_statistics_adaptors + \internal + + \tparam InputCatIt standard iterator type of \a inFirstCat_Y and \a inLastCat_Y + \tparam InputValueIt standard iterator type of \a inFirstValue_X and \a inLastValue_X + \param plotter the plotter to which to add the resulting graph + \param inFirstCat_Y iterator pointing to the first item in the category dataset to use \f$ c_1 \f$ (used for y-coordinates) + \param inLastCat_Y iterator pointing behind the last item in the category dataset to use \f$ c_N \f$ (used for y-coordinates) + \param inFirstValue_X iterator pointing to the first item in the category dataset to use \f$ v_1 \f$ (used for x-coordinates) + \param inLastValue_X iterator pointing behind the last item in the category dataset to use \f$ v_N \f$ (used for x-coordinates) + \param quantile1Spec specifies which quantile to calculate for \a qantile1 (range: 0..1) + \param quantile2Spec specifies which quantile to calculate for \a qantile2 (range: 0..1) + \param minimumQuantile specifies a quantile for the return value minimum (default is 0 for the real minimum, but you could e.g. use 0.05 for the 5% quantile!) + \param maximumQuantile specifies a quantile for the return value maximum (default is 1 for the real maximum, but you could e.g. use 0.95 for the 95% quantile!) + \param groupDefFunc assigns a group \f$ c_{\text{out},j} \f$ to each category value \f$ c_i \f$ . + \param columnBaseName string component used to build the names of the columns generated by this function + \return the boxplot graph + + + + \see jkqtpstatGroupData(), \ref JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat +*/ +template +inline JKQTPBoxplotVerticalGraph* jkqtpstatVAddBoxplots(JKQTBasePlotter* plotter, InputCatIt inFirstCat_Y, InputCatIt inLastCat_Y, InputValueIt inFirstValue_X, InputValueIt inLastValue_X, double quantile1Spec=0.25, double quantile2Spec=0.75, double minimumQuantile=0, double maximumQuantile=1.0, JKQTPStatGroupDefinitionFunctor1D groupDefFunc=&jkqtpstatGroupingIdentity1D, const QString& columnBaseName=QString("grouped boxplot data")) { + + return jkqtpstatAddBoxplots(plotter, inFirstCat_Y, inLastCat_Y, inFirstValue_X, inLastValue_X, quantile1Spec, quantile2Spec, minimumQuantile, maximumQuantile, groupDefFunc, columnBaseName); +} + +/*! \brief create horizontal boxplots of type \­a JKQTPBoxplotHorizontalGraph, from the 5-value-summary of groups in the input data + \ingroup jkqtptools_math_statistics_adaptors + \internal + + \tparam InputCatIt standard iterator type of \a inFirstCat_X and \a inLastCat_X + \tparam InputValueIt standard iterator type of \a inFirstValue_Y and \a inLastValue_Y + \param plotter the plotter to which to add the resulting graph + \param inFirstCat_X iterator pointing to the first item in the category dataset to use \f$ c_1 \f$ (used for y-coordinates) + \param inLastCat_X iterator pointing behind the last item in the category dataset to use \f$ c_N \f$ (used for y-coordinates) + \param inFirstValue_Y iterator pointing to the first item in the category dataset to use \f$ v_1 \f$ (used for x-coordinates) + \param inLastValue_Y iterator pointing behind the last item in the category dataset to use \f$ v_N \f$ (used for x-coordinates) + \param quantile1Spec specifies which quantile to calculate for \a qantile1 (range: 0..1) + \param quantile2Spec specifies which quantile to calculate for \a qantile2 (range: 0..1) + \param minimumQuantile specifies a quantile for the return value minimum (default is 0 for the real minimum, but you could e.g. use 0.05 for the 5% quantile!) + \param maximumQuantile specifies a quantile for the return value maximum (default is 1 for the real maximum, but you could e.g. use 0.95 for the 95% quantile!) + \param groupDefFunc assigns a group \f$ c_{\text{out},j} \f$ to each category value \f$ c_i \f$ . + \param columnBaseName string component used to build the names of the columns generated by this function + \return the boxplot graph + + + + \see jkqtpstatGroupData(), \ref JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat +*/ +template +inline JKQTPBoxplotHorizontalGraph* jkqtpstatVAddBoxplots(JKQTBasePlotter* plotter, InputCatIt inFirstCat_X, InputCatIt inLastCat_X, InputValueIt inFirstValue_Y, InputValueIt inLastValue_Y, double quantile1Spec=0.25, double quantile2Spec=0.75, double minimumQuantile=0, double maximumQuantile=1.0, JKQTPStatGroupDefinitionFunctor1D groupDefFunc=&jkqtpstatGroupingIdentity1D, const QString& columnBaseName=QString("grouped boxplot data")) { + + return jkqtpstatAddBoxplots(plotter, inFirstCat_X, inLastCat_X, inFirstValue_Y, inLastValue_Y, quantile1Spec, quantile2Spec, minimumQuantile, maximumQuantile, groupDefFunc, columnBaseName); +} + +/*! \brief create vertical boxplots of type \­a JKQTPBoxplotVerticalGraph, from the 5-value-summary of groups in the input data, also adds a graph showing the outliers + \ingroup jkqtptools_math_statistics_adaptors + + \tparam InputCatIt standard iterator type of \a inFirstCat_X and \a inLastCat_X + \tparam InputValueIt standard iterator type of \a inFirstValue_Y and \a inLastValue_Y + \param plotter the plotter to which to add the resulting graph + \param inFirstCat_X iterator pointing to the first item in the category dataset to use \f$ c_1 \f$ (used for y-coordinates) + \param inLastCat_X iterator pointing behind the last item in the category dataset to use \f$ c_N \f$ (used for y-coordinates) + \param inFirstValue_Y iterator pointing to the first item in the category dataset to use \f$ v_1 \f$ (used for x-coordinates) + \param inLastValue_Y iterator pointing behind the last item in the category dataset to use \f$ v_N \f$ (used for x-coordinates) + \param quantile1Spec specifies which quantile to calculate for \a qantile1 (range: 0..1) + \param quantile2Spec specifies which quantile to calculate for \a qantile2 (range: 0..1) + \param minimumQuantile specifies a quantile for the return value minimum (default is 0 for the real minimum, but you could e.g. use 0.05 for the 5% quantile!) + \param maximumQuantile specifies a quantile for the return value maximum (default is 1 for the real maximum, but you could e.g. use 0.95 for the 95% quantile!) + \param groupDefFunc assigns a group \f$ c_{\text{out},j} \f$ to each category value \f$ c_i \f$ . + \param columnBaseName string component used to build the names of the columns generated by this function + \return the boxplot graph (return.first) and the outliers graph (return.second) + + + + \see jkqtpstatGroupData(), \ref JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat +*/ +template +inline std::pair jkqtpstatAddVBoxplotsAndOutliers(JKQTBasePlotter* plotter, InputCatIt inFirstCat_X, InputCatIt inLastCat_X, InputValueIt inFirstValue_Y, InputValueIt inLastValue_Y, double quantile1Spec=0.25, double quantile2Spec=0.75, double minimumQuantile=0.03, double maximumQuantile=0.97, JKQTPStatGroupDefinitionFunctor1D groupDefFunc=&jkqtpstatGroupingIdentity1D, const QString& columnBaseName=QString("grouped boxplot data")) { + std::map > groupeddataBar; + jkqtpstatGroupData(inFirstCat_X, inLastCat_X, inFirstValue_Y, inLastValue_Y, groupeddataBar, groupDefFunc); + + size_t colGroup=plotter->getDatastore()->addColumn(columnBaseName+", group"); + size_t colMin=plotter->getDatastore()->addColumn(columnBaseName+", minimum"); + size_t colQ25=plotter->getDatastore()->addColumn(columnBaseName+", quartile25"); + size_t colMedian=plotter->getDatastore()->addColumn(columnBaseName+", median"); + size_t colIQRSig=plotter->getDatastore()->addColumn(columnBaseName+", MedianSignificance"); + size_t colAverage=plotter->getDatastore()->addColumn(columnBaseName+", average"); + size_t colQ75=plotter->getDatastore()->addColumn(columnBaseName+", quartile75"); + size_t colMax=plotter->getDatastore()->addColumn(columnBaseName+", maximum"); + size_t colOutlierG=plotter->getDatastore()->addColumn(columnBaseName+", outlier-group"); + size_t colOutlierV=plotter->getDatastore()->addColumn(columnBaseName+", outlier-value"); + + for (auto it=groupeddataBar.begin(); it!=groupeddataBar.end(); ++it) { + + auto stat5=jkqtpstat5NumberStatistics(it->second.begin(), it->second.end(), quantile1Spec, quantile2Spec, minimumQuantile, maximumQuantile); + + plotter->getDatastore()->appendToColumn(colGroup, it->first); + plotter->getDatastore()->appendToColumn(colAverage, jkqtpstatAverage(it->second.begin(), it->second.end())); + plotter->getDatastore()->appendToColumn(colMin, stat5.minimum); + plotter->getDatastore()->appendToColumn(colQ25, stat5.quantile1); + plotter->getDatastore()->appendToColumn(colMedian, stat5.median); + plotter->getDatastore()->appendToColumn(colQ75, stat5.quantile2); + plotter->getDatastore()->appendToColumn(colMax, stat5.maximum); + plotter->getDatastore()->appendToColumn(colIQRSig, stat5.IQRSignificanceEstimate()); + std::fill_n(plotter->getDatastore()->backInserter(colOutlierG), stat5.outliers.size(), jkqtp_todouble(it->first)); + plotter->getDatastore()->appendToColumn(colOutlierV, stat5.outliers.begin(), stat5.outliers.end()); + } + + JKQTPBoxplotVerticalGraph* graph; + plotter->addGraph(graph=new JKQTPBoxplotVerticalGraph(plotter)); + graph->setPositionColumn(colGroup); + graph->setMinColumn(colMin); + graph->setMaxColumn(colMax); + graph->setMedianColumn(colMedian); + graph->setMeanColumn(colAverage); + graph->setPercentile25Column(colQ25); + graph->setPercentile75Column(colQ75); + graph->setMedianConfidenceColumn(colIQRSig); + + JKQTPXYLineGraph* graphOL; + plotter->addGraph(graphOL=new JKQTPXYLineGraph(plotter)); + graphOL->setXColumn(colOutlierG); + graphOL->setYColumn(colOutlierV); + graphOL->setDrawLine(false); + graphOL->setSymbolSize(graphOL->getSymbolSize()/2.0); + graphOL->setColor(graph->getLineColor()); + return std::pair(graph, graphOL); +} + + +/*! \brief create vertical boxplots of type \­a JKQTPBoxplotHorizontalGraph, from the 5-value-summary of groups in the input data, also adds a graph showing the outliers + \ingroup jkqtptools_math_statistics_adaptors + + \tparam InputCatIt standard iterator type of \a inFirstCat_Y and \a inLastCat_Y + \tparam InputValueIt standard iterator type of \a inFirstValue_X and \a inLastValue_X + \param plotter the plotter to which to add the resulting graph + \param inFirstCat_Y iterator pointing to the first item in the category dataset to use \f$ c_1 \f$ (used for y-coordinates) + \param inLastCat_Y iterator pointing behind the last item in the category dataset to use \f$ c_N \f$ (used for y-coordinates) + \param inFirstValue_X iterator pointing to the first item in the category dataset to use \f$ v_1 \f$ (used for x-coordinates) + \param inLastValue_X iterator pointing behind the last item in the category dataset to use \f$ v_N \f$ (used for x-coordinates) + \param quantile1Spec specifies which quantile to calculate for \a qantile1 (range: 0..1) + \param quantile2Spec specifies which quantile to calculate for \a qantile2 (range: 0..1) + \param minimumQuantile specifies a quantile for the return value minimum (default is 0 for the real minimum, but you could e.g. use 0.05 for the 5% quantile!) + \param maximumQuantile specifies a quantile for the return value maximum (default is 1 for the real maximum, but you could e.g. use 0.95 for the 95% quantile!) + \param groupDefFunc assigns a group \f$ c_{\text{out},j} \f$ to each category value \f$ c_i \f$ . + \param columnBaseName string component used to build the names of the columns generated by this function + \return the boxplot graph (return.first) and the outliers graph (return.second) + + + + \see jkqtpstatGroupData(), \ref JKQTPlotterBasicJKQTPDatastoreStatisticsGroupedStat +*/ +template +inline std::pair jkqtpstatAddHBoxplotsAndOutliers(JKQTBasePlotter* plotter, InputCatIt inFirstCat_Y, InputCatIt inLastCat_Y, InputValueIt inFirstValue_X, InputValueIt inLastValue_X, double quantile1Spec=0.25, double quantile2Spec=0.75, double minimumQuantile=0.03, double maximumQuantile=0.97, JKQTPStatGroupDefinitionFunctor1D groupDefFunc=&jkqtpstatGroupingIdentity1D, const QString& columnBaseName=QString("grouped boxplot data")) { + std::map > groupeddataBar; + jkqtpstatGroupData(inFirstCat_Y, inLastCat_Y, inFirstValue_X, inLastValue_X, groupeddataBar, groupDefFunc); + + size_t colGroup=plotter->getDatastore()->addColumn(columnBaseName+", group"); + size_t colMin=plotter->getDatastore()->addColumn(columnBaseName+", minimum"); + size_t colQ25=plotter->getDatastore()->addColumn(columnBaseName+", quartile25"); + size_t colMedian=plotter->getDatastore()->addColumn(columnBaseName+", median"); + size_t colIQRSig=plotter->getDatastore()->addColumn(columnBaseName+", MedianSignificance"); + size_t colAverage=plotter->getDatastore()->addColumn(columnBaseName+", average"); + size_t colQ75=plotter->getDatastore()->addColumn(columnBaseName+", quartile75"); + size_t colMax=plotter->getDatastore()->addColumn(columnBaseName+", maximum"); + size_t colOutlierG=plotter->getDatastore()->addColumn(columnBaseName+", outlier-group"); + size_t colOutlierV=plotter->getDatastore()->addColumn(columnBaseName+", outlier-value"); + + for (auto it=groupeddataBar.begin(); it!=groupeddataBar.end(); ++it) { + + auto stat5=jkqtpstat5NumberStatistics(it->second.begin(), it->second.end(), quantile1Spec, quantile2Spec, minimumQuantile, maximumQuantile); + + plotter->getDatastore()->appendToColumn(colGroup, it->first); + plotter->getDatastore()->appendToColumn(colAverage, jkqtpstatAverage(it->second.begin(), it->second.end())); + plotter->getDatastore()->appendToColumn(colMin, stat5.minimum); + plotter->getDatastore()->appendToColumn(colQ25, stat5.quantile1); + plotter->getDatastore()->appendToColumn(colMedian, stat5.median); + plotter->getDatastore()->appendToColumn(colQ75, stat5.quantile2); + plotter->getDatastore()->appendToColumn(colMax, stat5.maximum); + plotter->getDatastore()->appendToColumn(colIQRSig, stat5.IQRSignificanceEstimate()); + std::fill_n(plotter->getDatastore()->backInserter(colOutlierG), stat5.outliers.size(), jkqtp_todouble(it->first)); + plotter->getDatastore()->appendToColumn(colOutlierV, stat5.outliers.begin(), stat5.outliers.end()); + } + + JKQTPBoxplotHorizontalGraph* graph; + plotter->addGraph(graph=new JKQTPBoxplotHorizontalGraph(plotter)); + graph->setPositionColumn(colGroup); + graph->setMinColumn(colMin); + graph->setMaxColumn(colMax); + graph->setMedianColumn(colMedian); + graph->setMeanColumn(colAverage); + graph->setPercentile25Column(colQ25); + graph->setPercentile75Column(colQ75); + graph->setMedianConfidenceColumn(colIQRSig); + + JKQTPXYLineGraph* graphOL; + plotter->addGraph(graphOL=new JKQTPXYLineGraph(plotter)); + graphOL->setYColumn(colOutlierG); + graphOL->setXColumn(colOutlierV); + graphOL->setDrawLine(false); + graphOL->setSymbolSize(graphOL->getSymbolSize()/2.0); + graphOL->setColor(graph->getLineColor()); + return std::pair(graph, graphOL); +} + + + + + + #endif // JKQTPGRAPHSSTATISTICSADAPTORS_H_INCLUDED diff --git a/screenshots/jkqtplotter_simpletest_datastore_groupedstat.png b/screenshots/jkqtplotter_simpletest_datastore_groupedstat.png new file mode 100644 index 0000000000000000000000000000000000000000..06555f908eafd776caa8068e56052ed8230f4e8e GIT binary patch literal 43936 zcmdSAbySpH+XrePf`AIrrGOwHT|-MtcQ;7q(2XJu10oFrNOyOOz#s@SbeD{zbT{V) z_4(fS`=0afS?jC?i(%&8dtdu^)o)+do)85&2~2b%^jo)XVM@LcRl0TSuIH^=w`(5U z0p5`me{cl+x$USVA$+R@PO=L8bI(jjR_NBP^2mqhhA6=QX!dV39dF&jYQO%y-DQ{e z7I+ieNle2@+0N9-)zHD@mV|?eiLIl#os*vG17JuYzNDy-iksd>3MzD1lsITPf-cWV zq5NH*hJcum7>Y&4vj;CTKEHeRGBngkD9J%$i}W_Fp9=2d$FzP=gHW%s5k(_Gbnphhw}AA-kn%mw?~|@j8yDoP6PSI4#n8EbGLlmu4mG1gYC{yn_FRFB)qwe+xnELZVmZ zk=xuRP`=Er(|JC^%Uohn?GWo3(?h!Ef39w`loT*Ta6!k1G=*MSbN}S%?(X)5&uxo0 zN>=lppR0|ftsRnG!N0IbM3U6I9E8RVl0#s=10ejO$)%cO{meDBtEw}75dWEU(gPNk z17n{2N6+g--9AuWo{I_{b6<-RfvKotGd2)lKjtEtGS zkMY12v7^txi(fP`Qjd6`8$^j$GCNZ5x+WE?S~O8@z)`J^7d2Gqx~hHySouNO*t7y< zZ)nTs%4(yotiPzXx`MAPr;=8_cu6_hZJW-_;q5am3e8WBW~E} zri)1=^jJFOi`*9!Iafu}_#R7$6|M0;pzLID$)rn%6Ot)>pl%S~e2NHoL>QH}_g_>?ct>sbI39#Y0)n zi0!ouiWai4tFDC9)Z5pdGXA)8#D!eD!l$(P#Px_}o!c3=F@jOE6=9`t{Ee~sy(8vE z^?Bji=bBL8Ba$<#i`mxEjdb6!AFZDFg{y-{sO!st58K({sy-rbIH^-+Dm6B4pAG-E zyJlqGn2rLZ(ji_*)$^#7rA)proqzT?{UOovNTK=wt4R|4RE21H!I^gk{|Z^SmUeJH z#jN-%phGaG-uFu5QEuT_0oT5Sa|w^MxW#A2-NI4R6wjdiFyoffk&#LnBd(*8eMsZQ z%Gz>a2SP{8d-UM^W1O>dq345=jW*57poN8=s;Qd9Ndq0)(eD=e+Ie#rk+495G|BAU zF3}4L7}@l)k!Ncmo4^RcA~;*%Jq&Ub9M7;n@?nZ@C0{y4jV4^aq84+zLfkiU;A*Jz z#?}@r8@<;I`X{m)wASbjNzOdmeM$$yacK4j=m4PJUwcXz|7otX#80XjuI`=Jbn8>ojd_UPr`}0f;)*rnut>FfMdk-y%3()q1+sY3O$ET=F6P z5b+T`jz*>o`AUvd?mAwZMUf)00hbEM3zDoy6k5scTFBMxe0PG}HOuM(Qjsti{q_UAS&ePq3LoeeI@^EysLbQ=82?a>E64`<3{Uh)m7zU88n9 zhmV;0e)=1FC~xIdYsrkME-Z{RMrL-(hl}h1A?wxV>^Nvl`;y?0uqVhfO@QZU_Su*F z#JoGK>LIm#+reXItkojZ*!7>sMl2j--Skt=YTVrmysf<~n{3mwRo!%@JU{VseK{kF zz2^}=o62~TcU{s}GS=n?eII#X@=P$!-57;=`FJG&O@nBE!n;D|sxiOh;$mT>_vCtTH89JCBISXRaZF0NM z?M(!6T+M0T3`3kXoSJ!GN~XL{%oAMKA1l(*msd~fQ>FS+oH+W9=<0)TzcI92L~D9V z2Vr5;oa)$&S}Ub1YPFi{SS<7o^RO8+)sdDg`3IqiCX$W2x$B{97re8+C(fLBey#1h zPH62&_Tko9{$a~bN!H=~a`mA`XRCKo10DE-a{TTom8U>$s#TVod-!GA@7*6lMJ=Xk zu43A%sy=_TIlzQ-0J{a^{S&>;P#Xz#Yu*Z@Us16=tr9HswX_4JcXB;3RPM#}SoTR- zH*Gw)Y{ZVADLDMhSoqrp+GmyZ91qWN>%f@v9=p}j_DH%fm{2*%2CsvjMf!Lq{W|;0 zY2W3`v#sfXy{mLEot@Ail9R0Onm^s8EGkE93r z&_>csOx<@Y8JIKPzjCS{{R)IoD1qGA?Pyg}z6FjM)RzuR^lKABND5XUaFF~^Et>R^ ziO;l*D4zByTK>YK6~uo(qD{&hQ_h&!B+BNrGj`=BK~L9hRgG0`jpM2gvgtM)txaay zq3ZUPk^AbEFMd8-*lR7Bb@bDQj2Toed8;SnJ2_<7ei)*lhmCP@P7EZP1% z7ZSco%QNszN(NN4M%tyzot~^#0V}?rJF*~W;sIezzbs@|_7j^>`W>QjF3g%tH%g2F zDq}!RUrQzoan|+{N8l$#q?MNLq@pk=QmQ58SMk^3xz9@HEZ;M}yBE@ciKTpQ^TQvj zx)&2oKcqLh_a>9D%-oloa6g`&)TPR%+G)gT5WTws&*?ll0kgACvzFYlZCf`~DYc zEe?V^{w2$Yf#G|x>Uukt&}D)^!|NXy`u((y&t@4m6=2riZ2iz+TM~nq=7zar7}EeB zm;L?7#pwSJAB7u39BsyO1I$4oQfi6A3cp2d@jkq>=`Sm`@Na}_&M@6h<<)>jX-Qsf$ zml+4TP0vrY^{~FZnf9!})Z~ml7`k9`C$o$j^Dz}H5tiuT|Ap3lyq{F0dA>OD>IDJy zGd`?Db1LWQwZJTzhNxAOtj2`lepT3^W#%x^n9;Fsm@%Joz^uZu+)<5tzPWQx`|G^- zW2l{H&)TzY1NegmLCTR&L5n;Jby%)z10PHqw?}(zJ{cCZBR__SD|HpWI(A^Vcx5yB z))NiW<^oOp>-X7ok;GqC43j#g$;)5+@7g?A@XjAT!Dtsn{~&J!HHS82eyD8^awuXU z;cp^0nJyrNQC!Q|F;il{{Uorh06w@@70^_p4r_?7OsG;=qjcXRb?0qYsc#6c3m)qaV~fpP9JB+{uhL5B?@fDG*4E^(NBNS}G%$K)l+_Qv{;s zM7}0)goS@OwX%FV$%h~-6Q5!)m0l%g#=v=kArKNmhTM1kwml%D-p8!=<7N)=yw>-> zY;xl1W?sW2OOaDsS}Oa}9t++jo~3$|Zo0)SO05uJQ}=#v>Xb6n<@1-!pkE)~JHs-A zYE5S&g)xlr$bw|pGl*+Jvh51QQK#vWFIB7jCC;jKiE|ELXs;$oE2$?;*2g!Cw3WGB ziGB6<)|)cOp2B->EA5_$V3tW>0hAgA%jfm$ri|Y?4UW(JB*8|$K>%aF227O8Hg-Uw z|JiV)#F)8kH4EZ!GT3EgCFy0}cT4Bf1CI!gY))t0U~RU@`qbp*330W0h11>gnB`Gm z&uYM)!8zble&PAsoxN>$lL;eaFpQsEi~H#?(8OUY7^CSCtBe-lkKrPa=o@zp3{e6` z86w~QcHi|5z=%2J{Uk7ERMWU z!hX(rkv$a*G0&Zm`ovf|;&<~I%!`T7fPyhVoVVb>JY_4Elr zG?8h;;*YLpsqN1?-lyyH?XxUZqZSrE@MinSu9>gur;)Eal}H^KkB0SU_}EGk<(-T$wTL^<&qJxWv>4b`gOEzSpyT0EGYNKJX zhCcscwNJcOo+qSQ!kHS2P1ndJ@;3YaNP$9Z7X{Iz4yI;%7L8YPQJ}}cf%@l)S`-t= zVX0_}h#TGumC=!BEtUyRk=YJ2|g<-t>*Oig8}HV z&KwU^^Al(-V?pWZBi|`9I9V17^+Y%oc$p9Ld)1=E{CnssRx~5cIUIy-GM>=`$c=~> z#^4ZH;-Jtqe%8X(=Xel1gC8&E-3*QUMnpYL;Dy}|BgEG0DoGzye?sHcCtPJw9UJc< zDhM$bGr1iW#=LsJv@}0$jDMb8&aGHNI8hu!jG7c@ySj$Qw6{tKW9T#^_1(egebXEc zj99&}7M?Z>{Yv&GwKc`F?FkyA1Zm}2Elcx*Ku({W>Wmh$0eo*+N}*SqR(M7)Sq-ma zB`5r&f<*O`q+Fwy6k4)h;;{Am_@>U1vY9*>JG0ofUmW@`bUWUSY3el|Sg)GJb@YF!Z1xe-9|}%?*(O!m zzy3M}m_-fAE#0n8Eco3cWpP3Vdw)SM`FScs;)9#qWcJ35w5#Pl*Qj(=2J|CAD3NKB zVYVoJ4_Zizc|vEFU{g!SW!Jl9o#+kzzd}3%VNj`Xaxwp;2v&>12`;q=856^pe<1q0VBQGfl$LW_0ML-9xqTLYa9STogwVvkU^jXx1*m9e zE@XK@3zC-VUAfTpZII@5eyXH&zE0oVQZ@QbN^& zC*jq%Yn}hY3=b#!*hAJE^O2}NWiG$fzBxxK4v~+k#c^V1$DP}#A!f~EVqUVu4{YN^ zt>z~N5wAju#ttqviWG;PiUa7>sn4ryhZwBq$Q1BH6 z4)q_{N$OJj`S4W=DZ;dxq5YXPN}nqv*xFxVY7Vb0`irKN8oZ>a2$YoXm;Qy-HzdU7 zRJlKdt(kd2uekg;RSYdwHLpii5(QVYz2m`t0T2L4+1tC0h29P**d@d%Ok~n0?S27t zVy2EwSEvwMTf#?wz!7K>0*#Bs7E#QoIfxj2E%ydPEM!vWeGgq=2ANIVl~~!!s3EOv zXum@8O9jn<0u^8~rmzRG{CK&v_!SWUf&^ycKXmSL{w_Pf_l$7?Edu1A_2O{q!y|#? zHdJzj2`nr!zF_aTSDK%CI2EfcRo7iMCaYCyKGfJu8KqH5L}XQv%B1oUo}K@Umd~?B zZFJp&2}s8?%9>&grXErCR!+Ko$bLGj` zDw5_H+tXP{2qt4LrjitWie3F#& zgE9T%mutiMB8$wG<^vhuN;ivZXEPiUH0$i2muzm-&gB5F%M665)VX{wy>xVE8l3Y< zOnXU>Z)FNrN;7?HT~T&p5_uz})S=`8`+# z>ZK8+5v06vJudGqL%rje^iz8{!CTEoMzz02i!&XZSk2%hUQdtX@5Z+Gu1{2goe}&; zB*5z;%}RUcY^hiPfkbXc>Js>&V5RqyA0t2J#_M>_qZ4`zO=01Lbq%a0Jqp~t=Z8~v zbqy6J-<7y~Wzqyl)zwRMxaqp60JbUG40hR?C4H2yvJH8Ggo3~eWrpo}^Z0wP6JYDU zXB)LD+hOj==~iFgLbc-Fy090>^~vgA)Af$|^Tg6HZrf>4w3$ujFsSuxv#~pxM!Ah? z2`B;#X4_2FWc%sAA~b7s*#PWG7-h~JCTY?tlfvWt<=yR@Njtppg1E5Q=+0oDz*wX_ zPxUqFj$gnajsqED8&kEpeinY5@PLQ-E-U@-(R0uyK_9er`_bzR+Wa1rmKb+Ms4&a+ z!w9F>fTvFOmR;9|L;gHPN=ySBU1Wxd&7vs8wn<~qTgu-;4}Oi6YLrvpo*T`AcDy%p zl5I2m^m~byVG!c-7i2v7>%eZ@ms4C_Hm4JB22@*(l_+JSyjb2DW5CcAwo{Y(y2CJO)XqlnN&E6{Q z=2`#~&$-!~fcNRo?&zQ!sXdQ2Q@HH(=n|;SQ96+I-MW(U{_6Nu;KTI^Hs8zho4rnC zN<`j4L04#F7a}%0+MM}N7t;j`0fHeCgzUH-MXOBhm2X?W7>6qRk=xO9x;k$@`E09| z{H*um^SiMZpV-Xgywr32^ef(W`_14n>Lm2E2qA(aBXj-q6~fBpr!(``iK}d^HS-jn z)1Bo2Fpuw!YTIR3=x?8NwWH#CQI0Sh!@^Y#AC{ahCEEcLZKHJB?&y2k{j3h<#FDk; zyySk*M1p(nhahkXLy*OKJ9d$}c64?swlN*ghIlWu0z)H2NFacvW$WG_xtDgE?il6B zY8?-iD%RWB$8FAeOm}?nr$AVrJt`m*3F}ja_ei`)QXq#F7QW+o{-fy+krdiB)+(Hs zE#H&84i2UP&uik5-*!uFQ8(L#yp~TYg8x($mbWk5b5syEZcAYgji3_(!d^HsTsf8F zErI72Pc?d^h&ZK9P{s?lX;vAP$6^w(pRYMT7^Yckt9#A{BBcZt-Ps}#f|zt0*m2KyzubLfP&_WVg!1hgPr2?c($RFsbt}!bV*n*u{De6>D>yD<`1)Sd11C z9$p7ey0aGmE-AdOIevD4ulac|!BdiZeB6&_T**DxSpeZW@%1}w&yKd_P3FFRZ;Pgp z&#ABz4<~rfkmk9`O?QTp+`I!L2CSQp)-P*Ap~|16_B69=yxh1vM>GUmg;Q2oSQrlF z&NoP6F^0{rrF!j3(k18UHoDY1EN1vw_?+)Z=*%d~^NX1bc>y7Vx~nF2AX5Uhmd8sh z!^9Z~itMqn9}2%m(>%hJq1-y4)2yJ1l^p_-N|XDZQX9MA`S#tC%|x@*^a=|g4H)m* z$RWN(Q3BCU;Oteb^S9dLmpldcAVA2DV3Tvlr`%mDEcHX{&j`dM7Dt(bay#BX>qz<7 zR#fXD|9!W04 zo^-RvL4(T%5Cy*Rl3+xBgs5Aq@AP{BG9kCiySWVCYUzz`Mn`9QyB49E=$_w(&_^vd;1Y`m|BO@Sa zQEU+l4Z6J45{}Tm7!bK!+&9%8}qnpTTl4qSXuSRpV38b1= zv9fzZ@`AC9x=H9cnpNgcN=ktAro!3R5rXr<4b4w)#vcvGPlNQ5Fubt6wLIUNNJsZ^ z>9HTmLwjdjI+f2A7O+Ti@(y}|>wHSu(x^DWj)XxT8aex3p3sz(0LfWp^!eOE=1Lwm z`|m`O)bRP$Yx@UEgIbcw|dx&#DcYT)

!lDdGRAt= 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 UnJ=XR&*X?83TpC&4^3bEFL}q|KmY&$ literal 0 HcmV?d00001 diff --git a/screenshots/jkqtplotter_simpletest_datastore_groupedstat_barchartrawdata.png b/screenshots/jkqtplotter_simpletest_datastore_groupedstat_barchartrawdata.png new file mode 100644 index 0000000000000000000000000000000000000000..738c1e5925f998fa3425b48a6bbd5ac1f62e544b GIT binary patch literal 5327 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

L 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 literal 0 HcmV?d00001 diff --git a/screenshots/jkqtplotter_simpletest_datastore_groupedstat_scatter.png b/screenshots/jkqtplotter_simpletest_datastore_groupedstat_scatter.png new file mode 100644 index 0000000000000000000000000000000000000000..5d34a9fd969dad2024c57ab9cf3cbda9db35a591 GIT binary patch literal 26042 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 YA9EG%`BJk1f6Xi@@54ZTQ_E-Enz5K6$1 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 Ef^>RyM*si- literal 0 HcmV?d00001