#include "Plottables.h" #include "../plot/Plot_Core.h" #include "../render/Blend2D_Cache.h" #include #include #include #include #include namespace renderive { namespace { RectF mapped_rect(const Axis_Transform& horizontal, const Axis_Transform& vertical, Range horizontal_range, Range vertical_range) { const double x1 = horizontal.coord_to_pixel(horizontal_range.origin); const double x2 = horizontal.coord_to_pixel(horizontal_range.target); const double y1 = vertical.coord_to_pixel(vertical_range.origin); const double y2 = vertical.coord_to_pixel(vertical_range.target); return {std::min(x1, x2), std::min(y1, y2), std::abs(x2 - x1), std::abs(y2 - y1)}; } double normalized_value(double value, Range range) { if (range.length() == 0.0) return 0.0; return std::clamp((value - range.origin) / range.length(), 0.0, 1.0); } struct Frequency_Columns { int first{}; int last{}; Range range; }; std::optional frequency_columns(Range data_range, Range visible_range, int column_count, bool visible_only) { if (column_count <= 0) return std::nullopt; if (!visible_only || column_count == 1 || data_range.length() == 0.0) return Frequency_Columns{0, column_count - 1, data_range}; const auto [data_low, data_high] = std::minmax(data_range.origin, data_range.target); const auto [visible_low, visible_high] = std::minmax(visible_range.origin, visible_range.target); const double clipped_low = std::max(data_low, visible_low); const double clipped_high = std::min(data_high, visible_high); if (clipped_low > clipped_high) return std::nullopt; const auto position = [data_range, column_count](double coordinate) { return (coordinate - data_range.origin) / data_range.length() * static_cast(column_count - 1); }; const auto [position_low, position_high] = std::minmax(position(clipped_low), position(clipped_high)); int first = std::clamp(static_cast(std::floor(position_low)), 0, column_count - 1); int last = std::clamp(static_cast(std::ceil(position_high)), first, column_count - 1); if (first == last) { if (last + 1 < column_count) ++last; else if (first > 0) --first; } const auto coordinate = [data_range, column_count](int index) { return data_range.origin + data_range.length() * static_cast(index) / static_cast(column_count - 1); }; return Frequency_Columns{first, last, {coordinate(first), coordinate(last)}}; } } // namespace Waterfall::Waterfall(Plot_Core& plot, std::shared_ptr frequency_axis, std::shared_ptr time_axis) : Renderable(plot) { state_.frequency_axis = std::move(frequency_axis); state_.time_axis = std::move(time_axis); } void Waterfall::append_row(int tick, std::span values) { std::shared_ptr axis; { std::lock_guard lock(mutex_); state_.rows.push_back(Row{tick, {values.begin(), values.end()}}); if (state_.bin_count <= 0) state_.bin_count = static_cast(values.size()); axis = state_.time_axis; } const std::size_t limit = axis ? static_cast(std::max(2, axis->visible_time_point_count())) : 256; { std::lock_guard lock(mutex_); while (state_.rows.size() > limit) state_.rows.pop_front(); } changed(); } void Waterfall::append_row(int tick, std::pmr::vector&& values) { append_row(tick, std::span(values.data(), values.size())); } void Waterfall::append_row(Time_Of_Day time, std::span values) { auto axis = time_axis(); if (!axis) return; append_row(axis->append_time(time), values); } void Waterfall::append_row(Time_Of_Day time, std::pmr::vector&& values) { append_row(time, std::span(values.data(), values.size())); } std::shared_ptr Waterfall::frequency_axis() const { std::lock_guard lock(mutex_); return state_.frequency_axis; } std::shared_ptr Waterfall::time_axis() const { std::lock_guard lock(mutex_); return state_.time_axis; } #define RENDERIVE_WATERFALL_PROPERTY(Type, Method, Field) \ Type Waterfall::Method() const { std::lock_guard lock(mutex_); return state_.Field; } \ void Waterfall::set_##Method(Type value) { { std::lock_guard lock(mutex_); state_.Field = std::move(value); } changed(); } RENDERIVE_WATERFALL_PROPERTY(Range, frequency_range, frequency_range) RENDERIVE_WATERFALL_PROPERTY(Range, power_range, power_range) RENDERIVE_WATERFALL_PROPERTY(bool, visible_range_only, visible_only) RENDERIVE_WATERFALL_PROPERTY(Image_Interpolation_Mode, interpolation_mode, interpolation) #undef RENDERIVE_WATERFALL_PROPERTY int Waterfall::frequency_bin_count() const { std::lock_guard lock(mutex_); return state_.bin_count; } void Waterfall::set_frequency_bin_count(int value) { { std::lock_guard lock(mutex_); state_.bin_count = std::max(0, value); } changed(); } std::size_t Waterfall::row_count() const { std::lock_guard lock(mutex_); return state_.rows.size(); } std::size_t Waterfall::stored_point_count() const { std::lock_guard lock(mutex_); std::size_t count{}; for (const auto& row : state_.rows) count += row.values.size(); return count; } std::size_t Waterfall::rendered_cell_count() const { const State state = state_snapshot(); if (!state.frequency_axis || state.rows.empty()) return 0; const int source_width = std::min( state.bin_count, static_cast(std::min_element( state.rows.begin(), state.rows.end(), [](const Row& left, const Row& right) { return left.values.size() < right.values.size(); })->values.size())); if (source_width <= 0) return 0; const auto columns = frequency_columns(state.frequency_range, state.frequency_axis->coord_range(), source_width, state.visible_only); return columns ? static_cast(columns->last - columns->first + 1) * state.rows.size() : 0; } Waterfall::State Waterfall::state_snapshot() const { std::lock_guard lock(mutex_); return state_; } void Waterfall::handle_event(const Event& event) { update_hover(event); } void Waterfall::paint(detail::Painter& painter) { const State state = state_snapshot(); if (!state.frequency_axis || !state.time_axis || state.rows.empty()) return; const int source_width = std::min(state.bin_count, static_cast(std::min_element( state.rows.begin(), state.rows.end(), [](const Row& left, const Row& right) { return left.values.size() < right.values.size(); })->values.size())); const int height = static_cast(state.rows.size()); if (source_width <= 0 || height <= 0) return; const Axis_Transform horizontal = state.frequency_axis->transform(); const auto columns = frequency_columns(state.frequency_range, horizontal.coordinate_range, source_width, state.visible_only); if (!columns) return; const int width = columns->last - columns->first + 1; std::vector pixels(static_cast(width) * height); for (int y = 0; y < height; ++y) { const auto& row = state.rows[static_cast(y)].values; for (int x = 0; x < width; ++x) { pixels[static_cast(y) * width + x] = state.color_map.at_normalized(normalized_value( row[static_cast(columns->first + x)], state.power_range)); } } const Axis_Transform vertical = state.time_axis->transform(); const Range time_range{static_cast(state.rows.front().tick), static_cast(state.rows.back().tick)}; RectF target = mapped_rect(horizontal, vertical, columns->range, time_range); if (target.height < 1.0) target.height = std::max(1.0, static_cast(state.time_axis->pixel_length())); painter.heatmap(target, width, height, pixels, state.interpolation); const Hover_Tooltip_Snapshot tooltip = tooltip_snapshot(); if (tooltip.enabled && tooltip.active && target.contains(tooltip.position)) { const double frequency = horizontal.pixel_to_coord(tooltip.position.x); std::ostringstream text; text << std::fixed << std::setprecision(2) << frequency << " Hz"; const RectF box{tooltip.position.x + 8.0, tooltip.position.y + 8.0, 110.0, 24.0}; painter.rect(box, Pen{tooltip.text_pen.color}, tooltip.background); painter.text({box.x + 4.0, box.y + 3.0}, text.str(), tooltip.font, tooltip.text_pen); } } Waterfall::Builder::Builder(std::shared_ptr parent, std::shared_ptr frequency_axis, std::shared_ptr time_axis) : parent_(std::move(parent)), frequency_axis_(std::move(frequency_axis)), time_axis_(std::move(time_axis)) {} std::shared_ptr Waterfall::Builder::build() { if (!parent_ || !frequency_axis_ || !time_axis_) return {}; auto result = parent_->plot().make_renderable(parent_, frequency_axis_, time_axis_); result->set_frequency_range(frequency_range_); result->set_power_range(power_range_); result->set_frequency_bin_count(bin_count_); result->set_visible_range_only(visible_only_); result->set_interpolation_mode(interpolation_); { std::lock_guard lock(result->mutex_); result->state_.color_map = color_map_; } return result; } Frequency_Trace::Frequency_Trace(Plot_Core& plot, std::shared_ptr time_axis, std::shared_ptr value_axis) : Renderable(plot), time_axis_(std::move(time_axis)), value_axis_(std::move(value_axis)) {} void Frequency_Trace::append_sample(int tick, double value) { int limit = 512; { std::lock_guard lock(mutex_); samples_.emplace_back(tick, value); if (time_axis_) limit = std::max(2, time_axis_->visible_time_point_count()); while (samples_.size() > static_cast(limit)) samples_.pop_front(); } changed(); } void Frequency_Trace::append_sample(Time_Of_Day time, double value) { auto axis = time_axis(); if (axis) append_sample(axis->append_time(time), value); } std::shared_ptr Frequency_Trace::time_axis() const { std::lock_guard lock(mutex_); return time_axis_; } std::shared_ptr Frequency_Trace::value_axis() const { std::lock_guard lock(mutex_); return value_axis_; } std::size_t Frequency_Trace::sample_count() const { std::lock_guard lock(mutex_); return samples_.size(); } std::size_t Frequency_Trace::rendered_point_count() const { std::lock_guard lock(mutex_); return time_axis_ && value_axis_ && samples_.size() >= 2 ? samples_.size() : 0; } void Frequency_Trace::paint(detail::Painter& painter) { std::shared_ptr time_axis; std::shared_ptr value_axis; std::deque> samples; Pen pen; { std::lock_guard lock(mutex_); time_axis = time_axis_; value_axis = value_axis_; samples = samples_; pen = pen_; } if (!time_axis || !value_axis || samples.size() < 2) return; const Axis_Transform x = time_axis->transform(); const Axis_Transform y = value_axis->transform(); std::vector points; points.reserve(samples.size()); for (const auto& [tick, value] : samples) points.push_back({x.coord_to_pixel(tick), y.coord_to_pixel(value)}); painter.polyline(points, pen); } Frequency_Trace::Builder::Builder(std::shared_ptr parent, std::shared_ptr time_axis, std::shared_ptr value_axis) : parent_(std::move(parent)), time_axis_(std::move(time_axis)), value_axis_(std::move(value_axis)) {} std::shared_ptr Frequency_Trace::Builder::build() { if (!parent_ || !time_axis_ || !value_axis_) return {}; auto result = parent_->plot().make_renderable(parent_, time_axis_, value_axis_); { std::lock_guard lock(result->mutex_); result->pen_ = pen_; } return result; } Afterglow::Afterglow(Plot_Core& plot, std::shared_ptr frequency_axis, std::shared_ptr power_axis) : Renderable(plot) { state_.frequency_axis = std::move(frequency_axis); state_.power_axis = std::move(power_axis); } std::shared_ptr Afterglow::frequency_axis() const { std::lock_guard lock(mutex_); return state_.frequency_axis; } std::shared_ptr Afterglow::power_axis() const { std::lock_guard lock(mutex_); return state_.power_axis; } #define RENDERIVE_AFTERGLOW_PROPERTY(Type, Method, Field) \ Type Afterglow::Method() const { std::lock_guard lock(mutex_); return state_.Field; } \ void Afterglow::set_##Method(Type value) { { std::lock_guard lock(mutex_); state_.Field = std::move(value); } changed(); } RENDERIVE_AFTERGLOW_PROPERTY(Range, frequency_range, frequency_range) RENDERIVE_AFTERGLOW_PROPERTY(Range, power_range, power_range) RENDERIVE_AFTERGLOW_PROPERTY(bool, interpolate, interpolate) #undef RENDERIVE_AFTERGLOW_PROPERTY int Afterglow::frequency_point_size() const { std::lock_guard lock(mutex_); return state_.frequency_count; } void Afterglow::set_frequency_point_size(int value) { { std::lock_guard lock(mutex_); state_.frequency_count = std::max(0, value); } changed(); } int Afterglow::power_point_size() const { std::lock_guard lock(mutex_); return state_.power_count; } void Afterglow::set_power_point_size(int value) { { std::lock_guard lock(mutex_); state_.power_count = std::max(0, value); } changed(); } double Afterglow::attenuation_rate() const { std::lock_guard lock(mutex_); return state_.attenuation; } void Afterglow::set_attenuation_rate(double value) { if (!std::isfinite(value)) value = 0.0; { std::lock_guard lock(mutex_); state_.attenuation = std::clamp(value, 0.0, 1.0); } changed(); } std::size_t Afterglow::history_count() const { std::lock_guard lock(mutex_); return state_.history.size(); } std::size_t Afterglow::latest_spectrum_point_count() const { std::lock_guard lock(mutex_); return state_.history.empty() ? 0 : state_.history.back().size(); } std::size_t Afterglow::rendered_cell_count() const { const State state = state_snapshot(); if (!state.power_axis || state.history.empty()) return 0; const int width = std::min(state.frequency_count, static_cast(state.history.back().size())); const int height = state.power_count > 0 ? state.power_count : std::max(1, static_cast(state.power_axis->pixel_length())); return width > 0 && height > 0 ? static_cast(width) * static_cast(height) : 0; } void Afterglow::append_spectrum(std::span values) { { std::lock_guard lock(mutex_); state_.history.emplace_back(values.begin(), values.end()); if (state_.frequency_count <= 0) state_.frequency_count = static_cast(values.size()); while (state_.history.size() > 64) state_.history.pop_front(); } changed(); } void Afterglow::append_spectrum(std::pmr::vector&& values) { append_spectrum(std::span(values.data(), values.size())); } Afterglow::State Afterglow::state_snapshot() const { std::lock_guard lock(mutex_); return state_; } void Afterglow::paint(detail::Painter& painter) { const State state = state_snapshot(); if (!state.frequency_axis || !state.power_axis || state.history.empty()) return; const int width = std::min(state.frequency_count, static_cast(state.history.back().size())); const int height = state.power_count > 0 ? state.power_count : std::max(1, static_cast(state.power_axis->pixel_length())); if (width <= 0 || height <= 0) return; std::vector intensity(static_cast(width) * height); double weight = 1.0; const double decay = 1.0 - state.attenuation; for (auto iterator = state.history.rbegin(); iterator != state.history.rend(); ++iterator) { const auto& spectrum = *iterator; const int count = std::min(width, static_cast(spectrum.size())); for (int x = 0; x < count; ++x) { const double normalized = normalized_value(spectrum[static_cast(x)], state.power_range); const int y = std::clamp(height - 1 - static_cast(normalized * (height - 1)), 0, height - 1); intensity[static_cast(y) * width + x] += weight; if (state.interpolate && y + 1 < height) intensity[static_cast(y + 1) * width + x] += weight * 0.35; } weight *= decay; if (weight < 0.01) break; } const double maximum = std::max(1.0, *std::max_element(intensity.begin(), intensity.end())); std::vector pixels(intensity.size()); for (std::size_t index = 0; index < pixels.size(); ++index) pixels[index] = state.color_map.at_normalized(intensity[index] / maximum); const RectF target = mapped_rect(state.frequency_axis->transform(), state.power_axis->transform(), state.frequency_range, state.power_range); painter.heatmap(target, width, height, pixels, Image_Interpolation_Mode::Bilinear); } Afterglow::Builder::Builder(std::shared_ptr parent, std::shared_ptr frequency_axis, std::shared_ptr power_axis) : parent_(std::move(parent)), frequency_axis_(std::move(frequency_axis)), power_axis_(std::move(power_axis)) {} std::shared_ptr Afterglow::Builder::build() { if (!parent_ || !frequency_axis_ || !power_axis_) return {}; auto result = parent_->plot().make_renderable(parent_, frequency_axis_, power_axis_); result->set_frequency_range(frequency_range_); result->set_power_range(power_range_); result->set_frequency_point_size(frequency_count_); result->set_power_point_size(power_count_); result->set_interpolate(interpolate_); result->set_attenuation_rate(attenuation_); { std::lock_guard lock(result->mutex_); result->state_.color_map = color_map_; } return result; } } // namespace renderive