summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorVasile Vilvoiu <vasi@vilvoiu.ro>2021-07-22 20:46:32 +0300
committerVasile Vilvoiu <vasi@vilvoiu.ro>2021-07-22 20:46:32 +0300
commitaa91ba3ab50320d9f3cf9b41a68d9c001f769df6 (patch)
treedd8d8801b0bf3f317ef79b929c339b4848259b61
parent88ff6bb85fdc5e58d672950a1c7fcdaa0f91c1df (diff)
Adjust the way scale is computed for axes.
Add extra digit only when error is greater than 1% of scale domain. Add unit test for Renderer (for axes related routines). Fixes #26.
-rw-r--r--CMakeLists.txt1
-rw-r--r--src/renderer.cpp115
-rw-r--r--src/renderer.hpp19
-rw-r--r--test/test-renderer.cpp184
4 files changed, 276 insertions, 43 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 6a7d5f1..d6810b1 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -96,6 +96,7 @@ if (TESTING)
set (UNIT_TEST_SOURCES
test/test.cpp
test/test-fft.cpp
+ test/test-renderer.cpp
test/test-input-reader.cpp
test/test-input-parser.cpp
test/test-color-map.cpp
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);
diff --git a/test/test-renderer.cpp b/test/test-renderer.cpp
new file mode 100644
index 0000000..7a091d9
--- /dev/null
+++ b/test/test-renderer.cpp
@@ -0,0 +1,184 @@
+/*
+ * Copyright (c) 2020-2021 Vasile Vilvoiu <vasi@vilvoiu.ro>
+ *
+ * specgram is free software; you can redistribute it and/or modify
+ * it under the terms of the MIT license. See LICENSE for details.
+ */
+#include "test.hpp"
+#include "../src/renderer.hpp"
+
+class ExposedRenderer : public Renderer
+{
+public:
+ using AxisTick = Renderer::AxisTick;
+
+ ExposedRenderer(const Configuration& conf, const ColorMap& cmap, const ValueMap& vmap, std::size_t fft_count)
+ : Renderer(conf, cmap, vmap, fft_count) { };
+
+ static std::string EValueToShortString(double value, int scale, const std::string& unit)
+ {
+ return ValueToShortString(value, scale, unit);
+ }
+
+ static std::list<AxisTick> EGetLinearTicks(double v_min, double v_max, const std::string& v_unit,
+ unsigned int num_ticks)
+ {
+ return Renderer::GetLinearTicks(v_min, v_max, v_unit, num_ticks);
+ }
+
+ std::list<Renderer::AxisTick> EGetNiceTicks(double v_min, double v_max, const std::string& v_unit,
+ unsigned int length_px, unsigned int min_tick_length_px, bool rotated)
+ {
+ return GetNiceTicks(v_min, v_max, v_unit, length_px, min_tick_length_px, rotated);
+ }
+};
+
+TEST(TestRenderer, ValueToShortString)
+{
+ /* unit prefix */
+ EXPECT_EQ(ExposedRenderer::EValueToShortString(1e20, -20, ""), "100000000T");
+ EXPECT_EQ(ExposedRenderer::EValueToShortString(1e12, -12, ""), "1T");
+ EXPECT_EQ(ExposedRenderer::EValueToShortString(1e9, -9, ""), "1G");
+ EXPECT_EQ(ExposedRenderer::EValueToShortString(1e6, -6, ""), "1M");
+ EXPECT_EQ(ExposedRenderer::EValueToShortString(1e3, -3, ""), "1k");
+ EXPECT_EQ(ExposedRenderer::EValueToShortString(1, 0, ""), "1");
+ EXPECT_EQ(ExposedRenderer::EValueToShortString(1e-3, 3, ""), "1m");
+ EXPECT_EQ(ExposedRenderer::EValueToShortString(1e-6, 6, ""), "1u");
+ EXPECT_EQ(ExposedRenderer::EValueToShortString(1e-9, 9, ""), "1n");
+ EXPECT_EQ(ExposedRenderer::EValueToShortString(1e-12, 12, ""), "1p");
+ EXPECT_EQ(ExposedRenderer::EValueToShortString(1e-20, 20, ""), "0.00000001p");
+
+ /* rounding */
+ EXPECT_EQ(ExposedRenderer::EValueToShortString(0.000049, 9, "V"), "49000nV");
+ EXPECT_EQ(ExposedRenderer::EValueToShortString(0.000049, 8, "V"), "49.00uV");
+ EXPECT_EQ(ExposedRenderer::EValueToShortString(0.000049, 7, "V"), "49.0uV");
+ EXPECT_EQ(ExposedRenderer::EValueToShortString(0.000049, 6, "V"), "49uV");
+ EXPECT_EQ(ExposedRenderer::EValueToShortString(0.000049, 5, "V"), "0.05mV");
+ EXPECT_EQ(ExposedRenderer::EValueToShortString(0.000049, 4, "V"), "0.0mV");
+ EXPECT_EQ(ExposedRenderer::EValueToShortString(0.000049, 3, "V"), "0mV");
+ EXPECT_EQ(ExposedRenderer::EValueToShortString(0.000049, 2, "V"), "0.00V");
+ EXPECT_EQ(ExposedRenderer::EValueToShortString(0.000049, 1, "V"), "0.0V");
+ EXPECT_EQ(ExposedRenderer::EValueToShortString(0.000049, 0, "V"), "0V");
+ EXPECT_EQ(ExposedRenderer::EValueToShortString(0.000049, -1, "V"), "0V");
+ EXPECT_EQ(ExposedRenderer::EValueToShortString(0.000049, -2, "V"), "0V");
+ EXPECT_EQ(ExposedRenderer::EValueToShortString(0.000049, -3, "V"), "0kV");
+
+ EXPECT_EQ(ExposedRenderer::EValueToShortString(0.000044, 9, "V"), "44000nV");
+ EXPECT_EQ(ExposedRenderer::EValueToShortString(0.000044, 8, "V"), "44.00uV");
+ EXPECT_EQ(ExposedRenderer::EValueToShortString(0.000044, 7, "V"), "44.0uV");
+ EXPECT_EQ(ExposedRenderer::EValueToShortString(0.000044, 6, "V"), "44uV");
+ EXPECT_EQ(ExposedRenderer::EValueToShortString(0.000044, 5, "V"), "0.04mV");
+ EXPECT_EQ(ExposedRenderer::EValueToShortString(0.000044, 4, "V"), "0.0mV");
+ EXPECT_EQ(ExposedRenderer::EValueToShortString(0.000044, 3, "V"), "0mV");
+ EXPECT_EQ(ExposedRenderer::EValueToShortString(0.000044, 2, "V"), "0.00V");
+ EXPECT_EQ(ExposedRenderer::EValueToShortString(0.000044, 1, "V"), "0.0V");
+ EXPECT_EQ(ExposedRenderer::EValueToShortString(0.000044, 0, "V"), "0V");
+ EXPECT_EQ(ExposedRenderer::EValueToShortString(0.000044, -1, "V"), "0V");
+ EXPECT_EQ(ExposedRenderer::EValueToShortString(0.000044, -2, "V"), "0V");
+ EXPECT_EQ(ExposedRenderer::EValueToShortString(0.000044, -3, "V"), "0kV");
+
+ /* round to zero */
+ EXPECT_EQ(ExposedRenderer::EValueToShortString(1e-10, 0, "V"), "0V");
+ EXPECT_EQ(ExposedRenderer::EValueToShortString(-1e-10, 0, "V"), "0V");
+}
+
+TEST(TestRenderer, GetLinearTicks)
+{
+ constexpr double epsilon = 1e-9;
+
+ EXPECT_THROW_MATCH(ExposedRenderer::EGetLinearTicks(0, 0, "", 0),
+ std::runtime_error, "requires at least two ticks");
+ EXPECT_THROW_MATCH(ExposedRenderer::EGetLinearTicks(0, 0, "", 1),
+ std::runtime_error, "requires at least two ticks");
+
+ EXPECT_THROW_MATCH(ExposedRenderer::EGetLinearTicks(1.0, 1.0, "", 2),
+ std::runtime_error, "minimum and maximum values are not in order");
+ EXPECT_THROW_MATCH(ExposedRenderer::EGetLinearTicks(1.0+1e-9, 1.0, "", 2),
+ std::runtime_error, "minimum and maximum values are not in order");
+ EXPECT_NO_THROW(ExposedRenderer::EGetLinearTicks(1.0-1e-9, 1.0, "", 2));
+
+ using Test = struct { double l; double h; std::string unit; std::size_t num; std::vector<ExposedRenderer::AxisTick> ticks; };
+ std::vector<Test> tests {
+ {0.0, 3000.0, "V", 5, { {0.0/4.0, "0V"}, {1.0/4.0, "750V"}, {2.0/4.0, "1500V"}, {3.0/4.0, "2250V"}, {4.0/4.0, "3000V"} } },
+ {0.0, 3000.0, "V", 4, { {0.0/3.0, "0kV"}, {1.0/3.0, "1kV"}, {2.0/3.0, "2kV"}, {3.0/3.0, "3kV"} } },
+
+ {-5.0, 5.0, "V", 11, { {0.0, "-5V"}, {0.1, "-4V"}, {0.2, "-3V"}, {0.3, "-2V"}, {0.4, "-1V"}, {0.5, "0V"}, {0.6, "1V"}, {0.7, "2V"}, {0.8, "3V"}, {0.9, "4V"}, {1.0, "5V"} } },
+ {-50.0, 50.0, "V", 11, { {0.0, "-50V"}, {0.1, "-40V"}, {0.2, "-30V"}, {0.3, "-20V"}, {0.4, "-10V"}, {0.5, "0V"}, {0.6, "10V"}, {0.7, "20V"}, {0.8, "30V"}, {0.9, "40V"}, {1.0, "50V"} } },
+
+ {0.0, 2000.0, "V", 4, { {0.0/3.0, "0V"}, {1.0/3.0, "667V"}, {2.0/3.0, "1333V"}, {3.0/3.0, "2000V"} } },
+ {0.0, 200.0, "V", 4, { {0.0/3.0, "0V"}, {1.0/3.0, "67V"}, {2.0/3.0, "133V"}, {3.0/3.0, "200V"} } },
+ {0.0, 20.0, "V", 4, { {0.0/3.0, "0.0V"}, {1.0/3.0, "6.7V"}, {2.0/3.0, "13.3V"}, {3.0/3.0, "20.0V"} } },
+ {0.0, 2.0, "V", 4, { {0.0/3.0, "0.00V"}, {1.0/3.0, "0.67V"}, {2.0/3.0, "1.33V"}, {3.0/3.0, "2.00V"} } },
+ {0.0, 0.2, "V", 4, { {0.0/3.0, "0mV"}, {1.0/3.0, "67mV"}, {2.0/3.0, "133mV"}, {3.0/3.0, "200mV"} } },
+ {0.0, 0.02, "V", 4, { {0.0/3.0, "0.0mV"}, {1.0/3.0, "6.7mV"}, {2.0/3.0, "13.3mV"}, {3.0/3.0, "20.0mV"} } },
+ {0.0, 0.002, "V", 4, { {0.0/3.0, "0.00mV"}, {1.0/3.0, "0.67mV"}, {2.0/3.0, "1.33mV"}, {3.0/3.0, "2.00mV"} } },
+ };
+
+ for (auto& test : tests) {
+ auto out = ExposedRenderer::EGetLinearTicks(test.l, test.h, test.unit, test.num);
+ EXPECT_EQ(out.size(), test.num);
+ EXPECT_EQ(out.size(), test.ticks.size());
+
+ auto it = out.begin();
+ for (std::size_t i=0; i<test.num; i++, it++) {
+ EXPECT_LE(std::get<0>(*it) - std::get<0>(test.ticks[i]), epsilon);
+ EXPECT_EQ(std::get<1>(*it), std::get<1>(test.ticks[i]));
+ }
+ }
+}
+
+TEST(TestRenderer, GetNiceTicks)
+{
+ constexpr double epsilon = 1e-9;
+
+ /* configuration */
+ const char *args[] { "program", "-l" };
+ auto[conf, rc, exit] = Configuration::Build(2, args);
+ EXPECT_EQ(rc, 0);
+ EXPECT_FALSE(exit);
+
+ /* renderer */
+ EXPECT_THROW_MATCH(ExposedRenderer(conf.GetForLive(), JetColorMap(), DecibelValueMap(-120.0, 0.0, "FS"), 0),
+ std::runtime_error, "positive number of FFT windows required by renderer");
+ ExposedRenderer renderer(conf.GetForLive(), JetColorMap(), DecibelValueMap(-120.0, 0.0, "FS"), 128);
+
+ EXPECT_THROW_MATCH(renderer.EGetNiceTicks(1.0, 1.0, "", 100, 30, false),
+ std::runtime_error, "minimum and maximum values are not in order");
+ EXPECT_THROW_MATCH(renderer.EGetNiceTicks(1.0+1e-9, 1.0, "", 100, 30, false),
+ std::runtime_error, "minimum and maximum values are not in order");
+
+ EXPECT_THROW_MATCH(renderer.EGetNiceTicks(0.0, 1.0, "", 0, 30, false),
+ std::runtime_error, "length in pixels must be positive");
+ EXPECT_THROW_MATCH(renderer.EGetNiceTicks(0.0, 1.0, "", 100, 0, false),
+ std::runtime_error, "minimum tick length in pixels must be positive");
+
+ using Test = struct { double l; double h; std::string unit; std::size_t len; std::size_t min_size; bool rot; std::vector<ExposedRenderer::AxisTick> ticks; };
+ std::vector<Test> tests {
+ /* very small minimum tick length, limited by text width */
+ {0.0, 3000.0, "", 200, 1, false, { {0.0/7.0, "0"}, {1.0/6.0, "500"}, {2.0/6.0, "1000"}, {3.0/6.0, "1500"}, {4.0/6.0, "2000"}, {5.0/6.0, "2500"}, {6.0/6.0, "3000"} } },
+ {0.0, 3000.0, "V", 200, 1, false, { {0.0/3.0, "0kV"}, {1.0/3.0, "1kV"}, {2.0/3.0, "2kV"}, {3.0/3.0, "3kV"} } },
+
+ /* increase tick limit */
+ {0.0, 3000.0, "V", 200, 30, false, { {0.0/3.0, "0kV"}, {1.0/3.0, "1kV"}, {2.0/3.0, "2kV"}, {3.0/3.0, "3kV"} } },
+ {0.0, 3000.0, "V", 200, 90, false, { {0.0/2.0, "0V"}, {1.0/2.0, "1500V"}, {2.0/2.0, "3000V"} } },
+ {0.0, 3000.0, "V", 200, 120, false, { {0.0, "0kV"}, {2.0/3.0, "2kV"} } },
+
+ /* first nice value */
+ {-300.0, 3000.0, "V", 200, 120, false, { {1.0/10.0, "0kV"}, {7.0/10.0, "2kV"} } },
+
+ /* Issue #26 */
+ {-70.0, -20.0, "dBFS", 907, 50, false, { {0.02, "-69dBFS"}, {0.08, "-66dBFS"}, {0.14, "-63dBFS"}, {0.2, "-60dBFS"}, {0.26, "-57dBFS"}, {0.32, "-54dBFS"}, {0.38, "-51dBFS"}, {0.44, "-48dBFS"}, {0.50, "-45dBFS"}, {0.56, "-42dBFS"}, {0.62, "-39dBFS"}, {0.68, "-36dBFS"}, {0.74, "-33dBFS"}, {0.80, "-30dBFS"}, {0.86, "-27dBFS"}, {0.92, "-24dBFS"}, {0.98, "-21dBFS"} } },
+ {-70.0, 0.0, "dBFS", 907, 50, false, { {0.0/14.0, "-70dBFS"}, {1.0/14.0, "-65dBFS"}, {2.0/14.0, "-60dBFS"}, {3.0/14.0, "-55dBFS"}, {4.0/14.0, "-50dBFS"}, {5.0/14.0, "-45dBFS"}, {6.0/14.0, "-40dBFS"}, {7.0/14.0, "-35dBFS"}, {8.0/14.0, "-30dBFS"}, {9.0/14.0, "-25dBFS"}, {10.0/14.0, "-20dBFS"}, {11.0/14.0, "-15dBFS"}, {12.0/14.0, "-10dBFS"}, {13.0/14.0, "-5dBFS"}, {14.0/14.0, "0dBFS"} } },
+ };
+
+ for (auto& test : tests) {
+ auto out = renderer.EGetNiceTicks(test.l, test.h, test.unit, test.len, test.min_size, test.rot);
+ EXPECT_EQ(out.size(), test.ticks.size());
+
+ auto it = out.begin();
+ for (std::size_t i=0; i<test.ticks.size(); i++, it++) {
+ EXPECT_LE(std::get<0>(*it) - std::get<0>(test.ticks[i]), epsilon);
+ EXPECT_EQ(std::get<1>(*it), std::get<1>(test.ticks[i]));
+ }
+ }
+} \ No newline at end of file