Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2295,6 +2295,16 @@ version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"

[[package]]
name = "log_axes"
version = "0.1.0"
dependencies = [
"eframe",
"egui_plot",
"env_logger",
"examples_utils",
]

[[package]]
name = "markers"
version = "0.1.0"
Expand Down
124 changes: 124 additions & 0 deletions egui_plot/src/axis/linear.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
use crate::GridInput;
use crate::axis::transform::AxisSpace;
use emath::{Rangef, remap};
use std::ops::RangeInclusive;

#[derive(Debug, Copy, Clone)]
pub struct LinearAxisSpace {
min: f64,
max: f64,
invert: bool,
frame_start: f32,
frame_end: f32,
}

impl LinearAxisSpace {
pub fn new(value_range: RangeInclusive<f64>, frame_range: Rangef, invert: bool) -> Self {
Self {
min: *value_range.start(),
max: *value_range.end(),
frame_start: frame_range.min,
frame_end: frame_range.max,
invert,
}
}
fn clamp_to_finite(&mut self) {
self.min = self.min.clamp(f64::MIN, f64::MAX);
if self.min.is_nan() {
self.min = 0.0;
}

self.max = self.max.clamp(f64::MIN, f64::MAX);
if self.max.is_nan() {
self.max = 0.0;
}
}

/// Specifies the output range of the space, inverting it
/// if inversion is selected.
fn frame_range(&self) -> RangeInclusive<f32> {
if self.invert {
(self.frame_end)..=(self.frame_start)
} else {
(self.frame_start)..=(self.frame_end)
}
}

/// Get the frame range as f64 format. This is not the natural
/// format for screen units but is needed for remapping to and
/// from values.
fn frame_range_f64(&self) -> RangeInclusive<f64> {
let as_f32 = self.frame_range();
*as_f32.start() as f64..=*as_f32.end() as f64
}

fn dvalue_per_dpos(&self) -> f64 {
let frame_range = self.frame_range_f64();
self.value_length() / (frame_range.end() - frame_range.start())
}
}

impl AxisSpace for LinearAxisSpace {
fn value_min(&self) -> f64 {
self.min
}

fn value_max(&self) -> f64 {
self.max
}

fn frame_min(&self) -> f32 {
self.frame_start
}

fn frame_max(&self) -> f32 {
self.frame_end
}

fn set_inverted(&mut self, invert: bool) {
self.invert = invert;
}

fn set_value_range(&mut self, range: RangeInclusive<f64>) {
self.min = *range.start();
self.max = *range.end();
self.clamp_to_finite();
}

fn position_from_value(&self, value: f64) -> f32 {
remap(value, self.min..=self.max, self.frame_range_f64()) as f32
}

fn value_from_position(&self, position: f32) -> f64 {
remap(position as f64, self.frame_range_f64(), self.min..=self.max)
}

fn position_delta_from_screen_delta(&self, _start_position: f64, drag_delta: f32) -> f64 {
drag_delta as f64 * self.dvalue_per_dpos()
}

fn grid_input(&self, spacing: f32) -> GridInput {
GridInput {
bounds: (self.min, self.max),
base_step_size: self.dvalue_per_dpos().abs() * (spacing as f64),
}
}

fn screen_distance_between_values(&self, value1: f64, value2: f64) -> f32 {
let delta = value2 - value1;
(delta / self.dvalue_per_dpos()) as f32
}

fn translate(&mut self, frame_distance: f32) {
let dvalue_per_dpos = self.dvalue_per_dpos();
let value_translation = frame_distance as f64 * dvalue_per_dpos;
self.min += value_translation;
self.max += value_translation;
self.clamp_to_finite();
}

fn zoom(&mut self, zoom_factor: f32, center: f64) {
self.min = center + (self.min - center) / (zoom_factor as f64);
self.max = center + (self.max - center) / (zoom_factor as f64);
}
}
212 changes: 212 additions & 0 deletions egui_plot/src/axis/log.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
use crate::GridInput;
use crate::axis::linear::LinearAxisSpace;
use crate::axis::transform::AxisSpace;
use emath::Rangef;
use std::ops::RangeInclusive;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogBase {
Base10,
Base2,
}

impl LogBase {
/// Returns the exponent of the given value (the logarithm of the value).
///
/// Returns `None` if the value is negative or zero.
pub fn exponent(&self, value: f64) -> Option<f64> {
if value <= 0.0 {
return None;
}
match self {
Self::Base10 => Some(value.log10()),
Self::Base2 => Some(value.log2()),
}
}

pub fn power(&self, value: f64) -> f64 {
match self {
Self::Base10 => f64::powf(10.0, value),
Self::Base2 => value.exp2(),
}
}
}

#[derive(Debug, Clone, Copy)]
pub struct LogAxis {
/// A linear scale for the exponent of the logarithm.
///
/// This allows us to reuse much of the linear logic.
exponent_scale: LinearAxisSpace,
/// The base of the logarithm.
base: LogBase,
}

impl LogAxis {
pub fn new(value_range: RangeInclusive<f64>, frame_range: Rangef, invert: bool, base: LogBase) -> Self {
let exponent_min = base.exponent(*value_range.start()).unwrap_or(-9.0);
let exponent_max = base.exponent(*value_range.end()).unwrap_or(-9.0);
Self {
exponent_scale: LinearAxisSpace::new(exponent_min..=exponent_max, frame_range, invert),
base,
}
}
}

impl AxisSpace for LogAxis {
fn value_min(&self) -> f64 {
self.base.power(self.exponent_scale.value_min())
}

fn value_max(&self) -> f64 {
self.base.power(self.exponent_scale.value_max())
}

fn frame_min(&self) -> f32 {
self.exponent_scale.frame_min()
}

fn frame_max(&self) -> f32 {
self.exponent_scale.frame_max()
}

fn set_inverted(&mut self, invert: bool) {
self.exponent_scale.set_inverted(invert);
}

fn set_value_range(&mut self, range: RangeInclusive<f64>) {
let exponent_min = self.base.exponent(*range.start()).unwrap_or(-9.0);
let exponent_max = self.base.exponent(*range.end()).unwrap_or(-9.0);
self.exponent_scale.set_value_range(exponent_min..=exponent_max);
}

fn position_from_value(&self, value: f64) -> f32 {
//todo: appropriate response, NaN?
let exponent = self.base.exponent(value).unwrap_or(0.0);
self.exponent_scale.position_from_value(exponent)
}

fn value_from_position(&self, position: f32) -> f64 {
let exponent = self.exponent_scale.value_from_position(position);
self.base.power(exponent)
}

fn position_delta_from_screen_delta(&self, start_position_value: f64, delta: f32) -> f64 {
let start_exponent = self.base.exponent(start_position_value).unwrap_or(0.0);
let exponent_delta = self
.exponent_scale
.position_delta_from_screen_delta(start_position_value, delta);
let end_exponent = start_exponent + exponent_delta;
let end_value = self.base.power(end_exponent);
end_value - start_position_value
}

fn grid_input(&self, _spacing: f32) -> GridInput {
//todo: This still isn't right
let max_exponent = self.exponent_scale.value_max();
let min_step_exponent = max_exponent - 3.0;

GridInput {
bounds: (self.value_min(), self.value_max()),
base_step_size: self.base.power(min_step_exponent),
}
}

fn screen_distance_between_values(&self, value1: f64, value2: f64) -> f32 {
let exponent1 = self.base.exponent(value1).unwrap_or(0.0);
let exponent2 = self.base.exponent(value2).unwrap_or(0.0);
self.exponent_scale.screen_distance_between_values(exponent1, exponent2)
}

fn translate(&mut self, frame_distance: f32) {
self.exponent_scale.translate(frame_distance);
}

fn zoom(&mut self, factor: f32, center: f64) {
if let Some(exponent) = self.base.exponent(center) {
self.exponent_scale.zoom(factor, exponent);
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use assertables::{assert_approx_eq, assert_in_delta};

#[test]
fn basic_log10_conversions() {
let pairings = [[100.0, 2.0], [1000.0, 3.0], [0.1, -1.0]];
for [value, exponent] in pairings {
assert_approx_eq!(exponent, LogBase::Base10.exponent(value).unwrap());
assert_approx_eq!(value, LogBase::Base10.power(exponent));
}
assert!(LogBase::Base10.exponent(0.0).is_none());
assert!(LogBase::Base10.exponent(-1.0).is_none());
}
#[test]
fn test_value_changes_map_to_linear_and_back() {
let mut log_axis = LogAxis::new(1.0..=1000.0, Rangef::new(0.0, 1.0), false, LogBase::Base10);

let value = 100.0;
let position = log_axis.position_from_value(value);
let value_back = log_axis.value_from_position(position);
assert_in_delta!(value, value_back, 1e-4);
assert_approx_eq!(position, 0.666666666);

log_axis.set_value_range(10.0..=10000.0);
let position = log_axis.position_from_value(value);
let value_back = log_axis.value_from_position(position);
assert_in_delta!(value, value_back, 1e-4);
assert_approx_eq!(position, 0.333333333);
assert_approx_eq!(log_axis.value_min(), 10.0);
assert_approx_eq!(log_axis.value_max(), 10000.0);
assert_approx_eq!(log_axis.frame_min(), 0.0);
assert_approx_eq!(log_axis.frame_max(), 1.0);
}
#[test]
fn test_value_changes_map_to_linear_and_back_base2() {
let mut log_axis = LogAxis::new(1.0..=8.0, Rangef::new(0.0, 1.0), false, LogBase::Base2);

let value = 4.0;
let position = log_axis.position_from_value(value);
let value_back = log_axis.value_from_position(position);
assert_in_delta!(value, value_back, 1e-4);
assert_approx_eq!(position, 0.666666666);

log_axis.set_value_range(2.0..=16.0);
let position = log_axis.position_from_value(value);
let value_back = log_axis.value_from_position(position);
assert_in_delta!(value, value_back, 1e-4);
assert_in_delta!(position, 0.33333333, 1e-4);
assert_approx_eq!(log_axis.value_min(), 2.0);
assert_approx_eq!(log_axis.value_max(), 16.0);
assert_approx_eq!(log_axis.frame_min(), 0.0);
assert_approx_eq!(log_axis.frame_max(), 1.0);
}

#[test]
fn value_delta_from_screen_delta() {
let log_axis = LogAxis::new(1.0..=1000.0, Rangef::new(0.0, 1.0), false, LogBase::Base10);

let screen_start = 0.1;
let screen_end = 0.2;

let value_start = log_axis.value_from_position(screen_start);
let value_end = log_axis.value_from_position(screen_end);
let delta = value_end - value_start;

let calculated_delta = log_axis.position_delta_from_screen_delta(value_start, 0.1);
assert_approx_eq!(calculated_delta, delta);

//non linear means that offsetting should still compensate.
let screen_start = 0.3;
let screen_end = 0.4;
let value_start = log_axis.value_from_position(screen_start);
let value_end = log_axis.value_from_position(screen_end);
let delta = value_end - value_start;

let calculated_delta = log_axis.position_delta_from_screen_delta(value_start, 0.1);
assert_approx_eq!(calculated_delta, delta);
}
}
Loading