407 lines
17 KiB
C++
407 lines
17 KiB
C++
#include "Axis.h"
|
|
|
|
#include "../plot/Plot_Core.h"
|
|
#include "../render/Blend2D_Cache.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <iomanip>
|
|
#include <sstream>
|
|
|
|
namespace renderive {
|
|
namespace {
|
|
|
|
bool valid_range(Range range) {
|
|
return std::isfinite(range.origin) && std::isfinite(range.target) && range.size() > 0.0;
|
|
}
|
|
|
|
std::string fixed_number(double value, int precision) {
|
|
std::ostringstream stream;
|
|
stream << std::fixed << std::setprecision(std::clamp(precision, 0, 12)) << value;
|
|
std::string result = stream.str();
|
|
if (result.find('.') != std::string::npos) {
|
|
while (!result.empty() && result.back() == '0')
|
|
result.pop_back();
|
|
if (!result.empty() && result.back() == '.')
|
|
result.pop_back();
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::string localized_number(double value, int precision, Number_Locale locale) {
|
|
std::string result = fixed_number(value, precision);
|
|
if (locale.decimal_point != '.')
|
|
std::replace(result.begin(), result.end(), '.', locale.decimal_point);
|
|
return result;
|
|
}
|
|
|
|
std::string formatted_time(Time_Of_Day time, std::string_view format) {
|
|
const auto total = time.milliseconds;
|
|
const int hours = static_cast<int>((total / 3'600'000) % 24);
|
|
const int minutes = static_cast<int>((total / 60'000) % 60);
|
|
const int seconds = static_cast<int>((total / 1'000) % 60);
|
|
const int milliseconds = static_cast<int>(total % 1'000);
|
|
const auto digits = [](int value, int width) {
|
|
std::ostringstream stream;
|
|
stream << std::setfill('0') << std::setw(width) << value;
|
|
return stream.str();
|
|
};
|
|
std::string result;
|
|
for (std::size_t index = 0; index < format.size();) {
|
|
const std::string_view rest = format.substr(index);
|
|
if (rest.starts_with("zzz")) {
|
|
result += digits(milliseconds, 3);
|
|
index += 3;
|
|
} else if (rest.starts_with("hh") || rest.starts_with("HH")) {
|
|
result += digits(hours, 2);
|
|
index += 2;
|
|
} else if (rest.starts_with("mm")) {
|
|
result += digits(minutes, 2);
|
|
index += 2;
|
|
} else if (rest.starts_with("ss")) {
|
|
result += digits(seconds, 2);
|
|
index += 2;
|
|
} else {
|
|
result.push_back(format[index++]);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
Abs_Axis::Abs_Axis(Plot_Core& plot, Orientation orientation) : Renderable(plot, true) {
|
|
axis_state_.orientation = orientation;
|
|
}
|
|
|
|
Abs_Axis::~Abs_Axis() = default;
|
|
|
|
#define RENDERIVE_AXIS_PROPERTY(Type, Name) \
|
|
Type Abs_Axis::Name() const { std::lock_guard lock(axis_mutex_); return axis_state_.Name; } \
|
|
void Abs_Axis::set_##Name(Type value) { \
|
|
{ std::lock_guard lock(axis_mutex_); if (axis_state_.Name == value) return; axis_state_.Name = std::move(value); } \
|
|
changed(); \
|
|
}
|
|
|
|
RENDERIVE_AXIS_PROPERTY(int, x)
|
|
RENDERIVE_AXIS_PROPERTY(int, y)
|
|
RENDERIVE_AXIS_PROPERTY(Orientation, orientation)
|
|
RENDERIVE_AXIS_PROPERTY(std::size_t, pixel_length)
|
|
RENDERIVE_AXIS_PROPERTY(int, tick_length)
|
|
RENDERIVE_AXIS_PROPERTY(int, sub_tick_length)
|
|
RENDERIVE_AXIS_PROPERTY(Color, color)
|
|
RENDERIVE_AXIS_PROPERTY(Number_Locale, locale)
|
|
RENDERIVE_AXIS_PROPERTY(std::string, unit_text)
|
|
RENDERIVE_AXIS_PROPERTY(Font, unit_text_font)
|
|
RENDERIVE_AXIS_PROPERTY(Pen, unit_text_pen)
|
|
RENDERIVE_AXIS_PROPERTY(Brush, unit_text_background_brush)
|
|
RENDERIVE_AXIS_PROPERTY(int, label_rotation_degrees)
|
|
|
|
#undef RENDERIVE_AXIS_PROPERTY
|
|
|
|
Abs_Axis::State Abs_Axis::axis_state() const {
|
|
std::lock_guard lock(axis_mutex_);
|
|
return axis_state_;
|
|
}
|
|
|
|
Axis_Transform Abs_Axis::transform() const {
|
|
const State state = axis_state();
|
|
return {
|
|
coord_range(),
|
|
state.orientation == Orientation::Horizontal ? static_cast<double>(state.x)
|
|
: static_cast<double>(state.y),
|
|
static_cast<double>(state.pixel_length)
|
|
};
|
|
}
|
|
|
|
double Abs_Axis::pixel_to_coord(double pixel) const { return transform().pixel_to_coord(pixel); }
|
|
double Abs_Axis::coord_to_pixel(double coordinate) const { return transform().coord_to_pixel(coordinate); }
|
|
double Abs_Axis::start_coord() const { return coord_range().origin; }
|
|
double Abs_Axis::end_coord() const { return coord_range().target; }
|
|
|
|
int Abs_Axis::pixel_sample_count(Range range) const {
|
|
const Axis_Transform value = transform();
|
|
const double first = value.coord_to_pixel(range.origin);
|
|
const double last = value.coord_to_pixel(range.target);
|
|
return std::max(0, static_cast<int>(std::abs(last - first)) + 1);
|
|
}
|
|
|
|
int Abs_Axis::pixel_sample_count() const {
|
|
return static_cast<int>(pixel_length()) + (pixel_length() > 0 ? 1 : 0);
|
|
}
|
|
|
|
double Abs_Axis::tick_step(Range range) const {
|
|
const double raw = range.size() / 5.0;
|
|
if (!(raw > 0.0) || !std::isfinite(raw))
|
|
return 1.0;
|
|
const double scale = std::pow(10.0, std::floor(std::log10(raw)));
|
|
const double normalized = raw / scale;
|
|
const double nice = normalized <= 1.0 ? 1.0 : normalized <= 2.0 ? 2.0 : normalized <= 5.0 ? 5.0 : 10.0;
|
|
return nice * scale;
|
|
}
|
|
|
|
int Abs_Axis::sub_tick_count(double) const { return 4; }
|
|
|
|
std::string Abs_Axis::tick_label(double tick) const {
|
|
return localized_number(tick, 2, locale());
|
|
}
|
|
|
|
void Abs_Axis::paint(detail::Painter& painter) {
|
|
const State state = axis_state();
|
|
if (state.pixel_length == 0)
|
|
return;
|
|
const Range coordinates = coord_range();
|
|
const double step = tick_step(coordinates);
|
|
if (!(step > 0.0))
|
|
return;
|
|
|
|
const Pen axis_pen{state.color, 1.0};
|
|
const PointF first{static_cast<double>(state.x), static_cast<double>(state.y)};
|
|
const PointF last = state.orientation == Orientation::Horizontal
|
|
? PointF{first.x + state.pixel_length, first.y}
|
|
: PointF{first.x, first.y + state.pixel_length};
|
|
painter.line(first, last, axis_pen);
|
|
|
|
const auto [low, high] = std::minmax(coordinates.origin, coordinates.target);
|
|
const double initial = std::ceil(low / step) * step;
|
|
int tick_index{};
|
|
for (double tick = initial; tick <= high + step * 1e-6 && tick_index < 1000;
|
|
tick += step, ++tick_index) {
|
|
const double pixel = coord_to_pixel(tick);
|
|
PointF tick_start;
|
|
PointF tick_end;
|
|
PointF label;
|
|
if (state.orientation == Orientation::Horizontal) {
|
|
tick_start = {pixel, static_cast<double>(state.y)};
|
|
tick_end = {pixel, static_cast<double>(state.y + state.tick_length)};
|
|
label = {pixel + 2.0, static_cast<double>(state.y + state.tick_length + 2)};
|
|
} else {
|
|
tick_start = {static_cast<double>(state.x), pixel};
|
|
tick_end = {static_cast<double>(state.x + state.tick_length), pixel};
|
|
label = {static_cast<double>(state.x + state.tick_length + 2), pixel - 7.0};
|
|
}
|
|
painter.line(tick_start, tick_end, axis_pen);
|
|
painter.text(label, tick_label(tick), state.unit_text_font, state.unit_text_pen,
|
|
state.label_rotation_degrees);
|
|
const int subdivisions = std::max(0, sub_tick_count(step));
|
|
for (int sub_index = 1; sub_index <= subdivisions; ++sub_index) {
|
|
const double sub_tick = tick + step * sub_index / (subdivisions + 1.0);
|
|
if (sub_tick >= high)
|
|
break;
|
|
const double sub_pixel = coord_to_pixel(sub_tick);
|
|
if (state.orientation == Orientation::Horizontal) {
|
|
painter.line({sub_pixel, static_cast<double>(state.y)},
|
|
{sub_pixel, static_cast<double>(state.y + state.sub_tick_length)},
|
|
axis_pen);
|
|
} else {
|
|
painter.line({static_cast<double>(state.x), sub_pixel},
|
|
{static_cast<double>(state.x + state.sub_tick_length), sub_pixel},
|
|
axis_pen);
|
|
}
|
|
}
|
|
}
|
|
if (!state.unit_text.empty()) {
|
|
const PointF position{last.x + 4.0, last.y + 4.0};
|
|
const double estimated_width = std::max(4.0, state.unit_text.size() * state.unit_text_font.size * 0.65);
|
|
painter.rect({position.x - 2.0, position.y - 2.0,
|
|
estimated_width + 4.0, state.unit_text_font.size * 1.5 + 4.0},
|
|
Pen{.style = Line_Style::None}, state.unit_text_background_brush);
|
|
painter.text(position, state.unit_text,
|
|
state.unit_text_font, state.unit_text_pen);
|
|
}
|
|
}
|
|
|
|
Axis::Axis(Plot_Core& plot, Orientation orientation) : Abs_Axis(plot, orientation) {}
|
|
|
|
Range Axis::coord_range() const { std::lock_guard lock(interaction_mutex_); return coordinates_; }
|
|
int Axis::label_precision() const { std::lock_guard lock(interaction_mutex_); return precision_; }
|
|
void Axis::set_label_precision(int value) { { std::lock_guard lock(interaction_mutex_); precision_ = std::clamp(value, 0, 12); } changed(); }
|
|
double Axis::coord_start() const { return coord_range().origin; }
|
|
void Axis::set_coord_start(double value) { auto range = coord_range(); set_coord_range({value, value + range.length()}); }
|
|
double Axis::coord_length() const { return coord_range().length(); }
|
|
void Axis::set_coord_length(double value) { auto range = coord_range(); set_coord_range({range.origin, range.origin + value}); }
|
|
void Axis::set_coord_range(Range range) {
|
|
if (!valid_range(range))
|
|
return;
|
|
{
|
|
std::lock_guard lock(interaction_mutex_);
|
|
if (coordinates_ == range)
|
|
return;
|
|
coordinates_ = range;
|
|
}
|
|
changed();
|
|
}
|
|
void Axis::set_use_wheel(bool value) { std::lock_guard lock(interaction_mutex_); wheel_enabled_ = value; }
|
|
void Axis::set_use_drag(bool value) { std::lock_guard lock(interaction_mutex_); drag_enabled_ = value; }
|
|
bool Axis::use_wheel() const { std::lock_guard lock(interaction_mutex_); return wheel_enabled_; }
|
|
bool Axis::use_drag() const { std::lock_guard lock(interaction_mutex_); return drag_enabled_; }
|
|
|
|
void Axis::handle_event(const Event& event) {
|
|
if (event.type == Event_Type::Wheel && use_wheel()) {
|
|
const auto& wheel = static_cast<const Wheel_Event&>(event);
|
|
Range range = coord_range();
|
|
const double anchor_pixel = orientation() == Orientation::Horizontal ? wheel.position.x : wheel.position.y;
|
|
const double anchor = pixel_to_coord(anchor_pixel);
|
|
const double factor = wheel.angle_delta_y >= 0.0 ? 0.9 : 1.1;
|
|
set_coord_range({anchor + (range.origin - anchor) * factor,
|
|
anchor + (range.target - anchor) * factor});
|
|
event.accept();
|
|
return;
|
|
}
|
|
if (!use_drag())
|
|
return;
|
|
if (event.type == Event_Type::Pointer_Press) {
|
|
const auto& pointer = static_cast<const Pointer_Event&>(event);
|
|
if (pointer.button == Mouse_Button::Left) {
|
|
std::lock_guard lock(interaction_mutex_);
|
|
dragging_ = true;
|
|
last_pointer_ = pointer.position;
|
|
event.accept();
|
|
}
|
|
} else if (event.type == Event_Type::Pointer_Move) {
|
|
const auto& pointer = static_cast<const Pointer_Event&>(event);
|
|
PointF previous;
|
|
{
|
|
std::lock_guard lock(interaction_mutex_);
|
|
if (!dragging_)
|
|
return;
|
|
previous = last_pointer_;
|
|
last_pointer_ = pointer.position;
|
|
}
|
|
const double delta = orientation() == Orientation::Horizontal
|
|
? pointer.position.x - previous.x
|
|
: pointer.position.y - previous.y;
|
|
Range range = coord_range();
|
|
const double shift = pixel_length() == 0 ? 0.0 : -delta * range.length() / pixel_length();
|
|
set_coord_range({range.origin + shift, range.target + shift});
|
|
event.accept();
|
|
} else if (event.type == Event_Type::Pointer_Release) {
|
|
std::lock_guard lock(interaction_mutex_);
|
|
if (dragging_) {
|
|
dragging_ = false;
|
|
event.accept();
|
|
}
|
|
}
|
|
}
|
|
|
|
std::string Axis::tick_label(double tick) const {
|
|
return localized_number(tick, label_precision(), locale());
|
|
}
|
|
|
|
Axis::Builder::Builder(std::shared_ptr<Renderable> parent, Orientation orientation)
|
|
: Axis_Builder_Base(std::move(parent), orientation) {}
|
|
|
|
std::shared_ptr<Axis> Axis::Builder::build() {
|
|
if (!parent_)
|
|
return {};
|
|
auto result = parent_->plot().make_renderable<Axis>(parent_, orientation_);
|
|
apply(*result);
|
|
result->set_coord_range(coordinates_);
|
|
result->set_label_precision(precision_);
|
|
result->set_use_wheel(wheel_);
|
|
result->set_use_drag(drag_);
|
|
return result;
|
|
}
|
|
|
|
Frequency_Axis::Frequency_Axis(Plot_Core& plot, Orientation orientation) : Axis(plot, orientation) {}
|
|
|
|
std::string Frequency_Axis::tick_label(double tick) const {
|
|
const double absolute = std::abs(tick);
|
|
if (absolute >= 1'000'000.0)
|
|
return localized_number(tick / 1'000'000.0, label_precision(), locale()) + " MHz";
|
|
if (absolute >= 1'000.0)
|
|
return localized_number(tick / 1'000.0, label_precision(), locale()) + " kHz";
|
|
return localized_number(tick, label_precision(), locale()) + " Hz";
|
|
}
|
|
|
|
Frequency_Axis::Builder::Builder(std::shared_ptr<Renderable> parent, Orientation orientation)
|
|
: Axis_Builder_Base(std::move(parent), orientation) {}
|
|
|
|
std::shared_ptr<Frequency_Axis> Frequency_Axis::Builder::build() {
|
|
if (!parent_)
|
|
return {};
|
|
auto result = parent_->plot().make_renderable<Frequency_Axis>(parent_, orientation_);
|
|
apply(*result);
|
|
result->set_coord_range(coordinates_);
|
|
result->set_label_precision(precision_);
|
|
result->set_use_wheel(wheel_);
|
|
result->set_use_drag(drag_);
|
|
return result;
|
|
}
|
|
|
|
Time_Axis::Time_Axis(Plot_Core& plot, Orientation orientation) : Abs_Axis(plot, orientation) {}
|
|
|
|
int Time_Axis::visible_time_point_count() const { std::lock_guard lock(time_mutex_); return time_state_.visible_count; }
|
|
void Time_Axis::set_visible_time_point_count(int value) { { std::lock_guard lock(time_mutex_); time_state_.visible_count = std::max(2, value); } changed(); }
|
|
int Time_Axis::tick_label_spacing_px() const { std::lock_guard lock(time_mutex_); return time_state_.tick_label_spacing_px; }
|
|
void Time_Axis::set_tick_label_spacing_px(int value) { { std::lock_guard lock(time_mutex_); time_state_.tick_label_spacing_px = std::max(0, value); } changed(); }
|
|
std::string Time_Axis::time_format() const { std::lock_guard lock(time_mutex_); return time_state_.format; }
|
|
void Time_Axis::set_time_format(std::string value) { { std::lock_guard lock(time_mutex_); time_state_.format = std::move(value); } changed(); }
|
|
Font Time_Axis::font() const { return unit_text_font(); }
|
|
void Time_Axis::set_font(Font value) { set_unit_text_font(value); }
|
|
bool Time_Axis::newest_at_axis_start() const { std::lock_guard lock(time_mutex_); return time_state_.newest_at_start; }
|
|
void Time_Axis::set_newest_at_axis_start(bool value) { { std::lock_guard lock(time_mutex_); time_state_.newest_at_start = value; } changed(); }
|
|
std::size_t Time_Axis::time_point_count() const { std::lock_guard lock(time_mutex_); return time_state_.samples.size(); }
|
|
|
|
int Time_Axis::append_time(Time_Of_Day time) {
|
|
int tick{};
|
|
{
|
|
std::lock_guard lock(time_mutex_);
|
|
tick = time_state_.next_tick++;
|
|
time_state_.samples.emplace_back(tick, time);
|
|
const auto limit = static_cast<std::size_t>(std::max(512, time_state_.visible_count * 4));
|
|
while (time_state_.samples.size() > limit)
|
|
time_state_.samples.pop_front();
|
|
}
|
|
changed();
|
|
return tick;
|
|
}
|
|
|
|
Time_Of_Day Time_Axis::tick_to_time(int tick) const {
|
|
std::lock_guard lock(time_mutex_);
|
|
auto iterator = std::find_if(time_state_.samples.begin(), time_state_.samples.end(),
|
|
[tick](const auto& value) { return value.first == tick; });
|
|
return iterator == time_state_.samples.end() ? Time_Of_Day{} : iterator->second;
|
|
}
|
|
|
|
Range Time_Axis::coord_range() const {
|
|
std::lock_guard lock(time_mutex_);
|
|
const int latest = std::max(1, time_state_.next_tick - 1);
|
|
const int earliest = std::max(0, latest - time_state_.visible_count + 1);
|
|
return time_state_.newest_at_start ? Range{static_cast<double>(latest), static_cast<double>(earliest)}
|
|
: Range{static_cast<double>(earliest), static_cast<double>(latest)};
|
|
}
|
|
|
|
double Time_Axis::tick_step(Range range) const {
|
|
const double available = static_cast<double>(pixel_length());
|
|
const double label_width = std::max(48.0, font().size * 7.0);
|
|
const double spacing = static_cast<double>(tick_label_spacing_px());
|
|
const double label_count = std::max(1.0, available / (label_width + spacing));
|
|
return std::max(1.0, std::ceil(range.size() / label_count));
|
|
}
|
|
|
|
std::string Time_Axis::tick_label(double tick) const {
|
|
const Time_Of_Day time = tick_to_time(static_cast<int>(std::llround(tick)));
|
|
if (!time.valid())
|
|
return {};
|
|
return formatted_time(time, time_format());
|
|
}
|
|
|
|
Time_Axis::Builder::Builder(std::shared_ptr<Renderable> parent, Orientation orientation)
|
|
: Axis_Builder_Base(std::move(parent), orientation) {}
|
|
|
|
std::shared_ptr<Time_Axis> Time_Axis::Builder::build() {
|
|
if (!parent_)
|
|
return {};
|
|
auto result = parent_->plot().make_renderable<Time_Axis>(parent_, orientation_);
|
|
apply(*result);
|
|
result->set_visible_time_point_count(visible_count_);
|
|
result->set_tick_label_spacing_px(spacing_);
|
|
result->set_time_format(format_);
|
|
result->set_font(font_);
|
|
result->set_newest_at_axis_start(newest_at_start_);
|
|
return result;
|
|
}
|
|
|
|
} // namespace renderive
|