diff --git a/forms/wildmonchart.ui b/forms/wildmonchart.ui
index 5542afc3..88d3a590 100644
--- a/forms/wildmonchart.ui
+++ b/forms/wildmonchart.ui
@@ -6,8 +6,8 @@
0
0
- 697
- 349
+ 776
+ 445
@@ -15,19 +15,131 @@
- 0
+ 4
- 0
+ 4
- 0
+ 4
- 0
+ 4
-
-
+
+
+
+ Species Distribution
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ QPainter::Antialiasing
+
+
+
+
+
+
+
+ Level Distribution
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+ QFrame::NoFrame
+
+
+ QFrame::Plain
+
+
+
+ 12
+
+
+ 0
+
+
-
+
+
+ Group
+
+
+
+ -
+
+
+ false
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ Species
+
+
+
+ -
+
+
+ true
+
+
+ QComboBox::NoInsert
+
+
+
+
+
+
+ -
+
+
+ QPainter::Antialiasing
+
+
+
+
+
+
diff --git a/include/ui/wildmonchart.h b/include/ui/wildmonchart.h
index 36ab8619..7c523355 100644
--- a/include/ui/wildmonchart.h
+++ b/include/ui/wildmonchart.h
@@ -4,6 +4,7 @@
#include "encountertablemodel.h"
#include
+#include
namespace Ui {
class WildMonChart;
@@ -18,11 +19,40 @@ public:
public slots:
void setTable(const EncounterTableModel *table);
- void updateChart();
+ void createCharts();
private:
Ui::WildMonChart *ui;
const EncounterTableModel *table;
+
+ QStringList groupNames;
+ QMap tableIndexToGroupName;
+
+ struct Summary {
+ double speciesFrequency = 0.0;
+ QMap levelFrequencies;
+ };
+
+ int tableMinLevel;
+ int tableMaxLevel;
+
+ // GroupedData maps a group name ("old_rod", "good_rod"...)
+ // to any summarized data needed for the charts.
+ typedef QMap GroupedData;
+
+ QMap speciesToGroupedData;
+
+ QStringList getSpeciesNames() const;
+ double getSpeciesFrequency(const QString&, const QString&) const;
+ QMap getLevelFrequencies(const QString &, const QString &) const;
+ bool usesGroupLabels() const;
+
+ void clearTableData();
+ void readTable();
+ void createSpeciesDistributionChart();
+ void createLevelDistributionChart();
+
+ void stopChartAnimation();
};
#endif // WILDMONCHART_H
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index f4557ac4..da39f555 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -2578,6 +2578,8 @@ void MainWindow::on_pushButton_SummaryChart_clicked() {
QTableView *table = this->editor->getCurrentWildMonTable();
EncounterTableModel *data = table ? static_cast(table->model()) : nullptr;
this->wildMonChart = new WildMonChart(this, data);
+ } else {
+ this->wildMonChart->createCharts();
}
openSubWindow(this->wildMonChart);
}
@@ -3027,6 +3029,10 @@ bool MainWindow::closeSupplementaryWindows() {
return false;
this->customScriptsEditor = nullptr;
+ if (this->wildMonChart && !this->wildMonChart->close())
+ return false;
+ this->wildMonChart = nullptr;
+
if (this->projectSettingsEditor) this->projectSettingsEditor->closeQuietly();
this->projectSettingsEditor = nullptr;
diff --git a/src/ui/wildmonchart.cpp b/src/ui/wildmonchart.cpp
index eccec380..b7080aae 100644
--- a/src/ui/wildmonchart.cpp
+++ b/src/ui/wildmonchart.cpp
@@ -4,16 +4,16 @@
#include "log.h"
-#include
-
// TODO: Make level range its own chart(s)?
// TODO: Draw species icons below legend icons?
-// TODO: Add hover behavior to display species name (and click prompt?)
+// TODO: NoScrollComboBoxes
+
+static const QString baseWindowTitle = QString("Wild Pokémon Summary Charts");
struct ChartData {
int minLevel;
int maxLevel;
- QMap values; // One value for each wild encounter group
+ QMap valueMap; // One value for each wild encounter group
};
WildMonChart::WildMonChart(QWidget *parent, const EncounterTableModel *table) :
@@ -24,7 +24,8 @@ WildMonChart::WildMonChart(QWidget *parent, const EncounterTableModel *table) :
setAttribute(Qt::WA_DeleteOnClose);
setWindowFlags(Qt::Window);
- ui->chartView->setRenderHint(QPainter::Antialiasing);
+ connect(ui->comboBox_Species, &QComboBox::currentTextChanged, this, &WildMonChart::createLevelDistributionChart);
+ connect(ui->comboBox_Group, &QComboBox::currentTextChanged, this, &WildMonChart::createLevelDistributionChart);
setTable(table);
};
@@ -35,95 +36,132 @@ WildMonChart::~WildMonChart() {
void WildMonChart::setTable(const EncounterTableModel *table) {
this->table = table;
- updateChart();
+ readTable();
+ createCharts();
}
-static const bool showLevelRange = false;
+void WildMonChart::clearTableData() {
+ this->groupNames.clear();
+ this->tableIndexToGroupName.clear();
+ this->speciesToGroupedData.clear();
+ this->tableMinLevel = INT_MAX;
+ this->tableMaxLevel = INT_MIN;
+ setWindowTitle(baseWindowTitle);
+}
-void WildMonChart::updateChart() {
+// Extract all the data from the table that we need for the charts
+void WildMonChart::readTable() {
+ clearTableData();
if (!this->table)
return;
- setWindowTitle(QString("Wild Pokémon Summary -- %1").arg(this->table->encounterField().name));
+ setWindowTitle(QString("%1 - %2").arg(baseWindowTitle).arg(this->table->encounterField().name));
- // Read data about encounter groups, e.g. for "fishing_mons" we want to know indexes 2-4 belong to good_rod (group index 1).
- // Each group will be represented as a separate bar on the graph.
- QList groupNames;
- QMap tableIndexToGroupIndex;
- int groupIndex = 0;
+ // Read data about encounter groups, e.g. for "fishing_mons" we want to know table indexes 2-4 belong to "good_rod"
for (auto groupPair : this->table->encounterField().groups) {
- groupNames.prepend(groupPair.first);
- for (auto i : groupPair.second) {
- tableIndexToGroupIndex.insert(i, groupIndex);
- }
- groupIndex++;
+ // Prepending names here instead of appending so that charts can match the order in the table visually.
+ this->groupNames.prepend(groupPair.first);
+ for (auto i : groupPair.second)
+ this->tableIndexToGroupName.insert(i, groupPair.first);
}
- const int numGroups = qMax(1, groupNames.length()); // Implicitly 1 group when none are listed
+ if (this->groupNames.isEmpty())
+ this->groupNames.append(QString()); // Implicitly 1 unnamed group when none are listed
- // Read data from the table, combining data for duplicate species entries
- const QList tableValues = this->table->percentages();
+ // Read data from the table, combining data for duplicate entries
+ const QList tableFrequencies = this->table->percentages();
const QVector tablePokemon = this->table->encounterData().wildPokemon;
- QMap speciesToChartData;
- for (int i = 0; i < qMin(tableValues.length(), tablePokemon.length()); i++) {
- const double value = tableValues.at(i);
- const WildPokemon pokemon = tablePokemon.at(i);
- groupIndex = tableIndexToGroupIndex.value(i, 0);
-
- if (speciesToChartData.contains(pokemon.species)) {
- // Duplicate species entry
- ChartData *entry = &speciesToChartData[pokemon.species];
- entry->values[groupIndex] += value;
- if (entry->minLevel > pokemon.minLevel)
- entry->minLevel = pokemon.minLevel;
- if (entry->maxLevel < pokemon.maxLevel)
- entry->maxLevel = pokemon.maxLevel;
- } else {
- // New species entry
- ChartData entry;
- entry.minLevel = pokemon.minLevel;
- entry.maxLevel = pokemon.maxLevel;
- entry.values.insert(groupIndex, value);
- speciesToChartData.insert(pokemon.species, entry);
- }
- }
-
- // Populate chart
- QList barSets;
+ const int numRows = qMin(tableFrequencies.length(), tablePokemon.length());
const QString speciesPrefix = projectConfig.getIdentifier(ProjectIdentifier::define_species_prefix);
- for (auto mapPair = speciesToChartData.cbegin(), end = speciesToChartData.cend(); mapPair != end; mapPair++) {
- const ChartData entry = mapPair.value();
+ for (int i = 0; i < numRows; i++) {
+ const double frequency = tableFrequencies.at(i);
+ const WildPokemon pokemon = tablePokemon.at(i);
+ const QString groupName = this->tableIndexToGroupName.value(i);
- // Strip 'SPECIES_' prefix
- QString label = mapPair.key();
+ // Create species label (strip 'SPECIES_' prefix).
+ QString label = pokemon.species;
if (label.startsWith(speciesPrefix))
label.remove(0, speciesPrefix.length());
- // Add level range to label
- if (showLevelRange) {
- if (entry.minLevel == entry.maxLevel)
- label.append(QString(" (Lv %1)").arg(entry.minLevel));
- else
- label.append(QString(" (Lv %1-%2)").arg(entry.minLevel).arg(entry.maxLevel));
- }
+ // Add species/level frequency data
+ Summary *summary = &this->speciesToGroupedData[label][groupName];
+ summary->speciesFrequency += frequency;
+ if (pokemon.minLevel > pokemon.maxLevel)
+ continue; // Invalid
+ int numLevels = pokemon.maxLevel - pokemon.minLevel + 1;
+ for (int level = pokemon.minLevel; level <= pokemon.maxLevel; level++)
+ summary->levelFrequencies[level] += frequency / numLevels;
- auto set = new QBarSet(label);
+ if (this->tableMinLevel > pokemon.minLevel)
+ this->tableMinLevel = pokemon.minLevel;
+ if (this->tableMaxLevel < pokemon.maxLevel)
+ this->tableMaxLevel = pokemon.maxLevel;
+ }
- // Add encounter chance data (in reverse order, to match the table's group order visually)
- for (int i = numGroups - 1; i >= 0; i--)
- set->append(entry.values.value(i, 0));
+ // Populate combo boxes
+ const QSignalBlocker blocker1(ui->comboBox_Species);
+ const QSignalBlocker blocker2(ui->comboBox_Group);
+ ui->comboBox_Species->clear();
+ ui->comboBox_Species->addItems(getSpeciesNames());
+ ui->comboBox_Group->clear();
+ if (usesGroupLabels()) {
+ ui->comboBox_Group->addItems(this->groupNames);
+ ui->comboBox_Group->setEnabled(true);
+ } else {
+ ui->comboBox_Group->setEnabled(false);
+ }
+}
- // Insert bar set. We order them from lowest to highest total, left-to-right.
- int i = 0;
- for (; i < barSets.length(); i++){
- if (barSets.at(i)->sum() > set->sum())
+void WildMonChart::createCharts() {
+ createSpeciesDistributionChart();
+ createLevelDistributionChart();
+
+ // Turn off the animation once it's played, otherwise it replays any time the window changes size.
+ // TODO: Store timer, disable if closing or creating new chart
+ //QTimer::singleShot(chart->animationDuration() + 500, this, &WildMonChart::stopChartAnimation);
+}
+
+QStringList WildMonChart::getSpeciesNames() const {
+ return this->speciesToGroupedData.keys();
+}
+
+double WildMonChart::getSpeciesFrequency(const QString &species, const QString &groupName) const {
+ return this->speciesToGroupedData[species][groupName].speciesFrequency;
+}
+
+QMap WildMonChart::getLevelFrequencies(const QString &species, const QString &groupName) const {
+ return this->speciesToGroupedData[species][groupName].levelFrequencies;
+}
+
+bool WildMonChart::usesGroupLabels() const {
+ return this->groupNames.length() > 1;
+}
+
+void WildMonChart::createSpeciesDistributionChart() {
+ QList barSets;
+ for (const auto species : getSpeciesNames()) {
+ // Add encounter chance data
+ auto set = new QBarSet(species);
+ for (auto groupName : this->groupNames)
+ set->append(getSpeciesFrequency(species, groupName) * 100);
+
+ // We order the bar sets from lowest to highest total, left-to-right.
+ for (int i = 0; i < barSets.length() + 1; i++){
+ if (i >= barSets.length() || barSets.at(i)->sum() > set->sum()) {
+ barSets.insert(i, set);
break;
+ }
}
- barSets.insert(i, set);
+
+ // Show species name and % when hovering over a bar set. This covers some shortfalls in our ability to control the chart design
+ // (i.e. bar segments may be too narrow to see the % label, or colors may be hard to match to the legend).
+ connect(set, &QBarSet::hovered, [set, species] (bool on, int i) {
+ QString text = on ? QString("%1 - %2%").arg(species).arg(set->at(i)) : "";
+ QToolTip::showText(QCursor::pos(), text);
+ });
}
auto series = new QHorizontalPercentBarSeries();
series->setLabelsVisible();
- //series->setLabelsPrecision(x); // This appears to have no effect for any value 'x'? Ideally we'd display 1-2 decimal places
series->append(barSets);
auto chart = new QChart();
@@ -135,29 +173,93 @@ void WildMonChart::updateChart() {
// X-axis is the values (percentages). We're already showing percentages on the bar, so we just display 0/50/100%
auto axisX = new QValueAxis();
-#if (QT_VERSION >= QT_VERSION_CHECK(6, 7, 0))
- // Not critical, but the percentage ticks on the x-axis have no need for decimals.
- // This property doesn't exist prior to Qt 6.7
- axisX->setLabelDecimals(0);
-#endif
+ axisX->setLabelFormat("%u%%");
axisX->setTickCount(3);
chart->addAxis(axisX, Qt::AlignBottom);
series->attachAxis(axisX);
// Y-axis is the names of encounter groups (e.g. Old Rod, Good Rod...)
- if (numGroups > 1) {
+ if (usesGroupLabels()) {
auto axisY = new QBarCategoryAxis();
- axisY->setCategories(groupNames);
+ axisY->setCategories(this->groupNames);
chart->addAxis(axisY, Qt::AlignLeft);
series->attachAxis(axisY);
+ } else {
+ // TODO: y-axis has weird labels for a few frames on opening
}
- delete ui->chartView->chart();
- ui->chartView->setChart(chart);
-
- // Turn off the animation once it's played, otherwise it replays any time the window changes size.
- QTimer::singleShot(chart->animationDuration() + 500, [this] {
- if (ui->chartView->chart())
- ui->chartView->chart()->setAnimationOptions(QChart::NoAnimation);
- });
+ // TODO: Delete old chart
+ ui->chartView_SpeciesDistribution->setChart(chart);
+}
+
+void WildMonChart::createLevelDistributionChart() {
+ // TODO: Handle combined chart
+ const QString species = ui->comboBox_Species->currentText();
+ const QString groupName = ui->comboBox_Group->currentText();
+
+ const double speciesFrequency = getSpeciesFrequency(species, groupName);
+ const QMap levelFrequencies = getLevelFrequencies(species, groupName);
+ const QList levels = levelFrequencies.keys();
+
+ int minLevel = !levels.isEmpty() ? levels.first() : 0;
+ int maxLevel = !levels.isEmpty() ? levels.last() : 0;
+ if (maxLevel < minLevel)
+ return;
+
+ double maxPercent = 0.0;
+ QStringList categories;
+ auto set = new QBarSet(species);
+ for (int i = minLevel; i <= maxLevel; i++) {
+ double percent = (levelFrequencies.value(i, 0) / speciesFrequency) * 100;
+ if (maxPercent < percent)
+ maxPercent = percent;
+ set->append(percent);
+ categories.append(QString::number(i));
+ }
+
+ // Show level and % when hovering over a bar set. This covers some shortfalls in our ability to control the chart design.
+ connect(set, &QBarSet::hovered, [set, categories] (bool on, int i) {
+ QString text = on ? QString("Lv%1 - %2%").arg(categories.at(i)).arg(set->at(i)) : "";
+ QToolTip::showText(QCursor::pos(), text);
+ });
+
+ auto series = new QBarSeries();
+ series->append(set);
+ //series->setLabelsVisible();
+
+ auto chart = new QChart();
+ chart->addSeries(series);
+ //chart->setTitle("");
+ chart->setAnimationOptions(QChart::SeriesAnimations);
+ chart->legend()->setVisible(true);
+ chart->legend()->setShowToolTips(true);
+ chart->legend()->setAlignment(Qt::AlignBottom);
+
+ QBarCategoryAxis *axisX = new QBarCategoryAxis();
+ axisX->append(categories);
+ chart->addAxis(axisX, Qt::AlignBottom);
+ series->attachAxis(axisX);
+
+ auto roundUp = [](int num, int multiple) {
+ auto remainder = num % multiple;
+ if (remainder == 0)
+ return num;
+ return num + multiple - remainder;
+ };
+
+ QValueAxis *axisY = new QValueAxis();
+ axisY->setMax(roundUp(qCeil(maxPercent), 5));
+ //axisY->setTickType(QValueAxis::TicksDynamic);
+ //axisY->setTickInterval(5);
+ axisY->setLabelFormat("%u%%");
+ chart->addAxis(axisY, Qt::AlignLeft);
+ series->attachAxis(axisY);
+
+ // TODO: Cache old chart
+ ui->chartView_LevelDistribution->setChart(chart);
+}
+
+void WildMonChart::stopChartAnimation() {
+ if (ui->chartView_SpeciesDistribution->chart())
+ ui->chartView_SpeciesDistribution->chart()->setAnimationOptions(QChart::NoAnimation);
}