Skip to content

Commit

Permalink
Add logarithmic plot axes
Browse files Browse the repository at this point in the history
This commit is an initial implementation for adding logarithmic plotting
axis.

This very much needs more testing!

The basic idea is, that everything stays the same, but PlotTransform
does the much needed coordinate transformation for us.

That is, unfortunatley not all of the story.

 * In a lot of places, we need estimates of "how many pixels does 1 plot
   space unit take" and the likes, either for overdraw reduction, or
   generally to size things. PlotTransform has been modifed for that for
   now, so this should work.
 * While the normal grid spacer renders just fine, it will also casually
   try to generate 100s of thousands of lines for a bigger range log
   plot. So GridInput has been made aware if there is a log axis
   present. The default spacer has also been modified to work initially.
 * All of the PlotBound transformations within PlotTransform need to be
   aware and handle the log scaling properly. This is done and works
   well, but its a bit.. icky, for lack of a better word. If someone has
   a better idea how to handle this, be my guest :D
 * PlotPoint generation from generator functions has to become aware of
   logarithmic plotting, otherwise the resolution of the plotted points
   will suffer.

Especially the spacer generation is still kinda WIP; it is messy at best
right now. Especially for zooming in, it currently only adds lines on the
lower bound due to the way the generator function works right now.

I will address this in a follow up commit/--amend (or someone else will).
  • Loading branch information
mkalte666 committed Jan 11, 2025
1 parent 80e2199 commit 12f6cd3
Show file tree
Hide file tree
Showing 9 changed files with 630 additions and 117 deletions.
121 changes: 116 additions & 5 deletions demo/src/plot_demo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ use std::f64::consts::TAU;
use std::ops::RangeInclusive;

use egui::{
remap, vec2, Color32, ComboBox, NumExt, Pos2, Response, ScrollArea, Stroke, TextWrapMode, Vec2b,
remap, vec2, Color32, ComboBox, DragValue, NumExt, Pos2, Response, ScrollArea, Stroke,
TextWrapMode, Vec2b,
};

use egui_plot::{
Arrows, AxisHints, Bar, BarChart, BoxElem, BoxPlot, BoxSpread, CoordinatesFormatter, Corner,
GridInput, GridMark, HLine, Legend, Line, LineStyle, MarkerShape, Plot, PlotImage, PlotPoint,
PlotPoints, PlotResponse, Points, Polygon, Text, VLine,
Arrows, AxisHints, AxisTransform, AxisTransforms, Bar, BarChart, BoxElem, BoxPlot, BoxSpread,
CoordinatesFormatter, Corner, GridInput, GridMark, HLine, Legend, Line, LineStyle, MarkerShape,
Plot, PlotBounds, PlotImage, PlotPoint, PlotPoints, PlotResponse, Points, Polygon, Text, VLine,
};

// ----------------------------------------------------------------------------
Expand All @@ -23,6 +23,7 @@ enum Panel {
Interaction,
CustomAxes,
LinkedAxes,
LogAxes,
}

impl Default for Panel {
Expand All @@ -43,6 +44,7 @@ pub struct PlotDemo {
interaction_demo: InteractionDemo,
custom_axes_demo: CustomAxesDemo,
linked_axes_demo: LinkedAxesDemo,
log_axes_demo: LogAxesDemo,
open_panel: Panel,
}

Expand Down Expand Up @@ -74,6 +76,7 @@ impl PlotDemo {
ui.selectable_value(&mut self.open_panel, Panel::Interaction, "Interaction");
ui.selectable_value(&mut self.open_panel, Panel::CustomAxes, "Custom Axes");
ui.selectable_value(&mut self.open_panel, Panel::LinkedAxes, "Linked Axes");
ui.selectable_value(&mut self.open_panel, Panel::LogAxes, "Log Axes");
});
ui.separator();

Expand Down Expand Up @@ -102,6 +105,9 @@ impl PlotDemo {
Panel::LinkedAxes => {
self.linked_axes_demo.ui(ui);
}
Panel::LogAxes => {
self.log_axes_demo.ui(ui);
}
}
}
}
Expand Down Expand Up @@ -691,6 +697,111 @@ impl LinkedAxesDemo {
}
}

// ----------------------------------------------------------------------------
#[derive(PartialEq, serde::Deserialize, serde::Serialize, Default)]
struct LogAxesDemo {
axis_transforms: AxisTransforms,
}

/// Helper function showing how to do arbitrary transform picking
fn transform_edit(id: &str, old_transform: AxisTransform, ui: &mut egui::Ui) -> AxisTransform {
ui.horizontal(|ui| {
ui.label(format!("Transform for {id}"));
if ui
.radio(matches!(old_transform, AxisTransform::Linear), "Linear")
.clicked()
{
return AxisTransform::Linear;
}
if ui
.radio(
matches!(old_transform, AxisTransform::Logarithmic(_)),
"Logarithmic",
)
.clicked()
{
let reuse_base = if let AxisTransform::Logarithmic(base) = old_transform {
base
} else {
10.0
};
return AxisTransform::Logarithmic(reuse_base);
}

// no change, but perhaps additional things?
match old_transform {
// Nah?
AxisTransform::Logarithmic(mut base) => {
ui.label("Base:");
ui.add(DragValue::new(&mut base).range(2.0..=100.0));
AxisTransform::Logarithmic(base)
}
_ => old_transform,

Check failure on line 739 in demo/src/plot_demo.rs

View workflow job for this annotation

GitHub Actions / Check wasm32

wildcard matches only a single variant and will also match any future added variants

Check failure on line 739 in demo/src/plot_demo.rs

View workflow job for this annotation

GitHub Actions / Rust

wildcard matches only a single variant and will also match any future added variants
}
})
.inner
}

impl LogAxesDemo {
fn line_exp() -> Line {
Line::new(PlotPoints::from_explicit_callback(
move |x| 10.0_f64.powf(x / 200.0),
0.1..=1000.0,
1000,
))
.name("y = 10^(x/200)")
.color(Color32::RED)
}

fn line_lin() -> Line {
Line::new(PlotPoints::from_explicit_callback(
move |x| -5.0 + x,
0.1..=1000.0,
1000,
))
.name("y = -5 + x")
.color(Color32::GREEN)
}

fn line_log() -> Line {
Line::new(PlotPoints::from_explicit_callback(
move |x| x.log10(),
0.1..=1000.0,
1000,
))
.name("y = log10(x)")
.color(Color32::BLUE)
}

fn ui(&mut self, ui: &mut egui::Ui) -> Response {
let old_transforms = self.axis_transforms;
self.axis_transforms.horizontal =
transform_edit("horizontal axis", self.axis_transforms.horizontal, ui);
self.axis_transforms.vertical =
transform_edit("vertical axis", self.axis_transforms.vertical, ui);
let just_changed = old_transforms != self.axis_transforms;
Plot::new("log_demo")
.axis_transforms(self.axis_transforms)
.x_axis_label("x")
.y_axis_label("y")
.show_axes(Vec2b::new(true, true))
.legend(Legend::default())
.show(ui, |ui| {
if just_changed {
if let AxisTransform::Logarithmic(_) = self.axis_transforms.horizontal {
ui.set_plot_bounds(PlotBounds::from_min_max([0.1, 0.1], [1e3, 1e4]));
} else {
ui.set_plot_bounds(PlotBounds::from_min_max([0.0, 0.0], [3.0, 1000.0]));
}
}
ui.line(Self::line_exp());
ui.line(Self::line_lin());
ui.line(Self::line_log());
})
.response
}
}

// ----------------------------------------------------------------------------

#[derive(Default, PartialEq, serde::Deserialize, serde::Serialize)]
Expand Down
7 changes: 5 additions & 2 deletions egui_plot/src/axis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -339,8 +339,11 @@ impl<'a> AxisWidget<'a> {
for step in self.steps.iter() {
let text = (self.hints.formatter)(*step, &self.range);
if !text.is_empty() {
let spacing_in_points =
(transform.dpos_dvalue()[usize::from(axis)] * step.step_size).abs() as f32;
let spacing_in_points = transform.points_at_pos_range(
[step.value, step.value],
[step.step_size, step.step_size],
)[usize::from(axis)]
.abs();

if spacing_in_points <= label_spacing.min {
// Labels are too close together - don't paint them.
Expand Down
2 changes: 1 addition & 1 deletion egui_plot/src/items/bar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ impl RectElement for Bar {
}

fn default_values_format(&self, transform: &PlotTransform) -> String {
let scale = transform.dvalue_dpos();
let scale = transform.smallest_distance_per_point();
let scale = match self.orientation {
Orientation::Horizontal => scale[0],
Orientation::Vertical => scale[1],
Expand Down
2 changes: 1 addition & 1 deletion egui_plot/src/items/box_elem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ impl RectElement for BoxElem {
}

fn default_values_format(&self, transform: &PlotTransform) -> String {
let scale = transform.dvalue_dpos();
let scale = transform.smallest_distance_per_point();
let scale = match self.orientation {
Orientation::Horizontal => scale[0],
Orientation::Vertical => scale[1],
Expand Down
35 changes: 18 additions & 17 deletions egui_plot/src/items/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ pub trait PlotItem {
fn shapes(&self, ui: &Ui, transform: &PlotTransform, shapes: &mut Vec<Shape>);

/// For plot-items which are generated based on x values (plotting functions).
fn initialize(&mut self, x_range: RangeInclusive<f64>);
fn initialize(&mut self, x_range: RangeInclusive<f64>, log_base: Option<f64>);

fn name(&self) -> &str;

Expand Down Expand Up @@ -228,7 +228,7 @@ impl PlotItem for HLine {
style.style_line(points, *stroke, *highlight, shapes);
}

fn initialize(&mut self, _x_range: RangeInclusive<f64>) {}
fn initialize(&mut self, _x_range: RangeInclusive<f64>, _log_base: Option<f64>) {}

fn name(&self) -> &str {
&self.name
Expand Down Expand Up @@ -371,7 +371,7 @@ impl PlotItem for VLine {
style.style_line(points, *stroke, *highlight, shapes);
}

fn initialize(&mut self, _x_range: RangeInclusive<f64>) {}
fn initialize(&mut self, _x_range: RangeInclusive<f64>, _log_base: Option<f64>) {}

fn name(&self) -> &str {
&self.name
Expand Down Expand Up @@ -581,8 +581,8 @@ impl PlotItem for Line {
style.style_line(values_tf, *stroke, *highlight, shapes);
}

fn initialize(&mut self, x_range: RangeInclusive<f64>) {
self.series.generate_points(x_range);
fn initialize(&mut self, x_range: RangeInclusive<f64>, log_base: Option<f64>) {
self.series.generate_points(x_range, log_base);
}

fn name(&self) -> &str {
Expand Down Expand Up @@ -737,8 +737,8 @@ impl PlotItem for Polygon {
style.style_line(values_tf, *stroke, *highlight, shapes);
}

fn initialize(&mut self, x_range: RangeInclusive<f64>) {
self.series.generate_points(x_range);
fn initialize(&mut self, x_range: RangeInclusive<f64>, log_base: Option<f64>) {
self.series.generate_points(x_range, log_base);
}

fn name(&self) -> &str {
Expand Down Expand Up @@ -879,7 +879,7 @@ impl PlotItem for Text {
}
}

fn initialize(&mut self, _x_range: RangeInclusive<f64>) {}
fn initialize(&mut self, _x_range: RangeInclusive<f64>, _log_base: Option<f64>) {}

fn name(&self) -> &str {
self.name.as_str()
Expand Down Expand Up @@ -1157,8 +1157,8 @@ impl PlotItem for Points {
});
}

fn initialize(&mut self, x_range: RangeInclusive<f64>) {
self.series.generate_points(x_range);
fn initialize(&mut self, x_range: RangeInclusive<f64>, log_base: Option<f64>) {
self.series.generate_points(x_range, log_base);
}

fn name(&self) -> &str {
Expand Down Expand Up @@ -1312,10 +1312,11 @@ impl PlotItem for Arrows {
});
}

fn initialize(&mut self, _x_range: RangeInclusive<f64>) {
fn initialize(&mut self, _x_range: RangeInclusive<f64>, log_base: Option<f64>) {
self.origins
.generate_points(f64::NEG_INFINITY..=f64::INFINITY);
self.tips.generate_points(f64::NEG_INFINITY..=f64::INFINITY);
.generate_points(f64::NEG_INFINITY..=f64::INFINITY, log_base);
self.tips
.generate_points(f64::NEG_INFINITY..=f64::INFINITY, log_base);
}

fn name(&self) -> &str {
Expand Down Expand Up @@ -1504,7 +1505,7 @@ impl PlotItem for PlotImage {
}
}

fn initialize(&mut self, _x_range: RangeInclusive<f64>) {}
fn initialize(&mut self, _x_range: RangeInclusive<f64>, _log_base: Option<f64>) {}

fn name(&self) -> &str {
self.name.as_str()
Expand Down Expand Up @@ -1699,7 +1700,7 @@ impl PlotItem for BarChart {
}
}

fn initialize(&mut self, _x_range: RangeInclusive<f64>) {
fn initialize(&mut self, _x_range: RangeInclusive<f64>, _log_base: Option<f64>) {
// nothing to do
}

Expand Down Expand Up @@ -1873,7 +1874,7 @@ impl PlotItem for BoxPlot {
}
}

fn initialize(&mut self, _x_range: RangeInclusive<f64>) {
fn initialize(&mut self, _x_range: RangeInclusive<f64>, _log_base: Option<f64>) {
// nothing to do
}

Expand Down Expand Up @@ -2059,7 +2060,7 @@ pub(super) fn rulers_at_value(
};

let text = {
let scale = plot.transform.dvalue_dpos();
let scale = plot.transform.smallest_distance_per_point();
let x_decimals = ((-scale[0].abs().log10()).ceil().at_least(0.0) as usize).clamp(1, 6);
let y_decimals = ((-scale[1].abs().log10()).ceil().at_least(0.0) as usize).clamp(1, 6);
if let Some(custom_label) = label_formatter {
Expand Down
21 changes: 17 additions & 4 deletions egui_plot/src/items/values.rs
Original file line number Diff line number Diff line change
Expand Up @@ -276,15 +276,28 @@ impl PlotPoints {

/// If initialized with a generator function, this will generate `n` evenly spaced points in the
/// given range.
pub(super) fn generate_points(&mut self, x_range: RangeInclusive<f64>) {
pub(super) fn generate_points(&mut self, x_range: RangeInclusive<f64>, log_base: Option<f64>) {
if let Self::Generator(generator) = self {
*self = Self::range_intersection(&x_range, &generator.x_range)
.map(|intersection| {
let increment =
(intersection.end() - intersection.start()) / (generator.points - 1) as f64;
let increment = match log_base {
Some(base) => {
(intersection.end().log(base) - intersection.start().log(base))
/ (generator.points - 1) as f64
}
None => {
(intersection.end() - intersection.start())
/ (generator.points - 1) as f64
}
};
(0..generator.points)
.map(|i| {
let x = intersection.start() + i as f64 * increment;
let x = match log_base {
Some(base) => {
base.powf(intersection.start().log(base) + i as f64 * increment)
}
None => intersection.start() + i as f64 * increment,
};
let y = (generator.function)(x);
[x, y]
})
Expand Down
Loading

0 comments on commit 12f6cd3

Please sign in to comment.