diff --git a/CMakeLists.txt b/CMakeLists.txt
index 39daef98348177ba4f33506209a005aaff59aaa9..335be2c5c2a136f16b5f07d47975f5767803a4ea 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.21)
 
 project(
     bewertunggemischterklausuren
-    VERSION 0.5.0
+    VERSION 0.6.0
     LANGUAGES CXX
 )
 
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index ccaff218e14e66ed6574a2a9a8e1cfce6b16ef2a..65ef49f5f7beb95e446a4fa77946ec5116bdf593 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -41,14 +41,19 @@ set(PROJECT_SOURCES
 
     util/defaults.hpp
     util/enums.hpp
+    util/exception.hpp
+    util/font.hpp
+    util/font.cpp
     util/grade.hpp
     util/grade_boundary.cpp
     util/grade_boundary.hpp
     util/grade_boundary_step.hpp
+    util/heuristics.cpp
     util/heuristics.hpp
     util/points.cpp
     util/points.hpp
     util/storage_helper.hpp
+    util/strings.hpp
 
     view/boundary_window/boundary_tab.cpp
     view/boundary_window/boundary_tab.hpp
diff --git a/src/data/bonus_points.cpp b/src/data/bonus_points.cpp
index 736fdd6d935303fccbbab7f03aa90412548fed87..b974b934d8158eaa34956dd15a8dd6fd4cfdb20c 100644
--- a/src/data/bonus_points.cpp
+++ b/src/data/bonus_points.cpp
@@ -1,13 +1,37 @@
 #include "bonus_points.hpp"
 
+#include "util/exception.hpp"
+#include "util/heuristics.hpp"
+#include "util/strings.hpp"
+
 #include <algorithm>
+#include <optional>
 #include <ranges>
 #include <stdexcept>
 #include <string>
 
 BonusPoints::BonusPoints(const CsvDump &c) noexcept(false) : mBonusPoints(c) {
     if (c.data.empty())
-        throw std::invalid_argument("Read bonus-CSV: Empty file");
+        throw BonusParseException(str::error::empty);
+
+    if (c.data[0].empty())
+        throw BonusParseException(str::error::noHeader);
+
+    // Quick check: reading wrong file type?
+    {
+        const auto fileType =
+            getApproximateFileType(QString::fromStdString(c.data[0][0]));
+
+        switch (fileType) {
+        case FileType::dynexite:
+            throw BonusParseException(str::error::unexpectedDynexite);
+        case FileType::rwth:
+            throw BonusParseException(str::error::unexpectedRwthOnline);
+        default:
+            // the file might still be invalid, but that will be decided later
+            break;
+        }
+    }
 
     const auto nrRows = c.data.size();
 
@@ -28,23 +52,45 @@ BonusPoints::BonusPoints(const CsvDump &c) noexcept(false) : mBonusPoints(c) {
 
     // Throw if relevant columns not found
     if (registrationNrColumn >= c.data[0].size())
-        throw std::invalid_argument(
-            "Read bonus-CSV: No registration nr column found");
+        throw BonusParseException(str::error::noIdColumn);
     if (bonusPointColumn >= c.data[0].size())
-        throw std::invalid_argument(
-            "Read bonus-CSV: No bonus point column found");
+        throw BonusParseException(str::error::noBonusPointColumn);
 
     // Insert relevant data into lists
     for (std::size_t i = 1; i < nrRows; ++i) {
+
         // Registration number column sometimes contains an email address
         // instead of a registration number. Todo: Try to match them up via name
+        std::optional<int> registrationNr;
+        std::optional<Points> bonusPoints;
+
+        // If stoi fails, registrationNr is empty
         try {
-            mRegistrationNr.emplace_back(
-                std::stoi(c.data[i][registrationNrColumn]));
+            registrationNr = std::stoi(c.data[i][registrationNrColumn]);
         } catch (...) {
-            continue;
         }
-        mNrBonusPoints.emplace_back(
-            stringToPoints(c.data[i][bonusPointColumn]));
+
+        // If stringToPoints fails, bonusPoints is empty
+        try {
+            bonusPoints = stringToPoints(c.data[i][bonusPointColumn]);
+        } catch (...) {
+        }
+
+        // Only add ID and points to the lists if both can be successfully
+        // parsed
+        if (bonusPoints && registrationNr) {
+            mRegistrationNr.emplace_back(*registrationNr);
+            mNrBonusPoints.emplace_back(*bonusPoints);
+        }
+        // Write the student to the invalid registration number list
+        // instead, to generate a warning later on
+        // Ignore the error if the student got no bonus points anyway
+        else if (!bonusPoints || (!registrationNr && bonusPoints->p != 0.)) {
+            mInvalidRegNrs.emplace_back(c.data[i][registrationNrColumn],
+                                        c.data[i][bonusPointColumn]);
+        }
     }
+
+    if (mRegistrationNr.size() != mNrBonusPoints.size())
+        throw BonusParseException(str::error::bonusArraySizeMismatch);
 }
diff --git a/src/data/bonus_points.hpp b/src/data/bonus_points.hpp
index 3d3ba76964bda4f7cbee9a5a7d617e0deb65273b..311b600b92dc8f949b9b12f71961cb88b7ad7055 100644
--- a/src/data/bonus_points.hpp
+++ b/src/data/bonus_points.hpp
@@ -4,6 +4,9 @@
 #include "util/points.hpp"
 
 #include <QList>
+#include <QPair>
+
+#include <string>
 
 struct BonusPoints {
     BonusPoints() = default;
@@ -12,6 +15,10 @@ struct BonusPoints {
     QList<int> mRegistrationNr{};
     QList<Points> mNrBonusPoints{};
 
+    // A list of all students with invalid (non-numerical) registration numbers
+    // and their respective bonus point count
+    QList<QPair<std::string, std::string>> mInvalidRegNrs{};
+
     // Store the entire csv dump so that we can more easily export it later
     CsvDump mBonusPoints;
 };
diff --git a/src/data/nodes/dynexite_exam.cpp b/src/data/nodes/dynexite_exam.cpp
index f5ad45bbd0491e96354d86e04a11cacdc4b94fd4..21644115c10bd110e153b5a48cd7b78d6aac6f5f 100644
--- a/src/data/nodes/dynexite_exam.cpp
+++ b/src/data/nodes/dynexite_exam.cpp
@@ -1,6 +1,9 @@
 #include "dynexite_exam.hpp"
 
 #include "util/enums.hpp"
+#include "util/exception.hpp"
+#include "util/heuristics.hpp"
+#include "util/strings.hpp"
 
 #include <algorithm>
 #include <ranges>
@@ -10,18 +13,17 @@
 static void checkHeader(const std::string &found,
                         const std::string &expected) noexcept(false) {
     if (found != expected) {
-        throw std::invalid_argument(
-            "Read dynexite-CSV: Header validation error: expected \"" +
-            expected + "\", found \"" + found + "\"");
+        throw DynexiteParseException("Header validation error: expected \"" +
+                                     expected + "\", found \"" + found + "\"");
     }
 }
 
 static void checkHeaderEndsWith(const std::string &found,
                                 const std::string &ending) noexcept(false) {
     if (!found.ends_with(ending)) {
-        throw std::invalid_argument(
-            "Read dynexite-CSV: Header validation error: expected ending \"" +
-            ending + "\", found string \"" + found + "\"");
+        throw DynexiteParseException(
+            "Header validation error: expected ending \"" + ending +
+            "\", found string \"" + found + "\"");
     }
 }
 
@@ -33,8 +35,7 @@ static void validateHeaders(const CsvDump &c,
 
     // Do headers exist at all?
     if (c.data.empty())
-        throw std::invalid_argument(
-            "Read dynexite-CSV: Header validation: no headers found");
+        throw DynexiteParseException(str::error::empty);
 
     const auto &headers = c.data[0];
     const auto nrColumns = c.data[0].size();
@@ -65,14 +66,33 @@ static void validateHeaders(const CsvDump &c,
 
 DynexiteExam::DynexiteExam(const CsvDump &c) noexcept(false) : mDynexite(c) {
     if (c.data.empty())
-        throw std::invalid_argument("Read dynexite-CSV: Empty file");
+        throw DynexiteParseException(str::error::empty);
+
+    if (c.data[0].empty())
+        throw DynexiteParseException(str::error::noHeader);
+
+    // Quick check: reading wrong file type?
+    {
+        const auto fileType =
+            getApproximateFileType(QString::fromStdString(c.data[0][0]));
+
+        switch (fileType) {
+        case FileType::bonus:
+            throw DynexiteParseException(str::error::unexpectedBonus);
+        case FileType::rwth:
+            throw DynexiteParseException(str::error::unexpectedRwthOnline);
+        default:
+            // the file might still be invalid, but that will be decided later
+            break;
+        }
+    }
 
     const auto nrRows = c.data.size();
     const auto nrColumns = c.data[0].size();
 
     // No data aside from header?
     if (nrRows < 2)
-        throw std::invalid_argument("Read dynexite-CSV: No data rows");
+        throw DynexiteParseException(str::error::noData);
 
     // Non-attending students are listed as lines in the csv-file, but the lines
     // are empty aside from column 1, especially they are empty in line 0
@@ -83,14 +103,14 @@ DynexiteExam::DynexiteExam(const CsvDump &c) noexcept(false) : mDynexite(c) {
 
     // No-one attended?
     if (nrStudents <= 0)
-        throw std::invalid_argument("Read dynexite-CSV: No student attended?");
+        throw DynexiteParseException(str::error::noStudents);
 
     // Assert that all necessary columns are there and the number of columns
     // makes sense (4 columns per task, 2 extra columns to the left and 12 extra
     // columns to the right)
     if (!(nrColumns > 14 && (nrColumns - 14) % 4 == 0))
-        throw std::invalid_argument(
-            "Read dynexite-CSV: Unexpected number of columns");
+        throw DynexiteParseException(std::string(str::error::invalidColumnNr) +
+                                     std::to_string(nrColumns));
 
     const auto nrTasks = (nrColumns - 14) / 4;
 
@@ -101,7 +121,7 @@ DynexiteExam::DynexiteExam(const CsvDump &c) noexcept(false) : mDynexite(c) {
     validateHeaders(c, nrTasks);
 
     if (!insertData(c, nrTasks)) {
-        throw std::invalid_argument("Read dynexite-CSV: Inserting data failed");
+        throw DynexiteParseException(str::error::insertFailed);
     }
 
     /* todo: run some data sanity checks:
diff --git a/src/data/nodes/student_data.cpp b/src/data/nodes/student_data.cpp
index 8e926630001dba97c4b782e8e287899d51b4af46..503d1569478a7f2fb637af99bf8dc976755d92ae 100644
--- a/src/data/nodes/student_data.cpp
+++ b/src/data/nodes/student_data.cpp
@@ -1,5 +1,9 @@
 #include "student_data.hpp"
 
+#include "util/exception.hpp"
+#include "util/heuristics.hpp"
+#include "util/strings.hpp"
+
 #include <algorithm>
 #include <array>
 #include <ranges>
@@ -13,9 +17,9 @@
 [[maybe_unused]] static void checkHeader(const std::string &found,
                                          const std::string &expected) {
     if (found != expected) {
-        throw std::invalid_argument(
-            "Read rwthonline-CSV: Header validation error: expected \"" +
-            expected + "\", found \"" + found + "\"");
+        throw RwthOnlineParseException("Header validation error: expected \"" +
+                                       expected + "\", found \"" + found +
+                                       "\"");
     }
 }
 
@@ -26,8 +30,7 @@ static std::pair<std::size_t, std::size_t> validateHeaders(CsvDump &c) {
 
     // Do headers exist at all?
     if (c.data.empty())
-        throw std::invalid_argument(
-            "Read rwthonline-CSV: Header validation: no headers found");
+        throw RwthOnlineParseException(str::error::empty);
 
     auto &headers = c.data[0];
 
@@ -40,9 +43,11 @@ static std::pair<std::size_t, std::size_t> validateHeaders(CsvDump &c) {
     // ignore all other header names.
 #if !RWTHONLINE_PARSER_PERMISSIVE
     if (c.data[0].size() != 24) {
-        throw std::invalid_argument("Read rwthonline-CSV: Header validation: "
-                                    "Unexpected number of columns: " +
-                                    std::to_string(c.data[0].size()));
+        throw RwthOnlineParseException("Header validation: "
+                                       "Unexpected number of columns: " +
+                                       std::to_string(c.data[0].size()) +
+                                       " (To disable this check, set "
+                                       "RWTHONLINE_PARSER_PERMISSIVE to true)");
     }
 
     static constexpr std::array headerStrings = {"STUDY_PROGRAMME",
@@ -85,12 +90,10 @@ static std::pair<std::size_t, std::size_t> validateHeaders(CsvDump &c) {
         std::ranges::begin(c.data[0]));
 
     if (registrationNrIdx >= c.data[0].size()) {
-        throw std::invalid_argument(
-            "Read rwthonline-CSV: No registration number column found");
+        throw RwthOnlineParseException(str::error::noIdColumn);
     }
     if (attemptNrIdx >= c.data[0].size()) {
-        throw std::invalid_argument(
-            "Read rwthonline-CSV: No attempt number column found");
+        throw RwthOnlineParseException(str::error::noAttemptNrColumn);
     }
     return {registrationNrIdx, attemptNrIdx};
 #endif
@@ -98,13 +101,32 @@ static std::pair<std::size_t, std::size_t> validateHeaders(CsvDump &c) {
 
 StudentData::StudentData(CsvDump &c) noexcept(false) : mRwthOnline(c) {
     if (c.data.empty())
-        throw std::invalid_argument("Read rwthonline-CSV: Empty file");
+        throw RwthOnlineParseException(str::error::empty);
+
+    if (c.data[0].empty())
+        throw RwthOnlineParseException(str::error::noHeader);
+
+    // Quick check: reading wrong file type?
+    {
+        const auto fileType =
+            getApproximateFileType(QString::fromStdString(c.data[0][0]));
+
+        switch (fileType) {
+        case FileType::bonus:
+            throw RwthOnlineParseException(str::error::unexpectedBonus);
+        case FileType::dynexite:
+            throw RwthOnlineParseException(str::error::unexpectedDynexite);
+        default:
+            // the file might still be invalid, but that will be decided later
+            break;
+        }
+    }
 
     const auto nrRows = c.data.size();
 
     // No data aside from header?
     if (nrRows < 2)
-        throw std::invalid_argument("Read rwthonline-CSV: No data rows");
+        throw RwthOnlineParseException(str::error::noData);
 
     const auto nrStudents = nrRows - 1;
     reserveArrays(static_cast<qsizetype>(nrStudents));
@@ -119,7 +141,6 @@ void StudentData::reserveArrays(const qsizetype nrStudents) noexcept {
     mAttemptNr.reserve(nrStudents);
 }
 
-// std::stoi might throw
 void StudentData::insertData(CsvDump &c, const std::size_t regIdx,
                              const std::size_t attemptIdx) noexcept(false) {
 
@@ -130,7 +151,25 @@ void StudentData::insertData(CsvDump &c, const std::size_t regIdx,
         std::erase(line[regIdx], '"');
         std::erase(line[attemptIdx], '"');
 
-        mRegistrationNr.emplaceBack(std::stoi(line[regIdx]));
-        mAttemptNr.emplaceBack(std::stoi(line[attemptIdx]));
+        try {
+            mRegistrationNr.emplaceBack(std::stoi(line[regIdx]));
+        } catch (...) {
+            throw RwthOnlineParseException(
+                std::string("Failed to parse registration number ") +
+                line[regIdx]);
+        }
+
+        try {
+            mAttemptNr.emplaceBack(std::stoi(line[attemptIdx]));
+        } catch (...) {
+            throw RwthOnlineParseException(
+                std::string("Failed to parse attempt number ") +
+                line[attemptIdx]);
+        }
+
+        // Assert that the two arrays don't get out of sync
+        if (mRegistrationNr.size() != mAttemptNr.size()) {
+            throw RwthOnlineParseException(str::error::rwthArraySizeMismatch);
+        }
     }
 }
diff --git a/src/data/nodes/student_data.hpp b/src/data/nodes/student_data.hpp
index ec5f7cedcd2fa25c5042e1c426e68f4da6cbecbe..7229212341ebd1754b2f6442e5d4282853731654 100644
--- a/src/data/nodes/student_data.hpp
+++ b/src/data/nodes/student_data.hpp
@@ -12,7 +12,7 @@ struct StudentData : Node<> {
     using Node<>::Node;
 
     /**
-     * \brief Interprets the CsvDump as a RWTHOnline-csv-file and copies all
+     * \brief Interprets the CsvDump as an RWTHOnline-csv-file and copies all
      * necessary info. Also runs sanity checks on the given data and throws on
      * error.
      *
diff --git a/src/file_io/csv_parser.cpp b/src/file_io/csv_parser.cpp
index f06ac35c36d9b4735e3773c18196c255e61785ce..7efa4365341a90d04d3998ea2350b28eb44de2d8 100644
--- a/src/file_io/csv_parser.cpp
+++ b/src/file_io/csv_parser.cpp
@@ -6,6 +6,8 @@
 
 #include "csv_parser.hpp"
 
+#include "util/strings.hpp"
+
 #include <algorithm>
 #include <array>
 #include <cassert>
@@ -168,7 +170,7 @@ std::unique_ptr<CsvDump> readCsv(const std::string &s) {
         } catch (std::runtime_error &) {
         }
     }
-    throw std::runtime_error("No valid delimiter found");
+    throw std::runtime_error(str::error::csvDelimiter);
 }
 
 std::unique_ptr<CsvDump> readCsv(std::istream &in) {
diff --git a/src/file_io/yml_parser.cpp b/src/file_io/yml_parser.cpp
index a1eae9d8d2ffbf32ad60261971f327acfd802e79..a8e72a4fbfb8233c6e28f262f83c2f3d9bc44e89 100644
--- a/src/file_io/yml_parser.cpp
+++ b/src/file_io/yml_parser.cpp
@@ -3,6 +3,8 @@
 #include "csv_parser.hpp"
 #include "data/data_manager.hpp"
 #include "util/enums.hpp"
+#include "util/exception.hpp"
+#include "util/strings.hpp"
 
 #include <algorithm>
 #include <fstream>
@@ -264,7 +266,7 @@ std::unique_ptr<ExamSave> readYml(const std::filesystem::path &p) {
         std::string str;
         std::getline(infile, str, '\n');
         if (str != "---")
-            throw std::invalid_argument{"Invalid yaml header"};
+            throw D2RParseException{str::error::ymlHeader};
     }
 
     // Read key-by-key until EOF
diff --git a/src/model/input_model.cpp b/src/model/input_model.cpp
index 739ad6c55a2d7a0d31d3f29946097bb91e987a5f..3373fd5474499a824fc03e6d95738e31e2f20b86 100644
--- a/src/model/input_model.cpp
+++ b/src/model/input_model.cpp
@@ -1,15 +1,23 @@
 #include "input_model.hpp"
 
 #include "file_io/csv_parser.hpp"
+#include "util/defaults.hpp"
+#include "util/exception.hpp"
+#include "util/strings.hpp"
+
+#include <QList>
+#include <QMessageBox>
+#include <QString>
 
 InputModel::InputModel(const FileType t) : mType(t) {}
 
-bool InputModel::loadCsv(const std::filesystem::path &p) {
+FileOpenError InputModel::loadCsv(const std::filesystem::path &p) {
 
     // We could move this to a separate thread to keep the interface
     // responsive, but it only takes 1ms on my machine, so let's not
     // bother.
     try {
+        FileOpenError error;
         if (mType == FileType::d2r) {
 
             mLoadedData.emplace<std::unique_ptr<ExamSave>>(readYml(p));
@@ -32,20 +40,45 @@ bool InputModel::loadCsv(const std::filesystem::path &p) {
             case FileType::bonus:
                 mLoadedData.emplace<std::unique_ptr<BonusPoints>>(
                     new BonusPoints(*dump));
+                for (const auto &regPair :
+                     std::get<std::unique_ptr<BonusPoints>>(mLoadedData)
+                         ->mInvalidRegNrs) {
+                    error.mWarnings.append(
+                        QString("Failed to add ") +
+                        QString::fromStdString(regPair.second) +
+                        " bonus points to student with registration number " +
+                        QString::fromStdString(regPair.first));
+                }
                 break;
             }
         }
 
-        return true;
-    } catch ([[maybe_unused]] const std::exception &e) {
-        // csv file read or object instantiation failed!
-        // todo: look at error more closely, display what went wrong
-        return false;
+        return error;
+    } catch (const std::exception &e) {
+        switch (mType) {
+        default:
+            assert(false);
+            [[fallthrough]];
+        case FileType::dynexite:
+            return FileOpenError{FileOpenErrorCode::UnknownError,
+                                 QString(str::error::defaultDynexite) +
+                                     e.what()};
+        case FileType::rwth:
+            return FileOpenError{FileOpenErrorCode::UnknownError,
+                                 QString(str::error::defaultRwthOnline) +
+                                     e.what()};
+        case FileType::bonus:
+            return FileOpenError{FileOpenErrorCode::UnknownError,
+                                 QString(str::error::defaultBonus) + e.what()};
+        case FileType::d2r:
+            return FileOpenError{FileOpenErrorCode::UnknownError,
+                                 QString(str::error::defaultD2R) + e.what()};
+        }
     }
 }
 
 std::variant<DynexiteExam *, StudentData *, BonusPoints *, ExamSave *>
-InputModel::read() {
+InputModel::read() const {
     std::variant<DynexiteExam *, StudentData *, BonusPoints *, ExamSave *> ret;
     if (std::holds_alternative<std::unique_ptr<DynexiteExam>>(mLoadedData)) {
 
@@ -61,3 +94,90 @@ InputModel::read() {
     }
     return ret;
 }
+
+void warnIfLowStudentIntersection(QWidget *parent,
+                                  const InputModel &dynexiteModel,
+                                  const InputModel &rwthModel,
+                                  const InputModel &bonusModel,
+                                  const FileSelectState &bonusState) noexcept {
+    auto dynexiteStudents =
+        std::get<DynexiteExam *>(dynexiteModel.read())->mIdentifier;
+    auto rwthOnlineStudents =
+        std::get<StudentData *>(rwthModel.read())->mRegistrationNr;
+    auto bonusStudents =
+        bonusState == FileSelectState::ok
+            ? std::get<BonusPoints *>(bonusModel.read())->mRegistrationNr
+            : QList<int>{};
+
+    std::ranges::sort(dynexiteStudents);
+    std::ranges::sort(rwthOnlineStudents);
+    std::ranges::sort(bonusStudents);
+
+    QList<int> dbIntersection, drIntersection;
+
+    // Find out number of students that are both in the dynexite file and the
+    // RWTHonline/bonus file
+    std::ranges::set_intersection(dynexiteStudents, bonusStudents,
+                                  std::back_inserter(dbIntersection));
+    std::ranges::set_intersection(dynexiteStudents, rwthOnlineStudents,
+                                  std::back_inserter(drIntersection));
+
+    QString warning = str::error::lowStudentIntersection;
+    QList<QString> singleWarnings;
+
+    // Display warning if few students in dynexite are also in RWTHonline
+    if (static_cast<double>(drIntersection.size()) <
+        defaults::studentIntersectWarningThreshold *
+            static_cast<double>(dynexiteStudents.size())) {
+        const auto percentage = static_cast<double>(drIntersection.size()) /
+                                static_cast<double>(dynexiteStudents.size()) *
+                                100.;
+
+        if (percentage > 0.) {
+            singleWarnings.append(
+                "\n\nOnly " + QString::number(percentage, 'f', 2) + "% (" +
+                QString::number(drIntersection.size()) + " of " +
+                QString::number(dynexiteStudents.size()) +
+                ") of students in the dynexite file can be found in the "
+                "RWTHonline file.");
+        } else {
+            // Special string for no intersection
+            singleWarnings.append(
+                "\n\nNone of the students in the dynexite file can be found in "
+                "the RWTHonline file.");
+        }
+    }
+
+    // Display warning if few students in dynexite are also in the bonus point
+    // file, but ignore this if no students are in the bonus point file since it
+    // is optional
+    if (!bonusStudents.empty() &&
+        static_cast<double>(dbIntersection.size()) <
+            defaults::studentIntersectWarningThreshold *
+                static_cast<double>(dynexiteStudents.size())) {
+        const auto percentage = static_cast<double>(dbIntersection.size()) /
+                                static_cast<double>(dynexiteStudents.size()) *
+                                100.;
+
+        if (percentage > 0.) {
+            singleWarnings.append(
+                "\n\nOnly " + QString::number(percentage, 'f', 2) + "% (" +
+                QString::number(dbIntersection.size()) + " of " +
+                QString::number(dynexiteStudents.size()) +
+                ") of students in the dynexite file can be found in the bonus "
+                "point file.");
+        } else {
+            singleWarnings.append(
+                "\n\nNone of the students in the dynexite file can be found in "
+                "the bonus point file.");
+        }
+    }
+
+    // Display all accumulated warnings with a standard prefix
+    if (!singleWarnings.empty()) {
+        for (const auto &s : singleWarnings) {
+            warning += s;
+        }
+        QMessageBox::warning(parent, "Reading file warnings", warning);
+    }
+}
diff --git a/src/model/input_model.hpp b/src/model/input_model.hpp
index 4f84c587063ad21379afb72c7b7d5427d0abc2c2..2b1d0568b0b95b69446b08baf65755aa624b043a 100644
--- a/src/model/input_model.hpp
+++ b/src/model/input_model.hpp
@@ -6,11 +6,29 @@
 #include "file_io/yml_parser.hpp"
 #include "util/enums.hpp"
 
+#include <QList>
+#include <QWidget>
+
 #include <filesystem>
 #include <memory>
 #include <utility>
 #include <variant>
 
+enum class FileOpenErrorCode {
+    Success,
+    OsError,
+    CsvParseError,
+    YmlParseError,
+    UnknownError
+};
+
+struct FileOpenError {
+    FileOpenErrorCode mEc = FileOpenErrorCode::Success;
+    QString mDetails{};
+
+    QList<QString> mWarnings{};
+};
+
 class InputModel {
 public:
     /**
@@ -27,7 +45,7 @@ public:
      *
      * \return Success
      */
-    bool loadCsv(const std::filesystem::path &p);
+    FileOpenError loadCsv(const std::filesystem::path &p);
 
     /**
      * \brief Which file type this model stores.
@@ -42,10 +60,18 @@ public:
     [[nodiscard]] auto get() { return std::move(mLoadedData); }
     [[nodiscard]] std::variant<DynexiteExam *, StudentData *, BonusPoints *,
                                ExamSave *>
-    read();
+    read() const;
 
 private:
     std::variant<std::unique_ptr<DynexiteExam>, std::unique_ptr<StudentData>,
                  std::unique_ptr<BonusPoints>, std::unique_ptr<ExamSave>>
         mLoadedData{};
 };
+
+// Display a warning pop-up if too low an amount of students in the dynexite
+// file are also in the RWTHonline file todo: noexcept?
+void warnIfLowStudentIntersection(QWidget *parent,
+                                  const InputModel &dynexiteModel,
+                                  const InputModel &rwthModel,
+                                  const InputModel &bonusModel,
+                                  const FileSelectState &bonusState) noexcept;
diff --git a/src/util/defaults.hpp b/src/util/defaults.hpp
index c4d2892b3002020d0598a24e549992a68a74c571..dc0ea09b89b53e369dde50132ddeec5e0e143a5d 100644
--- a/src/util/defaults.hpp
+++ b/src/util/defaults.hpp
@@ -25,6 +25,12 @@ inline constexpr bool requirePassAllParts = false;
 
 //-----------------------------------------------------------------------
 
+/** \brief Percentage of window width of the grade tab, which is given to the
+ * grade histogram. Between 0-100. */
+inline constexpr int gradeTabHistogramWidthPercentage = 70;
+
+//-----------------------------------------------------------------------
+
 /** \brief Maximum selectable open points passing limit. */
 inline constexpr double openPointsPassingLimit = 0.5;
 
@@ -40,4 +46,16 @@ inline constexpr double maxBucketSize = 5.;
 /** \brief Point histogram bucket size selector step size. */
 inline constexpr double bucketStep = 0.25;
 
+//-----------------------------------------------------------------------
+
+/** \brief The factor by which the small font size is smaller than the large
+ * font size. The small font size is never smaller than the default font size.*/
+inline constexpr double smallFontSizeRatio = 0.5;
+
+//-----------------------------------------------------------------------
+
+/** \brief If less than this share of dynexite students are in the RWTHonline or
+ * bonus point file, issue a warning */
+inline constexpr double studentIntersectWarningThreshold = 0.5;
+
 } // namespace defaults
diff --git a/src/util/exception.hpp b/src/util/exception.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..bcd81faf6c067547ce9d1322f15b6f5f1ffd0c3c
--- /dev/null
+++ b/src/util/exception.hpp
@@ -0,0 +1,16 @@
+#pragma once
+
+#include <stdexcept>
+
+struct DynexiteParseException : public std::invalid_argument {
+    using std::invalid_argument::invalid_argument;
+};
+struct RwthOnlineParseException : public std::invalid_argument {
+    using std::invalid_argument::invalid_argument;
+};
+struct BonusParseException : public std::invalid_argument {
+    using std::invalid_argument::invalid_argument;
+};
+struct D2RParseException : public std::invalid_argument {
+    using std::invalid_argument::invalid_argument;
+};
diff --git a/src/util/font.cpp b/src/util/font.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..41db0f02fe8165173774a103224b1587a8c8955f
--- /dev/null
+++ b/src/util/font.cpp
@@ -0,0 +1,50 @@
+#include "font.hpp"
+
+#include <QGuiApplication>
+
+#include <cassert>
+#include <qscreen.h>
+
+qreal FontSize::getPixelSize() const noexcept {
+
+    if (unit == FontSizeUnit::Pixel) {
+        return size;
+    } else {
+        return ptToPx(size,
+                      QGuiApplication::primaryScreen()->logicalDotsPerInch());
+    }
+}
+
+std::partial_ordering FontSize::operator<=>(const FontSize &that) const {
+
+    if (unit == that.unit) {
+        return size <=> that.size;
+    } else if (unit == FontSizeUnit::Pixel) {
+        return pxToPt(
+                   size,
+                   QGuiApplication::primaryScreen()->logicalDotsPerInch()) <=>
+               that.size;
+    } else {
+        return size <=>
+               pxToPt(that.size,
+                      QGuiApplication::primaryScreen()->logicalDotsPerInch());
+    }
+}
+
+QDebug operator<<(QDebug debug, const FontSize &object) {
+    debug.nospace() << object.size
+                    << (object.unit == FontSizeUnit::Pixel ? "px" : "pt");
+    return debug.maybeSpace();
+}
+
+void setFontSize(QFont &f, const FontSize size) noexcept {
+
+    // point / pixel size must be greater than zero
+    assert(size.size > 0);
+
+    if (size.unit == FontSizeUnit::Pixel) {
+        f.setPixelSize(static_cast<int>(size.size));
+    } else {
+        f.setPointSizeF(size.size);
+    }
+}
diff --git a/src/util/font.hpp b/src/util/font.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..12ef934feaf9b540bf9f914c5537b1e6a6a3c5e8
--- /dev/null
+++ b/src/util/font.hpp
@@ -0,0 +1,43 @@
+#pragma once
+
+#include <QDebug>
+#include <QFont>
+
+enum class FontSizeUnit { Pixel, Point };
+
+struct FontSize {
+    qreal size;
+    FontSizeUnit unit;
+
+    [[nodiscard]] qreal getPixelSize() const noexcept;
+
+    std::partial_ordering operator<=>(const FontSize &that) const;
+
+    template <typename T>
+    constexpr FontSize &operator*=(const T &rhs) noexcept {
+        size *= rhs;
+        return *this;
+    }
+};
+
+template <typename T>
+[[nodiscard]] constexpr FontSize operator*(FontSize lhs,
+                                           const T &rhs) noexcept {
+    lhs *= rhs;
+    return lhs;
+}
+
+QDebug operator<<(QDebug debug, const FontSize &object);
+
+// A point is 1/72 of an inch
+[[nodiscard]] constexpr double ptToPx(const double pt,
+                                      const double dpi) noexcept {
+    return pt / 72 * dpi;
+}
+
+[[nodiscard]] constexpr double pxToPt(const double px,
+                                      const double dpi) noexcept {
+    return px * 72 / dpi;
+}
+
+void setFontSize(QFont &f, FontSize size) noexcept;
diff --git a/src/util/heuristics.cpp b/src/util/heuristics.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..05b4feb7b996f089594de03628664dc775deb5e3
--- /dev/null
+++ b/src/util/heuristics.cpp
@@ -0,0 +1,17 @@
+#include "heuristics.hpp"
+
+#include "util/enums.hpp"
+
+FileType getApproximateFileType(const QString &s) noexcept {
+    if (QString::compare(s, QString("STUDY_PROGRAMME"), Qt::CaseInsensitive) ==
+        0) {
+        return FileType::rwth;
+    }
+    if (QString::compare(s, QString("AttemptID"), Qt::CaseInsensitive) == 0) {
+        return FileType::dynexite;
+    }
+    if (QString::compare(s, QString("Vorname"), Qt::CaseInsensitive) == 0) {
+        return FileType::bonus;
+    }
+    return FileType::NR_VALS;
+}
diff --git a/src/util/heuristics.hpp b/src/util/heuristics.hpp
index 72f46c7ef50f228686fe62f8cbf4762646feabd0..c3a959d3b03a4f549b3281932b9942994befab3e 100644
--- a/src/util/heuristics.hpp
+++ b/src/util/heuristics.hpp
@@ -1,10 +1,10 @@
 #pragma once
 
+#include "data/nodes/dynexite_exam.hpp"
 #include "util/grade_boundary_step.hpp"
 #include "util/points.hpp"
 
-#include "data/nodes/dynexite_exam.hpp"
-
+#include <QString>
 #include <QtGlobal>
 
 #include <algorithm>
@@ -50,6 +50,9 @@ guessGradeBoundaryStepSize(const DynexiteExam &exam) noexcept {
         exam.mTaskMaxPoints);
 }
 
+// Guess by the left-most header what file type this is
+[[nodiscard]] FileType getApproximateFileType(const QString &s) noexcept;
+
 //-----------------------------------------------------------------------
 // IMPLEMENTATION
 //-----------------------------------------------------------------------
diff --git a/src/util/points.cpp b/src/util/points.cpp
index 29bef39b0e780618a478a5f162ff2d54441d1660..99bfdc44d66132fc5b3d977578f03340d14e16e7 100644
--- a/src/util/points.cpp
+++ b/src/util/points.cpp
@@ -2,6 +2,7 @@
 
 #include <algorithm>
 #include <ranges>
+#include <stdexcept>
 
 /**
  * \brief Call std::stod, but interpret an empty string or "-" as 0.
@@ -12,5 +13,12 @@ Points stringToPoints(std::string s) noexcept(false) {
         return Points{0.};
     // dynexite file might use dot or comma as decimal separator
     std::ranges::replace(s, ',', '.');
-    return Points{std::stod(s)};
+
+    try {
+
+        return Points{std::stod(s)};
+    } catch (const std::invalid_argument &) {
+        throw std::invalid_argument{"Could not convert string \"" + s +
+                                    "\" to a number."};
+    }
 }
diff --git a/src/util/strings.hpp b/src/util/strings.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..41a311b4c141b933c9369418148d5eae0489cda8
--- /dev/null
+++ b/src/util/strings.hpp
@@ -0,0 +1,64 @@
+#pragma once
+
+namespace str {
+namespace error {
+// Error prefix for any error when parsing a dynexite file
+constexpr auto defaultDynexite =
+    "Please download the CSV results file from the \"Download the "
+    "results\" section from dynexite.\n\n";
+
+// Error prefix for any error when parsing an RWTH Online file
+constexpr auto defaultRwthOnline =
+    "Please download the export from RWTH Online as a CSV file with "
+    "encoding-setting ISO 8859-1.\n\n";
+
+// Error prefix for any error when parsing a bonus point file
+constexpr auto defaultBonus =
+    "Please use a properly formatted bonus point file with at "
+    "least the columns \"Matrikelnummer\" and \"Bonuspunkte "
+    "(Punkte)\" from moodle.\n\n";
+
+// Error prefix for any error when parsing a d2r file
+constexpr auto defaultD2R =
+    "The selected file is not a d2r file. Please select a file that you "
+    "previously exported with this app.\n\n";
+
+// Error when parsing an empty or non-text file
+constexpr auto empty = "File is empty or of the wrong file type.";
+
+// Error when there are no header columns
+constexpr auto noHeader = "No header.";
+
+// Error when there are no rows beside the header
+constexpr auto noData = "No data rows.";
+
+// Error when there are no rows beside the header in a dynexite file with
+// content besides the identifier
+constexpr auto noStudents = "No student attended?";
+
+constexpr auto unexpectedDynexite = "You selected a dynexite file.";
+constexpr auto unexpectedRwthOnline = "You selected an RWTH Online file.";
+constexpr auto unexpectedBonus = "You selected a bonus point file.";
+
+constexpr auto invalidColumnNr = "Unexpected number of columns: ";
+
+constexpr auto insertFailed = "Inserting data failed.";
+
+constexpr auto noIdColumn = "No registration number column found.";
+constexpr auto noBonusPointColumn = "No bonus point column found.";
+constexpr auto noAttemptNrColumn = "No attempt number column found.";
+
+constexpr auto bonusArraySizeMismatch =
+    "Internal error while reading bonus points (array size mismatch).";
+constexpr auto rwthArraySizeMismatch =
+    "Internal error while reading student info (array size mismatch).";
+
+constexpr auto lowStudentIntersection =
+    "Are you sure you selected files from the same exam?";
+
+constexpr auto ymlHeader = "Invalid yaml header.";
+
+constexpr auto csvDelimiter =
+    "Error while parsing the CSV file: No valid delimiter found";
+} // namespace error
+} // namespace str
diff --git a/src/view/gui/custom_slider.cpp b/src/view/gui/custom_slider.cpp
index 94788b8514b1e3960f33c533477430bd91268c0d..5dfcc78f0fe73c939c497768f52c49e31dfde2cb 100644
--- a/src/view/gui/custom_slider.cpp
+++ b/src/view/gui/custom_slider.cpp
@@ -28,9 +28,9 @@ public:
  * We could solve that problem with a map of DirectClickStyles, but that would
  * be overkill here. We only use the default native style anyways.
  */
-static DirectClickStyle *getDefaultStyle() { 
-    static auto style = new DirectClickStyle(); 
-    return style; 
+static DirectClickStyle *getDefaultStyle() {
+    static auto style = new DirectClickStyle();
+    return style;
 }
 
 CustomSlider::CustomSlider(QWidget *parent)
diff --git a/src/view/input_window/input_tab_csv_single_cohort.cpp b/src/view/input_window/input_tab_csv_single_cohort.cpp
index 46b4d95af5f95f3c872106287a2aa5e74ce5d0fa..8af1c4d982717e48e20cd05db0fed25523174bac 100644
--- a/src/view/input_window/input_tab_csv_single_cohort.cpp
+++ b/src/view/input_window/input_tab_csv_single_cohort.cpp
@@ -112,6 +112,11 @@ void InputTabCsvSingleCohort::fileSelectChange() {
     // Update clickability of import button and grade boundary steps
     if (mExamFileView.mFileState == FileSelectState::ok &&
         mStudentFileView.mFileState == FileSelectState::ok) {
+        // Display warning if the selected files don't reference the same
+        // students
+        warnIfLowStudentIntersection(
+            this, mExamFileView.model(), mStudentFileView.model(),
+            mBonusFileView.model(), mBonusFileView.mFileState);
 
         // Enable the import button
         mImportButton.setEnabled(true);
diff --git a/src/view/input_window/input_window.cpp b/src/view/input_window/input_window.cpp
index 3c27d069451105be9ec3ae22fb8bb313485d842b..fde0263deaa1f4400f38ab1dd0c356bc952573c0 100644
--- a/src/view/input_window/input_window.cpp
+++ b/src/view/input_window/input_window.cpp
@@ -6,11 +6,13 @@ inline constexpr int csvTabIdx = 0;
 
 InputWindow::InputWindow(const MainWindow &mainWindow, QWidget *parent)
     : QWidget(parent),
-      mHandreichungLink(
-          "<a "
-          "href=\"https://www.rwth-aachen.de/global/"
-          "show_document.asp?id=aaaaaaaaacxybai\">Document that details how to "
-          "grade multiple choice exams (in German)</a>"),
+      mHandreichungLink("Document that details how to grade multiple choice "
+                        "exams: <a "
+                        "href=\"https://www.rwth-aachen.de/global/"
+                        "show_document.asp?id=aaaaaaaaacxybai\">German</a> / "
+                        "<a "
+                        "href=\"https://www.rwth-aachen.de/global/"
+                        "show_document.asp?id=aaaaaaaaaejylxi\">English</a>"),
       mLayout(this) {
 
     setWindowTitle("Select files...");
diff --git a/src/view/input_window/load_file_view.cpp b/src/view/input_window/load_file_view.cpp
index c48f6331acf3e9a2707e9b05f0882d412d3c0272..f807898f20e0e4c91af7e3508aea26d2c95abc17 100644
--- a/src/view/input_window/load_file_view.cpp
+++ b/src/view/input_window/load_file_view.cpp
@@ -4,6 +4,7 @@
 
 #include <QFileDialog>
 #include <QFrame>
+#include <QMessageBox>
 #include <QStyle>
 
 #define HOME_MACHINE 1
@@ -111,9 +112,25 @@ void LoadFileView::handleBrowseButton() {
         // file type
         mFileNameLabel.setText(fileName);
 
-        if (mInputModel.loadCsv(mFileNameLabel.text().toStdWString())) {
+        const auto error =
+            mInputModel.loadCsv(mFileNameLabel.text().toStdWString());
+        if (error.mEc == FileOpenErrorCode::Success) {
+
+            // Display warnings if there are any
+            if (!error.mWarnings.empty()) {
+                QString errorText =
+                    "Reading the file generated the following warnings:\n";
+                for (const auto &s : error.mWarnings) {
+                    errorText += '\n' + s;
+                }
+                QMessageBox::warning(this, "Reading file warnings", errorText);
+            }
+
             setFileStatus(FileSelectState::ok);
         } else {
+
+            QMessageBox::warning(this, "Reading file failed", error.mDetails);
+
             setFileStatus(FileSelectState::error);
         }
     }
@@ -139,7 +156,8 @@ void LoadFileView::tempSetDeveloperPaths() {
         mFileNameLabel.setText(RWTHONLINE_EXAMPLE_FILE_PATH);
     }
 
-    if (mInputModel.loadCsv(mFileNameLabel.text().toStdWString())) {
+    if (mInputModel.loadCsv(mFileNameLabel.text().toStdWString()).mEc ==
+        FileOpenErrorCode::Success) {
         setFileStatus(FileSelectState::ok);
     } else {
         setFileStatus(FileSelectState::error);
diff --git a/src/view/input_window/load_file_view.hpp b/src/view/input_window/load_file_view.hpp
index 4f1d58272a032d0499ea579bfe75843873c171d5..2c4df16482a24b9dc4f28705726bbb6ff6d69360 100644
--- a/src/view/input_window/load_file_view.hpp
+++ b/src/view/input_window/load_file_view.hpp
@@ -27,6 +27,8 @@ public:
     }
     [[nodiscard]] auto read() { return mInputModel.read(); }
 
+    [[nodiscard]] const InputModel &model() noexcept { return mInputModel; };
+
     /** \brief State of each file selection. */
     FileSelectState mFileState = FileSelectState::empty;
 
diff --git a/src/view/main_window/grade_histogram.cpp b/src/view/main_window/grade_histogram.cpp
index 74d61d6598a4bd7b7bf785dcecff17ab7bcf75ef..04ed81fbeb155d29f51dae846159f79862cd8225 100644
--- a/src/view/main_window/grade_histogram.cpp
+++ b/src/view/main_window/grade_histogram.cpp
@@ -1,6 +1,7 @@
 #include "grade_histogram.hpp"
 
 #include "data/data_manager.hpp"
+#include "util/defaults.hpp"
 #include "util/enums.hpp"
 #include "util/grade.hpp"
 
@@ -10,11 +11,14 @@
 #include <QBarSet>
 #include <QChart>
 #include <QColor>
+#include <QDebug>
 #include <QEasingCurve>
+#include <QFontMetrics>
 #include <QGraphicsLayout>
 #include <QLegendMarker>
 #include <QStringList>
 #include <QTimer>
+#include <QToolTip>
 #include <QValueAxis>
 
 #include <algorithm>
@@ -58,7 +62,7 @@ GradeHistogram::GradeHistogram(const QList<GradingType> &gradingTypes,
 
         auto font = mBarSets.back()->labelFont();
         font.setBold(true);
-        font.setPointSize(font.pointSize() * 2);
+        // font.setPointSizeF(font.pointSizeF() * 2); // placeholder
         mBarSets.back()->setLabelFont(font);
 
         series->append(mBarSets.back());
@@ -134,14 +138,46 @@ GradeHistogram::GradeHistogram(const QList<GradingType> &gradingTypes,
     // receive update when grades change
     connect(&DataManager::getInstance(), &DataManager::gradesChanged, this,
             &GradeHistogram::gradesChanged);
+
+    // show bar value when bar set is hovered (wip)
+    connect(series, &QBarSeries::hovered, this, &GradeHistogram::showHoverText);
 }
 
-void GradeHistogram::handleResize(const QRectF plotArea) {
+void GradeHistogram::handleResize(const QRectF plotArea,
+                                  const FontSize largeFontSize,
+                                  const FontSize smallFontSize) {
 
     mPlotArea = plotArea;
 
     // adjust bar label font size
-    updateBarLabelSize();
+    updateBarLabelSize(largeFontSize);
+
+    // adjust x/y axis font size
+    {
+        auto xaxes = chart()->axes(Qt::Orientation::Horizontal);
+        for (auto &xax : xaxes) {
+
+            auto font = xax->labelsFont();
+            setFontSize(font, largeFontSize);
+            xax->setLabelsFont(font);
+        }
+    }
+    {
+        auto font = mAxisY->labelsFont();
+        setFontSize(font, largeFontSize);
+        mAxisY->setLabelsFont(font);
+    }
+    {
+        auto font = chart()->legend()->font();
+        setFontSize(font, smallFontSize);
+        chart()->legend()->setFont(font);
+    }
+
+    // Round down
+    mMaxNrYLabels = static_cast<int>(mPlotArea.height() /
+                                     (2 * largeFontSize.getPixelSize()));
+
+    updateYAxis();
 }
 
 // todo: QBarSet, QBarSeries probably don't have to be recreated
@@ -164,19 +200,22 @@ void GradeHistogram::gradesChanged(const EnumStorage<bool> &types) {
     }
 
     updateYAxis();
-    updateBarLabelSize();
 }
 
-void niceYAxis(QValueAxis *axis, const int maxBarHeight) {
+static void niceYAxis(QValueAxis *axis, const int maxBarHeight,
+                      const int maxNrYLabels) {
     /* TODO: use values proportional to total number of grades */
 
     auto newYAxisMax = 10;
-    auto tickDiff = 2;
+    auto tickDiff = maxNrYLabels < 6 ? 5 : 2;
 
     if (maxBarHeight > 10) {
         tickDiff = 5;
         while (true) {
-            if (maxBarHeight <= tickDiff * 10) {
+            if (tickDiff == 0) {
+                qDebug() << "wat";
+            }
+            if (maxBarHeight <= tickDiff * std::max(1, maxNrYLabels - 1)) {
                 newYAxisMax = static_cast<int>(
                     std::ceil(static_cast<double>(maxBarHeight) / tickDiff) *
                     tickDiff);
@@ -202,7 +241,7 @@ void GradeHistogram::updateYAxis() {
                               *std::ranges::max_element(model->mGradeCounts));
     }
 
-    niceYAxis(mAxisY, largestBar);
+    niceYAxis(mAxisY, largestBar, mMaxNrYLabels);
 
     // update y axis scale
     // #if GRADE_HISTOGRAM_DYNAMIC_SCALE
@@ -250,9 +289,76 @@ void GradeHistogram::updateYAxis() {
     // #endif
 }
 
-void GradeHistogram::updateBarLabelSize() {
+FontSize GradeHistogram::getFontSize(const QRectF plotArea) {
+
+    mPlotArea = plotArea;
+
+    const auto cur = DataManager::getCurrent();
+
+    // default value
+    if (!cur) {
+        return {16., FontSizeUnit::Pixel};
+    }
+
+    // Calculate the maximum number of digits the largest label on a bar will
+    // probably have
+    const auto nrStudents = cur->getGradeList(GradingType::Combined).size();
+    int threshold = 2;
+    int numberDigits = 0;
+    while (nrStudents >= threshold) {
+        ++numberDigits;
+        threshold *= 10;
+    }
+
+    // get width of single digit
+    auto font = mBarSets.back()->labelFont();
+    font.setPixelSize(16);
+    const QFontMetrics metrics(font);
+    const auto digitPixelWidth = metrics.horizontalAdvance("0");
+    const auto heightWidthRatio =
+        font.pixelSize() / static_cast<double>(digitPixelWidth);
+
+    // Set the font size so that a number with numberDigits of digits fits
+    // horizontally into a combined bar Note that the combined bar is narrower,
+    // if both closed and open points exist
+    const bool hasClosed =
+        cur->mExam.mPointsMax[GradingType::Closed] > Points{0.};
+    const bool hasOpen = cur->mExam.mPointsMax[GradingType::Open] > Points{0.};
+    const bool hasCombined = hasClosed && hasOpen;
+
+    // Width of the bar as percentage of the space reserved for one bar
+    const auto barWidthPortion = hasCombined ? 0.75 : 0.5;
+    const auto realPlotAreaWidth = mPlotArea.width();
+
+    const auto barSpaceWidth =
+        realPlotAreaWidth / static_cast<double>(Grade::NR_VALS);
+
+    // If we have a combined bar chart, there are multiple bar sets, resulting
+    // in thinner bars
+    const auto barWidth =
+        barSpaceWidth * barWidthPortion / (hasCombined ? 3 : 1);
+
+    const auto pxPerDigit = barWidth / numberDigits;
+
+    return {std::floor(pxPerDigit * heightWidthRatio), FontSizeUnit::Pixel};
+}
+
+void GradeHistogram::showHoverText(const bool status, const int index,
+                                   QBarSet * /*barset*/) {
+    qDebug() << "showHoverText called with status =" << status
+             << "and index =" << index;
+
+    if (status) {
+        QToolTip::showText(QCursor::pos(), "text");
+    } else {
+        QToolTip::hideText();
+    }
+}
+
+void GradeHistogram::updateBarLabelSize(const FontSize largeFontSize) {
     // todo: plot area of combined plot smaller?
 
+#if 0
     double xSmallestBarPoints, ySmallestBarPoints;
 
     {
@@ -308,4 +414,19 @@ void GradeHistogram::updateBarLabelSize() {
     for (auto &bs : mBarSets) {
         bs->setLabelFont(font);
     }
+#elif 0
+    for (auto &bs : mBarSets) {
+        auto f = bs->labelFont();
+        f.setPointSize(defaults::fontSizeBig);
+        bs->setLabelFont(f);
+    }
+#else
+    auto font = mBarSets.back()->labelFont();
+    font.setBold(true);
+    setFontSize(font, largeFontSize);
+    for (auto &bs : mBarSets) {
+        bs->setLabelFont(font);
+    }
+
+#endif
 }
diff --git a/src/view/main_window/grade_histogram.hpp b/src/view/main_window/grade_histogram.hpp
index ba83e1b936c83a0a10f8fd5e8949626cb5348bca..b7f73823831e18314acc99e9c37af43f2fb2b05c 100644
--- a/src/view/main_window/grade_histogram.hpp
+++ b/src/view/main_window/grade_histogram.hpp
@@ -2,6 +2,7 @@
 
 #include "data/nodes/dynexite_exam.hpp"
 #include "model/grade_model.hpp"
+#include "util/font.hpp"
 #include "util/storage_helper.hpp"
 
 #include <QChartView>
@@ -16,15 +17,20 @@ public:
     explicit GradeHistogram(const QList<GradingType> &gradingTypes,
                             QWidget *parent = nullptr);
 
-    void handleResize(QRectF plotArea);
+    void handleResize(QRectF plotArea, FontSize largeFontSize,
+                      FontSize smallFontSize);
+
+    FontSize getFontSize(QRectF plotArea);
 
 public slots:
     void gradesChanged(const EnumStorage<bool> &types);
 
+    void showHoverText(bool status, int index, QBarSet *barset);
+
 private:
     void updateYAxis();
 
-    void updateBarLabelSize();
+    void updateBarLabelSize(FontSize largeFontSize);
 
     /* todo: if it's already a QList, might as well make them member objects
      * instead of pointers */
@@ -35,4 +41,6 @@ private:
     QValueAxis *mAxisY;
 
     QRectF mPlotArea;
+
+    int mMaxNrYLabels = 6;
 };
diff --git a/src/view/main_window/grade_tab.cpp b/src/view/main_window/grade_tab.cpp
index 0659e45a1f5e07d286fde307b2f50773c0b73d70..a207a56b7b9f757bb69bf4a50d63c2e62648b16d 100644
--- a/src/view/main_window/grade_tab.cpp
+++ b/src/view/main_window/grade_tab.cpp
@@ -27,9 +27,13 @@ GradeTab::GradeTab(const ViewSettingsWindow &viewSettings, QWidget *parent)
     createGradeHistograms();
     createPointHistograms(viewSettings);
 
-    // 30% - 70% split
-    mLayout->setColumnStretch(0, 3);
-    mLayout->setColumnStretch(1, 7);
+    // 30% - 70% split (specified in defaults::gradeTabHistogramWidthPercentage)
+    static_assert(defaults::gradeTabHistogramWidthPercentage >= 0);
+    static_assert(defaults::gradeTabHistogramWidthPercentage <= 100);
+
+    mLayout->setColumnStretch(0,
+                              100 - defaults::gradeTabHistogramWidthPercentage);
+    mLayout->setColumnStretch(1, defaults::gradeTabHistogramWidthPercentage);
     mLayout->setRowStretch(0, 1);
     mLayout->setRowStretch(1, 1);
 
@@ -37,14 +41,16 @@ GradeTab::GradeTab(const ViewSettingsWindow &viewSettings, QWidget *parent)
             &GradeTab::pointsChanged);
 }
 
-void GradeTab::handleResize() const {
+void GradeTab::handleResize(const FontSize largeFontSize,
+                            const FontSize smallFontSize) const {
     const auto plotAreaGradeHistos =
         mGradeHistos[static_cast<std::size_t>(mGradeHistoTabs->currentIndex())]
             ->chart()
             ->plotArea();
 
     for (const auto &gradeHisto : mGradeHistos) {
-        gradeHisto->handleResize(plotAreaGradeHistos);
+        gradeHisto->handleResize(plotAreaGradeHistos, largeFontSize,
+                                 smallFontSize);
     }
 
     const auto plotAreaPointHistos =
@@ -52,8 +58,36 @@ void GradeTab::handleResize() const {
             ->chart()
             ->plotArea();
     for (const auto &pointHisto : mPointHistos) {
-        pointHisto->handleResize(plotAreaPointHistos);
+        pointHisto->handleResize(plotAreaPointHistos, largeFontSize,
+                                 smallFontSize);
     }
+
+    mScrews->changeFontSize(largeFontSize);
+}
+
+FontSize GradeTab::getFontPxSize(const QSize windowSize) const {
+
+#if 0
+    const auto plotAreaGradeHistos =
+        mGradeHistos[static_cast<std::size_t>(mGradeHistoTabs->currentIndex())]
+            ->chart()
+            ->plotArea();
+
+    // Guess plot area size
+    // We can't just read out the plot area size, because it is not updated if a
+    // different tab is loaded and it is updated wrong if the user restores the
+    // window size after a maximize
+
+    return mGradeHistos[0]->getFontSize(plotAreaGradeHistos);
+#else
+    qDebug() << "";
+    qDebug() << "Half of windowSize: " << windowSize * 0.5;
+    // qDebug() << "cellRect: " << mLayout->cellRect(0, 1);
+    // qDebug() << "mGradeHistoTabs: " << mGradeHistoTabs->size();
+
+    return mGradeHistos[0]->getFontSize(
+        QRectF(QPointF(0., 0.), QSizeF(windowSize * 0.5) /* todo */));
+#endif
 }
 
 void GradeTab::pointsChanged() {
diff --git a/src/view/main_window/grade_tab.hpp b/src/view/main_window/grade_tab.hpp
index 3a3b132298cef804140dddc05bc93f94cb37c238..45bb7173d3668d4efcda9e89882b0cf1e4c4f97b 100644
--- a/src/view/main_window/grade_tab.hpp
+++ b/src/view/main_window/grade_tab.hpp
@@ -19,7 +19,8 @@ public:
     explicit GradeTab(const ViewSettingsWindow &viewSettings,
                       QWidget *parent = nullptr);
 
-    void handleResize() const;
+    void handleResize(FontSize largeFontSize, FontSize smallFontSize) const;
+    [[nodiscard]] FontSize getFontPxSize(QSize windowSize) const;
 
 public slots:
     void pointsChanged();
diff --git a/src/view/main_window/main_window.cpp b/src/view/main_window/main_window.cpp
index 8e1a0b96e1310e5a19098e33ba7ebe400b2769d7..0d28fbc9df04f9ea315a6f680e0b917a1f3b3b91 100644
--- a/src/view/main_window/main_window.cpp
+++ b/src/view/main_window/main_window.cpp
@@ -9,19 +9,27 @@
 
 #include "data/data_manager.hpp"
 #include "file_io/yml_parser.hpp"
+#include "util/font.hpp"
 #include "util/grade.hpp"
 #include "util/grade_boundary.hpp"
 #include "util/heuristics.hpp"
 
 #include <QAction>
+#include <QApplication>
 #include <QClipboard>
+#include <QDesktopServices>
 #include <QFileDialog>
 #include <QGuiApplication>
 #include <QMenuBar>
+#include <QTimer>
 
 #include <algorithm>
 #include <cmath>
 
+#include <QGraphicsProxyWidget>
+#include <QGraphicsScene>
+#include <QGraphicsWidget>
+
 // Surround the given string evenly with spaces, until it has a size of `size`
 QString padToSize(QString str, const int size) {
     const auto spacesLeft = static_cast<int>(
@@ -162,6 +170,8 @@ MainWindow::MainWindow(QWidget *parent)
     mInputWindow.show();
 
     createMenuBar();
+
+    connect(&mTabs, &QTabWidget::currentChanged, this, &MainWindow::tabChanged);
 }
 
 void MainWindow::resetToInputFiles(const DynexiteExam &exam,
@@ -180,6 +190,9 @@ void MainWindow::resetToInputFiles(const DynexiteExam &exam,
         showMaximized();
 
         updateMenuItems();
+
+        // Change font size to adapt to new bar sizes
+        handleResize();
     }
 }
 
@@ -191,6 +204,12 @@ void MainWindow::menuOpenViewSettings() { mViewSettings.show(); }
 
 void MainWindow::menuOpenStatistics() { mStatisticsWindow.show(); }
 
+void MainWindow::tabChanged(int) {
+    // handle resize here again, because sizes of non-displayed tabs are not
+    // calculated correctly
+    handleResize();
+}
+
 void MainWindow::closeEvent(QCloseEvent *event) {
     for (const auto &window : mSubWindowPtrs) {
         window->hide();
@@ -202,7 +221,95 @@ void MainWindow::closeEvent(QCloseEvent *event) {
 void MainWindow::resizeEvent(QResizeEvent *event) {
     QMainWindow::resizeEvent(event);
 
-    mGradeTab->handleResize();
+    handleResize();
+
+    qDebug() << "Is maximized? " << windowState().testFlag(Qt::WindowMaximized);
+
+    // auto oldSize = this->size();
+    // QTimer::singleShot(1000, this,
+    //                    [this, oldSize]() { this->resize(oldSize); });
+
+    // static bool wasMaximized = windowState().testFlag(Qt::WindowMaximized);
+    // const bool isMaximized = windowState().testFlag(Qt::WindowMaximized);
+
+    // if (wasMaximized && !isMaximized) {
+    //     // update flag before call to resize
+    //     wasMaximized = isMaximized;
+
+    //    auto oldSize = this->size();
+    //    oldSize.setWidth(oldSize.width() - 1);
+    //    this->resize(oldSize);
+    //}
+    // wasMaximized = isMaximized;
+
+    // If the flag "WindowMaximized" is set here, that means the window WAS
+    // maximized before the current resize, so it isn't anymore
+    // if (windowState().testFlag(Qt::WindowMaximized)) {
+
+    //    // todo: explain this madness
+    //    auto mySize = this->size();
+    //    mySize.setWidth(mySize.width() - 1);
+    //    QTimer::singleShot(0, this, [this, mySize]() { this->resize(mySize);
+    //    });
+    //}
+
+    // static bool doubleExecution = true;
+    // if (doubleExecution) {
+    //     auto oldSize = this->size();
+    //     if (oldSize.width() < 2300) {
+
+    //        doubleExecution = false;
+    //        oldSize.setWidth(oldSize.width() - 1);
+    //        QTimer::singleShot(0, this,
+    //                           [this, oldSize]() { this->resize(oldSize); });
+    //    }
+
+    //} else {
+    //    doubleExecution = true;
+    //}
+}
+
+void MainWindow::handleResize() {
+
+    static FontSize defaultFontSize;
+
+    static bool firstExecution = true;
+    if (firstExecution) {
+
+        // Get default font
+        QLabel l;
+
+        // Default font size might be given in points or in pixels
+        if (l.font().pixelSize() > 0) {
+            defaultFontSize = {static_cast<qreal>(l.font().pixelSize()),
+                               FontSizeUnit::Pixel};
+        } else {
+            defaultFontSize = {l.font().pointSizeF(), FontSizeUnit::Point};
+        }
+
+        firstExecution = false;
+    }
+
+    // Find out new font size (not smaller than default font)
+    const auto possibleLargeFontSize = mGradeTab->getFontPxSize(this->size());
+
+    // Large font size may not be smaller than the default font size
+    const auto largeFontSize = std::max(possibleLargeFontSize, defaultFontSize);
+
+    // Small font size is a fixed ratio smaller than the large font size, but
+    // also not smaller than defaultFontSize
+    const auto smallFontSize =
+        std::max(largeFontSize * defaults::smallFontSizeRatio, defaultFontSize);
+
+    // Set font size of all labels, text boxes etc
+    auto defaultFont = QApplication::font();
+    setFontSize(defaultFont, smallFontSize);
+    QApplication::setFont(defaultFont);
+
+    // Set font size for legends and adapt axis spacing
+    mGradeTab->handleResize(largeFontSize, smallFontSize);
+
+    mTaskTab->handleResize(this->size(), largeFontSize);
 }
 
 void MainWindow::updateMenuItems() const {
@@ -265,7 +372,7 @@ void MainWindow::createMenuBar() {
     }
 
     /**************** Export *************/
-    mExportMenu = menuBar()->addMenu("&Export");
+    mExportMenu = menuBar()->addMenu("E&xport");
 
     // Export grading scheme to clipboard
     {
@@ -305,6 +412,46 @@ void MainWindow::createMenuBar() {
         connect(exportD2RAct, &QAction::triggered, [this] { exportD2R(this); });
         mExportMenu->addAction(exportD2RAct);
     }
+
+    /**************** Help *************/
+    mHelpMenu = menuBar()->addMenu("&Help");
+
+    // Open Handreichung / Guidelines
+    {
+        mOpenGuidelinesMenu = mHelpMenu->addMenu("Open grading guidelines");
+
+        // German
+        {
+            const auto mOpenGuidelinesGerman = new QAction("German", this);
+            connect(mOpenGuidelinesGerman, &QAction::triggered, [] {
+                QDesktopServices::openUrl(
+                    QUrl("https://www.rwth-aachen.de/global/"
+                         "show_document.asp?id=aaaaaaaaacxybai"));
+            });
+            mOpenGuidelinesMenu->addAction(mOpenGuidelinesGerman);
+        }
+
+        // English
+        {
+            const auto mOpenGuidelinesEnglish = new QAction("English", this);
+            connect(mOpenGuidelinesEnglish, &QAction::triggered, [] {
+                QDesktopServices::openUrl(
+                    QUrl("https://www.rwth-aachen.de/global/"
+                         "show_document.asp?id=aaaaaaaaaejylxi"));
+            });
+            mOpenGuidelinesMenu->addAction(mOpenGuidelinesEnglish);
+        }
+    }
+
+    // "Version": show version (directly in the menu item, not in a popup
+    // window)
+    {
+        const auto versionAct =
+            new QAction(QString("Version: ") + D2R_VERSION, this);
+        connect(versionAct, &QAction::triggered, this, [] {});
+        versionAct->setDisabled(true);
+        mHelpMenu->addAction(versionAct);
+    }
 }
 
 /* void MainWindow::handleCommandLineArgs() const {
diff --git a/src/view/main_window/main_window.hpp b/src/view/main_window/main_window.hpp
index 43877fe9f5d247e7b5a055f5d2c7f3ef5409ad1e..4c2e55713c5210e9bba9d92741d4d640cba4f9db 100644
--- a/src/view/main_window/main_window.hpp
+++ b/src/view/main_window/main_window.hpp
@@ -56,6 +56,7 @@ private slots:
     void menuOpenBoundaries();
     void menuOpenViewSettings();
     void menuOpenStatistics();
+    void tabChanged(int index);
 
 private:
     /**
@@ -69,6 +70,8 @@ private:
 
     void createMenuBar();
 
+    void handleResize();
+
 private:
     /**
      * Automatically input .d2r file if given
@@ -81,7 +84,7 @@ private:
     InputWindow mInputWindow;
 
     /**
-     * \brief The window where the user selects input files.
+     * \brief The window where the user selects options.
      */
     BoundaryWindow mBoundaryWindow;
 
@@ -117,6 +120,8 @@ private:
     QMenu *mExportMenu;
     QMenu *mExportGradingMenu;
     QMenu *mExportStudentsMenu;
+    QMenu *mHelpMenu;
+    QMenu *mOpenGuidelinesMenu;
     QAction *mExportUnknownStudentsAct;
 
     FRIEND_TEST(GuiDagTest, IntegrationTest);
diff --git a/src/view/main_window/min_max_point_screws.cpp b/src/view/main_window/min_max_point_screws.cpp
index 55ad673a9cf71322c6ad54458c9676a01246fe22..918d1bcc05b65d4f0cadf113a880a2809b830f1a 100644
--- a/src/view/main_window/min_max_point_screws.cpp
+++ b/src/view/main_window/min_max_point_screws.cpp
@@ -2,7 +2,13 @@
 
 #include "data/data_manager.hpp"
 
+#include "util/storage_helper.hpp"
+#include <QFileDialog>
+#include <QFrame>
+#include <QMessageBox>
 #include <QSignalBlocker>
+#include <QStyle>
+#include <QWidget>
 
 #include <algorithm>
 #include <cmath>
@@ -24,6 +30,8 @@ MinMaxPointScrewColumn::MinMaxPointScrewColumn(const GradingType grading,
     createLabel(grading, 1, "Minimal Points" + labelExt, 2, mParent);
     createSlider(grading, 1, 3, mParent);
 
+    createQuestionIcon(grading, 3, mParent);
+
     // receive update when points change
     connect(&DataManager::getInstance(), &DataManager::gradesChanged, this,
             &MinMaxPointScrewColumn::gradesChanged);
@@ -95,6 +103,20 @@ void MinMaxPointScrewColumn::createSlider(const GradingType grading,
     addLayout(mHLayouts[row]);
 }
 
+void MinMaxPointScrewColumn::createQuestionIcon(const GradingType grading,
+                                                const int row,
+                                                QWidget *parent) {
+    if (grading == GradingType::Closed) {
+        mQuestionIcon = new QLabel(parent);
+        mQuestionIcon->setPixmap(
+            mQuestionIcon->style()
+                ->standardIcon(QStyle::SP_MessageBoxQuestion)
+                .pixmap(16));
+
+        mHLayouts[row]->addWidget(mQuestionIcon);
+    }
+}
+
 void MinMaxPointScrewColumn::setSpinboxRange(
     const int row, const GradeBoundary min, const GradeBoundary max,
     const GradeBoundary start, const GradeBoundaryStepSize minPointStep) const {
@@ -176,6 +198,28 @@ void MinMaxPointScrewColumn::gradesChanged() {
                    totalPoints);
     setSliderRange(3, 1, GradeBoundary{0}, boundary4_0, currentBoundary4_0,
                    currentTotalPoints);
+
+    // Question icon hover text
+    if (mGrading == GradingType::Closed) {
+        const QString questionIconToolTip =
+            "Normally, the passing mark (Bestehensgrenze) of an exam should\n"
+            "not be set above 50% of the obtainable points. However, since it\n"
+            "can be assumed that a candidate who has the required knowledge\n"
+            "can guess the remaining questions and hence gain additional\n"
+            "points. Therefore the passing mark in the closed part should not\n"
+            "exceed 60% of the achievable points.";
+
+        if (mSpinBoxes[3]->maximum() > 59) {
+            mQuestionIcon->setToolTip(questionIconToolTip);
+        } else {
+            mQuestionIcon->setToolTip(
+                questionIconToolTip +
+                "\nHere, as a result of the legal situation (see Guidelines "
+                "on\n"
+                "Multiple-Choice Exams), the passing mark should not exceed " +
+                QString::number(mSpinBoxes[3]->maximum()) + "%.");
+        }
+    }
 }
 
 void MinMaxPointScrewColumn::sliderChange(const GradingType grading,
@@ -290,7 +334,10 @@ inline constexpr int nrPassedRow = 2;
 inline constexpr int textColumn = 0;
 inline constexpr int avgNumberColumn = 1;
 
-MinMaxPointScrews::MinMaxPointScrews(QWidget *parent) : QWidget(parent) {
+MinMaxPointScrews::MinMaxPointScrews(QWidget *parent)
+    : QWidget(parent), mAvgLabel("Average grade (Attended):"),
+      mAvgPassedLabel("Average grade (Passed):"),
+      mNrPassedLabel("Attended students passed:") {
 
     // Even space above and below the screw columns, avg at the very bottom
     mVLayout.addItem(
@@ -318,30 +365,31 @@ void MinMaxPointScrews::setEnabled(const GradingType type,
 }
 
 void MinMaxPointScrews::gradesChanged() {
+
     const auto cur = DataManager::getCurrent();
     if (!cur) {
-        mAverageGrade.setText("# -");
-        mAverageGradePassed.setText("# -");
-        mNrPassed.setText("# - of -");
+        mAverageGrade.setText("-");
+        mAverageGradePassed.setText("-");
+        mNrPassed.setText("- of -");
         return;
     }
 
     // Number of decimal places for average grade is always 2, independent of
     // the grade boundary step size
     mAverageGrade.setText(
-        "# " + QLocale().toString(cur->mGradeAverage.mAverageGrade, 'f', 2));
+        "" + QLocale().toString(cur->mGradeAverage.mAverageGrade, 'f', 2));
     mAverageGradePassed.setText(
-        "# " +
+        "" +
         QLocale().toString(cur->mGradeAverage.mAverageGradePassed, 'f', 2));
     {
         const auto &nrPassed = cur->mGradeAverage.mNrPassed;
         const auto &nrAttended = cur->mGrades.mGrades.size();
         if (nrAttended == 0) {
-            mNrPassed.setText("# - of -");
+            mNrPassed.setText("- of -");
         } else {
 
             mNrPassed.setText(
-                "# " + QLocale().toString(nrPassed) + " of " +
+                "" + QLocale().toString(nrPassed) + " of " +
                 QLocale().toString(nrAttended) + " (" +
                 QLocale().toString(static_cast<double>(nrPassed) /
                                        static_cast<double>(nrAttended) * 100.,
@@ -370,33 +418,27 @@ void MinMaxPointScrews::createScrewColumns() {
     mHLayout.addStretch(1);
 }
 
+void MinMaxPointScrews::changeFontSize(const FontSize newFontSize) {
+
+    auto font = mAverageGrade.font();
+    setFontSize(font, newFontSize);
+
+    for (const auto &label : {&mAverageGrade, &mAverageGradePassed, &mNrPassed,
+                              &mAvgLabel, &mAvgPassedLabel, &mNrPassedLabel}) {
+        label->setFont(font);
+    }
+}
+
 void MinMaxPointScrews::createAvgLabels() {
     mVLayout.addLayout(&mAvgLayout);
 
     // average text labels
-    {
-        const auto avgLabel = new QLabel("# Average grade (Attended):");
-        avgLabel->setTextFormat(Qt::MarkdownText);
-        mAvgLayout.addWidget(avgLabel, avgRow, textColumn);
-    }
-    {
-        const auto avgPassedLabel = new QLabel("# Average grade (Passed):");
-        avgPassedLabel->setTextFormat(Qt::MarkdownText);
-        mAvgLayout.addWidget(avgPassedLabel, avgPassedRow, textColumn);
-    }
-    {
-        const auto nrPassedLabel = new QLabel("# Attended students passed:");
-        nrPassedLabel->setTextFormat(Qt::MarkdownText);
-        mAvgLayout.addWidget(nrPassedLabel, nrPassedRow, textColumn);
-    }
+    mAvgLayout.addWidget(&mAvgLabel, avgRow, textColumn);
+    mAvgLayout.addWidget(&mAvgPassedLabel, avgPassedRow, textColumn);
+    mAvgLayout.addWidget(&mNrPassedLabel, nrPassedRow, textColumn);
 
     // average value labels
-    mAverageGrade.setTextFormat(Qt::MarkdownText);
     mAvgLayout.addWidget(&mAverageGrade, avgRow, avgNumberColumn);
-
-    mAverageGradePassed.setTextFormat(Qt::MarkdownText);
     mAvgLayout.addWidget(&mAverageGradePassed, avgPassedRow, avgNumberColumn);
-
-    mNrPassed.setTextFormat(Qt::MarkdownText);
     mAvgLayout.addWidget(&mNrPassed, nrPassedRow, avgNumberColumn);
 }
diff --git a/src/view/main_window/min_max_point_screws.hpp b/src/view/main_window/min_max_point_screws.hpp
index ca99e5a150ca7c7e83d07d4684ac03c9886ff46f..72b77d87b0d393ced85e19de8109a650158424f9 100644
--- a/src/view/main_window/min_max_point_screws.hpp
+++ b/src/view/main_window/min_max_point_screws.hpp
@@ -3,10 +3,10 @@
 #include "data/nodes/dynexite_exam.hpp"
 
 #include "util/enums.hpp"
+#include "util/font.hpp"
 #include "util/grade_boundary.hpp"
 #include "util/grade_boundary_step.hpp"
 #include "util/storage_helper.hpp"
-
 // #include "view/gui/custom_double_spin_box.hpp"
 #include "view/gui/custom_slider.hpp"
 
@@ -39,6 +39,8 @@ private:
     QDoubleSpinBox *mSpinBoxes[4]{};
     CustomSlider *mSliders[2]{};
 
+    QLabel *mQuestionIcon;
+
     void setSpinboxRange(int row, GradeBoundary min, GradeBoundary max,
                          GradeBoundary start,
                          GradeBoundaryStepSize minPointStep) const;
@@ -51,6 +53,8 @@ private:
     void createSlider(GradingType grading, int sliderIdx, int row,
                       QWidget *parent);
 
+    void createQuestionIcon(GradingType grading, int row, QWidget *parent);
+
 private slots:
     void gradesChanged();
     void sliderChange(GradingType grading, int sliderIdx, int value);
@@ -65,10 +69,13 @@ public:
 
     void setEnabled(GradingType type, bool enabled) const;
 
+    void changeFontSize(FontSize newFontSize);
+
 private slots:
     void gradesChanged();
 
-private:
+    // private:
+public:
     QVBoxLayout mVLayout{this};
     QHBoxLayout mHLayout{};
     QGridLayout mAvgLayout{};
@@ -77,6 +84,7 @@ private:
     MinMaxPointScrewColumn *mOpenColumn = nullptr;
 
     QLabel mAverageGrade{}, mAverageGradePassed{}, mNrPassed{};
+    QLabel mAvgLabel, mAvgPassedLabel, mNrPassedLabel;
 
     void createScrewColumns();
     void createAvgLabels();
diff --git a/src/view/main_window/point_histogram.cpp b/src/view/main_window/point_histogram.cpp
index 77e16ee9d5ab484359579217f70f420523449b6d..b572b0b926f65e7398dad3eb4b4a306551efecf2 100644
--- a/src/view/main_window/point_histogram.cpp
+++ b/src/view/main_window/point_histogram.cpp
@@ -62,11 +62,27 @@ PointHistogram::PointHistogram(const GradingType gradingType,
     mAxisX->setTickType(QValueAxis::TicksDynamic);
     mAxisX->setTickAnchor(0);
 
+    //// font
+    //{
+    //    auto font = mAxisX->labelsFont();
+    //    font.setPointSize(defaults::fontSizeBig);
+    //    mAxisX->setLabelsFont(font);
+    //}
+
     // y-axis
     mAxisY->setLabelFormat("%d");
     mAxisY->setTickType(QValueAxis::TicksDynamic);
     mAxisY->setTickAnchor(0);
 
+    //// font
+    //{
+    //    auto font = mAxisY->labelsFont();
+    //    font.setPointSize(defaults::fontSizeBig);
+    //    mAxisY->setLabelsFont(font);
+    //}
+
+    setMinimumHeight(380);
+
     // Stacked bar series
     // Bar width of one = no space between adjacent bars
     mSeries->setBarWidth(1.);
@@ -115,11 +131,43 @@ PointHistogram::PointHistogram(const GradingType gradingType,
             this, &PointHistogram::pointBucketSizeChanged);
 }
 
-void PointHistogram::handleResize(const QRectF plotArea) {
+void PointHistogram::handleResize(const QRectF plotArea,
+                                  const FontSize largeFontSize,
+                                  const FontSize smallFontSize) {
     mPlotArea = plotArea;
 
+#if OLDYLABELS
     rescaleXLabels();
     rescaleYLabels();
+#endif
+
+    // axis font size
+    {
+        auto font = mAxisX->labelsFont();
+        setFontSize(font, largeFontSize);
+        mAxisX->setLabelsFont(font);
+    }
+    {
+        auto font = mAxisY->labelsFont();
+        setFontSize(font, largeFontSize);
+        mAxisY->setLabelsFont(font);
+    }
+    {
+        auto font = chart()->legend()->font();
+        setFontSize(font, smallFontSize);
+        chart()->legend()->setFont(font);
+    }
+
+    // Round down
+    mMaxNrYLabels = static_cast<int>(mPlotArea.height() /
+                                     (2 * largeFontSize.getPixelSize()));
+
+    // todo
+#if OLDYLABELS
+#else
+    rescaleXLabels();
+    rescaleYLabels();
+#endif
 }
 
 void PointHistogram::pointsChanged() {
@@ -307,17 +355,30 @@ void PointHistogram::rescaleXLabels() const {
     const auto labelStep =
         std::ceil(static_cast<double>(minLabelStep) / 5.) * 5.;
 
-    mAxisX->setTickInterval(labelStep);
+    mAxisX->setTickInterval(labelStep * 2);
 }
 
 void PointHistogram::rescaleYLabels() const {
 
+#if OLDYLABELS
     const auto maxNrLabels = std::max(2, static_cast<int>(mPlotArea.height()) /
                                              (mAxisLabelCharHeight * 2));
 
+    qDebug() << "maxNrLabels:" << maxNrLabels;
+#else
+    qDebug() << "maxNrLabels:" << mMaxNrYLabels;
+#endif
+
     // We need an extra label for 0
-    const auto minLabelStep = static_cast<int>(
-        std::ceil(mAxisY->max() / static_cast<qreal>(maxNrLabels - 1)));
+    const auto minLabelStep =
+        static_cast<int>(std::ceil(mAxisY->max() / static_cast<qreal>(
+#if OLDYLABELS
+                                                       maxNrLabels
+
+#else
+                                                       mMaxNrYLabels
+#endif
+                                                       - 1)));
 
-    mAxisY->setTickInterval(minLabelStep);
+    mAxisY->setTickInterval(std::max(minLabelStep, 1));
 }
diff --git a/src/view/main_window/point_histogram.hpp b/src/view/main_window/point_histogram.hpp
index d30d1c7713ae6d62c2744f8a49201c27a6715f05..ce01a7888cdc0859fd0ff77ea30ad90791830756 100644
--- a/src/view/main_window/point_histogram.hpp
+++ b/src/view/main_window/point_histogram.hpp
@@ -5,6 +5,7 @@
 #pragma once
 
 #include "util/enums.hpp"
+#include "util/font.hpp"
 #include "util/storage_helper.hpp"
 #include "util/testing.hpp"
 
@@ -17,6 +18,8 @@ class QValueAxis;
 class QBarSet;
 class QStackedBarSeries;
 
+#define OLDYLABELS 0
+
 namespace impl {
 /**
  * \brief Get the bar set index for the given point value.
@@ -41,7 +44,8 @@ public:
                             const ViewSettingsWindow &viewSettings,
                             QWidget *parent = nullptr);
 
-    void handleResize(QRectF plotArea);
+    void handleResize(QRectF plotArea, FontSize largeFontSize,
+                      FontSize smallFontSize);
 
 public slots:
     /**
@@ -96,6 +100,7 @@ private:
      * spacing between axis labels.
      */
     int mAxisLabelCharWidth, mAxisLabelCharHeight;
+    int mMaxNrYLabels = 6;
 
     /**
      * \brief Correct plot area (the one returned by chart()->plotArea might be
diff --git a/src/view/main_window/task_tab.cpp b/src/view/main_window/task_tab.cpp
index b63d95f69040769967a5dd06d5caa12f7e0bda55..7db13465970e9bde98b7dc849f6682b61f1295d8 100644
--- a/src/view/main_window/task_tab.cpp
+++ b/src/view/main_window/task_tab.cpp
@@ -11,6 +11,7 @@
 #include <QCategoryAxis>
 #include <QChart>
 #include <QChartView>
+#include <QFontMetrics>
 #include <QLabel>
 #include <QValueAxis>
 
@@ -31,6 +32,316 @@ TaskTab::TaskTab(QWidget *parent) : QScrollArea(parent) {
             &TaskTab::pointsChanged);
 }
 
+void TaskTab::handleResize(const QSize windowSize,
+                           const FontSize largeFontSize) {
+    changeFontSize(largeFontSize);
+    correctAxisLabels(windowSize, largeFontSize);
+}
+
+void TaskTab::changeFontSize(const FontSize largeFontSize) {
+
+    for (auto &pchart : mPointCharts) {
+        // adjust x/y axis font size
+        {
+            auto xaxes = pchart->axes(Qt::Orientation::Horizontal);
+            for (auto &xax : xaxes) {
+
+                auto font = xax->labelsFont();
+                setFontSize(font, largeFontSize);
+                xax->setLabelsFont(font);
+            }
+        }
+        {
+            auto yaxes = pchart->axes(Qt::Orientation::Vertical);
+            for (auto &yax : yaxes) {
+
+                auto font = yax->labelsFont();
+                setFontSize(font, largeFontSize);
+                yax->setLabelsFont(font);
+            }
+        }
+    }
+
+    // resize the invisible axis of the box chart, which exists for alignment
+    // of the two charts
+    for (auto &bchart : mBoxCharts) {
+        {
+            auto axes = bchart->axes(Qt::Orientation::Horizontal);
+            for (auto &ax : axes) {
+
+                auto font = ax->labelsFont();
+                setFontSize(font, largeFontSize);
+                ax->setLabelsFont(font);
+            }
+        }
+    }
+}
+
+// todo: combine with function from gradeHistogram
+static void niceYAxis(QValueAxis *axis, const int maxBarHeight,
+                      const int maxNrYLabels) {
+    /* TODO: use values proportional to total number of grades */
+
+    // TODO: update pointChartYMax
+
+    if (maxNrYLabels < 2) {
+        axis->setTickInterval(maxBarHeight);
+        axis->setMax(maxBarHeight);
+    } else
+
+        if (maxBarHeight < maxNrYLabels) {
+
+        axis->setTickInterval(1);
+        axis->setMax(maxBarHeight);
+
+    } else {
+        auto newYAxisMax = 10;
+        auto tickDiff = maxNrYLabels < 6 ? 5 : 2;
+
+        qDebug() << "niceYAxis tasktab: maxBarHeight:" << maxBarHeight
+                 << ", maxNrYLabels:" << maxNrYLabels;
+
+        if (maxBarHeight > 10) {
+            tickDiff = 5;
+            while (true) {
+                if (tickDiff == 0) {
+                    qDebug() << "wat";
+                }
+                if (maxBarHeight <= tickDiff * std::max(1, maxNrYLabels - 1)) {
+                    newYAxisMax = static_cast<int>(
+                        std::ceil(static_cast<double>(maxBarHeight) /
+                                  tickDiff) *
+                        tickDiff);
+                    break;
+                } else {
+                    tickDiff *= 2;
+                }
+            }
+        }
+
+        if (axis->tickInterval() != tickDiff)
+            axis->setTickInterval(tickDiff);
+        if (axis->max() != newYAxisMax)
+            axis->setMax(newYAxisMax);
+    }
+}
+
+static void niceXAxis(QValueAxis *axis, const int maxNrLabels,
+                      const double stepSize) {
+
+    const auto largestValue = axis->max();
+
+    if (maxNrLabels < 2) {
+        axis->setTickInterval(largestValue);
+        return;
+    }
+
+    double interval = stepSize;
+
+    while (largestValue / interval + 1 > maxNrLabels) {
+        interval *= 2;
+    }
+
+    axis->setTickInterval(interval);
+}
+
+void TaskTab::correctAxisLabels(const QSize windowSize,
+                                const FontSize largeFontSize) {
+
+    qDebug() << "Cell rect 0,0:" << mLayout->cellRect(0, 0);
+
+    const auto cur = DataManager::getCurrent();
+    if (!cur) {
+        return;
+    }
+
+    int yAxisHeightPx = 200;
+
+    for (int i = 0; i < mPointCharts.size(); ++i) {
+        // y axis
+#if 0
+        const auto maxNrYLabels =
+            static_cast<int>(pchart->plotArea().height() / (2 * largeFontSize));
+        qDebug() << "Plot area height: " << pchart->plotArea().height();
+#else
+        const auto maxNrYLabels = static_cast<int>(
+            yAxisHeightPx / (2 * largeFontSize.getPixelSize()));
+#endif
+
+        // find largest bar
+        int largestBar = 0;
+        auto seriesList = mPointCharts[i]->series();
+        if (seriesList.size() != 1) {
+            qDebug() << "Wrong number of series in a task point chart";
+            assert(false);
+            continue;
+        }
+
+        auto series = static_cast<QAbstractBarSeries *>(seriesList[0]);
+
+        auto barSetList = series->barSets();
+        if (barSetList.size() != 1) {
+            qDebug() << "Wrong number of bar sets in a task point bar set";
+            assert(false);
+            continue;
+        }
+
+        auto barSet = barSetList[0];
+
+        for (int barSetIdx = 0; barSetIdx < barSet->count(); ++barSetIdx) {
+            if ((*barSet)[barSetIdx] > largestBar) {
+                largestBar = static_cast<int>((*barSet)[barSetIdx]);
+            }
+        }
+
+        {
+            auto yaxes = mPointCharts[i]->axes(Qt::Orientation::Vertical);
+            for (auto &yax : yaxes) {
+
+                niceYAxis(static_cast<QValueAxis *>(yax), largestBar,
+                          maxNrYLabels);
+                if (mBoxCharts.size() > i) {
+
+                    auto boxChartAxisList = mBoxCharts[i]->axes(Qt::Horizontal);
+                    if (boxChartAxisList.size() != 1) {
+                        qDebug() << "Wrong size of axis list of box chart:"
+                                 << boxChartAxisList.size();
+                        assert(false);
+                    } else {
+                        auto boxChartAxis =
+                            static_cast<QCategoryAxis *>(boxChartAxisList[0]);
+                        auto labels = boxChartAxis->categoriesLabels();
+                        for (auto &l : labels) {
+                            boxChartAxis->remove(l);
+                        }
+                        boxChartAxis->append(
+                            QString::number(static_cast<int>(
+                                static_cast<QValueAxis *>(yax)->max())),
+                            0);
+                    }
+                    // axisY->append(QString::number(pointChartYMax), 0);
+                } else {
+                    qDebug()
+                        << "No matching box chart found at index" << i << "!";
+                    assert(false);
+                }
+            }
+        }
+
+        // x axis
+        {
+            auto xaxes = mPointCharts[i]->axes(Qt::Orientation::Horizontal);
+            for (auto &xax : xaxes) {
+
+                // get number of characters of maximum label
+                auto xValueAx = static_cast<QValueAxis *>(xax);
+                const auto maxLabel = QString::number(
+                    xValueAx->max(), 'f',
+                    nrDecimalPlaces(
+                        cur->mUserPointConfig.mGradeBoundaryStepSize));
+                qDebug() << "Max label is" << maxLabel;
+
+                auto labelFont = xValueAx->labelsFont();
+                QFontMetrics fm(labelFont);
+                const auto maxLabelWidth = fm.horizontalAdvance(maxLabel);
+
+                // get width of x-ax
+                const auto xAxWidth = static_cast<int>(
+                    (windowSize.width() - mLayout->cellRect(0, 0).width()) *
+                    0.8);
+
+                const auto maxNrLabels =
+                    static_cast<int>(xAxWidth / maxLabelWidth / 1.2);
+                qDebug() << "Max nr labels:" << maxNrLabels;
+
+                niceXAxis(
+                    xValueAx, maxNrLabels,
+                    as<double>(cur->mUserPointConfig.mGradeBoundaryStepSize));
+            }
+        }
+    }
+
+    for (auto &pchart : mPointCharts) {
+
+        // y axis
+#if 0
+        const auto maxNrYLabels =
+            static_cast<int>(pchart->plotArea().height() / (2 * largeFontSize));
+        qDebug() << "Plot area height: " << pchart->plotArea().height();
+#else
+        const auto maxNrYLabels = static_cast<int>(
+            yAxisHeightPx / (2 * largeFontSize.getPixelSize()));
+#endif
+
+        // find largest bar
+        int largestBar = 0;
+        auto seriesList = pchart->series();
+        if (seriesList.size() != 1) {
+            qDebug() << "Wrong number of series in a task point chart";
+            assert(false);
+            continue;
+        }
+
+        auto series = static_cast<QAbstractBarSeries *>(seriesList[0]);
+
+        auto barSetList = series->barSets();
+        if (barSetList.size() != 1) {
+            qDebug() << "Wrong number of bar sets in a task point bar set";
+            assert(false);
+            continue;
+        }
+
+        auto barSet = barSetList[0];
+
+        for (int i = 0; i < barSet->count(); ++i) {
+            if ((*barSet)[i] > largestBar) {
+                largestBar = static_cast<int>((*barSet)[i]);
+            }
+        }
+
+        {
+            auto yaxes = pchart->axes(Qt::Orientation::Vertical);
+            for (auto &yax : yaxes) {
+
+                niceYAxis(static_cast<QValueAxis *>(yax), largestBar,
+                          maxNrYLabels);
+            }
+        }
+
+        // x axis
+        {
+            auto xaxes = pchart->axes(Qt::Orientation::Horizontal);
+            for (auto &xax : xaxes) {
+
+                // get number of characters of maximum label
+                auto xValueAx = static_cast<QValueAxis *>(xax);
+                const auto maxLabel = QString::number(
+                    xValueAx->max(), 'f',
+                    nrDecimalPlaces(
+                        cur->mUserPointConfig.mGradeBoundaryStepSize));
+                qDebug() << "Max label is" << maxLabel;
+
+                auto labelFont = xValueAx->labelsFont();
+                QFontMetrics fm(labelFont);
+                const auto maxLabelWidth = fm.horizontalAdvance(maxLabel);
+
+                // get width of x-ax
+                const auto xAxWidth =
+                    (windowSize.width() - mLayout->cellRect(0, 0).width()) *
+                    0.8;
+
+                const auto maxNrLabels =
+                    static_cast<int>(xAxWidth / maxLabelWidth / 1.2);
+                qDebug() << "Max nr labels:" << maxNrLabels;
+
+                niceXAxis(
+                    xValueAx, maxNrLabels,
+                    as<double>(cur->mUserPointConfig.mGradeBoundaryStepSize));
+            }
+        }
+    }
+}
+
 void TaskTab::pointsChanged() {
 
     // Delete previous charts
@@ -39,6 +350,8 @@ void TaskTab::pointsChanged() {
         // The following deletes all child widgets, i.e. the charts and
         // chartViews
         delete takeWidget();
+        mPointCharts.clear();
+        mBoxCharts.clear();
 
         const auto contents = new QWidget(this);
         mLayout = new QGridLayout(contents);
@@ -98,6 +411,7 @@ void TaskTab::pointsChanged() {
             // Point bucket plot
             {
                 auto pointChart = new QChart;
+                mPointCharts.push_back(pointChart);
 
                 // For each possible point value, count how many students
                 // got that many points
@@ -118,10 +432,9 @@ void TaskTab::pointsChanged() {
 
                 taskSeries->append(set);
 
-                pointChart->addSeries(taskSeries);
-
                 // x-axis
                 {
+#if 0
                     const auto axisX = new QBarCategoryAxis();
 
                     for (int j = 0; j < nrBuckets; ++j) {
@@ -133,8 +446,29 @@ void TaskTab::pointsChanged() {
                                  << (j * static_cast<double>(stepSize));
                         axisX->append(QString::fromStdString(category.str()));
                     }
+#else
+                    const auto axisX = new QValueAxis();
+                    axisX->setTickType(QValueAxis::TicksDynamic);
+                    axisX->setTickAnchor(0);
+                    axisX->setTickInterval(1);
+                    axisX->setMin(0 - static_cast<double>(stepSize) / 2.);
+                    axisX->setMax(nrBuckets * static_cast<double>(stepSize) -
+                                  static_cast<double>(stepSize) / 2.);
+                    axisX->setLabelFormat(
+                        QString("%.") +
+                        QString::number(nrDecimalPlaces(
+                            cur->mUserPointConfig.mGradeBoundaryStepSize)) +
+                        "f");
+#endif
+
+                    //// font
+                    //{
+                    //    auto font = axisX->labelsFont();
+                    //    font.setPointSize(defaults::fontSizeBig);
+                    //    axisX->setLabelsFont(font);
+                    //}
+
                     pointChart->addAxis(axisX, Qt::AlignBottom);
-                    taskSeries->attachAxis(axisX);
                 }
 
                 // y-axis
@@ -188,7 +522,15 @@ void TaskTab::pointsChanged() {
                         axisY->setMax(pointChartYMax);
                     }
 
+                    //// font
+                    //{
+                    //    auto font = axisY->labelsFont();
+                    //    font.setPointSize(defaults::fontSizeBig - 6);
+                    //    axisY->setLabelsFont(font);
+                    //}
+
                     pointChart->addAxis(axisY, Qt::AlignLeft);
+                    pointChart->addSeries(taskSeries);
                     taskSeries->attachAxis(axisY);
                 }
 
@@ -200,19 +542,20 @@ void TaskTab::pointsChanged() {
                 chartView->setInteractive(false);
 
                 mLayout->addWidget(chartView, i * 3, 1);
+
+                qDebug() << "PointChart Margins:" << pointChart->margins();
             }
 
             // BoxPlot
             {
                 auto boxChart = new QChart;
+                mBoxCharts.push_back(boxChart);
 
                 auto *taskSeries = new QBoxPlotSeries;
                 auto *set = new QBoxSet;
 
                 // Make sure the different plot elements don't overlap each
                 // other
-                // const qreal minDistance = static_cast<qreal>(nrBuckets) *
-                // 0.005;
                 const qreal minDistance = stepSize * 0.1;
 
                 const qreal median = sortedPoints[nrStudents / 2].p;
@@ -250,6 +593,13 @@ void TaskTab::pointsChanged() {
                     boxChart->addAxis(axisY, Qt::AlignBottom);
 
                     taskSeries->attachAxis(axisY);
+
+                    //// font
+                    //{
+                    //    auto font = axisY->labelsFont();
+                    //    font.setPointSize(defaults::fontSizeBig - 6);
+                    //    axisY->setLabelsFont(font);
+                    //}
                 }
 
                 // add x axis to specify min and max
@@ -286,7 +636,7 @@ void TaskTab::pointsChanged() {
                 // Furthermore, reduce the left and right because of
                 // our rotation, above and below) margin to have less space
                 // between box plot and bar chart.
-                boxChart->setMargins(QMargins(5, 14, 0, 20));
+                boxChart->setMargins(QMargins(5, 30, 0, 20));
             }
         }
 
diff --git a/src/view/main_window/task_tab.hpp b/src/view/main_window/task_tab.hpp
index 8920e4bf46db89529c9cd2554ce89ad634402ee6..cc7f55c4f67b31611d76361c949f7a6c5d600219 100644
--- a/src/view/main_window/task_tab.hpp
+++ b/src/view/main_window/task_tab.hpp
@@ -1,6 +1,10 @@
 #pragma once
 
+#include "util/font.hpp"
+
+#include <QChart>
 #include <QGridLayout>
+#include <QList>
 #include <QScrollArea>
 
 class TaskTab : public QScrollArea {
@@ -9,6 +13,10 @@ class TaskTab : public QScrollArea {
 public:
     explicit TaskTab(QWidget *parent = nullptr);
 
+    // Change the font size of the axis labels and potentially change the number
+    // of axis labels
+    void handleResize(QSize windowSize, FontSize largeFontSize);
+
 public slots:
     /**
      * \brief Redraw all charts.
@@ -16,5 +24,11 @@ public slots:
     void pointsChanged();
 
 private:
+    void changeFontSize(FontSize largeFontSize);
+    void correctAxisLabels(QSize windowSize, FontSize largeFontSize);
+
     QGridLayout *mLayout = nullptr;
+
+    QList<QChart *> mPointCharts;
+    QList<QChart *> mBoxCharts;
 };
diff --git a/src/view/statistic_window/statistic_window.cpp b/src/view/statistic_window/statistic_window.cpp
index c8a22e0fc1a0631f305ab658e069b18f92849b79..ac2005d3c484fb18464ee01cd7d4db0b0d597fcd 100644
--- a/src/view/statistic_window/statistic_window.cpp
+++ b/src/view/statistic_window/statistic_window.cpp
@@ -61,7 +61,8 @@ void StatisticWindow::pointsChanged() {
 
     const auto nrAttended = attendedStudents.size();
     const auto nrRegistered = registeredStudents.size();
-    const auto nrRegisteredAttended = static_cast<qsizetype>(intersection.size());
+    const auto nrRegisteredAttended =
+        static_cast<qsizetype>(intersection.size());
 
     mNrAttended.setText(QLocale().toString(nrAttended));
     mNrAttendedRegistered.setText(QLocale().toString(nrRegisteredAttended));