From df618aa4643c21a6e26950530c6f969ff6466bbd Mon Sep 17 00:00:00 2001 From: Adam Washington Date: Wed, 5 Jun 2024 16:45:27 +0100 Subject: [PATCH] fix: Smarter tick placement (#21) * Move more tick handling into the axes * Smarter tic spacing * Ensure plots are updated when axes are updated * Much better linear tick points * Better log axes * Fix c++ formatting * Fix QML formatting --- src/AxisModel.qml | 3 +-- src/LineModel.qml | 15 +++++++++++++ src/ScatterModel.qml | 15 +++++++++++++ src/axis.cpp | 42 +++++++++++++++++++++++++++++------ src/axis.h | 13 +++++++---- src/axisTickLabels.cpp | 25 ++++++++++----------- src/axisTickLabels.h | 2 +- src/logAxis.cpp | 50 ++++++++++++++++++++++++++++++++++++++---- src/logAxis.h | 5 ++++- src/main.qml | 26 ++++++++++++++-------- 10 files changed, 154 insertions(+), 42 deletions(-) diff --git a/src/AxisModel.qml b/src/AxisModel.qml index 419cc00..96a18d9 100644 --- a/src/AxisModel.qml +++ b/src/AxisModel.qml @@ -28,9 +28,8 @@ Node { model: axis.tickLabels delegate: Node { - position: axis.direction ? Qt.vector3d(-root.scl.x + 2 * tickX * root.scl.x / (rep.count - 1), -root.scl.y - 210, 0) : Qt.vector3d(-root.scl.x - 30, -root.scl.y - 180 + 2 * tickY * root.scl.y / (rep.count - 1), 0) + position: axis.direction ? Qt.vector3d(-root.scl.x + tickX * root.scl.x, -root.scl.y - 210, 0) : Qt.vector3d(-root.scl.x - 30, -root.scl.y - 180 + tickY * root.scl.y, 0) - /* position: axis.direction ? Qt.vector3d(-root.scl.x + 2 * index * root.scl.x / (rep.count - 1), -root.scl.y - 210, 0) : Qt.vector3d(-root.scl.x - 30.0, -root.scl.y - 180 + 2 * index * root.scl.y / (rep.count - 1), 0) */ Item { anchors.centerIn: parent height: 400 diff --git a/src/LineModel.qml b/src/LineModel.qml index e586cab..d7d0783 100644 --- a/src/LineModel.qml +++ b/src/LineModel.qml @@ -29,4 +29,19 @@ Model { baseColor: root.color } ] + + Connections { + function onDataChanged() { + plotLine.dataChanged(); + } + + target: xAxis + } + Connections { + function onDataChanged() { + plotLine.dataChanged(); + } + + target: yAxis + } } diff --git a/src/ScatterModel.qml b/src/ScatterModel.qml index 91a2e96..93bd71d 100644 --- a/src/ScatterModel.qml +++ b/src/ScatterModel.qml @@ -29,4 +29,19 @@ Model { baseColor: root.color } ] + + Connections { + function onDataChanged() { + plotLine.dataChanged(); + } + + target: xAxis + } + Connections { + function onDataChanged() { + plotLine.dataChanged(); + } + + target: yAxis + } } diff --git a/src/axis.cpp b/src/axis.cpp index 9a256b5..522224d 100644 --- a/src/axis.cpp +++ b/src/axis.cpp @@ -1,9 +1,10 @@ #include "axis.h" #include +#include #include -Axis::Axis() : direction_(false), minimum_(-1), maximum_(1), thickness_(0.001), tickLabels_(*this) +Axis::Axis() : minimum_(-1), maximum_(1), direction_(false), thickness_(0.001), tickLabels_(*this) { updateData(); connect(this, &Axis::dataChanged, this, &Axis::updateData); @@ -15,14 +16,42 @@ bool Axis::direction() const { return direction_; } double Axis::minimum() const { return minimum_; } double Axis::maximum() const { return maximum_; } -int Axis::tickCount() const { return tickLabels_.tickCount(); } +int Axis::tickCount() const { return tics_.size(); } -void Axis::setTickCount(const int count) { tickLabels_.setTickCount(count); } +void Axis::updateTicks_() +{ + + auto full_diff = maximum_ - minimum_; + auto diff = pow(10.0, floor(log(full_diff) / log(10.0))); + while (full_diff / diff < 4) + { + diff /= 2; + } + while (full_diff / diff > 9) + { + diff *= 2; + } + tics_.clear(); + + auto current = floor(minimum_ / diff) * diff; + + while (current < minimum_) + current += diff; + + while (current <= maximum_) + { + tics_.emplace_back(current); + current += diff; + } +} void Axis::updateData() { clear(); + updateTicks_(); + tickLabels_.reset(); + int stride = 3 * sizeof(float); QByteArray vertexData(6 * stride, Qt::Initialization::Uninitialized); @@ -92,7 +121,6 @@ std::vector Axis::convert(QList points) const return result; } -double Axis::tick(int index, int count) const -{ - return minimum_ + (double)index / ((double)count - 1.0) * (maximum_ - minimum_); -} +double Axis::tick(int index) const { return tics_[index]; } + +double Axis::tickCoord(int index) const { return 2.0 * (tics_[index] - minimum_) / (maximum_ - minimum_); } diff --git a/src/axis.h b/src/axis.h index 78b9048..27c3b9c 100644 --- a/src/axis.h +++ b/src/axis.h @@ -13,7 +13,7 @@ class Axis : public QQuick3DGeometry Q_PROPERTY(double maximum MEMBER maximum_ NOTIFY dataChanged) Q_PROPERTY(bool direction MEMBER direction_ NOTIFY dataChanged) Q_PROPERTY(AxisTickLabels *tickLabels READ tickLabels NOTIFY dataChanged) - Q_PROPERTY(int tickCount READ tickCount WRITE setTickCount) + Q_PROPERTY(int tickCount READ tickCount NOTIFY dataChanged) public: Axis(); @@ -23,15 +23,20 @@ class Axis : public QQuick3DGeometry double minimum() const; double maximum() const; int tickCount() const; - void setTickCount(const int count); - virtual double tick(int index, int count) const; + double tick(int index) const; + virtual double tickCoord(int index) const; Q_SIGNALS: void dataChanged(); + protected: + std::vector tics_; + double minimum_, maximum_; + private: + virtual void updateTicks_(); void updateData(); bool direction_; - double minimum_, maximum_, thickness_; + double thickness_; AxisTickLabels tickLabels_; }; diff --git a/src/axisTickLabels.cpp b/src/axisTickLabels.cpp index aab6f25..cc863d3 100644 --- a/src/axisTickLabels.cpp +++ b/src/axisTickLabels.cpp @@ -2,29 +2,20 @@ #include "axis.h" #include -AxisTickLabels::AxisTickLabels(Axis &parent) : parent_(parent), N_(5) {} +AxisTickLabels::AxisTickLabels(Axis &parent) : parent_(parent) {} -int AxisTickLabels::tickCount() const { return N_; } - -void AxisTickLabels::setTickCount(const int count) -{ - beginResetModel(); - N_ = count; - endResetModel(); -} - -int AxisTickLabels::rowCount([[maybe_unused]] const QModelIndex &parent) const { return N_; } +int AxisTickLabels::rowCount([[maybe_unused]] const QModelIndex &parent) const { return parent_.tickCount(); } QVariant AxisTickLabels::data(const QModelIndex &index, int role) const { switch (role) { case Qt::UserRole: - return QString("%1").arg(parent_.tick(index.row(), N_)); + return QString("%1").arg(parent_.tick(index.row())); case (Qt::UserRole + 1): - return parent_.direction() ? index.row() : 0; + return parent_.direction() ? parent_.tickCoord(index.row()) : 0; case (Qt::UserRole + 2): - return parent_.direction() ? 0 : index.row(); + return parent_.direction() ? 0 : parent_.tickCoord(index.row()); default: return index.row(); } @@ -38,3 +29,9 @@ QHash AxisTickLabels::roleNames() const roles[Qt::UserRole + 2] = "tickY"; return roles; } + +void AxisTickLabels::reset() +{ + beginResetModel(); + endResetModel(); +} diff --git a/src/axisTickLabels.h b/src/axisTickLabels.h index de7975f..0ce3098 100644 --- a/src/axisTickLabels.h +++ b/src/axisTickLabels.h @@ -16,8 +16,8 @@ class AxisTickLabels : public QAbstractListModel QHash roleNames() const override; int tickCount() const; void setTickCount(const int count); + void reset(); private: Axis &parent_; - int N_; }; diff --git a/src/logAxis.cpp b/src/logAxis.cpp index 9b2e400..520eaf2 100644 --- a/src/logAxis.cpp +++ b/src/logAxis.cpp @@ -3,6 +3,7 @@ #include #include +#include LogAxis::LogAxis() : Axis() {} @@ -19,10 +20,51 @@ std::vector LogAxis::convert(QList points) const return result; } -double LogAxis::tick(int index, int count) const +void LogAxis::updateTicks_() { - auto min = log(minimum()); - auto max = log(maximum()); + std::vector subTicks; + tics_.clear(); + + int orders = ceil(log(maximum_ / minimum_) / log(10.0)); + + switch (orders) + { + case 0: + case 1: + subTicks = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0}; + break; + case 2: + subTicks = {1.0, 2.0, 3.0, 5.0, 8.0}; + break; + case 3: + case 4: + subTicks = {1.0, 2.0, 5.0}; + break; + case 5: + subTicks = {1.0, 3.0}; + break; + default: + subTicks = {1.0}; + } + + double current = pow(10.0, floor(log(minimum_) / log(10.0))); + + while (current <= maximum_) + { + for (auto sub : subTicks) + { + if (sub * current >= minimum_ && sub * current <= maximum_) + tics_.emplace_back(sub * current); + } + current *= 10.0; + } +} + +double LogAxis::tickCoord(int index) const +{ + double actual = log10(tics_[index]); + double min = log10(minimum_); + double max = log10(maximum_); - return exp(min + (double)index / ((double)count - 1.0) * (max - min)); + return 2.0 * (actual - min) / (max - min); } diff --git a/src/logAxis.h b/src/logAxis.h index 444d986..5cd9735 100644 --- a/src/logAxis.h +++ b/src/logAxis.h @@ -13,7 +13,10 @@ class LogAxis : public Axis public: LogAxis(); std::vector convert(QList values) const override; - double tick(int index, int count) const override; + double tickCoord(int index) const override; + + private: + void updateTicks_() override; Q_SIGNALS: void dataChanged(); diff --git a/src/main.qml b/src/main.qml index fc6c295..305ab4e 100644 --- a/src/main.qml +++ b/src/main.qml @@ -50,10 +50,9 @@ ApplicationWindow { id: xAxis direction: true - maximum: 1.0 - minimum: -1.0 + maximum: yMax.value + minimum: yMin.value thickness: 0.01 - tickCount: ticCount.value } } AxisModel { @@ -67,7 +66,6 @@ ApplicationWindow { maximum: 1.0 minimum: 0.01 thickness: 0.01 - tickCount: ticCount.value } } } @@ -147,14 +145,24 @@ ApplicationWindow { onMoved: renderButton.onClicked() } Label { - text: "Tic Count" + text: "Y Min" } SpinBox { - id: ticCount + id: yMin - from: 2 - to: 10 - value: 5 + from: -30 + to: yMax.value + value: -1 + } + Label { + text: "Y Max" + } + SpinBox { + id: yMax + + from: 0 + to: 30 + value: 1 } ColorDialog { id: colorDialog