2021-01-01 21:37:55 +00:00
|
|
|
// SPDX-License-Identifier: GPL-2.0
|
|
|
|
#include "statsstate.h"
|
|
|
|
#include "statstranslations.h"
|
|
|
|
#include "statsvariables.h"
|
|
|
|
|
2021-12-31 17:29:06 +00:00
|
|
|
// Attn: The order must correspond to the enum ChartSubType
|
2021-01-01 21:37:55 +00:00
|
|
|
static const char *chart_subtype_names[] = {
|
|
|
|
QT_TRANSLATE_NOOP("StatsTranslations", "vertical"),
|
|
|
|
QT_TRANSLATE_NOOP("StatsTranslations", "grouped vertical"),
|
|
|
|
QT_TRANSLATE_NOOP("StatsTranslations", "stacked vertical"),
|
|
|
|
QT_TRANSLATE_NOOP("StatsTranslations", "horizontal"),
|
|
|
|
QT_TRANSLATE_NOOP("StatsTranslations", "grouped horizontal"),
|
|
|
|
QT_TRANSLATE_NOOP("StatsTranslations", "stacked horizontal"),
|
|
|
|
QT_TRANSLATE_NOOP("StatsTranslations", "data points"),
|
|
|
|
QT_TRANSLATE_NOOP("StatsTranslations", "box-whisker"),
|
|
|
|
QT_TRANSLATE_NOOP("StatsTranslations", "piechart"),
|
|
|
|
};
|
|
|
|
|
2021-12-31 17:29:06 +00:00
|
|
|
// Attn: The order must correspond to the enum ChartSortMode
|
|
|
|
static const char *sortmode_names[] = {
|
|
|
|
QT_TRANSLATE_NOOP("StatsTranslations", "Bin"),
|
|
|
|
QT_TRANSLATE_NOOP("StatsTranslations", "Count"),
|
|
|
|
QT_TRANSLATE_NOOP("StatsTranslations", "Value"),
|
|
|
|
};
|
|
|
|
|
2021-01-01 21:37:55 +00:00
|
|
|
enum class SupportedVariable {
|
2021-01-02 23:31:55 +00:00
|
|
|
None,
|
2021-01-01 21:37:55 +00:00
|
|
|
Categorical, // Implies that the variable is binned
|
|
|
|
Continuous, // Implies that the variable is binned
|
|
|
|
Numeric
|
|
|
|
};
|
|
|
|
|
2021-01-19 10:18:10 +00:00
|
|
|
static const int ChartFeatureLabels = 1 << 0;
|
|
|
|
static const int ChartFeatureLegend = 1 << 1;
|
|
|
|
static const int ChartFeatureMedian = 1 << 2;
|
|
|
|
static const int ChartFeatureMean = 1 << 3;
|
|
|
|
static const int ChartFeatureQuartiles = 1 << 4;
|
|
|
|
static const int ChartFeatureRegression = 1 << 5;
|
|
|
|
static const int ChartFeatureConfidence = 1 << 6;
|
2021-01-01 21:37:55 +00:00
|
|
|
|
|
|
|
static const struct ChartTypeDesc {
|
|
|
|
ChartType id;
|
|
|
|
const char *name;
|
|
|
|
SupportedVariable var1;
|
|
|
|
SupportedVariable var2;
|
2021-01-02 23:31:55 +00:00
|
|
|
bool var1Binned, var2Binned, var2HasOperations;
|
2021-01-01 21:37:55 +00:00
|
|
|
const std::vector<ChartSubType> subtypes;
|
|
|
|
int features;
|
|
|
|
} chart_types[] = {
|
|
|
|
{
|
|
|
|
ChartType::ScatterPlot,
|
|
|
|
QT_TRANSLATE_NOOP("StatsTranslations", "Scattergraph"),
|
|
|
|
SupportedVariable::Continuous,
|
|
|
|
SupportedVariable::Numeric,
|
2021-01-02 23:31:55 +00:00
|
|
|
false, false, false,
|
2021-01-01 21:37:55 +00:00
|
|
|
{ ChartSubType::Dots },
|
2021-01-19 10:18:10 +00:00
|
|
|
ChartFeatureRegression | ChartFeatureConfidence
|
2021-01-01 21:37:55 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
ChartType::HistogramCount,
|
|
|
|
QT_TRANSLATE_NOOP("StatsTranslations", "Histogram"),
|
|
|
|
SupportedVariable::Continuous,
|
2021-01-02 23:31:55 +00:00
|
|
|
SupportedVariable::None,
|
|
|
|
true, false, false,
|
2021-01-01 21:37:55 +00:00
|
|
|
{ ChartSubType::Vertical, ChartSubType::Horizontal },
|
|
|
|
ChartFeatureLabels | ChartFeatureMedian | ChartFeatureMean
|
|
|
|
},
|
|
|
|
{
|
|
|
|
ChartType::HistogramValue,
|
|
|
|
QT_TRANSLATE_NOOP("StatsTranslations", "Histogram"),
|
|
|
|
SupportedVariable::Continuous,
|
|
|
|
SupportedVariable::Numeric,
|
2021-01-02 23:31:55 +00:00
|
|
|
true, false, true,
|
2021-01-01 21:37:55 +00:00
|
|
|
{ ChartSubType::Vertical, ChartSubType::Horizontal },
|
|
|
|
ChartFeatureLabels
|
|
|
|
},
|
|
|
|
{
|
|
|
|
ChartType::HistogramBox,
|
|
|
|
QT_TRANSLATE_NOOP("StatsTranslations", "Histogram"),
|
|
|
|
SupportedVariable::Continuous,
|
|
|
|
SupportedVariable::Numeric,
|
2021-01-02 23:31:55 +00:00
|
|
|
true, false, false,
|
2021-01-01 21:37:55 +00:00
|
|
|
{ ChartSubType::Box },
|
|
|
|
0
|
|
|
|
},
|
|
|
|
{
|
|
|
|
ChartType::HistogramStacked,
|
|
|
|
QT_TRANSLATE_NOOP("StatsTranslations", "Histogram"),
|
|
|
|
SupportedVariable::Continuous,
|
|
|
|
SupportedVariable::Categorical,
|
2021-01-02 23:31:55 +00:00
|
|
|
true, true, false,
|
2021-01-01 21:37:55 +00:00
|
|
|
{ ChartSubType::VerticalStacked, ChartSubType::HorizontalStacked },
|
|
|
|
ChartFeatureLabels | ChartFeatureLegend
|
|
|
|
},
|
|
|
|
{
|
|
|
|
ChartType::DiscreteScatter,
|
|
|
|
QT_TRANSLATE_NOOP("StatsTranslations", "Categorical"),
|
|
|
|
SupportedVariable::Categorical,
|
|
|
|
SupportedVariable::Numeric,
|
2021-01-02 23:31:55 +00:00
|
|
|
true, false, false,
|
2021-01-01 21:37:55 +00:00
|
|
|
{ ChartSubType::Dots },
|
|
|
|
ChartFeatureQuartiles
|
|
|
|
},
|
|
|
|
{
|
|
|
|
ChartType::DiscreteValue,
|
|
|
|
QT_TRANSLATE_NOOP("StatsTranslations", "Categorical"),
|
|
|
|
SupportedVariable::Categorical,
|
|
|
|
SupportedVariable::Numeric,
|
2021-01-02 23:31:55 +00:00
|
|
|
true, false, true,
|
2021-01-01 21:37:55 +00:00
|
|
|
{ ChartSubType::Vertical, ChartSubType::Horizontal },
|
|
|
|
ChartFeatureLabels
|
|
|
|
},
|
|
|
|
{
|
|
|
|
ChartType::DiscreteCount,
|
|
|
|
QT_TRANSLATE_NOOP("StatsTranslations", "Categorical"),
|
|
|
|
SupportedVariable::Categorical,
|
2021-01-02 23:31:55 +00:00
|
|
|
SupportedVariable::None,
|
|
|
|
true, false, false,
|
2021-01-01 21:37:55 +00:00
|
|
|
{ ChartSubType::Vertical, ChartSubType::Horizontal },
|
|
|
|
ChartFeatureLabels
|
|
|
|
},
|
|
|
|
{
|
|
|
|
ChartType::DiscreteBox,
|
|
|
|
QT_TRANSLATE_NOOP("StatsTranslations", "Categorical"),
|
|
|
|
SupportedVariable::Categorical,
|
|
|
|
SupportedVariable::Numeric,
|
2021-01-02 23:31:55 +00:00
|
|
|
true, false, false,
|
2021-01-01 21:37:55 +00:00
|
|
|
{ ChartSubType::Box },
|
|
|
|
0
|
|
|
|
},
|
|
|
|
{
|
|
|
|
ChartType::Pie,
|
|
|
|
QT_TRANSLATE_NOOP("StatsTranslations", "Categorical"),
|
|
|
|
SupportedVariable::Categorical,
|
2021-01-02 23:31:55 +00:00
|
|
|
SupportedVariable::None,
|
|
|
|
true, false, false,
|
2021-01-01 21:37:55 +00:00
|
|
|
{ ChartSubType::Pie },
|
|
|
|
ChartFeatureLabels | ChartFeatureLegend
|
|
|
|
},
|
|
|
|
{
|
|
|
|
ChartType::DiscreteBar,
|
|
|
|
QT_TRANSLATE_NOOP("StatsTranslations", "Barchart"),
|
|
|
|
SupportedVariable::Categorical,
|
|
|
|
SupportedVariable::Categorical,
|
2021-01-02 23:31:55 +00:00
|
|
|
true, true, false,
|
2021-01-01 21:37:55 +00:00
|
|
|
{ ChartSubType::VerticalGrouped, ChartSubType::VerticalStacked, ChartSubType::HorizontalGrouped, ChartSubType::HorizontalStacked },
|
|
|
|
ChartFeatureLabels | ChartFeatureLegend
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// Some charts are valid, but not preferrable. For example a numeric variable
|
|
|
|
// is better plotted in a histogram than in a categorical bar chart. To
|
|
|
|
// describe this use an enum: good, bad, invalid. Default to "good" charts
|
|
|
|
// first, but ultimately let the user decide.
|
|
|
|
enum ChartValidity {
|
|
|
|
Good,
|
|
|
|
Undesired,
|
|
|
|
Invalid
|
|
|
|
};
|
|
|
|
|
2021-01-02 23:31:55 +00:00
|
|
|
static const int none_idx = -1; // Special index meaning no second variable
|
2021-01-01 21:37:55 +00:00
|
|
|
|
|
|
|
StatsState::StatsState() :
|
|
|
|
var1(stats_variables[0]),
|
|
|
|
var2(nullptr),
|
|
|
|
type(ChartType::DiscreteBar),
|
|
|
|
subtype(ChartSubType::Vertical),
|
|
|
|
labels(true),
|
|
|
|
legend(true),
|
|
|
|
median(false),
|
|
|
|
mean(false),
|
|
|
|
quartiles(true),
|
2021-01-19 10:18:10 +00:00
|
|
|
regression(true),
|
|
|
|
confidence(true),
|
2021-01-01 21:37:55 +00:00
|
|
|
var1Binner(nullptr),
|
|
|
|
var2Binner(nullptr),
|
2021-12-31 17:29:06 +00:00
|
|
|
var2Operation(StatsOperation::Invalid),
|
|
|
|
sortMode1(ChartSortMode::Bin)
|
2021-01-01 21:37:55 +00:00
|
|
|
{
|
|
|
|
validate(true);
|
|
|
|
}
|
|
|
|
|
2021-01-02 23:31:55 +00:00
|
|
|
static StatsState::VariableList createVariableList(const StatsVariable *selected, bool addNone, const StatsVariable *omit)
|
2021-01-01 21:37:55 +00:00
|
|
|
{
|
|
|
|
StatsState::VariableList res;
|
2021-01-02 23:31:55 +00:00
|
|
|
res.variables.reserve(stats_variables.size() + addNone);
|
2021-01-01 21:37:55 +00:00
|
|
|
res.selected = -1;
|
2021-01-02 23:31:55 +00:00
|
|
|
if (addNone) {
|
2021-01-01 21:37:55 +00:00
|
|
|
if (selected == nullptr)
|
|
|
|
res.selected = (int)res.variables.size();
|
2021-01-02 23:31:55 +00:00
|
|
|
res.variables.push_back({ StatsTranslations::tr("none"), none_idx });
|
2021-01-01 21:37:55 +00:00
|
|
|
}
|
|
|
|
for (int i = 0; i < (int)stats_variables.size(); ++i) {
|
|
|
|
const StatsVariable *variable = stats_variables[i];
|
|
|
|
if (variable == omit)
|
|
|
|
continue;
|
|
|
|
if (variable == selected)
|
|
|
|
res.selected = (int)res.variables.size();
|
|
|
|
res.variables.push_back({ variable->name(), i });
|
|
|
|
}
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
|
|
|
// This is a bit lame: we pass Chart/SubChart as an integer to the UI,
|
|
|
|
// by placing one in the lower and one in the upper 16 bit of a 32 bit integer.
|
|
|
|
static int toInt(ChartType type, ChartSubType subtype)
|
|
|
|
{
|
|
|
|
return ((int)type << 16) | (int)subtype;
|
|
|
|
}
|
|
|
|
|
|
|
|
static std::pair<ChartType, ChartSubType> fromInt(int id)
|
|
|
|
{
|
|
|
|
return { (ChartType)(id >> 16), (ChartSubType)(id & 0xff) };
|
|
|
|
}
|
|
|
|
|
2021-01-02 23:31:55 +00:00
|
|
|
static ChartValidity variableValidity(StatsVariable::Type type, const StatsBinner *binner, SupportedVariable var, bool binned)
|
2021-01-01 21:37:55 +00:00
|
|
|
{
|
2021-01-02 23:31:55 +00:00
|
|
|
if (!!binner != binned)
|
|
|
|
return ChartValidity::Invalid;
|
2021-01-01 21:37:55 +00:00
|
|
|
switch (var) {
|
|
|
|
default:
|
2021-01-02 23:31:55 +00:00
|
|
|
case SupportedVariable::None:
|
|
|
|
return ChartValidity::Invalid; // None has been special cased outside of this function
|
2021-01-01 21:37:55 +00:00
|
|
|
case SupportedVariable::Categorical:
|
|
|
|
return type == StatsVariable::Type::Continuous || type == StatsVariable::Type::Numeric ?
|
|
|
|
ChartValidity::Undesired : ChartValidity::Good;
|
|
|
|
case SupportedVariable::Continuous:
|
|
|
|
return type == StatsVariable::Type::Discrete ? ChartValidity::Invalid : ChartValidity::Good;
|
|
|
|
case SupportedVariable::Numeric:
|
|
|
|
return type != StatsVariable::Type::Numeric ? ChartValidity::Invalid : ChartValidity::Good;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-02 23:31:55 +00:00
|
|
|
static ChartValidity chartValidity(const ChartTypeDesc &desc,
|
|
|
|
const StatsVariable *var1, const StatsVariable *var2,
|
|
|
|
const StatsBinner *binner1, const StatsBinner *binner2,
|
|
|
|
StatsOperation operation)
|
2021-01-01 21:37:55 +00:00
|
|
|
{
|
|
|
|
if (!var1)
|
2021-01-02 23:31:55 +00:00
|
|
|
return ChartValidity::Invalid; // Huh? We don't support no independent variable
|
2021-01-01 21:37:55 +00:00
|
|
|
|
|
|
|
// Check the first variable
|
2021-01-02 23:31:55 +00:00
|
|
|
ChartValidity valid1 = variableValidity(var1->type(), binner1, desc.var1, desc.var1Binned);
|
2021-01-01 21:37:55 +00:00
|
|
|
if (valid1 == ChartValidity::Invalid)
|
|
|
|
return ChartValidity::Invalid;
|
|
|
|
|
|
|
|
// Then, check the second variable
|
2021-01-02 23:31:55 +00:00
|
|
|
if (var2 == nullptr) // Our special marker for "none"
|
|
|
|
return desc.var2 == SupportedVariable::None ? valid1 : ChartValidity::Invalid;
|
2021-01-01 21:37:55 +00:00
|
|
|
|
2021-01-02 23:31:55 +00:00
|
|
|
ChartValidity valid2 = variableValidity(var2->type(), binner2, desc.var2, desc.var2Binned);
|
2021-01-01 21:37:55 +00:00
|
|
|
if (valid2 == ChartValidity::Invalid)
|
|
|
|
return ChartValidity::Invalid;
|
|
|
|
|
2021-01-02 23:31:55 +00:00
|
|
|
// Check whether the chart supports operations.
|
|
|
|
if ((operation != StatsOperation::Invalid) != desc.var2HasOperations)
|
|
|
|
return ChartValidity::Invalid;
|
|
|
|
|
2021-01-01 21:37:55 +00:00
|
|
|
return valid1 == ChartValidity::Undesired || valid2 == ChartValidity::Undesired ?
|
|
|
|
ChartValidity::Undesired : ChartValidity::Good;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns a list of (chart-type, warning) pairs
|
2021-01-02 23:31:55 +00:00
|
|
|
const std::vector<std::pair<const ChartTypeDesc &, bool>> validCharts(const StatsVariable *var1, const StatsVariable *var2,
|
|
|
|
const StatsBinner *binner1, const StatsBinner *binner2,
|
|
|
|
StatsOperation operation)
|
2021-01-01 21:37:55 +00:00
|
|
|
{
|
|
|
|
std::vector<std::pair<const ChartTypeDesc &, bool>> res;
|
|
|
|
res.reserve(std::size(chart_types));
|
|
|
|
for (const ChartTypeDesc &desc: chart_types) {
|
2021-01-02 23:31:55 +00:00
|
|
|
ChartValidity valid = chartValidity(desc, var1, var2, binner1, binner2, operation);
|
2021-01-01 21:37:55 +00:00
|
|
|
if (valid == ChartValidity::Invalid)
|
|
|
|
continue;
|
|
|
|
res.emplace_back(desc, valid == ChartValidity::Undesired);
|
|
|
|
}
|
|
|
|
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
2021-01-02 23:31:55 +00:00
|
|
|
static StatsState::ChartList createChartList(const StatsVariable *var1, const StatsVariable *var2,
|
|
|
|
const StatsBinner *binner1, const StatsBinner *binner2,
|
|
|
|
StatsOperation operation,
|
|
|
|
ChartType selectedType, ChartSubType selectedSubType)
|
2021-01-01 21:37:55 +00:00
|
|
|
{
|
|
|
|
StatsState::ChartList res;
|
|
|
|
res.selected = -1;
|
2021-01-02 23:31:55 +00:00
|
|
|
for (auto [desc, warn]: validCharts(var1, var2, binner1, binner2, operation)) {
|
2021-01-01 21:37:55 +00:00
|
|
|
QString name = StatsTranslations::tr(desc.name);
|
|
|
|
for (ChartSubType subtype: desc.subtypes) {
|
|
|
|
int id = toInt(desc.id, subtype);
|
|
|
|
if (selectedType == desc.id && selectedSubType == subtype)
|
|
|
|
res.selected = id;
|
|
|
|
QString subtypeName = StatsTranslations::tr(chart_subtype_names[(int)subtype]);
|
|
|
|
res.charts.push_back({ name, subtypeName, subtype, toInt(desc.id, subtype), warn });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If none of the charts are recommended - remove the warning flag.
|
|
|
|
// This can happen if if first variable is numerical, but the second is categorical.
|
|
|
|
if (std::all_of(res.charts.begin(), res.charts.end(), [] (const StatsState::Chart &c) { return c.warning; })) {
|
|
|
|
for (StatsState::Chart &c: res.charts)
|
|
|
|
c.warning = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
2021-01-02 23:31:55 +00:00
|
|
|
// For non-discrete types propose a "no-binning" option unless this is
|
|
|
|
// the second variable and has no operations (i.e. is numeric)
|
|
|
|
static bool noBinningAllowed(const StatsVariable *var, bool second)
|
|
|
|
{
|
|
|
|
if (var->type() == StatsVariable::Type::Discrete)
|
|
|
|
return false;
|
|
|
|
return !second || var->type() == StatsVariable::Type::Numeric;
|
|
|
|
}
|
|
|
|
|
|
|
|
static StatsState::BinnerList createBinnerList(const StatsVariable *var, const StatsBinner *binner, bool binningAllowed, bool second)
|
2021-01-01 21:37:55 +00:00
|
|
|
{
|
|
|
|
StatsState::BinnerList res;
|
|
|
|
res.selected = -1;
|
2021-01-02 23:31:55 +00:00
|
|
|
if (!var || !binningAllowed)
|
2021-01-01 21:37:55 +00:00
|
|
|
return res;
|
|
|
|
std::vector<const StatsBinner *> binners = var->binners();
|
2021-01-02 23:31:55 +00:00
|
|
|
res.binners.reserve(binners.size() + 1);
|
|
|
|
if (var->type() == StatsVariable::Type::Discrete) {
|
|
|
|
if (binners.size() <= 1)
|
|
|
|
return res; // Don't show combo boxes for single binners
|
|
|
|
} else if (noBinningAllowed(var, second)) {
|
|
|
|
if (!second || var->type() == StatsVariable::Type::Numeric) {
|
|
|
|
if (!binner)
|
|
|
|
res.selected = (int)res.binners.size();
|
|
|
|
res.binners.push_back(StatsTranslations::tr("none"));
|
|
|
|
}
|
|
|
|
}
|
2021-01-01 21:37:55 +00:00
|
|
|
for (const StatsBinner *bin: binners) {
|
|
|
|
if (bin == binner)
|
|
|
|
res.selected = (int)res.binners.size();
|
|
|
|
res.binners.push_back(bin->name());
|
|
|
|
}
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
2021-01-02 23:31:55 +00:00
|
|
|
static StatsState::VariableList createOperationsList(const StatsVariable *var, StatsOperation operation, const StatsBinner *var1Binner)
|
2021-01-01 21:37:55 +00:00
|
|
|
{
|
|
|
|
StatsState::VariableList res;
|
|
|
|
res.selected = -1;
|
2021-01-02 23:31:55 +00:00
|
|
|
// Operations only possible if the first variable is binned
|
|
|
|
if (!var || !var1Binner)
|
2021-01-01 21:37:55 +00:00
|
|
|
return res;
|
|
|
|
std::vector<StatsOperation> operations = var->supportedOperations();
|
2021-01-02 23:31:55 +00:00
|
|
|
if (operations.empty())
|
|
|
|
return res;
|
|
|
|
|
|
|
|
res.variables.reserve(operations.size() + 1);
|
|
|
|
|
|
|
|
// Add a "none" entry
|
|
|
|
if (operation == StatsOperation::Invalid)
|
|
|
|
res.selected = (int)res.variables.size();
|
|
|
|
res.variables.push_back({ StatsTranslations::tr("none"), (int)StatsOperation::Invalid });
|
2021-01-01 21:37:55 +00:00
|
|
|
for (StatsOperation op: operations) {
|
|
|
|
if (op == operation)
|
|
|
|
res.selected = (int)res.variables.size();
|
|
|
|
res.variables.push_back({ StatsVariable::operationName(op), (int)op });
|
|
|
|
}
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
2021-01-19 10:18:10 +00:00
|
|
|
static std::vector<StatsState::Feature> createFeaturesList(int chartFeatures, const StatsState &state)
|
2021-01-01 21:37:55 +00:00
|
|
|
{
|
|
|
|
std::vector<StatsState::Feature> res;
|
|
|
|
if (chartFeatures & ChartFeatureLabels)
|
2021-01-19 10:18:10 +00:00
|
|
|
res.push_back({ StatsTranslations::tr("labels"), ChartFeatureLabels, state.labels });
|
2021-01-01 21:37:55 +00:00
|
|
|
if (chartFeatures & ChartFeatureLegend)
|
2021-01-19 10:18:10 +00:00
|
|
|
res.push_back({ StatsTranslations::tr("legend"), ChartFeatureLegend, state.legend });
|
2021-01-01 21:37:55 +00:00
|
|
|
if (chartFeatures & ChartFeatureMedian)
|
2021-01-19 10:18:10 +00:00
|
|
|
res.push_back({ StatsTranslations::tr("median"), ChartFeatureMedian, state.median });
|
2021-01-01 21:37:55 +00:00
|
|
|
if (chartFeatures & ChartFeatureMean)
|
2021-01-19 10:18:10 +00:00
|
|
|
res.push_back({ StatsTranslations::tr("mean"), ChartFeatureMean, state.mean });
|
2021-01-01 21:37:55 +00:00
|
|
|
if (chartFeatures & ChartFeatureQuartiles)
|
2021-01-19 10:18:10 +00:00
|
|
|
res.push_back({ StatsTranslations::tr("quartiles"), ChartFeatureQuartiles, state.quartiles });
|
|
|
|
if (chartFeatures & ChartFeatureRegression)
|
|
|
|
res.push_back({ StatsTranslations::tr("linear regression"), ChartFeatureRegression, state.regression });
|
|
|
|
if (chartFeatures & ChartFeatureConfidence)
|
|
|
|
res.push_back({ StatsTranslations::tr("95% confidence area"), ChartFeatureConfidence, state.confidence });
|
2021-01-01 21:37:55 +00:00
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
2021-12-31 17:29:06 +00:00
|
|
|
// For creating the sort mode list, the ChartSortMode enum is misused to
|
|
|
|
// indicate which sort modes are allowed:
|
|
|
|
// bin -> none (list is redundant: only one mode)
|
|
|
|
// count -> bin, count
|
|
|
|
// value -> bin, count, value
|
|
|
|
// In principle, the "highest possible" mode is given. If a mode is possible,
|
|
|
|
// all the lower modes are likewise possible.
|
|
|
|
static StatsState::VariableList createSortModeList(ChartSortMode allowed, ChartSortMode selectedSortMode)
|
|
|
|
{
|
|
|
|
StatsState::VariableList res;
|
|
|
|
res.selected = -1;
|
|
|
|
if ((int)allowed <= (int)ChartSortMode::Bin)
|
|
|
|
return res;
|
|
|
|
for (int i = 0; i <= (int)allowed; ++i) {
|
|
|
|
ChartSortMode mode = static_cast<ChartSortMode>(i);
|
|
|
|
QString name = StatsTranslations::tr(sortmode_names[i]);
|
|
|
|
if (selectedSortMode == mode)
|
|
|
|
res.selected = (int)res.variables.size();
|
|
|
|
res.variables.push_back({ name, i });
|
|
|
|
}
|
|
|
|
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
|
|
|
static StatsState::VariableList createSortModeList1(ChartType type, ChartSortMode selectedSortMode)
|
|
|
|
{
|
|
|
|
ChartSortMode allowed = ChartSortMode::Bin; // Default: no extra sorting
|
|
|
|
if (type == ChartType::DiscreteBar || type == ChartType::DiscreteCount || type == ChartType::Pie)
|
|
|
|
allowed = ChartSortMode::Count;
|
|
|
|
else if (type == ChartType::DiscreteValue)
|
|
|
|
allowed = ChartSortMode::Value;
|
|
|
|
return createSortModeList(allowed, selectedSortMode);
|
|
|
|
}
|
|
|
|
|
2021-01-01 21:37:55 +00:00
|
|
|
StatsState::UIState StatsState::getUIState() const
|
|
|
|
{
|
|
|
|
UIState res;
|
|
|
|
res.var1 = createVariableList(var1, false, nullptr);
|
|
|
|
res.var2 = createVariableList(var2, true, var1);
|
|
|
|
res.var1Name = var1 ? var1->name() : QString();
|
|
|
|
res.var2Name = var2 ? var2->name() : QString();
|
2021-01-02 23:31:55 +00:00
|
|
|
res.charts = createChartList(var1, var2, var1Binner, var2Binner, var2Operation, type, subtype);
|
|
|
|
res.binners1 = createBinnerList(var1, var1Binner, true, false);
|
|
|
|
// Second variable can only be binned if first variable is binned.
|
|
|
|
res.binners2 = createBinnerList(var2, var2Binner, var1Binner != nullptr, true);
|
|
|
|
res.operations2 = createOperationsList(var2, var2Operation, var1Binner);
|
2021-01-19 10:18:10 +00:00
|
|
|
res.features = createFeaturesList(chartFeatures, *this);
|
2021-12-31 17:29:06 +00:00
|
|
|
res.sortMode1 = createSortModeList1(type, sortMode1);
|
2021-01-01 21:37:55 +00:00
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
2021-01-02 23:31:55 +00:00
|
|
|
static const StatsBinner *idxToBinner(const StatsVariable *v, int idx, bool second)
|
2021-01-01 21:37:55 +00:00
|
|
|
{
|
|
|
|
if (!v)
|
|
|
|
return nullptr;
|
2021-01-02 23:31:55 +00:00
|
|
|
|
|
|
|
// Special case: for non-discrete variables, the first entry means "none".
|
|
|
|
if (noBinningAllowed(v, second)) {
|
|
|
|
if (idx == 0)
|
|
|
|
return nullptr;
|
|
|
|
--idx;
|
|
|
|
}
|
|
|
|
|
2021-01-01 21:37:55 +00:00
|
|
|
auto binners = v->binners();
|
|
|
|
return idx >= 0 && idx < (int)binners.size() ? binners[idx] : 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
void StatsState::var1Changed(int id)
|
|
|
|
{
|
|
|
|
var1 = stats_variables[std::clamp(id, 0, (int)stats_variables.size())];
|
|
|
|
validate(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
void StatsState::binner1Changed(int idx)
|
|
|
|
{
|
2021-01-02 23:31:55 +00:00
|
|
|
var1Binner = idxToBinner(var1, idx, false);
|
|
|
|
|
|
|
|
// If the first variable is not binned, the second variable must be of the "numeric" type.
|
|
|
|
if(!var1Binner && (!var2 || var2->type() != StatsVariable::Type::Numeric)) {
|
|
|
|
// Find first variable that is numeric, but not the same as the first
|
|
|
|
auto it = std::find_if(stats_variables.begin(), stats_variables.end(),
|
|
|
|
[v1 = var1] (const StatsVariable *v)
|
|
|
|
{ return v != v1 && v->type() == StatsVariable::Type::Numeric; });
|
|
|
|
var2 = it != stats_variables.end() ? *it : nullptr;
|
|
|
|
}
|
|
|
|
|
2021-01-01 21:37:55 +00:00
|
|
|
validate(false);
|
|
|
|
}
|
|
|
|
|
|
|
|
void StatsState::var2Changed(int id)
|
|
|
|
{
|
2021-01-02 23:31:55 +00:00
|
|
|
// The "none" variable is represented by a nullptr
|
|
|
|
var2 = id == none_idx ? nullptr
|
|
|
|
: stats_variables[std::clamp(id, 0, (int)stats_variables.size())];
|
2021-01-01 21:37:55 +00:00
|
|
|
validate(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
void StatsState::binner2Changed(int idx)
|
|
|
|
{
|
2021-01-02 23:31:55 +00:00
|
|
|
var2Binner = idxToBinner(var2, idx, true);
|
|
|
|
|
|
|
|
// We do not support operations and binning at the same time.
|
|
|
|
if (var2Binner)
|
|
|
|
var2Operation = StatsOperation::Invalid;
|
|
|
|
|
2021-01-01 21:37:55 +00:00
|
|
|
validate(false);
|
|
|
|
}
|
|
|
|
|
|
|
|
void StatsState::var2OperationChanged(int id)
|
|
|
|
{
|
|
|
|
var2Operation = (StatsOperation)id;
|
2021-01-02 23:31:55 +00:00
|
|
|
|
|
|
|
// We do not support operations and binning at the same time.
|
|
|
|
if (var2Operation != StatsOperation::Invalid)
|
|
|
|
var2Binner = nullptr;
|
|
|
|
|
2021-01-01 21:37:55 +00:00
|
|
|
validate(false);
|
|
|
|
}
|
|
|
|
|
2021-12-31 17:29:06 +00:00
|
|
|
void StatsState::sortMode1Changed(int id)
|
|
|
|
{
|
|
|
|
sortMode1 = (ChartSortMode)id;
|
|
|
|
validate(false);
|
|
|
|
}
|
|
|
|
|
2021-01-01 21:37:55 +00:00
|
|
|
void StatsState::chartChanged(int id)
|
|
|
|
{
|
|
|
|
std::tie(type, subtype) = fromInt(id); // use std::tie to assign two values at once
|
|
|
|
validate(false);
|
|
|
|
}
|
|
|
|
|
|
|
|
void StatsState::featureChanged(int id, bool state)
|
|
|
|
{
|
|
|
|
if (id == ChartFeatureLabels)
|
|
|
|
labels = state;
|
|
|
|
else if (id == ChartFeatureLegend)
|
|
|
|
legend = state;
|
|
|
|
else if (id == ChartFeatureMedian)
|
|
|
|
median = state;
|
|
|
|
else if (id == ChartFeatureMean)
|
|
|
|
mean = state;
|
|
|
|
else if (id == ChartFeatureQuartiles)
|
|
|
|
quartiles = state;
|
2021-01-19 10:18:10 +00:00
|
|
|
else if (id == ChartFeatureRegression)
|
|
|
|
regression = state;
|
|
|
|
else if (id == ChartFeatureConfidence)
|
|
|
|
confidence = state;
|
2021-01-01 21:37:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Creates the new chart-type from the current chart-type and a list of possible chart types.
|
|
|
|
// If the flag "varChanged" is true, the current chart-type will be changed if the
|
|
|
|
// current chart-type is undesired.
|
|
|
|
const ChartTypeDesc &newChartType(ChartType type, std::vector<std::pair<const ChartTypeDesc &, bool>> charts,
|
|
|
|
bool varChanged)
|
|
|
|
{
|
|
|
|
for (auto [desc, warn]: charts) {
|
|
|
|
// Found it, but if the axis was changed, we change anyway if the chart is "undesired"
|
|
|
|
if (type == desc.id) {
|
|
|
|
if (!varChanged || !warn)
|
|
|
|
return desc;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Find the first non-undesired chart
|
|
|
|
for (auto [desc, warn]: charts) {
|
|
|
|
if (!warn)
|
|
|
|
return desc;
|
|
|
|
}
|
|
|
|
|
|
|
|
return charts.empty() ? chart_types[0] : charts[0].first;
|
|
|
|
}
|
|
|
|
|
2021-01-02 23:31:55 +00:00
|
|
|
static const StatsBinner *getFirstBinner(const StatsVariable *var)
|
|
|
|
{
|
|
|
|
if (!var)
|
|
|
|
return nullptr;
|
|
|
|
auto binners = var->binners();
|
|
|
|
return binners.empty() ? nullptr : binners.front();
|
|
|
|
}
|
|
|
|
|
|
|
|
static void validateBinner(const StatsBinner *&binner, const StatsVariable *var, bool second)
|
2021-01-01 21:37:55 +00:00
|
|
|
{
|
2021-01-02 23:31:55 +00:00
|
|
|
if (!var) {
|
2021-01-01 21:37:55 +00:00
|
|
|
binner = nullptr;
|
|
|
|
return;
|
|
|
|
}
|
2021-01-02 23:31:55 +00:00
|
|
|
|
|
|
|
bool noneOk = noBinningAllowed(var, second);
|
|
|
|
if (noneOk & !binner)
|
|
|
|
return;
|
|
|
|
|
2021-01-01 21:37:55 +00:00
|
|
|
auto binners = var->binners();
|
|
|
|
if (std::find(binners.begin(), binners.end(), binner) != binners.end())
|
|
|
|
return;
|
|
|
|
|
2021-01-02 23:31:55 +00:00
|
|
|
// For now choose the first binner or no binner if this is a non-discrete
|
|
|
|
// variable. However, we might try to be smarter here and adapt to the
|
|
|
|
// given screen size and the estimated number of bins.
|
|
|
|
binner = binners.empty() || noneOk ? nullptr : binners[0];
|
2021-01-01 21:37:55 +00:00
|
|
|
}
|
|
|
|
|
2021-01-02 23:31:55 +00:00
|
|
|
static void validateOperation(StatsOperation &operation, const StatsVariable *var, const StatsBinner *var1Binner)
|
2021-01-01 21:37:55 +00:00
|
|
|
{
|
2021-01-02 23:31:55 +00:00
|
|
|
// 1) No variable, no operation.
|
|
|
|
// 2) We allow operations only if the first variable is binned.
|
|
|
|
if (!var) {
|
2021-01-01 21:37:55 +00:00
|
|
|
operation = StatsOperation::Invalid;
|
|
|
|
return;
|
|
|
|
}
|
2021-01-02 23:31:55 +00:00
|
|
|
|
2021-01-01 21:37:55 +00:00
|
|
|
std::vector<StatsOperation> ops = var->supportedOperations();
|
|
|
|
if (std::find(ops.begin(), ops.end(), operation) != ops.end())
|
|
|
|
return;
|
|
|
|
|
2021-01-02 23:31:55 +00:00
|
|
|
operation = StatsOperation::Invalid;
|
2021-01-01 21:37:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// The var changed variable indicates whether this function is called
|
|
|
|
// after a variable change or a change of the chart type. In the
|
|
|
|
// former case, the chart type is switched, if it is not recommended.
|
|
|
|
// In the latter case, the user explicitly chose a non-recommended type,
|
|
|
|
// so let's use that.
|
|
|
|
void StatsState::validate(bool varChanged)
|
|
|
|
{
|
2021-01-02 23:31:55 +00:00
|
|
|
// We need at least one variable
|
|
|
|
if (!var1)
|
|
|
|
var1 = stats_variables[0];
|
|
|
|
|
2021-01-01 21:37:55 +00:00
|
|
|
// Take care that we don't plot a variable against itself.
|
|
|
|
if (var1 == var2)
|
|
|
|
var2 = nullptr;
|
|
|
|
|
2021-01-03 23:09:28 +00:00
|
|
|
validateBinner(var1Binner, var1, false);
|
|
|
|
|
2021-01-02 23:31:55 +00:00
|
|
|
// If there is no second variable or the second variable is not
|
|
|
|
// "numeric", the first variable must be binned.
|
2021-01-03 23:09:28 +00:00
|
|
|
if ((!var2 || var2->type() != StatsVariable::Type::Numeric) && !var1Binner)
|
2021-01-02 23:31:55 +00:00
|
|
|
var1Binner = getFirstBinner(var1);
|
|
|
|
|
|
|
|
// Check that the binners and operation are valid
|
|
|
|
if (!var1Binner) {
|
|
|
|
var2Binner = nullptr; // Second variable can only be binned if first variable is binned.
|
|
|
|
var2Operation = StatsOperation::Invalid;
|
|
|
|
}
|
|
|
|
validateBinner(var2Binner, var2, true);
|
|
|
|
validateOperation(var2Operation, var2, var1Binner);
|
|
|
|
|
2021-01-01 21:37:55 +00:00
|
|
|
// Let's see if the currently selected chart is one of the valid charts
|
2021-01-02 23:31:55 +00:00
|
|
|
auto charts = validCharts(var1, var2, var1Binner, var2Binner, var2Operation);
|
|
|
|
|
|
|
|
if (charts.empty()) {
|
|
|
|
// Ooops. No valid chart with these settings. The validation should be improved.
|
|
|
|
type = ChartType::Invalid;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-01-01 21:37:55 +00:00
|
|
|
const ChartTypeDesc &desc = newChartType(type, charts, varChanged);
|
|
|
|
type = desc.id;
|
|
|
|
|
|
|
|
// Check if the current subtype is supported by the chart
|
|
|
|
if (std::find(desc.subtypes.begin(), desc.subtypes.end(), subtype) == desc.subtypes.end())
|
|
|
|
subtype = desc.subtypes.empty() ? ChartSubType::Horizontal : desc.subtypes[0];
|
|
|
|
|
|
|
|
chartFeatures = desc.features;
|
2021-01-02 23:31:55 +00:00
|
|
|
// Median and mean currently only if the first variable is numeric
|
2021-01-01 21:37:55 +00:00
|
|
|
if (!var1 || var1->type() != StatsVariable::Type::Numeric)
|
|
|
|
chartFeatures &= ~(ChartFeatureMedian | ChartFeatureMean);
|
2021-12-31 17:29:06 +00:00
|
|
|
|
|
|
|
// By default, sort according to the used bin. Only for pie charts,
|
|
|
|
// sort by count, if the binning is on a categorical variable.
|
|
|
|
if (varChanged) {
|
|
|
|
sortMode1 = type == ChartType::Pie &&
|
|
|
|
var1->type() == StatsVariable::Type::Discrete ? ChartSortMode::Count :
|
|
|
|
ChartSortMode::Bin;
|
|
|
|
}
|
2021-01-01 21:37:55 +00:00
|
|
|
}
|