三维频谱做好
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
#include "Axis.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <iomanip>
|
||||
#include <limits>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace aethera::plot {
|
||||
namespace {
|
||||
|
||||
std::string trim_number(std::string value) {
|
||||
const auto exponent = value.find_first_of("eE");
|
||||
const auto fraction_end = exponent == std::string::npos ? value.size() : exponent;
|
||||
const auto decimal = value.find('.');
|
||||
if (decimal == std::string::npos || decimal >= fraction_end) return value;
|
||||
auto last = fraction_end;
|
||||
while (last > decimal + 1 && value[last - 1] == '0') --last;
|
||||
if (last == decimal + 1) --last;
|
||||
value.erase(last, fraction_end - last);
|
||||
return value;
|
||||
}
|
||||
|
||||
std::string numeric_label(double value, int precision) {
|
||||
std::ostringstream stream;
|
||||
const auto magnitude = std::abs(value);
|
||||
std::string suffix;
|
||||
if (magnitude >= 1.0e3 && magnitude < 1.0e6) {
|
||||
value /= 1.0e3;
|
||||
suffix = "k";
|
||||
stream << std::fixed;
|
||||
} else if ((magnitude >= 1.0e6 || (magnitude > 0.0 && magnitude < 1.0e-4)))
|
||||
stream << std::scientific;
|
||||
else
|
||||
stream << std::fixed;
|
||||
stream << std::setprecision(std::clamp(precision, 0, 12)) << value;
|
||||
auto result = trim_number(stream.str()) + suffix;
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<Axis_Tick> linear_ticks(const Axis_Descriptor& axis) {
|
||||
const auto step = nice_tick_step(axis.range, axis.target_tick_count);
|
||||
if (!(step > 0.0) || !std::isfinite(step)) return {};
|
||||
const auto [minimum, maximum] = std::minmax(axis.range.origin, axis.range.target);
|
||||
const auto first = std::ceil(minimum / step) * step;
|
||||
std::vector<Axis_Tick> ticks;
|
||||
const auto maximum_count = std::max<std::size_t>(2, axis.target_tick_count * 4 + 4);
|
||||
for (std::size_t index = 0; index < maximum_count; ++index) {
|
||||
const auto coordinate = first + static_cast<double>(index) * step;
|
||||
if (coordinate > maximum + step * 1.0e-9) break;
|
||||
ticks.push_back({coordinate, numeric_label(coordinate, axis.precision)});
|
||||
}
|
||||
if (axis.range.length() < 0.0) std::ranges::reverse(ticks);
|
||||
return ticks;
|
||||
}
|
||||
|
||||
std::vector<Axis_Tick> logarithmic_ticks(const Axis_Descriptor& axis) {
|
||||
const auto [minimum, maximum] = std::minmax(axis.range.origin, axis.range.target);
|
||||
if (!(minimum > 0.0) || !std::isfinite(maximum)) return {};
|
||||
const auto first_power = static_cast<int>(std::floor(std::log10(minimum)));
|
||||
const auto last_power = static_cast<int>(std::ceil(std::log10(maximum)));
|
||||
const auto decade_count = static_cast<std::size_t>(last_power - first_power + 1);
|
||||
const auto multiples = axis.target_tick_count <= decade_count
|
||||
? std::vector{1.0}
|
||||
: axis.target_tick_count <= decade_count * 2
|
||||
? std::vector{1.0, 5.0}
|
||||
: std::vector{1.0, 2.0, 5.0};
|
||||
std::vector<Axis_Tick> ticks;
|
||||
for (int power = first_power; power <= last_power; ++power) {
|
||||
const auto decade = std::pow(10.0, static_cast<double>(power));
|
||||
for (const double multiple : multiples) {
|
||||
const auto coordinate = multiple * decade;
|
||||
if (coordinate >= minimum * (1.0 - 1.0e-12) &&
|
||||
coordinate <= maximum * (1.0 + 1.0e-12))
|
||||
ticks.push_back({coordinate, numeric_label(coordinate, axis.precision)});
|
||||
}
|
||||
}
|
||||
const auto add_boundary = [&](double coordinate) {
|
||||
const auto tolerance = coordinate * 1.0e-9;
|
||||
if (std::ranges::none_of(ticks, [&](const Axis_Tick& tick) {
|
||||
return std::abs(tick.coordinate - coordinate) <= tolerance;
|
||||
})) {
|
||||
const auto logarithmic_span = std::log(maximum / minimum);
|
||||
const auto minimum_spacing = 0.5 /
|
||||
static_cast<double>(std::max<std::size_t>(2, axis.target_tick_count) - 1);
|
||||
if (logarithmic_span > 0.0) {
|
||||
const auto nearest = std::ranges::min_element(
|
||||
ticks, {}, [&](const Axis_Tick& tick) {
|
||||
return std::abs(std::log(tick.coordinate / coordinate)) /
|
||||
logarithmic_span;
|
||||
});
|
||||
if (nearest != ticks.end() &&
|
||||
std::abs(std::log(nearest->coordinate / coordinate)) /
|
||||
logarithmic_span < minimum_spacing)
|
||||
ticks.erase(nearest);
|
||||
}
|
||||
ticks.push_back({coordinate, numeric_label(coordinate, axis.precision)});
|
||||
}
|
||||
};
|
||||
add_boundary(minimum);
|
||||
add_boundary(maximum);
|
||||
std::ranges::sort(ticks, {}, &Axis_Tick::coordinate);
|
||||
if (axis.range.length() < 0.0) std::ranges::reverse(ticks);
|
||||
return ticks;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Axis_Coordinate Axis_Range::size() const noexcept {
|
||||
return std::abs(length());
|
||||
}
|
||||
|
||||
Axis_Coordinate Axis_Range::length() const noexcept {
|
||||
return target - origin;
|
||||
}
|
||||
|
||||
Axis_Coordinate Axis_Range::center() const noexcept {
|
||||
return origin + length() * 0.5;
|
||||
}
|
||||
|
||||
bool Axis_Range::contains(Axis_Coordinate coordinate) const noexcept {
|
||||
const auto [minimum, maximum] = std::minmax(origin, target);
|
||||
return coordinate >= minimum - 1.0e-9 && coordinate <= maximum + 1.0e-9;
|
||||
}
|
||||
|
||||
Axis_Coordinate nice_tick_step(Axis_Range range, std::size_t target_tick_count) {
|
||||
if (!std::isfinite(range.origin) || !std::isfinite(range.target) || range.size() <= 0.0)
|
||||
throw std::invalid_argument("axis range must be finite and non-empty");
|
||||
const auto count = std::max<std::size_t>(2, target_tick_count);
|
||||
const auto raw = range.size() / static_cast<double>(count - 1);
|
||||
const auto magnitude = std::pow(10.0, std::floor(std::log10(raw)));
|
||||
const auto normalized = raw / magnitude;
|
||||
const auto nice = normalized <= 1.0 ? 1.0 : normalized <= 2.0 ? 2.0 :
|
||||
normalized <= 5.0 ? 5.0 : 10.0;
|
||||
return nice * magnitude;
|
||||
}
|
||||
|
||||
std::vector<Axis_Tick> axis_ticks(const Axis_Descriptor& axis) {
|
||||
if (!axis.visible) return {};
|
||||
if (axis.scale == Axis_Scale::logarithmic) return logarithmic_ticks(axis);
|
||||
return linear_ticks(axis);
|
||||
}
|
||||
|
||||
} // namespace aethera::plot
|
||||
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace aethera::plot {
|
||||
|
||||
using Axis_Coordinate = double;
|
||||
|
||||
enum class Axis_Scale : std::uint8_t {
|
||||
linear,
|
||||
logarithmic,
|
||||
time
|
||||
};
|
||||
|
||||
struct Axis_Range {
|
||||
Axis_Coordinate origin{};
|
||||
Axis_Coordinate target{};
|
||||
|
||||
[[nodiscard]] Axis_Coordinate size() const noexcept;
|
||||
[[nodiscard]] Axis_Coordinate length() const noexcept;
|
||||
[[nodiscard]] Axis_Coordinate center() const noexcept;
|
||||
[[nodiscard]] bool contains(Axis_Coordinate coordinate) const noexcept;
|
||||
bool operator==(const Axis_Range&) const = default;
|
||||
};
|
||||
|
||||
struct Axis_Point {
|
||||
Axis_Coordinate horizontal{};
|
||||
Axis_Coordinate vertical{};
|
||||
bool operator==(const Axis_Point&) const = default;
|
||||
};
|
||||
|
||||
struct Axis_Rectangle {
|
||||
Axis_Range horizontal{};
|
||||
Axis_Range vertical{};
|
||||
bool operator==(const Axis_Rectangle&) const = default;
|
||||
};
|
||||
|
||||
struct Axis_Descriptor {
|
||||
Axis_Range range{};
|
||||
Axis_Scale scale{Axis_Scale::linear};
|
||||
std::string label{};
|
||||
std::string unit{};
|
||||
std::size_t target_tick_count{6};
|
||||
int precision{2};
|
||||
bool visible{true};
|
||||
bool grid_visible{true};
|
||||
bool labels_visible{true};
|
||||
bool operator==(const Axis_Descriptor&) const = default;
|
||||
};
|
||||
|
||||
struct Axis_Tick {
|
||||
Axis_Coordinate coordinate{};
|
||||
std::string label{};
|
||||
bool operator==(const Axis_Tick&) const = default;
|
||||
};
|
||||
|
||||
[[nodiscard]] Axis_Coordinate nice_tick_step(Axis_Range range,
|
||||
std::size_t target_tick_count = 6);
|
||||
[[nodiscard]] std::vector<Axis_Tick> axis_ticks(const Axis_Descriptor& axis);
|
||||
|
||||
} // namespace aethera::plot
|
||||
@@ -0,0 +1,51 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace aethera::plot {
|
||||
|
||||
struct Spatial_Point {
|
||||
double x{};
|
||||
double y{};
|
||||
double z{};
|
||||
bool operator==(const Spatial_Point&) const = default;
|
||||
};
|
||||
|
||||
enum class Camera_Projection : std::uint8_t {
|
||||
perspective,
|
||||
orthographic
|
||||
};
|
||||
|
||||
struct Camera_View {
|
||||
Spatial_Point eye{2.8, -3.2, 2.4};
|
||||
Spatial_Point target{};
|
||||
Spatial_Point up{0.0, 0.0, 1.0};
|
||||
bool operator==(const Camera_View&) const = default;
|
||||
};
|
||||
|
||||
struct Camera_Control {
|
||||
double yaw_speed{0.20};
|
||||
double pitch_speed{0.20};
|
||||
double zoom_speed{0.10};
|
||||
double pan_speed{0.002};
|
||||
double minimum_pitch{-1.45};
|
||||
double maximum_pitch{1.45};
|
||||
double minimum_distance{0.25};
|
||||
double maximum_distance{50.0};
|
||||
bool rotate_enabled{true};
|
||||
bool zoom_enabled{true};
|
||||
bool pan_enabled{true};
|
||||
bool operator==(const Camera_Control&) const = default;
|
||||
};
|
||||
|
||||
struct Camera_Descriptor {
|
||||
Camera_View initial_view{};
|
||||
Camera_Projection projection{Camera_Projection::perspective};
|
||||
Camera_Control control{};
|
||||
double vertical_field_of_view_degrees{45.0};
|
||||
double near_plane{0.01};
|
||||
double far_plane{100.0};
|
||||
bool operator==(const Camera_Descriptor&) const = default;
|
||||
};
|
||||
|
||||
} // namespace aethera::plot
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include "Axis.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace aethera::plot {
|
||||
|
||||
struct Time_Domain {
|
||||
Axis_Range seconds{};
|
||||
std::size_t visible_sample_count{};
|
||||
bool realtime{};
|
||||
bool newest_at_start{};
|
||||
bool operator==(const Time_Domain&) const = default;
|
||||
};
|
||||
|
||||
} // namespace aethera::plot
|
||||
Reference in New Issue
Block a user