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 6, 2025
1 parent 6a7ff72 commit 3c3fd09
Show file tree
Hide file tree
Showing 9 changed files with 493 additions and 111 deletions.
80 changes: 78 additions & 2 deletions demo/src/plot_demo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ use egui::{

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,
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,76 @@ impl LinkedAxesDemo {
}
}

// ----------------------------------------------------------------------------
#[derive(PartialEq, serde::Deserialize, serde::Serialize)]
struct LogAxesDemo {
log_axes: Vec2b,
}

impl Default for LogAxesDemo {
fn default() -> Self {
Self {
log_axes: Vec2b::new(false, true),
}
}
}

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 just_changed = ui.checkbox(&mut self.log_axes.x, "Log X-Axis").clicked();
ui.checkbox(&mut self.log_axes.y, "Log Y-Axis");
Plot::new("log_demo")
.log_axes(self.log_axes)
.x_axis_label("x")
.y_axis_label("y")
.show_axes(Vec2b::new(true, true))
.legend(Legend::default())
.show(ui, |ui| {
if just_changed {
if self.log_axes.x {
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 @@ -233,7 +233,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 @@ -381,7 +381,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 @@ -591,8 +591,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 @@ -747,8 +747,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 @@ -889,7 +889,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 @@ -1167,8 +1167,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 @@ -1322,10 +1322,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 @@ -1514,7 +1515,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 @@ -1709,7 +1710,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 @@ -1883,7 +1884,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 @@ -2069,7 +2070,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 3c3fd09

Please sign in to comment.