#include "Axis.h" #include "../plot/Plot_Core.h" #include "../render/Blend2D_Cache.h" #include #include #include #include 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((total / 3'600'000) % 24); const int minutes = static_cast((total / 60'000) % 60); const int seconds = static_cast((total / 1'000) % 60); const int milliseconds = static_cast(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(state.x) : static_cast(state.y), static_cast(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(std::abs(last - first)) + 1); } int Abs_Axis::pixel_sample_count() const { return static_cast(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(state.x), static_cast(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(state.y)}; tick_end = {pixel, static_cast(state.y + state.tick_length)}; label = {pixel + 2.0, static_cast(state.y + state.tick_length + 2)}; } else { tick_start = {static_cast(state.x), pixel}; tick_end = {static_cast(state.x + state.tick_length), pixel}; label = {static_cast(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(state.y)}, {sub_pixel, static_cast(state.y + state.sub_tick_length)}, axis_pen); } else { painter.line({static_cast(state.x), sub_pixel}, {static_cast(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(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(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(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 parent, Orientation orientation) : Axis_Builder_Base(std::move(parent), orientation) {} std::shared_ptr Axis::Builder::build() { if (!parent_) return {}; auto result = parent_->plot().make_renderable(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 parent, Orientation orientation) : Axis_Builder_Base(std::move(parent), orientation) {} std::shared_ptr Frequency_Axis::Builder::build() { if (!parent_) return {}; auto result = parent_->plot().make_renderable(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::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(latest), static_cast(earliest)} : Range{static_cast(earliest), static_cast(latest)}; } double Time_Axis::tick_step(Range range) const { const double available = static_cast(pixel_length()); const double label_width = std::max(48.0, font().size * 7.0); const double spacing = static_cast(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(std::llround(tick))); if (!time.valid()) return {}; return formatted_time(time, time_format()); } Time_Axis::Builder::Builder(std::shared_ptr parent, Orientation orientation) : Axis_Builder_Base(std::move(parent), orientation) {} std::shared_ptr Time_Axis::Builder::build() { if (!parent_) return {}; auto result = parent_->plot().make_renderable(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