#if __has_include() #include "wildmonchart.h" #include "ui_wildmonchart.h" #include "config.h" static const QString baseWindowTitle = QString("Wild Pokémon Summary Charts"); static const QList> themes = { {"Light", QChart::ChartThemeLight}, {"Dark", QChart::ChartThemeDark}, {"Blue Cerulean", QChart::ChartThemeBlueCerulean}, {"Brown Sand", QChart::ChartThemeBrownSand}, {"Blue NCS", QChart::ChartThemeBlueNcs}, {"High Contrast", QChart::ChartThemeHighContrast}, {"Blue Icy", QChart::ChartThemeBlueIcy}, {"Qt", QChart::ChartThemeQt}, }; WildMonChart::WildMonChart(QWidget *parent, const EncounterTableModel *table) : QWidget(parent), ui(new Ui::WildMonChart) { ui->setupUi(this); setAttribute(Qt::WA_DeleteOnClose); setWindowFlags(Qt::Window); connect(ui->button_Help, &QAbstractButton::clicked, this, &WildMonChart::showHelpDialog); // Changing these settings changes which level distribution chart is shown connect(ui->groupBox_Species, &QGroupBox::clicked, this, &WildMonChart::refreshLevelDistributionChart); connect(ui->comboBox_Species, &QComboBox::currentTextChanged, this, &WildMonChart::refreshLevelDistributionChart); connect(ui->comboBox_Group, &QComboBox::currentTextChanged, this, &WildMonChart::refreshLevelDistributionChart); connect(ui->tabWidget, &QTabWidget::currentChanged, this, &WildMonChart::limitChartAnimation); // Set up Theme combo box for (auto i : themes) ui->comboBox_Theme->addItem(i.first, i.second); connect(ui->comboBox_Theme, &QComboBox::currentTextChanged, this, &WildMonChart::updateTheme); // User's theme choice is saved in the config int configThemeIndex = ui->comboBox_Theme->findText(porymapConfig.wildMonChartTheme); if (configThemeIndex >= 0) { const QSignalBlocker blocker(ui->comboBox_Theme); ui->comboBox_Theme->setCurrentIndex(configThemeIndex); } else { porymapConfig.wildMonChartTheme = ui->comboBox_Theme->currentText(); } restoreGeometry(porymapConfig.wildMonChartGeometry); setTable(table); }; WildMonChart::~WildMonChart() { delete ui; }; void WildMonChart::setTable(const EncounterTableModel *table) { if (this->table == table) return; this->table = table; refresh(); } void WildMonChart::clearTable() { setTable(nullptr); } void WildMonChart::clearTableData() { this->groupNames.clear(); this->groupNamesReversed.clear(); this->speciesInLegendOrder.clear(); this->tableIndexToGroupName.clear(); this->groupedLevelRanges.clear(); this->speciesToGroupedData.clear(); this->speciesToColor.clear(); setWindowTitle(baseWindowTitle); const QSignalBlocker blocker1(ui->comboBox_Species); const QSignalBlocker blocker2(ui->comboBox_Group); ui->comboBox_Species->clear(); ui->comboBox_Group->clear(); ui->comboBox_Group->setEnabled(false); } // Extract all the data from the table that we need for the charts void WildMonChart::readTable() { clearTableData(); if (!this->table) return; 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 table indexes 2-4 belong to "good_rod" for (auto groupPair : this->table->encounterField().groups) { // Frustratingly when adding categories to the charts they insert bottom-to-top, but the table and combo box // insert top-to-bottom. To keep the order visually the same across displays we keep separate ordered lists. this->groupNames.append(groupPair.first); this->groupNamesReversed.prepend(groupPair.first); for (auto i : groupPair.second) this->tableIndexToGroupName.insert(i, groupPair.first); } // Implicitly 1 unnamed group when none are listed if (this->groupNames.isEmpty()) { this->groupNames.append(QString()); this->groupNamesReversed.append(QString()); } // Read data from the table, combining data for duplicate entries const QList tableFrequencies = this->table->percentages(); const QVector tablePokemon = this->table->encounterData().wildPokemon; const int numRows = qMin(tableFrequencies.length(), tablePokemon.length()); const QString speciesPrefix = projectConfig.getIdentifier(ProjectIdentifier::define_species_prefix); 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); // Create species label (strip 'SPECIES_' prefix). QString label = pokemon.species; if (label.startsWith(speciesPrefix)) label.remove(0, speciesPrefix.length()); // 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; // Update level min/max for showing level distribution across a group if (!this->groupedLevelRanges.contains(groupName)) { LevelRange *levelRange = &this->groupedLevelRanges[groupName]; levelRange->min = pokemon.minLevel; levelRange->max = pokemon.maxLevel; } else { LevelRange *levelRange = &this->groupedLevelRanges[groupName]; if (levelRange->min > pokemon.minLevel) levelRange->min = pokemon.minLevel; if (levelRange->max < pokemon.maxLevel) levelRange->max = pokemon.maxLevel; } } // Populate combo boxes const QSignalBlocker blocker1(ui->comboBox_Species); const QSignalBlocker blocker2(ui->comboBox_Group); ui->comboBox_Species->clear(); ui->comboBox_Species->addItems(getSpeciesNamesAlphabetical()); ui->comboBox_Group->clear(); ui->comboBox_Group->addItems(this->groupNames); ui->comboBox_Group->setEnabled(usesGroupLabels()); } void WildMonChart::refresh() { const QSignalBlocker blocker1(ui->comboBox_Species); const QSignalBlocker blocker2(ui->comboBox_Group); const QString oldSpecies = ui->comboBox_Species->currentText(); const QString oldGroup = ui->comboBox_Group->currentText(); readTable(); // If the old UI settings are still valid we should restore them int index = ui->comboBox_Species->findText(oldSpecies); if (index >= 0) ui->comboBox_Species->setCurrentIndex(index); index = ui->comboBox_Group->findText(oldGroup); if (index >= 0) ui->comboBox_Group->setCurrentIndex(index); refreshSpeciesDistributionChart(); refreshLevelDistributionChart(); } void WildMonChart::refreshSpeciesDistributionChart() { if (ui->chartView_SpeciesDistribution->chart()) ui->chartView_SpeciesDistribution->chart()->deleteLater(); ui->chartView_SpeciesDistribution->setChart(createSpeciesDistributionChart()); if (ui->tabWidget->currentWidget() == ui->tabSpecies) limitChartAnimation(); } void WildMonChart::refreshLevelDistributionChart() { if (ui->chartView_LevelDistribution->chart()) ui->chartView_LevelDistribution->chart()->deleteLater(); ui->chartView_LevelDistribution->setChart(createLevelDistributionChart()); if (ui->tabWidget->currentWidget() == ui->tabLevels) limitChartAnimation(); } QStringList WildMonChart::getSpeciesNamesAlphabetical() 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; } WildMonChart::LevelRange WildMonChart::getLevelRange(const QString &species, const QString &groupName) const { const QList levels = getLevelFrequencies(species, groupName).keys(); LevelRange range; if (levels.isEmpty()) { range.min = 0; range.max = 0; } else { range.min = levels.first(); range.max = levels.last(); } return range; } bool WildMonChart::usesGroupLabels() const { return this->groupNames.length() > 1; } QChart* WildMonChart::createSpeciesDistributionChart() { QList barSets; for (const auto &species : getSpeciesNamesAlphabetical()) { // Add encounter chance data auto set = new QBarSet(species); for (auto groupName : this->groupNamesReversed) 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; } } // 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] (bool on, int i) { QString text = on ? QString("%1 (%2%)").arg(set->label()).arg(set->at(i)) : ""; QToolTip::showText(QCursor::pos(), text); }); } // Preserve the order we set earlier so that the legend isn't shuffling around for the other all-species charts. this->speciesInLegendOrder.clear(); for (auto set : barSets) this->speciesInLegendOrder.append(set->label()); // Set up series auto series = new QHorizontalPercentBarSeries(); series->setLabelsVisible(); series->append(barSets); // Set up chart auto chart = new QChart(); chart->addSeries(series); chart->setTheme(currentTheme()); chart->setAnimationOptions(QChart::SeriesAnimations); chart->legend()->setVisible(true); chart->legend()->setShowToolTips(true); chart->legend()->setAlignment(Qt::AlignBottom); saveSpeciesColors(barSets); // X-axis is the % frequency. We're already showing percentages on the bar, so we just display 0/50/100% auto axisX = new QValueAxis(); 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 (usesGroupLabels()) { auto axisY = new QBarCategoryAxis(); axisY->setCategories(this->groupNamesReversed); chart->addAxis(axisY, Qt::AlignLeft); series->attachAxis(axisY); } return chart; } QBarSet* WildMonChart::createLevelDistributionBarSet(const QString &species, const QString &groupName, bool individual) { const double totalFrequency = individual ? getSpeciesFrequency(species, groupName) : 1.0; const QMap levelFrequencies = getLevelFrequencies(species, groupName); auto set = new QBarSet(species); LevelRange levelRange = individual ? getLevelRange(species, groupName) : this->groupedLevelRanges.value(groupName); for (int i = levelRange.min; i <= levelRange.max; i++) { set->append(levelFrequencies.value(i, 0) / totalFrequency * 100); } // Show data when hovering over a bar set. This covers some shortfalls in our ability to control the chart design. connect(set, &QBarSet::hovered, [=] (bool on, int i) { QString text = on ? QString("%1 Lv%2 (%3%)") .arg(individual ? "" : set->label()) .arg(QString::number(i + levelRange.min)) .arg(set->at(i)) : ""; QToolTip::showText(QCursor::pos(), text); }); // Clicking on a bar set in the stacked chart opens its individual chart if (!individual) { connect(set, &QBarSet::clicked, [this, species](int) { const QSignalBlocker blocker1(ui->groupBox_Species); const QSignalBlocker blocker2(ui->comboBox_Species); ui->groupBox_Species->setChecked(true); ui->comboBox_Species->setCurrentText(species); refreshLevelDistributionChart(); }); } return set; } QChart* WildMonChart::createLevelDistributionChart() { const QString groupName = ui->comboBox_Group->currentText(); LevelRange levelRange; QList barSets; // Create bar sets if (ui->groupBox_Species->isChecked()) { // Species box is active, we just display data for the selected species. const QString species = ui->comboBox_Species->currentText(); barSets.append(createLevelDistributionBarSet(species, groupName, true)); levelRange = getLevelRange(species, groupName); } else { // Species box is inactive, we display data for all species in the table. for (const auto &species : this->speciesInLegendOrder) barSets.append(createLevelDistributionBarSet(species, groupName, false)); levelRange = this->groupedLevelRanges.value(groupName); } // Set up series auto series = new QStackedBarSeries(); //series->setLabelsVisible(); series->append(barSets); // Set up chart auto chart = new QChart(); chart->addSeries(series); chart->setTheme(currentTheme()); chart->setAnimationOptions(QChart::SeriesAnimations); chart->legend()->setVisible(true); chart->legend()->setShowToolTips(true); chart->legend()->setAlignment(Qt::AlignBottom); applySpeciesColors(barSets); // Has to happen after theme is set // X-axis is the level range. QBarCategoryAxis *axisX = new QBarCategoryAxis(); for (int i = levelRange.min; i <= levelRange.max; i++) axisX->append(QString::number(i)); chart->addAxis(axisX, Qt::AlignBottom); series->attachAxis(axisX); // Y-axis is the % frequency QValueAxis *axisY = new QValueAxis(); axisY->setLabelFormat("%u%%"); chart->addAxis(axisY, Qt::AlignLeft); series->attachAxis(axisY); // We round the y-axis max up to a multiple of 5. auto roundUp = [](int num, int multiple) { auto remainder = num % multiple; if (remainder == 0) return num; return num + multiple - remainder; }; axisY->setMax(roundUp(qCeil(axisY->max()), 5)); return chart; } QChart::ChartTheme WildMonChart::currentTheme() const { return static_cast(ui->comboBox_Theme->currentData().toInt()); } void WildMonChart::updateTheme() { auto theme = currentTheme(); porymapConfig.wildMonChartTheme = ui->comboBox_Theme->currentText(); // In order to keep the color of each species in the legend consistent across // charts we save species->color mappings. The legend colors are overwritten // when we change themes, so we need to recalculate them. We let the species // distribution chart determine what those mapping are (it always includes every // species in the table) and then we apply those mappings to subsequent charts. QChart *chart = ui->chartView_SpeciesDistribution->chart(); if (!chart) return; chart->setTheme(theme); saveSpeciesColors(static_cast(chart->series().at(0))->barSets()); chart = ui->chartView_LevelDistribution->chart(); if (chart) { chart->setTheme(theme); applySpeciesColors(static_cast(chart->series().at(0))->barSets()); } } void WildMonChart::saveSpeciesColors(const QList &barSets) { this->speciesToColor.clear(); for (auto set : barSets) this->speciesToColor.insert(set->label(), set->color()); } void WildMonChart::applySpeciesColors(const QList &barSets) { for (auto set : barSets) set->setColor(this->speciesToColor.value(set->label())); } // Turn off the chart animation once it's played, otherwise it replays any time the window changes size. // The animation only begins when it's first displayed, so we'll only ever consider the chart for the current tab, // and when the tab changes we'll call this again. void WildMonChart::limitChartAnimation() { // Chart may be destroyed before the animation finishes QPointer chart; if (ui->tabWidget->currentWidget() == ui->tabSpecies) { chart = ui->chartView_SpeciesDistribution->chart(); } else if (ui->tabWidget->currentWidget() == ui->tabLevels) { chart = ui->chartView_LevelDistribution->chart(); } if (!chart || chart->animationOptions() == QChart::NoAnimation) return; // QChart has no signal for when the animation is finished, so we use a timer to stop the animation. // It is technically possible to get the chart to freeze mid-animation by resizing the window after // the timer starts but before it finishes, but 1. animations are short so this is difficult to do, // and 2. the issue resolves if the window is resized afterwards, so it's probably fine. QTimer::singleShot(chart->animationDuration(), Qt::PreciseTimer, [chart] { if (chart) chart->setAnimationOptions(QChart::NoAnimation); }); } void WildMonChart::showHelpDialog() { static const QString text = "This window provides some visualizations of the data in your current Wild Pokémon tab"; static const QString informative = "The Species Distribution tab shows the cumulative encounter chance for each species " "in the table. In other words, it answers the question \"What is the likelihood of encountering " "each species in a single encounter?\"" "

" "The Level Distribution tab shows the chance of encountering each species at a particular level. " "In the top left under Group you can select which encounter group to show data for. " "In the top right under Species you can select which species to show data for. " "

" "Individual Mode on the Level Distribution tab toggles whether data is shown for all species in the table. " "The percentages will update to reflect whether you're showing all species or just that individual species. " "In other words, while Individual Mode is checked the chart is answering the question \"If a species x " "is encountered, what is the likelihood that it will be level y\", and while Individual Mode is not checked, " "it answers the question \"For a single encounter, what is the likelihood of encountering a species x at level y.\""; QMessageBox msgBox(QMessageBox::Information, "porymap", text, QMessageBox::Close, this); msgBox.setTextFormat(Qt::RichText); msgBox.setInformativeText(informative); msgBox.exec(); } void WildMonChart::closeEvent(QCloseEvent *event) { porymapConfig.wildMonChartGeometry = saveGeometry(); QWidget::closeEvent(event); } #endif // __has_include()