profile: generalize tick generation

The number of ticks was generated for each axis with custom
code. This code was not aware of the size of the profile and
could result in overly dense or sparse ticks.

Generalize the generation of the ticks. For now, round tick
values to integers. In the future, try to use more "nice"
looking values as we do for the statistics tab.

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
This commit is contained in:
Berthold Stoeger 2021-09-26 19:10:39 +02:00 committed by Dirk Hohndel
parent 177df72d33
commit 0de40a85b7
4 changed files with 46 additions and 77 deletions

View file

@ -14,8 +14,8 @@ static const double labelSpaceVertical = 2.0; // space between label and ticks
void DiveCartesianAxis::setBounds(double minimum, double maximum) void DiveCartesianAxis::setBounds(double minimum, double maximum)
{ {
changed = !IS_FP_SAME(max, maximum) || !IS_FP_SAME(min, minimum); changed = !IS_FP_SAME(max, maximum) || !IS_FP_SAME(min, minimum);
min = minimum; dataMin = min = minimum;
max = maximum; dataMax = max = maximum;
} }
DiveCartesianAxis::DiveCartesianAxis(Position position, int integralDigits, int fractionalDigits, color_index_t gridColor, double dpr, DiveCartesianAxis::DiveCartesianAxis(Position position, int integralDigits, int fractionalDigits, color_index_t gridColor, double dpr,
@ -28,7 +28,6 @@ DiveCartesianAxis::DiveCartesianAxis(Position position, int integralDigits, int
orientation(LeftToRight), orientation(LeftToRight),
min(0), min(0),
max(0), max(0),
interval(1),
textVisibility(true), textVisibility(true),
lineVisibility(true), lineVisibility(true),
labelScale(labelScale), labelScale(labelScale),
@ -136,35 +135,59 @@ void DiveCartesianAxis::updateTicks(int animSpeed)
{ {
if (!changed && !printMode) if (!changed && !printMode)
return; return;
if (dataMax - dataMin < 1e-5)
return;
// Guess the number of tick marks.
QLineF m = line(); QLineF m = line();
double stepsInRange = (max - min) / interval; double spaceNeeded = position == Position::Bottom ? labelWidth * 3.0 / 2.0
int steps = (int)stepsInRange; : labelHeight * 2.0;
double size = position == Position::Bottom ? fabs(m.x2() - m.x1())
: fabs(m.y2() - m.y1());
int numTicks = lrint(size / spaceNeeded);
numTicks = std::clamp(numTicks, 2, 50);
double interval = (dataMax - dataMin) / numTicks;
// Round the interval to a sensible size in display units
double intervalDisplay = interval * transform.a;
intervalDisplay = ceil(intervalDisplay); // Currently, round to full integers, might want to improve.
// Choose full multiples of the interval as minumum and maximum values
double minDisplay = transform.to(dataMin);
double maxDisplay = transform.to(dataMax);
double firstDisplay = floor(minDisplay / intervalDisplay * (1.0 + 1e-5)) * intervalDisplay;
double lastDisplay = ceil(maxDisplay / intervalDisplay * (1.0 - 1e-5)) * intervalDisplay;
numTicks = lrint((lastDisplay - firstDisplay) / intervalDisplay) + 1;
numTicks = std::max(numTicks, 0);
min = transform.from(firstDisplay);
max = transform.from(lastDisplay);
double currValueText = min; double currValueText = min;
double currValueLine = min; double currValueLine = min;
if (steps < 1) emptyList(labels, numTicks, animSpeed);
emptyList(lines, numTicks, animSpeed);
if (numTicks == 0)
return; return;
emptyList(labels, steps, animSpeed); interval = numTicks > 1 ? (max - min) / (numTicks - 1) : 0;
emptyList(lines, steps, animSpeed); double stepSize = numTicks > 1 ? size / (numTicks - 1) : 0;
// Move the remaining grid lines / labels to their correct positions // Move the remaining grid lines / labels to their correct positions
// regarding the possible new values for the axis // regarding the possible new values for the axis
qreal begin, stepSize; double begin;
if (orientation == TopToBottom) { if (orientation == TopToBottom) {
begin = m.y1(); begin = m.y1();
stepSize = (m.y2() - m.y1());
} else if (orientation == BottomToTop) { } else if (orientation == BottomToTop) {
begin = m.y2(); begin = m.y2();
stepSize = (m.y2() - m.y1());
} else if (orientation == LeftToRight) { } else if (orientation == LeftToRight) {
begin = m.x1(); begin = m.x1();
stepSize = (m.x2() - m.x1());
} else /* if (orientation == RightToLeft) */ { } else /* if (orientation == RightToLeft) */ {
begin = m.x2(); begin = m.x2();
stepSize = (m.x2() - m.x1());
} }
stepSize /= stepsInRange;
for (int i = 0, count = labels.size(); i < count; i++, currValueText += interval) { for (int i = 0, count = labels.size(); i < count; i++, currValueText += interval) {
double childPos = (orientation == TopToBottom || orientation == LeftToRight) ? double childPos = (orientation == TopToBottom || orientation == LeftToRight) ?
@ -205,7 +228,7 @@ void DiveCartesianAxis::updateTicks(int animSpeed)
} }
// Add the rest of the needed labels. // Add the rest of the needed labels.
for (int i = labels.size(); i < steps; i++, currValueText += interval) { for (int i = labels.size(); i < numTicks; i++, currValueText += interval) {
double childPos; double childPos;
if (orientation == TopToBottom || orientation == LeftToRight) { if (orientation == TopToBottom || orientation == LeftToRight) {
childPos = begin + i * stepSize; childPos = begin + i * stepSize;
@ -237,7 +260,7 @@ void DiveCartesianAxis::updateTicks(int animSpeed)
} }
// Add the rest of the needed grid lines. // Add the rest of the needed grid lines.
for (int i = lines.size(); i < steps; i++, currValueText += interval) { for (int i = lines.size(); i < numTicks; i++, currValueText += interval) {
double childPos; double childPos;
if (orientation == TopToBottom || orientation == LeftToRight) { if (orientation == TopToBottom || orientation == LeftToRight) {
childPos = begin + i * stepSize; childPos = begin + i * stepSize;
@ -306,11 +329,6 @@ QString DiveCartesianAxis::textForValue(double value) const
return QStringLiteral("%L1").arg(transform.to(value), 0, 'f', fractionalDigits); return QStringLiteral("%L1").arg(transform.to(value), 0, 'f', fractionalDigits);
} }
void DiveCartesianAxis::setTickInterval(double i)
{
interval = i;
}
qreal DiveCartesianAxis::valueAt(const QPointF &p) const qreal DiveCartesianAxis::valueAt(const QPointF &p) const
{ {
double fraction; double fraction;
@ -390,17 +408,6 @@ QString TimeAxis::textForValue(double value) const
return QString::number(nr); return QString::number(nr);
} }
// TODO: replace by real dynamic axis - this is just weird.
void TimeAxis::updateTicks(int animSpeed)
{
DiveCartesianAxis::updateTicks(animSpeed);
if (maximum() > 600) {
for (int i = 0; i < labels.count(); i++) {
labels[i]->setVisible(i % 2);
}
}
}
PartialGasPressureAxis::PartialGasPressureAxis(const DivePlotDataModel &model, Position position, int integralDigits, int fractionalDigits, PartialGasPressureAxis::PartialGasPressureAxis(const DivePlotDataModel &model, Position position, int integralDigits, int fractionalDigits,
color_index_t gridColor, double dpr, double labelScale, bool printMode, bool isGrayscale, color_index_t gridColor, double dpr, double labelScale, bool printMode, bool isGrayscale,
ProfileScene &scene) : ProfileScene &scene) :
@ -429,6 +436,5 @@ void PartialGasPressureAxis::update(int animSpeed)
return; return;
setBounds(0.0, pp); setBounds(0.0, pp);
setTickInterval(pp > 4 ? 0.5 : 0.25);
updateTicks(animSpeed); updateTicks(animSpeed);
} }

View file

@ -38,7 +38,6 @@ public:
~DiveCartesianAxis(); ~DiveCartesianAxis();
void setBounds(double min, double max); void setBounds(double min, double max);
void setTransform(double a, double b = 0.0); void setTransform(double a, double b = 0.0);
void setTickInterval(double interval);
void setOrientation(Orientation orientation); void setOrientation(Orientation orientation);
double minimum() const; double minimum() const;
double maximum() const; double maximum() const;
@ -49,7 +48,7 @@ public:
void setTextVisible(bool arg1); void setTextVisible(bool arg1);
void setLinesVisible(bool arg1); void setLinesVisible(bool arg1);
void setLine(const QLineF &line); void setLine(const QLineF &line);
virtual void updateTicks(int animSpeed); void updateTicks(int animSpeed);
double width() const; // only for vertical axes double width() const; // only for vertical axes
double height() const; // only for horizontal axes double height() const; // only for horizontal axes
@ -68,9 +67,8 @@ protected:
Orientation orientation; Orientation orientation;
QList<DiveTextItem *> labels; QList<DiveTextItem *> labels;
QList<DiveLineItem *> lines; QList<DiveLineItem *> lines;
double min; double dataMin, dataMax;
double max; double min, max;
double interval;
bool textVisibility; bool textVisibility;
bool lineVisibility; bool lineVisibility;
double labelScale; double labelScale;
@ -101,7 +99,6 @@ class TimeAxis : public DiveCartesianAxis {
Q_OBJECT Q_OBJECT
public: public:
using DiveCartesianAxis::DiveCartesianAxis; using DiveCartesianAxis::DiveCartesianAxis;
void updateTicks(int animSpeed) override;
private: private:
QString textForValue(double value) const override; QString textForValue(double value) const override;
QColor colorForValue(double value) const override; QColor colorForValue(double value) const override;

View file

@ -84,7 +84,7 @@ QFont DiveTextItem::getFont(double dpr, double scale)
double DiveTextItem::outlineSpace(double dpr) double DiveTextItem::outlineSpace(double dpr)
{ {
return 2.0 * outlineSize * dpr; // Double because outline growths to both sides. return outlineSize * dpr;
} }
double DiveTextItem::fontHeight(double dpr, double scale) double DiveTextItem::fontHeight(double dpr, double scale)

View file

@ -82,24 +82,18 @@ ProfileScene::ProfileScene(double dpr, bool printMode, bool isGrayscale) :
// Initialize axes. Perhaps part of this should be moved down to the axes code? // Initialize axes. Perhaps part of this should be moved down to the axes code?
profileYAxis->setOrientation(DiveCartesianAxis::TopToBottom); profileYAxis->setOrientation(DiveCartesianAxis::TopToBottom);
profileYAxis->setTickInterval(M_OR_FT(10, 30));
gasYAxis->setOrientation(DiveCartesianAxis::BottomToTop); gasYAxis->setOrientation(DiveCartesianAxis::BottomToTop);
gasYAxis->setTickInterval(1);
#ifndef SUBSURFACE_MOBILE #ifndef SUBSURFACE_MOBILE
heartBeatAxis->setOrientation(DiveCartesianAxis::BottomToTop); heartBeatAxis->setOrientation(DiveCartesianAxis::BottomToTop);
heartBeatAxis->setTickInterval(10);
percentageAxis->setOrientation(DiveCartesianAxis::BottomToTop); percentageAxis->setOrientation(DiveCartesianAxis::BottomToTop);
percentageAxis->setTickInterval(10);
#endif #endif
temperatureAxis->setOrientation(DiveCartesianAxis::BottomToTop); temperatureAxis->setOrientation(DiveCartesianAxis::BottomToTop);
temperatureAxis->setTickInterval(300);
cylinderPressureAxis->setOrientation(DiveCartesianAxis::BottomToTop); cylinderPressureAxis->setOrientation(DiveCartesianAxis::BottomToTop);
cylinderPressureAxis->setTickInterval(30000);
heartBeatAxis->setTextVisible(true); heartBeatAxis->setTextVisible(true);
heartBeatAxis->setLinesVisible(true); heartBeatAxis->setLinesVisible(true);
@ -116,7 +110,7 @@ ProfileScene::ProfileScene(double dpr, bool printMode, bool isGrayscale) :
tankItem->setZValue(100); tankItem->setZValue(100);
// These axes are not locale-dependent. Set their scale factor once here. // These axes are not locale-dependent. Set their scale factor once here.
timeAxis->setTransform(60.0); timeAxis->setTransform(1.0/60.0);
heartBeatAxis->setTransform(1.0); heartBeatAxis->setTransform(1.0);
gasYAxis->setTransform(1.0); // Non-metric countries likewise use bar (disguised as "percentage") for partial pressure. gasYAxis->setTransform(1.0); // Non-metric countries likewise use bar (disguised as "percentage") for partial pressure.
@ -407,20 +401,8 @@ void ProfileScene::plotDive(const struct dive *dIn, int dcIn, DivePlannerPointsM
plotInfo.maxtemp - plotInfo.mintemp > 2000 ? plotInfo.maxtemp : plotInfo.mintemp + 2000); plotInfo.maxtemp - plotInfo.mintemp > 2000 ? plotInfo.maxtemp : plotInfo.mintemp + 2000);
if (hasHeartBeat) { if (hasHeartBeat) {
int heartBeatAxisMin = lrint(plotInfo.minhr / 5.0 - 0.5) * 5; heartBeatAxis->setBounds(plotInfo.minhr, plotInfo.maxhr);
int heartBeatAxisMax, heartBeatAxisTick; heartBeatAxis->updateTicks(animSpeed);
if (plotInfo.maxhr - plotInfo.minhr < 40)
heartBeatAxisTick = 10;
else if (plotInfo.maxhr - plotInfo.minhr < 80)
heartBeatAxisTick = 20;
else if (plotInfo.maxhr - plotInfo.minhr < 100)
heartBeatAxisTick = 25;
else
heartBeatAxisTick = 50;
for (heartBeatAxisMax = heartBeatAxisMin; heartBeatAxisMax < plotInfo.maxhr; heartBeatAxisMax += heartBeatAxisTick);
heartBeatAxis->setBounds(heartBeatAxisMin, heartBeatAxisMax + 1);
heartBeatAxis->setTickInterval(heartBeatAxisTick);
heartBeatAxis->updateTicks(animSpeed); // this shows the ticks
} }
percentageAxis->setBounds(0, 100); percentageAxis->setBounds(0, 100);
@ -430,22 +412,6 @@ void ProfileScene::plotDive(const struct dive *dIn, int dcIn, DivePlannerPointsM
if (calcMax) if (calcMax)
timeAxis->setBounds(0.0, maxtime); timeAxis->setBounds(0.0, maxtime);
int i, incr;
static int increments[8] = { 10, 20, 30, 60, 5 * 60, 10 * 60, 15 * 60, 30 * 60 };
/* Time markers: at most every 10 seconds, but no more than 12 markers.
* We start out with 10 seconds and increment up to 30 minutes,
* depending on the dive time.
* This allows for 6h dives - enough (I hope) for even the craziest
* divers - but just in case, for those 8h depth-record-breaking dives,
* we double the interval if this still doesn't get us to 12 or fewer
* time markers */
i = 0;
while (i < 7 && maxtime / increments[i] > 12)
i++;
incr = increments[i];
while (maxtime / incr > 12)
incr *= 2;
timeAxis->setTickInterval(incr);
timeAxis->updateTicks(animSpeed); timeAxis->updateTicks(animSpeed);
cylinderPressureAxis->setBounds(plotInfo.minpressure, plotInfo.maxpressure); cylinderPressureAxis->setBounds(plotInfo.minpressure, plotInfo.maxpressure);