[apps] Fix graph X axis labels that read the same

Scenario: just zoom in in a graph
Fix:
- Do not truncate labels.
- Label size is computed depending on available size and number of
 labels.
- If labels cannot be displayed properly, just display the minimal
and maximal labels!
This commit is contained in:
Léa Saviot
2018-12-21 17:49:54 +01:00
parent 48bccac8de
commit 8c6ffa6d42
11 changed files with 125 additions and 52 deletions

View File

@@ -18,7 +18,7 @@ public:
protected:
char * label(Axis axis, int index) const override;
private:
char m_labels[k_maxNumberOfXLabels][k_horizontalLabelBufferSize];
char m_labels[k_maxNumberOfXLabels][k_labelBufferMaxSize];
static float EvaluateAtAbscissa(float abscissa, void * model, void * context);
Law * m_law;
Calculation * m_calculation;

View File

@@ -15,8 +15,8 @@ public:
private:
char * label(Axis axis, int index) const override;
Store * m_store;
char m_xLabels[k_maxNumberOfXLabels][k_horizontalLabelBufferSize];
char m_yLabels[k_maxNumberOfYLabels][k_verticalLabelBufferSize];
char m_xLabels[k_maxNumberOfXLabels][k_labelBufferMaxSize];
char m_yLabels[k_maxNumberOfYLabels][k_labelBufferMaxSize];
Responder * m_controller;
};

View File

@@ -1,16 +1,17 @@
#include "curve_view.h"
#include "../constant.h"
#include <poincare/print_float.h>
#include <assert.h>
#include <string.h>
#include <cmath>
#include <float.h>
#include <poincare/print_float.h>
using namespace Poincare;
namespace Shared {
static inline int minInt(int x, int y) { return (x<y ? x : y); }
CurveView::CurveView(CurveViewRange * curveViewRange, CurveViewCursor * curveViewCursor, BannerView * bannerView,
View * cursorView, View * okView, bool displayBanner) :
View(),
@@ -110,6 +111,22 @@ KDCoordinate CurveView::pixelLength(Axis axis) const {
return (axis == Axis::Horizontal ? m_frame.width() : m_frame.height());
}
int CurveView::numberOfLabels(Axis axis) const {
Axis otherAxis = axis == Axis::Horizontal ? Axis::Vertical : Axis::Horizontal;
if (min(otherAxis) > 0.0f || max(otherAxis) < 0.0f) {
return 0;
}
float minA = min(axis);
float maxA = max(axis);
float margin = labelsDisplayMarginRatio(axis) * (maxA - minA);
float minVisibleInAxis = minA + margin;
float maxVisibleInAxis = maxA - margin;
float labelStep = 2.0f * gridUnit(axis);
float minLabel = std::ceil(minVisibleInAxis/labelStep);
float maxLabel = std::floor(maxVisibleInAxis/labelStep);
return maxLabel - minLabel + 1;
}
float CurveView::pixelToFloat(Axis axis, KDCoordinate p) const {
KDCoordinate pixels = axis == Axis::Horizontal ? p : pixelLength(axis)-p;
return min(axis) + pixels*((float)(max(axis)-min(axis)))/((float)pixelLength(axis));
@@ -132,31 +149,55 @@ float CurveView::floatToPixel(Axis axis, float f) const {
void CurveView::computeLabels(Axis axis) {
float step = gridUnit(axis);
int labelsCount = numberOfLabels(axis);
for (int index = 0; index < labelsCount; index++) {
float labelValue = 2.0f*step*(std::ceil(min(axis)/(2.0f*step)))+index*2.0f*step;
int axisLabelsCount = numberOfLabels(axis);
for (int i = 0; i < axisLabelsCount; i++) {
float labelValue = labelValueAtIndex(axis, i);
/* Label cannot hold more than k_labelBufferMaxSize characters to prevent
* them from overprinting one another.*/
int labelMaxSize = k_labelBufferMaxSize;
if (axis == Axis::Horizontal) {
float pixelsPerLabel = 320.0f/axisLabelsCount - 10; // 10 is a margin between the labels
labelMaxSize = minInt(k_labelBufferMaxSize, pixelsPerLabel/k_font->glyphSize().width());
}
if (labelValue < step && labelValue > -step) {
// Make sure the 0 value is really written 0
labelValue = 0.0f;
}
/* Label cannot hold more than k_labelBufferSize characters to prevent them
* from overprinting one another.*/
char * labelBuffer = label(axis, i);
PrintFloat::convertFloatToText<float>(
labelValue,
label(axis, index),
axis == Axis::Vertical ? k_verticalLabelBufferSize : k_horizontalLabelBufferSize,
axis == Axis::Vertical ? 6 : Constant::ShortNumberOfSignificantDigits,
Preferences::PrintFloatMode::Decimal);
labelBuffer,
labelMaxSize,
k_numberSignificantDigits,
Preferences::PrintFloatMode::Decimal,
axis == Axis::Vertical);
if (labelBuffer[0] == 0 && axis == Axis::Horizontal) {
/* Some labels are too big and may overlap their neighbours. We write the
* extrema labels only. */
computeHorizontalExtremaLabels();
return;
}
}
}
void CurveView::drawLabels(KDContext * ctx, KDRect rect, Axis axis, bool shiftOrigin, bool graduationOnly, bool fixCoordinate, KDCoordinate fixedCoordinate) const {
int labelsCount = numberOfLabels(axis);
if (labelsCount < 1) {
return;
}
float step = gridUnit(axis);
float start = 2.0f*step*(std::ceil(min(axis)/(2.0f*step)));
float end = max(axis);
float start = labelValueAtIndex(axis, 0);
float end = labelValueAtIndex(axis, labelsCount - 1);
float verticalCoordinate = fixCoordinate ? fixedCoordinate : std::round(floatToPixel(Axis::Vertical, 0.0f));
float horizontalCoordinate = fixCoordinate ? fixedCoordinate : std::round(floatToPixel(Axis::Horizontal, 0.0f));
int i = 0;
for (float x = start; x < end; x += 2.0f*step) {
for (float x = start; x <= end + step; x += 2.0f*step) {
/* When |start| >> step, start + step = start. In that case, quit the
* infinite loop. */
if (x == x-step || x == x+step) {
@@ -167,7 +208,7 @@ void CurveView::drawLabels(KDContext * ctx, KDRect rect, Axis axis, bool shiftOr
graduation = KDRect(horizontalCoordinate-(k_labelGraduationLength-2)/2, std::round(floatToPixel(Axis::Vertical, x)), k_labelGraduationLength, 1);
}
if (!graduationOnly) {
KDSize textSize = KDFont::SmallFont->stringSize(label(axis, i));
KDSize textSize = k_font->stringSize(label(axis, i));
KDPoint origin(std::round(floatToPixel(Axis::Horizontal, x)) - textSize.width()/2, verticalCoordinate + k_labelMargin);
if (axis == Axis::Vertical) {
origin = KDPoint(horizontalCoordinate + k_labelMargin, std::round(floatToPixel(Axis::Vertical, x)) - textSize.height()/2);
@@ -175,8 +216,8 @@ void CurveView::drawLabels(KDContext * ctx, KDRect rect, Axis axis, bool shiftOr
if (-step < x && x < step && shiftOrigin) {
origin = KDPoint(horizontalCoordinate + k_labelMargin, verticalCoordinate + k_labelMargin);
}
if (rect.intersects(KDRect(origin, KDFont::SmallFont->stringSize(label(axis, i))))) {
ctx->drawString(label(axis, i), origin, KDFont::SmallFont, KDColorBlack);
if (rect.intersects(KDRect(origin, k_font->stringSize(label(axis, i))))) {
ctx->drawString(label(axis, i), origin, k_font, KDColorBlack);
}
}
ctx->fillRect(graduation, KDColorBlack);
@@ -432,14 +473,6 @@ void CurveView::drawHistogram(KDContext * ctx, KDRect rect, EvaluateModelWithPar
}
}
int CurveView::numberOfLabels(Axis axis) const {
Axis otherAxis = axis == Axis::Horizontal ? Axis::Vertical : Axis::Horizontal;
if (min(otherAxis) > 0.0f || max(otherAxis) < 0.0f) {
return 0;
}
return std::ceil((max(axis) - min(axis))/(2*gridUnit(axis)));
}
void CurveView::jointDots(KDContext * ctx, KDRect rect, EvaluateModelWithParameter evaluation, void * model, void * context, float x, float y, float u, float v, KDColor color, int maxNumberOfRecursion) const {
float pyf = floatToPixel(Axis::Vertical, y);
float pvf = floatToPixel(Axis::Vertical, v);
@@ -599,4 +632,35 @@ View * CurveView::subviewAtIndex(int index) {
return m_bannerView;
}
void CurveView::computeHorizontalExtremaLabels() {
Axis axis = Axis::Horizontal;
int axisLabelsCount = numberOfLabels(axis);
// All labels but the extrema are empty
for (int i = 1; i < axisLabelsCount - 1 ; i++) {
label(axis, i)[0] = 0;
}
int minMax[] = {0, axisLabelsCount-1};
for (int i : minMax) {
// Compute the minimal and maximal label
PrintFloat::convertFloatToText<float>(
labelValueAtIndex(axis, i),
label(axis, i),
k_labelBufferMaxSize,
k_numberSignificantDigits,
Preferences::PrintFloatMode::Decimal,
false);
}
}
float CurveView::labelValueAtIndex(Axis axis, int i) const {
assert(i >= 0 && i < numberOfLabels(axis));
float minA = min(axis);
float maxA = max(axis);
float labelStep = 2.0f * gridUnit(axis);
float minVisibleInAxis = minA + labelsDisplayMarginRatio(axis) * (maxA - minA);
return labelStep*(std::ceil(minVisibleInAxis/labelStep)+i);
}
}

View File

@@ -11,6 +11,9 @@ namespace Shared {
class CurveView : public View {
public:
/* We want a 3 characters margin before the first label tick, so that most
* labels appear completely. This gives 3*charWidth/320 = 3*7/320= 0.066 */
static constexpr float k_labelsHorizontalMarginRatio = 0.066f;
typedef float (*EvaluateModelWithParameter)(float t, void * model, void * context);
enum class Axis {
Horizontal = 0,
@@ -41,17 +44,8 @@ protected:
constexpr static KDCoordinate k_okVerticalMargin = 23;
constexpr static KDCoordinate k_okHorizontalMargin = 10;
constexpr static KDCoordinate k_labelGraduationLength = 6;
/* The labels are bounds by ±1E-4 and ±1E-8 which in worst case can be written
* using 6 characters (including the null-terminating char).
* To avoid overlapping horizontal labels, k_horizontalLabelBufferSize should
* verify:
* k_horizontalLabelBufferSize = Ion::Display::Width /
* ((CurveViewRange::k_maxNumberOfXGridUnits/2)*KDFont::SmallFont->glyphWidth)
* = 320/((18/2)*7) ~ 5.
* We take 6, which creates a small overlap in worst cases but prevents from
* truncating labels (ie, "-1E-"). */
constexpr static int k_horizontalLabelBufferSize = 6;
constexpr static int k_verticalLabelBufferSize = 14; // '-' + 6 significant digits + '.' + "E-" + 3 digits + null-terminating char
constexpr static int k_numberSignificantDigits = 6;
constexpr static int k_labelBufferMaxSize = 1 + k_numberSignificantDigits + 3 + 3 + 1; // '-' + 6 significant digits + '.' + "E-" + 3 digits + null-terminating char
constexpr static int k_maxNumberOfXLabels = CurveViewRange::k_maxNumberOfXGridUnits;
constexpr static int k_maxNumberOfYLabels = CurveViewRange::k_maxNumberOfYGridUnits;
constexpr static int k_externRectMargin = 2;
@@ -74,6 +68,7 @@ protected:
View * m_bannerView;
CurveViewCursor * m_curveViewCursor;
private:
static constexpr const KDFont * k_font = KDFont::SmallFont;
/* The window bounds are deduced from the model bounds but also take into
account a margin (computed with k_marginFactor) */
float min(Axis axis) const;
@@ -99,6 +94,9 @@ private:
View * subviewAtIndex(int index) override;
/* m_curveViewRange has to be non null but the cursor model, the banner and
* cursor views may be nullptr if not needed. */
void computeHorizontalExtremaLabels();
float labelValueAtIndex(Axis axis, int i) const;
float labelsDisplayMarginRatio(Axis axis) const { return axis == Axis::Horizontal ? k_labelsHorizontalMarginRatio : 0.0f; }
CurveViewRange * m_curveViewRange;
View * m_cursorView;
View * m_okView;

View File

@@ -1,4 +1,5 @@
#include "curve_view_range.h"
#include "curve_view.h"
#include <cmath>
#include <ion.h>
#include <assert.h>
@@ -18,7 +19,9 @@ float CurveViewRange::yGridUnit() {
return 0.0f;
}
float CurveViewRange::computeGridUnit(Axis axis, float range) {
float CurveViewRange::computeGridUnit(Axis axis, float range1) {
float range = axis == Axis::Y ? range1 : ((1.0f-2.0f*CurveView::k_labelsHorizontalMarginRatio)*range1);
// TODO share the axis enum of curve_view and then call a static method CurveView::marginRatio(Axis)
int a = 0;
int b = 0;
float maxNumberOfUnits = (axis == Axis::X) ? k_maxNumberOfXGridUnits : k_maxNumberOfYGridUnits;

View File

@@ -27,8 +27,8 @@ protected:
bool m_shouldColorHighlighted;
private:
char * label(Axis axis, int index) const override;
char m_xLabels[k_maxNumberOfXLabels][k_horizontalLabelBufferSize];
char m_yLabels[k_maxNumberOfYLabels][k_verticalLabelBufferSize];
char m_xLabels[k_maxNumberOfXLabels][k_labelBufferMaxSize];
char m_yLabels[k_maxNumberOfYLabels][k_labelBufferMaxSize];
Poincare::Context * m_context;
};

View File

@@ -27,8 +27,8 @@ protected:
bool m_shouldColorHighlighted;
private:
char * label(Axis axis, int index) const override;
char m_xLabels[k_maxNumberOfXLabels][k_horizontalLabelBufferSize];
char m_yLabels[k_maxNumberOfYLabels][k_verticalLabelBufferSize];
char m_xLabels[k_maxNumberOfXLabels][k_labelBufferMaxSize];
char m_yLabels[k_maxNumberOfYLabels][k_labelBufferMaxSize];
Poincare::Context * m_context;
};

View File

@@ -20,7 +20,7 @@ public:
private:
constexpr static KDCoordinate k_axisMargin = 3;
char * label(Axis axis, int index) const override;
char m_labels[k_maxNumberOfXLabels][k_horizontalLabelBufferSize];
char m_labels[k_maxNumberOfXLabels][k_labelBufferMaxSize];
BoxRange m_boxRange;
};

View File

@@ -24,7 +24,7 @@ private:
char * label(Axis axis, int index) const override;
HistogramController * m_controller;
Store * m_store;
char m_labels[k_maxNumberOfXLabels][k_horizontalLabelBufferSize];
char m_labels[k_maxNumberOfXLabels][k_labelBufferMaxSize];
static float EvaluateHistogramAtAbscissa(float abscissa, void * model, void * context);
float m_highlightedBarStart;
float m_highlightedBarEnd;

View File

@@ -45,7 +45,7 @@ namespace PrintFloat {
* ConvertFloatToText return the number of characters that have been written
* in buffer (excluding the last \O character) */
template <class T>
int convertFloatToText(T d, char * buffer, int bufferSize, int numberOfSignificantDigits, Preferences::PrintFloatMode mode);
int convertFloatToText(T d, char * buffer, int bufferSize, int numberOfSignificantDigits, Preferences::PrintFloatMode mode, bool allowRounding = true);
template <class T>
static int convertFloatToTextPrivate(T f, char * buffer, int numberOfSignificantDigits, Preferences::PrintFloatMode mode, int * numberOfRemovedZeros);
}

View File

@@ -41,20 +41,28 @@ void PrintFloat::printBase10IntegerWithDecimalMarker(char * buffer, int bufferLe
template <class T>
int PrintFloat::convertFloatToText(T f, char * buffer, int bufferSize,
int numberOfSignificantDigits, Preferences::PrintFloatMode mode) {
int numberOfSignificantDigits, Preferences::PrintFloatMode mode, bool allowRounding)
{
assert(numberOfSignificantDigits > 0);
assert(bufferSize > 0);
char tempBuffer[PrintFloat::k_maxFloatBufferLength];
int numberOfZerosRemoved = 0;
int requiredLength = convertFloatToTextPrivate(f, tempBuffer, numberOfSignificantDigits, mode, &numberOfZerosRemoved);
/* if the required buffer size overflows the buffer size, we first force the
/* If the required buffer size overflows the buffer size, we first force the
* display mode to scientific and decrease the number of significant digits to
* fit the buffer size. If the buffer size is still to small, we only write
* the beginning of the float and truncate it (which can result in a non sense
* text) */
* fit the buffer size. */
if (mode == Preferences::PrintFloatMode::Decimal && requiredLength >= bufferSize) {
requiredLength = convertFloatToTextPrivate(f, tempBuffer, numberOfSignificantDigits, Preferences::PrintFloatMode::Scientific, &numberOfZerosRemoved);
}
if (requiredLength >= bufferSize) {
/* If the buffer size is still too small and rounding is allowed, we only
* write the beginning of the float and truncate it (which can result in a
* non sense text). If no rounding is allowed, we set the text to null. */
if (!allowRounding) {
buffer[0] = 0;
return requiredLength;
}
int adjustedNumberOfSignificantDigits = numberOfSignificantDigits - numberOfZerosRemoved - requiredLength + bufferSize - 1;
adjustedNumberOfSignificantDigits = adjustedNumberOfSignificantDigits < 1 ? 1 : adjustedNumberOfSignificantDigits;
requiredLength = convertFloatToTextPrivate(f, tempBuffer, adjustedNumberOfSignificantDigits, Preferences::PrintFloatMode::Scientific, &numberOfZerosRemoved);
@@ -194,7 +202,7 @@ int PrintFloat::convertFloatToTextPrivate(T f, char * buffer, int numberOfSignif
return (numberOfCharsForMantissaWithSign+1+numberOfCharExponent);
}
template int PrintFloat::convertFloatToText<float>(float, char*, int, int, Preferences::Preferences::PrintFloatMode);
template int PrintFloat::convertFloatToText<double>(double, char*, int, int, Preferences::Preferences::PrintFloatMode);
template int PrintFloat::convertFloatToText<float>(float, char*, int, int, Preferences::Preferences::PrintFloatMode, bool);
template int PrintFloat::convertFloatToText<double>(double, char*, int, int, Preferences::Preferences::PrintFloatMode, bool);
}