Files
Renderive/Core2/plottable/Heatmaps.cpp
T
2026-08-11 06:21:51 +08:00

485 lines
20 KiB
C++

#include "Plottables.h"
#include "../plot/Plot_Core.h"
#include "../render/Blend2D_Cache.h"
#include <algorithm>
#include <cmath>
#include <iomanip>
#include <optional>
#include <sstream>
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> 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<double>(column_count - 1);
};
const auto [position_low, position_high] =
std::minmax(position(clipped_low), position(clipped_high));
int first = std::clamp(static_cast<int>(std::floor(position_low)),
0, column_count - 1);
int last = std::clamp(static_cast<int>(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<double>(index) /
static_cast<double>(column_count - 1);
};
return Frequency_Columns{first, last, {coordinate(first), coordinate(last)}};
}
} // namespace
Waterfall::Waterfall(Plot_Core& plot,
std::shared_ptr<Frequency_Axis> frequency_axis,
std::shared_ptr<Time_Axis> 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<const double> values) {
std::shared_ptr<Time_Axis> 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<int>(values.size());
axis = state_.time_axis;
}
const std::size_t limit = axis
? static_cast<std::size_t>(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<double>&& values) {
append_row(tick, std::span<const double>(values.data(), values.size()));
}
void Waterfall::append_row(Time_Of_Day time, std::span<const double> 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<double>&& values) {
append_row(time, std::span<const double>(values.data(), values.size()));
}
std::shared_ptr<Frequency_Axis> Waterfall::frequency_axis() const { std::lock_guard lock(mutex_); return state_.frequency_axis; }
std::shared_ptr<Time_Axis> 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<int>(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<std::size_t>(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<int>(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<int>(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<Pixel> pixels(static_cast<std::size_t>(width) * height);
for (int y = 0; y < height; ++y) {
const auto& row = state.rows[static_cast<std::size_t>(y)].values;
for (int x = 0; x < width; ++x) {
pixels[static_cast<std::size_t>(y) * width + x] =
state.color_map.at_normalized(normalized_value(
row[static_cast<std::size_t>(columns->first + x)],
state.power_range));
}
}
const Axis_Transform vertical = state.time_axis->transform();
const Range time_range{static_cast<double>(state.rows.front().tick),
static_cast<double>(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<double>(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<Renderable> parent,
std::shared_ptr<Frequency_Axis> frequency_axis,
std::shared_ptr<Time_Axis> time_axis)
: parent_(std::move(parent)), frequency_axis_(std::move(frequency_axis)),
time_axis_(std::move(time_axis)) {}
std::shared_ptr<Waterfall> Waterfall::Builder::build() {
if (!parent_ || !frequency_axis_ || !time_axis_)
return {};
auto result = parent_->plot().make_renderable<Waterfall>(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> time_axis,
std::shared_ptr<Axis> 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<std::size_t>(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<Time_Axis> Frequency_Trace::time_axis() const { std::lock_guard lock(mutex_); return time_axis_; }
std::shared_ptr<Axis> 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> time_axis;
std::shared_ptr<Axis> value_axis;
std::deque<std::pair<int, double>> 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<PointF> 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<Renderable> parent,
std::shared_ptr<Time_Axis> time_axis,
std::shared_ptr<Axis> value_axis)
: parent_(std::move(parent)), time_axis_(std::move(time_axis)),
value_axis_(std::move(value_axis)) {}
std::shared_ptr<Frequency_Trace> Frequency_Trace::Builder::build() {
if (!parent_ || !time_axis_ || !value_axis_)
return {};
auto result = parent_->plot().make_renderable<Frequency_Trace>(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> frequency_axis,
std::shared_ptr<Axis> power_axis)
: Renderable(plot) {
state_.frequency_axis = std::move(frequency_axis);
state_.power_axis = std::move(power_axis);
}
std::shared_ptr<Frequency_Axis> Afterglow::frequency_axis() const { std::lock_guard lock(mutex_); return state_.frequency_axis; }
std::shared_ptr<Axis> 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<int>(state.history.back().size()));
const int height = state.power_count > 0
? state.power_count
: std::max(1, static_cast<int>(state.power_axis->pixel_length()));
return width > 0 && height > 0
? static_cast<std::size_t>(width) * static_cast<std::size_t>(height)
: 0;
}
void Afterglow::append_spectrum(std::span<const double> values) {
{
std::lock_guard lock(mutex_);
state_.history.emplace_back(values.begin(), values.end());
if (state_.frequency_count <= 0)
state_.frequency_count = static_cast<int>(values.size());
while (state_.history.size() > 64)
state_.history.pop_front();
}
changed();
}
void Afterglow::append_spectrum(std::pmr::vector<double>&& values) {
append_spectrum(std::span<const double>(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<int>(state.history.back().size()));
const int height = state.power_count > 0
? state.power_count
: std::max(1, static_cast<int>(state.power_axis->pixel_length()));
if (width <= 0 || height <= 0)
return;
std::vector<double> intensity(static_cast<std::size_t>(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<int>(spectrum.size()));
for (int x = 0; x < count; ++x) {
const double normalized = normalized_value(spectrum[static_cast<std::size_t>(x)],
state.power_range);
const int y = std::clamp(height - 1 - static_cast<int>(normalized * (height - 1)),
0, height - 1);
intensity[static_cast<std::size_t>(y) * width + x] += weight;
if (state.interpolate && y + 1 < height)
intensity[static_cast<std::size_t>(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<Pixel> 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<Renderable> parent,
std::shared_ptr<Frequency_Axis> frequency_axis,
std::shared_ptr<Axis> power_axis)
: parent_(std::move(parent)), frequency_axis_(std::move(frequency_axis)),
power_axis_(std::move(power_axis)) {}
std::shared_ptr<Afterglow> Afterglow::Builder::build() {
if (!parent_ || !frequency_axis_ || !power_axis_)
return {};
auto result = parent_->plot().make_renderable<Afterglow>(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