Four review findings on the Task 7 renderer: - InstanceStore::epoch() is derived from gl::Buffer::generation() -- advanced by every allocation, deletion and move -- plus the slot the instances were written to, so the orphan fallback of grow_persistent() can no longer leave the vertex array's attribute pointers bound to a deleted buffer: the epoch reports the new identity and upload() re-points them before the draw. - PlotLayout reserves y_title_area/x_title_area outside axes (sized from the text metrics) and draw_titles() draws in them, so a title can no longer land on the data, whatever the envelope does. - draw() computes plot.layout() once and hands it to the helpers, instead of six times per frame, each of which rebuilt the ticks and the grid. - A refused fence is no longer dropped: mark_drawn() falls back to the orphan path and reports the failure rather than rotating through an unsynchronised slot. New smoke cases force both refusals through the GL entry points (glMapBufferRange, glFenceSync) and assert the pixels plus the live transfer mode; the title case asserts the title colour is absent from the bands inside axes and present in both strips. All three fail against the pre-fix sources.
415 lines
18 KiB
C++
415 lines
18 KiB
C++
#include "harness.hpp"
|
|
#include <plot/plot/plot.hpp>
|
|
#include <plot/data/array_series.hpp>
|
|
#include <plot/data/stream_series.hpp>
|
|
#include <plot/render/bitmap_font.hpp>
|
|
#include <cmath>
|
|
#include <vector>
|
|
|
|
namespace {
|
|
|
|
std::vector<f32> ramp(u64 n) {
|
|
std::vector<f32> y(n);
|
|
for (u64 i = 0; i < n; ++i) y[i] = f32(i);
|
|
return y;
|
|
}
|
|
|
|
f32 widest_major_label(const plot::Axis& a, const plot::render::TextBackend& t) {
|
|
f32 w = 0.0f;
|
|
for (const plot::Tick& tick : a.ticks) {
|
|
if (!tick.major) continue;
|
|
const f32 m = t.measure(tick.label.c_str());
|
|
if (m > w) w = m;
|
|
}
|
|
return w;
|
|
}
|
|
|
|
/// A log axis must never be left inverted or non-positive by an auto-fit.
|
|
void check_log_axis_range(const plot::Range& r) {
|
|
PLOT_CHECK(std::isfinite(r.min) && std::isfinite(r.max));
|
|
PLOT_CHECK(r.min > 0.0);
|
|
PLOT_CHECK(r.min < r.max);
|
|
}
|
|
|
|
bool has_vertical(const plot::Grid& g, f64 x, bool major) {
|
|
for (const plot::GridLine& l : g.lines()) {
|
|
if (l.x0 == l.x1 && l.x0 == x && l.major == major) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool has_horizontal(const plot::Grid& g, f64 y, bool major) {
|
|
for (const plot::GridLine& l : g.lines()) {
|
|
if (l.y0 == l.y1 && l.y0 == y && l.major == major) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
PLOT_TEST(layout_without_text_backend_uses_default_label_areas) {
|
|
plot::Plot p;
|
|
p.set_viewport({0.0f, 0.0f, 800.0f, 400.0f});
|
|
p.add_y_axis({"v", {-1.0, 1.0}, false, plot::AxisPlace::Left, false});
|
|
const plot::PlotLayout& l = p.layout();
|
|
PLOT_CHECK_NEAR(l.y_axis_width, 56.0f, 1e-6);
|
|
PLOT_CHECK_NEAR(l.x_axis_height, 22.0f, 1e-6);
|
|
PLOT_CHECK_NEAR(l.axes.x, 56.0f, 1e-6);
|
|
PLOT_CHECK_NEAR(l.axes.w, 744.0f, 1e-6);
|
|
PLOT_CHECK_NEAR(l.axes.h, 378.0f, 1e-6);
|
|
PLOT_CHECK_NEAR(l.y_label_area.x, 0.0f, 1e-6);
|
|
PLOT_CHECK_NEAR(l.y_label_area.w, 56.0f, 1e-6);
|
|
PLOT_CHECK_NEAR(l.y_label_area.h, 378.0f, 1e-6);
|
|
PLOT_CHECK_NEAR(l.x_label_area.y, 378.0f, 1e-6);
|
|
PLOT_CHECK_NEAR(l.x_label_area.w, 744.0f, 1e-6);
|
|
PLOT_CHECK_NEAR(l.x_label_area.h, 22.0f, 1e-6);
|
|
// "v" is a title, but no backend can measure it, so nothing is reserved: the
|
|
// defaults above are what a plot without a text backend keeps laying out with.
|
|
PLOT_CHECK_NEAR(l.y_title_width, 0.0f, 1e-6);
|
|
PLOT_CHECK_NEAR(l.x_title_height, 0.0f, 1e-6);
|
|
PLOT_CHECK_NEAR(l.y_title_area.w, 0.0f, 1e-6);
|
|
PLOT_CHECK_NEAR(l.x_title_area.h, 0.0f, 1e-6);
|
|
}
|
|
|
|
PLOT_TEST(layout_derives_label_areas_from_font_metrics) {
|
|
plot::render::BitmapFont font;
|
|
plot::Plot p;
|
|
p.set_text_backend(&font);
|
|
p.set_viewport({0.0f, 0.0f, 800.0f, 400.0f});
|
|
const u32 ay = p.add_y_axis({"volts", {-1000.0, 1000.0}, false, plot::AxisPlace::Left, false});
|
|
const plot::PlotLayout& l = p.layout();
|
|
const f32 pad = p.style().grid_label_pad;
|
|
PLOT_CHECK_NEAR(l.y_axis_width, widest_major_label(p.y_axis(ay), font) + 2.0f * pad, 1e-4);
|
|
PLOT_CHECK_NEAR(l.x_axis_height, font.height() + 2.0f * pad, 1e-4);
|
|
// The y axis carries the title "volts": it is drawn horizontally, so the strip
|
|
// left of the label strip is as wide as the measured title. There is no x axis,
|
|
// so the x direction reserves nothing beyond its label strip.
|
|
PLOT_CHECK_NEAR(l.y_title_width, font.measure("volts") + 2.0f * pad, 1e-4);
|
|
PLOT_CHECK_NEAR(l.x_title_height, 0.0f, 1e-6);
|
|
PLOT_CHECK_NEAR(l.axes.x, l.y_title_width + l.y_axis_width, 1e-4);
|
|
PLOT_CHECK_NEAR(l.axes.w, 800.0f - (l.y_title_width + l.y_axis_width), 1e-4);
|
|
PLOT_CHECK_NEAR(l.axes.h, 400.0f - (l.x_axis_height + l.x_title_height), 1e-4);
|
|
// The strips tile the viewport outside `axes`: title, then labels, then data.
|
|
PLOT_CHECK_NEAR(l.y_title_area.x, 0.0f, 1e-6);
|
|
PLOT_CHECK_NEAR(l.y_title_area.right(), l.y_label_area.x, 1e-4);
|
|
PLOT_CHECK_NEAR(l.y_label_area.right(), l.axes.x, 1e-4);
|
|
PLOT_CHECK_NEAR(l.x_title_area.y, l.x_label_area.bottom(), 1e-4);
|
|
PLOT_CHECK_NEAR(l.x_title_area.bottom(), l.axes.bottom() + l.x_label_area.h + l.x_title_height,
|
|
1e-4);
|
|
PLOT_CHECK_NEAR(l.x_label_area.y, l.axes.bottom(), 1e-4);
|
|
}
|
|
|
|
/// An axis with no title reserves nothing: the strips exist so that a titled axis
|
|
/// has somewhere outside `axes` to draw in, not as a fixed cost per plot.
|
|
PLOT_TEST(layout_reserves_no_title_strip_without_a_title) {
|
|
plot::render::BitmapFont font;
|
|
plot::Plot p;
|
|
p.set_text_backend(&font);
|
|
p.set_viewport({0.0f, 0.0f, 800.0f, 400.0f});
|
|
p.add_x_axis({"", {0.0, 1.0}, false, plot::AxisPlace::Bottom, false});
|
|
p.add_y_axis({"", {-1.0, 1.0}, false, plot::AxisPlace::Left, false});
|
|
const plot::PlotLayout& l = p.layout();
|
|
PLOT_CHECK_NEAR(l.y_title_width, 0.0f, 1e-6);
|
|
PLOT_CHECK_NEAR(l.x_title_height, 0.0f, 1e-6);
|
|
PLOT_CHECK_NEAR(l.y_title_area.w, 0.0f, 1e-6);
|
|
PLOT_CHECK_NEAR(l.x_title_area.h, 0.0f, 1e-6);
|
|
PLOT_CHECK_NEAR(l.y_label_area.x, 0.0f, 1e-6);
|
|
PLOT_CHECK_NEAR(l.axes.x, l.y_axis_width, 1e-4);
|
|
PLOT_CHECK_NEAR(l.axes.h, 400.0f - l.x_axis_height, 1e-4);
|
|
}
|
|
|
|
PLOT_TEST(time_axis_tick_labels_use_format_time) {
|
|
plot::Plot p;
|
|
p.set_viewport({0.0f, 0.0f, 800.0f, 400.0f});
|
|
const u32 ax = p.add_x_axis(
|
|
{"t", {0.0, 120.0}, false, plot::AxisPlace::Bottom, false, plot::TickFormat::Time});
|
|
(void)p.layout();
|
|
const std::vector<plot::Tick>& ticks = p.x_axis(ax).ticks;
|
|
PLOT_CHECK(!ticks.empty());
|
|
bool saw_minute_label = false;
|
|
for (const plot::Tick& tick : ticks) {
|
|
if (!tick.major) continue;
|
|
PLOT_CHECK(!tick.label.empty());
|
|
if (tick.value == 60.0) {
|
|
PLOT_CHECK(tick.label == "1:00"); // format_time, not format_si
|
|
saw_minute_label = true;
|
|
}
|
|
}
|
|
PLOT_CHECK(saw_minute_label);
|
|
}
|
|
|
|
PLOT_TEST(x_transform_maps_range_onto_axes_rect) {
|
|
plot::Plot p;
|
|
p.set_viewport({0.0f, 0.0f, 800.0f, 400.0f});
|
|
const u32 ax = p.add_x_axis({"t", {100.0, 200.0}, false, plot::AxisPlace::Bottom, false});
|
|
const u32 ay = p.add_y_axis({"v", {-2.0, 2.0}, false, plot::AxisPlace::Left, false});
|
|
const plot::Rect a = p.layout().axes;
|
|
const plot::Transform tx = p.x_transform(ax);
|
|
const plot::Transform ty = p.y_transform(ay);
|
|
PLOT_CHECK_NEAR(tx.to_screen_x(100.0), a.x, 1e-9);
|
|
PLOT_CHECK_NEAR(tx.to_screen_x(200.0), a.right(), 1e-9);
|
|
PLOT_CHECK_NEAR(ty.to_screen_y(-2.0), a.bottom(), 1e-9); // range min sits at the bottom
|
|
PLOT_CHECK_NEAR(ty.to_screen_y(2.0), a.y, 1e-9); // range max sits at the top
|
|
PLOT_CHECK_NEAR(tx.to_data_x(tx.to_screen_x(137.0)), 137.0, 1e-9);
|
|
PLOT_CHECK_NEAR(ty.to_data_y(ty.to_screen_y(0.5)), 0.5, 1e-9);
|
|
// An axis index that does not exist must not crash and must stay invertible.
|
|
PLOT_CHECK_NEAR(p.x_transform(9).to_data_x(p.x_transform(9).to_screen_x(0.25)), 0.25, 1e-9);
|
|
PLOT_CHECK_NEAR(p.y_transform(9).to_data_y(p.y_transform(9).to_screen_y(0.25)), 0.25, 1e-9);
|
|
}
|
|
|
|
PLOT_TEST(auto_fit_uses_trace_bounds_with_margin) {
|
|
std::vector<f32> y = {0.0f, 1.0f, 2.0f, 3.0f};
|
|
plot::data::ArraySeries s(std::move(y), 0.0, 1.0);
|
|
plot::Plot p;
|
|
p.set_viewport({0.0f, 0.0f, 800.0f, 400.0f});
|
|
const u32 ax = p.add_x_axis({"t", {0.0, 1000.0}, true, plot::AxisPlace::Bottom, false});
|
|
const u32 ay = p.add_y_axis({"v", {0.0, 1000.0}, true, plot::AxisPlace::Left, false});
|
|
p.add_trace({&s, plot::Color::hex(0x40C0FF), 1.5f, ax, ay});
|
|
p.auto_fit_x();
|
|
p.auto_fit_y(ay);
|
|
const plot::Range xr = p.x_range(ax);
|
|
const plot::Range yr = p.y_range(ay);
|
|
PLOT_CHECK_NEAR(xr.min, 0.0 - 3.0 * 0.02, 1e-12); // 4 samples span x 0..3, 2% margin
|
|
PLOT_CHECK_NEAR(xr.max, 3.0 + 3.0 * 0.02, 1e-12);
|
|
PLOT_CHECK_NEAR(yr.min, 0.0 - 3.0 * 0.02, 1e-12);
|
|
PLOT_CHECK_NEAR(yr.max, 3.0 + 3.0 * 0.02, 1e-12);
|
|
|
|
// An explicit range pins the axis; a later fit re-enables auto scaling.
|
|
p.set_y_range(ay, {0.0, 1.0});
|
|
PLOT_CHECK(!p.y_axis(ay).auto_scale);
|
|
p.auto_fit_y(ay);
|
|
PLOT_CHECK(p.y_axis(ay).auto_scale);
|
|
PLOT_CHECK_NEAR(p.y_range(ay).max, 3.0 + 3.0 * 0.02, 1e-12);
|
|
// auto_scale = false survives layout(): the pinned range is not snapped back.
|
|
p.set_y_range(ay, {0.0, 1.0});
|
|
(void)p.layout();
|
|
PLOT_CHECK_NEAR(p.y_range(ay).max, 1.0, 1e-12);
|
|
}
|
|
|
|
PLOT_TEST(layout_refits_axes_left_on_auto_scale) {
|
|
std::vector<f32> y = {0.0f, 1.0f, 2.0f, 3.0f};
|
|
plot::data::ArraySeries s(std::move(y), 0.0, 1.0);
|
|
plot::Plot p;
|
|
p.set_viewport({0.0f, 0.0f, 800.0f, 400.0f});
|
|
const u32 ax = p.add_x_axis({"t", {0.0, 1000.0}, true, plot::AxisPlace::Bottom, false});
|
|
const u32 ay = p.add_y_axis({"v", {0.0, 1000.0}, true, plot::AxisPlace::Left, false});
|
|
p.add_trace({&s, plot::Color::hex(0x40C0FF), 1.5f, ax, ay});
|
|
(void)p.layout(); // no explicit fit needed
|
|
PLOT_CHECK_NEAR(p.x_range(ax).max, 3.0 + 3.0 * 0.02, 1e-12);
|
|
PLOT_CHECK_NEAR(p.y_range(ay).max, 3.0 + 3.0 * 0.02, 1e-12);
|
|
// An explicit range wins from then on: layout() must not snap it back.
|
|
p.set_x_range(ax, {0.0, 1.0});
|
|
p.set_y_range(ay, {0.0, 2.0});
|
|
(void)p.layout();
|
|
PLOT_CHECK_NEAR(p.x_range(ax).max, 1.0, 1e-12);
|
|
PLOT_CHECK_NEAR(p.y_range(ay).max, 2.0, 1e-12);
|
|
}
|
|
|
|
PLOT_TEST(auto_fit_handles_degenerate_and_null_series) {
|
|
std::vector<f32> one{5.0f};
|
|
plot::data::ArraySeries s(std::move(one), 0.0, 1.0);
|
|
plot::Plot p;
|
|
p.set_viewport({0.0f, 0.0f, 800.0f, 400.0f});
|
|
const u32 ax = p.add_x_axis({"t", {0.0, 1.0}, true, plot::AxisPlace::Bottom, false});
|
|
const u32 ay = p.add_y_axis({"v", {0.0, 1.0}, true, plot::AxisPlace::Left, false});
|
|
p.add_trace({nullptr, plot::Color::hex(0xFFFFFF), 1.0f, ax, ay}); // skipped, not a crash
|
|
PLOT_CHECK(p.add_trace({&s, plot::Color::hex(0xFFFFFF), 1.0f, ax, ay}) == 1u);
|
|
PLOT_CHECK(p.trace_count() == 2u);
|
|
|
|
p.auto_fit_x();
|
|
p.auto_fit_y(ay);
|
|
const plot::Range xr = p.x_range(ax);
|
|
const plot::Range yr = p.y_range(ay);
|
|
PLOT_CHECK(xr.span() > 0.0); // degenerate {0,0} widened
|
|
PLOT_CHECK(xr.min < 0.0 && xr.max > 0.0);
|
|
PLOT_CHECK(yr.span() > 0.0); // degenerate {5,5} widened
|
|
PLOT_CHECK(yr.min < 5.0 && yr.max > 5.0);
|
|
(void)p.layout(); // must survive the null trace
|
|
}
|
|
|
|
PLOT_TEST(measurement_matches_direct_computation_and_tracks_cursor_moves) {
|
|
plot::data::ArraySeries s(ramp(100), 0.0, 1.0);
|
|
plot::Plot p;
|
|
p.set_viewport({0.0f, 0.0f, 800.0f, 400.0f});
|
|
const u32 ax = p.add_x_axis({"t", {0.0, 99.0}, false, plot::AxisPlace::Bottom, false});
|
|
const u32 ay = p.add_y_axis({"v", {0.0, 99.0}, false, plot::AxisPlace::Left, false});
|
|
p.add_trace({&s, plot::Color::hex(0x40C0FF), 1.5f, ax, ay});
|
|
const u32 a = p.add_cursor(10.0, plot::Color::hex(0xFFB020));
|
|
const u32 b = p.add_cursor(19.0, plot::Color::hex(0xFFB020));
|
|
const u32 m = p.add_measure(a, b);
|
|
const plot::Measurement& r = p.measurement(m);
|
|
PLOT_CHECK(r.valid);
|
|
double sum = 0.0, sq = 0.0;
|
|
for (u64 i = 10; i <= 19; ++i) {
|
|
sum += double(i);
|
|
sq += double(i) * double(i);
|
|
}
|
|
PLOT_CHECK_NEAR(r.mean, sum / 10.0, 1e-9);
|
|
PLOT_CHECK_NEAR(r.rms, std::sqrt(sq / 10.0), 1e-9);
|
|
PLOT_CHECK_NEAR(r.integral, sum, 1e-9); // dx == 1
|
|
PLOT_CHECK_NEAR(r.ymin, 10.0, 1e-9);
|
|
PLOT_CHECK_NEAR(r.ymax, 19.0, 1e-9);
|
|
PLOT_CHECK_NEAR(r.dx, 9.0, 1e-9);
|
|
PLOT_CHECK_NEAR(r.dy, 9.0, 1e-9);
|
|
PLOT_CHECK_NEAR(r.slope, 1.0, 1e-9);
|
|
|
|
// Moving a cursor the measure depends on must invalidate the cached result.
|
|
p.set_cursor_x(b, 14.0);
|
|
const plot::Measurement& r2 = p.measurement(m);
|
|
PLOT_CHECK(r2.valid);
|
|
PLOT_CHECK_NEAR(r2.ymax, 14.0, 1e-9);
|
|
PLOT_CHECK_NEAR(r2.dx, 4.0, 1e-9);
|
|
}
|
|
|
|
PLOT_TEST(measurement_recomputes_when_the_series_grows) {
|
|
plot::data::StreamSeries s(1024, 0.0, 1.0);
|
|
for (u64 i = 0; i < 10; ++i) s.push(f32(i));
|
|
s.acquire();
|
|
plot::Plot p;
|
|
p.set_viewport({0.0f, 0.0f, 800.0f, 400.0f});
|
|
const u32 ax = p.add_x_axis({"t", {0.0, 39.0}, false, plot::AxisPlace::Bottom, false});
|
|
const u32 ay = p.add_y_axis({"v", {0.0, 39.0}, false, plot::AxisPlace::Left, false});
|
|
p.add_trace({&s, plot::Color::hex(0xFFFFFF), 1.0f, ax, ay});
|
|
const u32 a = p.add_cursor(2.0, plot::Color::hex(0xFFB020));
|
|
const u32 b = p.add_cursor(20.0, plot::Color::hex(0xFFB020));
|
|
const u32 m = p.add_measure(a, b);
|
|
PLOT_CHECK(p.measurement(m).valid);
|
|
// Only 10 samples exist: x = 20 snaps to the last one, index 9.
|
|
PLOT_CHECK_NEAR(p.measurement(m).ymax, 9.0, 1e-9);
|
|
PLOT_CHECK_NEAR(p.measurement(m).dx, 7.0, 1e-9);
|
|
|
|
for (u64 i = 10; i < 40; ++i) s.push(f32(i));
|
|
s.acquire();
|
|
// Same cursors, longer series: the cached result must be recomputed.
|
|
PLOT_CHECK(p.measurement(m).valid);
|
|
PLOT_CHECK_NEAR(p.measurement(m).ymax, 20.0, 1e-9);
|
|
PLOT_CHECK_NEAR(p.measurement(m).dx, 18.0, 1e-9);
|
|
}
|
|
|
|
PLOT_TEST(inverted_and_out_of_range_measurements_are_invalid) {
|
|
plot::data::ArraySeries s(ramp(10), 0.0, 1.0);
|
|
plot::Plot p;
|
|
p.set_viewport({0.0f, 0.0f, 800.0f, 400.0f});
|
|
const u32 ax = p.add_x_axis({"t", {0.0, 9.0}, false, plot::AxisPlace::Bottom, false});
|
|
const u32 ay = p.add_y_axis({"v", {0.0, 9.0}, false, plot::AxisPlace::Left, false});
|
|
const u32 a = p.add_cursor(8.0, plot::Color::hex(0xFFB020));
|
|
const u32 b = p.add_cursor(2.0, plot::Color::hex(0xFFB020));
|
|
const u32 m_inverted = p.add_measure(a, b);
|
|
PLOT_CHECK(!p.measurement(m_inverted).valid);
|
|
p.add_trace({nullptr, plot::Color::hex(0xFFFFFF), 1.0f, ax, ay}); // trace 0 has no series
|
|
const u32 m_null = p.add_measure(b, a, 0);
|
|
PLOT_CHECK(!p.measurement(m_null).valid);
|
|
const u32 m_bad_cursors = p.add_measure(50u, 51u);
|
|
PLOT_CHECK(!p.measurement(m_bad_cursors).valid);
|
|
PLOT_CHECK(!p.measurement(99u).valid); // unknown measure id
|
|
}
|
|
|
|
PLOT_TEST(auto_fit_log_axis_stays_positive_and_ordered) {
|
|
// All-negative trace bounds: nothing to show on a log axis, so the fit is
|
|
// refused and the caller's (positive) range survives untouched.
|
|
{
|
|
std::vector<f32> y = {-5.0f, -3.0f, -1.0f};
|
|
plot::data::ArraySeries s(std::move(y), 0.0, 1.0);
|
|
plot::Plot p;
|
|
p.set_viewport({0.0f, 0.0f, 800.0f, 400.0f});
|
|
const u32 ax = p.add_x_axis({"t", {1.0, 10.0}, false, plot::AxisPlace::Bottom, false});
|
|
const u32 ay = p.add_y_axis({"v", {1.0, 100.0}, true, plot::AxisPlace::Left, true});
|
|
p.add_trace({&s, plot::Color::hex(0x40C0FF), 1.5f, ax, ay});
|
|
p.auto_fit_y(ay);
|
|
check_log_axis_range(p.y_range(ay));
|
|
PLOT_CHECK_NEAR(p.y_range(ay).min, 1.0, 1e-12);
|
|
PLOT_CHECK_NEAR(p.y_range(ay).max, 100.0, 1e-12);
|
|
(void)p.layout(); // the per-layout refit refuses too
|
|
check_log_axis_range(p.y_range(ay));
|
|
PLOT_CHECK_NEAR(p.y_range(ay).max, 100.0, 1e-12);
|
|
}
|
|
|
|
// All-zero bounds: same refusal, no "0" or negative bound on the axis. One
|
|
// sample at x = 0 makes both bounds exactly {0, 0}.
|
|
{
|
|
std::vector<f32> y = {0.0f};
|
|
plot::data::ArraySeries s(std::move(y), 0.0, 1.0);
|
|
plot::Plot p;
|
|
p.set_viewport({0.0f, 0.0f, 800.0f, 400.0f});
|
|
const u32 ax = p.add_x_axis({"t", {1.0, 10.0}, true, plot::AxisPlace::Bottom, true});
|
|
const u32 ay = p.add_y_axis({"v", {1.0, 10.0}, true, plot::AxisPlace::Left, true});
|
|
p.add_trace({&s, plot::Color::hex(0x40C0FF), 1.5f, ax, ay});
|
|
p.auto_fit_x();
|
|
p.auto_fit_y(ay);
|
|
check_log_axis_range(p.x_range(ax));
|
|
check_log_axis_range(p.y_range(ay));
|
|
PLOT_CHECK_NEAR(p.x_range(ax).min, 1.0, 1e-12); // refused: range untouched
|
|
PLOT_CHECK_NEAR(p.x_range(ax).max, 10.0, 1e-12);
|
|
PLOT_CHECK_NEAR(p.y_range(ay).min, 1.0, 1e-12);
|
|
PLOT_CHECK_NEAR(p.y_range(ay).max, 10.0, 1e-12);
|
|
}
|
|
|
|
// Spanning zero with some positive samples: the fit uses the positive part, and
|
|
// the positive data stays inside the range.
|
|
{
|
|
std::vector<f32> y = {-1.0f, 0.5f, 3.0f};
|
|
plot::data::ArraySeries s(std::move(y), -1.0, 1.0); // x bounds {-1, 1}
|
|
plot::Plot p;
|
|
p.set_viewport({0.0f, 0.0f, 800.0f, 400.0f});
|
|
const u32 ax = p.add_x_axis({"t", {1.0, 10.0}, true, plot::AxisPlace::Bottom, true});
|
|
const u32 ay = p.add_y_axis({"v", {1.0, 100.0}, true, plot::AxisPlace::Left, true});
|
|
p.add_trace({&s, plot::Color::hex(0x40C0FF), 1.5f, ax, ay});
|
|
p.auto_fit_x();
|
|
p.auto_fit_y(ay);
|
|
check_log_axis_range(p.x_range(ax));
|
|
check_log_axis_range(p.y_range(ay));
|
|
PLOT_CHECK(p.x_range(ax).min <= 1.0); // the positive x data
|
|
PLOT_CHECK(p.x_range(ax).max >= 1.0);
|
|
PLOT_CHECK(p.y_range(ay).min <= 3.0); // the positive y data
|
|
PLOT_CHECK(p.y_range(ay).max >= 3.0);
|
|
}
|
|
|
|
// Purely positive stays a plain log fit, still ordered and positive.
|
|
{
|
|
std::vector<f32> y = {1.0f, 2.0f, 3.0f};
|
|
plot::data::ArraySeries s(std::move(y), 1.0, 1.0); // x bounds {1, 3}
|
|
plot::Plot p;
|
|
p.set_viewport({0.0f, 0.0f, 800.0f, 400.0f});
|
|
const u32 ax = p.add_x_axis({"t", {1.0, 10.0}, true, plot::AxisPlace::Bottom, true});
|
|
const u32 ay = p.add_y_axis({"v", {1.0, 100.0}, true, plot::AxisPlace::Left, true});
|
|
p.add_trace({&s, plot::Color::hex(0x40C0FF), 1.5f, ax, ay});
|
|
p.auto_fit_x();
|
|
p.auto_fit_y(ay);
|
|
check_log_axis_range(p.x_range(ax));
|
|
check_log_axis_range(p.y_range(ay));
|
|
PLOT_CHECK(p.x_range(ax).min < 1.0 && p.x_range(ax).max > 3.0);
|
|
PLOT_CHECK(p.y_range(ay).min < 1.0 && p.y_range(ay).max > 3.0);
|
|
}
|
|
}
|
|
|
|
PLOT_TEST(grid_lines_are_tick_aligned_data_space_segments) {
|
|
plot::Plot p;
|
|
p.set_viewport({0.0f, 0.0f, 800.0f, 400.0f});
|
|
const u32 ax = p.add_x_axis({"t", {0.0, 10.0}, false, plot::AxisPlace::Bottom, false});
|
|
const u32 ay = p.add_y_axis({"v", {-1.0, 1.0}, false, plot::AxisPlace::Left, false});
|
|
(void)p.layout();
|
|
const plot::Grid& g = p.grid();
|
|
const std::vector<plot::Tick>& xt = p.x_axis(ax).ticks;
|
|
const std::vector<plot::Tick>& yt = p.y_axis(ay).ticks;
|
|
PLOT_CHECK(!xt.empty());
|
|
PLOT_CHECK(!yt.empty());
|
|
PLOT_CHECK(g.size() == xt.size() + yt.size());
|
|
for (const plot::Tick& t : xt) PLOT_CHECK(has_vertical(g, t.value, t.major));
|
|
for (const plot::Tick& t : yt) PLOT_CHECK(has_horizontal(g, t.value, t.major));
|
|
// Vertical grid lines span the y range, horizontal ones the x range: data
|
|
// space only, so the renderer's uniform transform can place them.
|
|
for (const plot::GridLine& l : g.lines()) {
|
|
if (l.x0 == l.x1) {
|
|
PLOT_CHECK_NEAR(l.y0, -1.0, 1e-12);
|
|
PLOT_CHECK_NEAR(l.y1, 1.0, 1e-12);
|
|
} else {
|
|
PLOT_CHECK_NEAR(l.y0, l.y1, 1e-12);
|
|
PLOT_CHECK_NEAR(l.x0, 0.0, 1e-12);
|
|
PLOT_CHECK_NEAR(l.x1, 10.0, 1e-12);
|
|
}
|
|
}
|
|
u32 minor = 0;
|
|
for (const plot::GridLine& l : g.lines()) minor += l.major ? 0u : 1u;
|
|
PLOT_CHECK(minor > 0); // minor grid is generated too
|
|
}
|