summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/renderer.cpp115
-rw-r--r--src/renderer.hpp19
2 files changed, 91 insertions, 43 deletions
diff --git a/src/renderer.cpp b/src/renderer.cpp
index 141383e..4337bbf 100644
--- a/src/renderer.cpp
+++ b/src/renderer.cpp
@@ -13,33 +13,38 @@
#include <cassert>
#include <cstring>
-static std::string
-ValueToShortString(double value, int prec, const std::string& unit)
+static double compute_error_for_scale(double v, int scale, double v_min, double v_max)
+{
+ double rsv = v * std::pow(10, scale);
+ double sv = std::round(rsv);
+ return std::abs(rsv - sv) / std::pow(10, scale) / (v_max - v_min);
+}
+
+std::string
+Renderer::ValueToShortString(double value, int scale, const std::string& unit)
{
static const std::vector<std::string> PREFIXES = { "p", "n", "u", "m", "", "k", "M", "G", "T" };
std::size_t pidx = 4;
- while ((prec >= 3) && (pidx > 0)) {
- prec -= 3;
+ while ((scale >= 3) && (pidx > 0)) {
+ scale -= 3;
pidx--;
value *= 1000.0f;
}
- while ((prec <= -3) && (pidx < PREFIXES.size() - 1)) {
- prec += 3;
+ while ((scale <= -3) && (pidx < PREFIXES.size() - 1)) {
+ scale += 3;
pidx++;
value /= 1000.0f;
}
- prec++;
-
- /* round very low values to zero */
+ /* round very low values to zero, as to avoid printing "-0" */
/* theoretically this function should not be asked to print values as small as 1e-9 */
if (std::abs(value) < 1e-9) {
value = 0.0;
}
std::stringstream ss;
- ss << std::fixed << std::setprecision(prec > 0 ? prec : 0) << value << PREFIXES[pidx] << unit;
+ ss << std::fixed << std::setprecision(scale > 0 ? scale : 0) << value << PREFIXES[pidx] << unit;
return ss.str();
}
@@ -50,7 +55,7 @@ Renderer::Renderer(const Configuration& conf, const ColorMap& cmap, const ValueM
throw std::runtime_error("failed to copy color map");
}
if (fft_count == 0) {
- throw std::runtime_error("positive number of FFT windows required by rendere");
+ throw std::runtime_error("positive number of FFT windows required by renderer");
}
/* load font */
@@ -259,28 +264,40 @@ Renderer::Renderer(const Configuration& conf, const ColorMap& cmap, const ValueM
Renderer::GetLinearTicks(double v_min, double v_max, const std::string& v_unit, unsigned int num_ticks)
{
if (num_ticks <= 1) {
- throw std::runtime_error("GetLinearTicks() requires at least two ticks");
+ throw std::runtime_error("requires at least two ticks");
}
if (v_min >= v_max) {
throw std::runtime_error("minimum and maximum values are not in order");
}
- int prec = 0;
+ /* find a scale that advances each tick value in the single digits */
+ int scale = 0;
double dist = (v_max - v_min) / ((double) num_ticks - 1);
while (dist >= 10.0f) {
- prec--;
+ scale--;
dist /= 10.0f;
}
while (dist < 1.0f) {
- prec++;
+ scale++;
dist *= 10.0f;
}
+ /* still, if the error of this scale is comparable to the input domain, add one more decimal place */
+ for (unsigned int i = 0; i < num_ticks; i ++) {
+ double k = (double) i / (double)(num_ticks - 1);
+ double v = v_min + k * (v_max - v_min);
+
+ if (::compute_error_for_scale(v, scale, v_min, v_max) > 0.01) { /* greater than 1% => one more decimal place */
+ scale ++;
+ break;
+ }
+ }
+
std::list<AxisTick> ticks;
for (unsigned int i = 0; i < num_ticks; i ++) {
double k = (double) i / (double)(num_ticks - 1);
double v = v_min + k * (v_max - v_min);
- ticks.emplace_back(std::make_tuple(k, ::ValueToShortString(v, prec, v_unit)));
+ ticks.emplace_back(std::make_tuple(k, ValueToShortString(v, scale, v_unit)));
}
return ticks;
@@ -297,7 +314,7 @@ Renderer::GetNiceTicks(double v_min, double v_max, const std::string& v_unit, un
throw std::runtime_error("length in pixels must be positive");
}
if (min_tick_length_px == 0) {
- throw std::runtime_error("estimate tick length in pixels must be positive");
+ throw std::runtime_error("minimum tick length in pixels must be positive");
}
std::list<AxisTick> ticks;
@@ -330,20 +347,20 @@ Renderer::GetNiceTicks(double v_min, double v_max, const std::string& v_unit, un
return mfact;
};
- /* computes precision */
- auto compute_precision = [](double factor) -> int
+ /* computes scale */
+ auto compute_scale = [](double factor) -> int
{
- int prec = 0;
+ int scale = 0;
double dist = factor;
while (dist > 10.0f) {
- prec--;
+ scale--;
dist /= 10.0f;
}
while (dist < 1.0f) {
- prec++;
+ scale++;
dist *= 10.0f;
}
- return prec;
+ return scale;
};
/* computes text width */
@@ -356,24 +373,52 @@ Renderer::GetNiceTicks(double v_min, double v_max, const std::string& v_unit, un
return (rotated ? text.getLocalBounds().height : text.getLocalBounds().width);
};
+ /* find the first nice value */
+ auto find_first_value = [v_min, v_max](double factor) -> double { ;
+ double fval = v_min / factor;
+ constexpr double ROUND_EPSILON = 1e-6;
+ if (std::abs(fval - std::floor(fval)) > ROUND_EPSILON) {
+ fval = std::floor(fval) + 1.0f;
+ }
+ fval *= factor;
+ assert(v_min <= fval);
+ assert(fval <= v_max);
+
+ return fval;
+ };
+
/* find a factor and a precision for the ticks */
double factor = 1.0;
- int precision = 0;
+ int scale = 0;
unsigned int target_tick_length = min_tick_length_px;
constexpr std::size_t MAXIMUM_ITERATIONS = 10;
std::size_t iteration = 0; /* theoretically the below loop will stop; practically, we don't take chances */
+ double fval = 0.0; /* first nice value */
+
+ /* adjust upper limit with a slight epsilon so that we have tickmark for v_max in most "nice" cases */
+ /* otherwise we might miss it because of representation errors */
+ double upper_limit = v_max + (v_max - v_min) * 1e-6;
do {
/* compute a nice factor and get the actual tick length */
factor = find_factor(target_tick_length);
auto actual_tick_length = px_per_v * factor;
- precision = compute_precision(factor);
+ scale = compute_scale(factor);
+ fval = find_first_value(factor);
+
+ /* see if we need another decimal place */
+ for (double value = fval; value <= upper_limit; value += factor) {
+ if (::compute_error_for_scale(value, scale, v_min, v_max) > 0.01) { /* greater than 1% => one more decimal place */
+ scale ++;
+ break;
+ }
+ }
/* make sure the tick length is higher than the label sizes for this precision */
constexpr double min_tick_spacing = 5.0;
- auto lb_size = compute_text_size(::ValueToShortString(v_min, precision, v_unit)) + min_tick_spacing;
- auto ub_size = compute_text_size(::ValueToShortString(v_max, precision, v_unit)) + min_tick_spacing;
+ auto lb_size = compute_text_size(ValueToShortString(v_min, scale, v_unit)) + min_tick_spacing;
+ auto ub_size = compute_text_size(ValueToShortString(v_max, scale, v_unit)) + min_tick_spacing;
if ((lb_size > actual_tick_length) || (ub_size > actual_tick_length)) {
target_tick_length = std::max( { static_cast<unsigned int>(lb_size),
static_cast<unsigned int>(ub_size),
@@ -385,24 +430,10 @@ Renderer::GetNiceTicks(double v_min, double v_max, const std::string& v_unit, un
iteration++;
} while (iteration < MAXIMUM_ITERATIONS); /* maybe we should issue warning if we reach max iterations? */
- /* find the first nice value */
- double fval = v_min / factor;
- constexpr double ROUND_EPSILON = 1e-6;
- if (std::abs(fval - std::floor(fval)) > ROUND_EPSILON) {
- fval = std::floor(fval) + 1.0f;
- }
- fval *= factor;
- assert(v_min <= fval);
- assert(fval <= v_max);
-
- /* adjust upper limit with a slight epsilon so that we have tickmark for v_max in most "nice" cases */
- /* otherwise we might miss it because of representation errors */
- double upper_limit = v_max + (v_max - v_min) * 1e-6;
-
/* add ticks */
for (double value = fval; value <= upper_limit; value += factor) {
double k = (value - v_min) / (v_max - v_min);
- ticks.emplace_back(std::make_tuple(k, ::ValueToShortString(value, precision, v_unit)));
+ ticks.emplace_back(std::make_tuple(k, ValueToShortString(value, scale, v_unit)));
}
return ticks;
diff --git a/src/renderer.hpp b/src/renderer.hpp
index 1a27276..9e97870 100644
--- a/src/renderer.hpp
+++ b/src/renderer.hpp
@@ -24,7 +24,7 @@ enum class Orientation {
* Spectrogram rendering class
*/
class Renderer {
-private:
+protected: /* for all intents and purposes this should be private, but we want to unit test a few of the methods here */
const Configuration configuration_; /* configuration is cached as it contains multiple settings regarding spacing and sizing */
const std::size_t fft_count_; /* number of windows to render */
const std::unique_ptr<const ColorMap> color_map_; /* color map used for rendering */
@@ -51,6 +51,22 @@ private:
std::list<AxisTick> live_ticks_;
/**
+ * Return a short representation of the value (using unit prefixes like, m, k, M ...).
+ * @param value The value to encode.
+ * @param scale Scale of the value (in the numeric sense).
+ * @param unit Unit of the value.
+ * @return Short representation string.
+ *
+ * NOTE: The unit prefix is computed just from the scale, not the value itself.
+ * For example, ValueToShortString(0.00004, 5, "V") will yield "0.04mV",
+ * not "40uV" (which would otherwise be the more natural representation).
+ * This behaviour is purposeful, as it allows more flexibility (e.g. if
+ * we want the same prefix and number of decimal places for all values
+ * of an axis).
+ */
+ static std::string ValueToShortString(double value, int scale, const std::string& unit);
+
+ /**
* Build an array of ticks with linear spacing.
* @param v_min Lowest value on the axis.
* @param v_max Highest value on the axis.
@@ -74,6 +90,7 @@ private:
*
* NOTE: This method attempts to find some nice values for the ticks that
* also have sufficient spacing for the text to fit properly.
+ * NOTE: This method enforces an error of <1% between tick text and tick value.
*/
std::list<AxisTick> GetNiceTicks(double v_min, double v_max, const std::string& v_unit,
unsigned int length_px, unsigned int min_tick_length_px, bool rotated);