Skip to content

Commit 12f6cd3

Browse files
committed
Add logarithmic plot axes
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).
1 parent 80e2199 commit 12f6cd3

File tree

9 files changed

+630
-117
lines changed

9 files changed

+630
-117
lines changed

demo/src/plot_demo.rs

+116-5
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ use std::f64::consts::TAU;
22
use std::ops::RangeInclusive;
33

44
use egui::{
5-
remap, vec2, Color32, ComboBox, NumExt, Pos2, Response, ScrollArea, Stroke, TextWrapMode, Vec2b,
5+
remap, vec2, Color32, ComboBox, DragValue, NumExt, Pos2, Response, ScrollArea, Stroke,
6+
TextWrapMode, Vec2b,
67
};
7-
88
use egui_plot::{
9-
Arrows, AxisHints, Bar, BarChart, BoxElem, BoxPlot, BoxSpread, CoordinatesFormatter, Corner,
10-
GridInput, GridMark, HLine, Legend, Line, LineStyle, MarkerShape, Plot, PlotImage, PlotPoint,
11-
PlotPoints, PlotResponse, Points, Polygon, Text, VLine,
9+
Arrows, AxisHints, AxisTransform, AxisTransforms, Bar, BarChart, BoxElem, BoxPlot, BoxSpread,
10+
CoordinatesFormatter, Corner, GridInput, GridMark, HLine, Legend, Line, LineStyle, MarkerShape,
11+
Plot, PlotBounds, PlotImage, PlotPoint, PlotPoints, PlotResponse, Points, Polygon, Text, VLine,
1212
};
1313

1414
// ----------------------------------------------------------------------------
@@ -23,6 +23,7 @@ enum Panel {
2323
Interaction,
2424
CustomAxes,
2525
LinkedAxes,
26+
LogAxes,
2627
}
2728

2829
impl Default for Panel {
@@ -43,6 +44,7 @@ pub struct PlotDemo {
4344
interaction_demo: InteractionDemo,
4445
custom_axes_demo: CustomAxesDemo,
4546
linked_axes_demo: LinkedAxesDemo,
47+
log_axes_demo: LogAxesDemo,
4648
open_panel: Panel,
4749
}
4850

@@ -74,6 +76,7 @@ impl PlotDemo {
7476
ui.selectable_value(&mut self.open_panel, Panel::Interaction, "Interaction");
7577
ui.selectable_value(&mut self.open_panel, Panel::CustomAxes, "Custom Axes");
7678
ui.selectable_value(&mut self.open_panel, Panel::LinkedAxes, "Linked Axes");
79+
ui.selectable_value(&mut self.open_panel, Panel::LogAxes, "Log Axes");
7780
});
7881
ui.separator();
7982

@@ -102,6 +105,9 @@ impl PlotDemo {
102105
Panel::LinkedAxes => {
103106
self.linked_axes_demo.ui(ui);
104107
}
108+
Panel::LogAxes => {
109+
self.log_axes_demo.ui(ui);
110+
}
105111
}
106112
}
107113
}
@@ -691,6 +697,111 @@ impl LinkedAxesDemo {
691697
}
692698
}
693699

700+
// ----------------------------------------------------------------------------
701+
#[derive(PartialEq, serde::Deserialize, serde::Serialize, Default)]
702+
struct LogAxesDemo {
703+
axis_transforms: AxisTransforms,
704+
}
705+
706+
/// Helper function showing how to do arbitrary transform picking
707+
fn transform_edit(id: &str, old_transform: AxisTransform, ui: &mut egui::Ui) -> AxisTransform {
708+
ui.horizontal(|ui| {
709+
ui.label(format!("Transform for {id}"));
710+
if ui
711+
.radio(matches!(old_transform, AxisTransform::Linear), "Linear")
712+
.clicked()
713+
{
714+
return AxisTransform::Linear;
715+
}
716+
if ui
717+
.radio(
718+
matches!(old_transform, AxisTransform::Logarithmic(_)),
719+
"Logarithmic",
720+
)
721+
.clicked()
722+
{
723+
let reuse_base = if let AxisTransform::Logarithmic(base) = old_transform {
724+
base
725+
} else {
726+
10.0
727+
};
728+
return AxisTransform::Logarithmic(reuse_base);
729+
}
730+
731+
// no change, but perhaps additional things?
732+
match old_transform {
733+
// Nah?
734+
AxisTransform::Logarithmic(mut base) => {
735+
ui.label("Base:");
736+
ui.add(DragValue::new(&mut base).range(2.0..=100.0));
737+
AxisTransform::Logarithmic(base)
738+
}
739+
_ => old_transform,
740+
}
741+
})
742+
.inner
743+
}
744+
745+
impl LogAxesDemo {
746+
fn line_exp() -> Line {
747+
Line::new(PlotPoints::from_explicit_callback(
748+
move |x| 10.0_f64.powf(x / 200.0),
749+
0.1..=1000.0,
750+
1000,
751+
))
752+
.name("y = 10^(x/200)")
753+
.color(Color32::RED)
754+
}
755+
756+
fn line_lin() -> Line {
757+
Line::new(PlotPoints::from_explicit_callback(
758+
move |x| -5.0 + x,
759+
0.1..=1000.0,
760+
1000,
761+
))
762+
.name("y = -5 + x")
763+
.color(Color32::GREEN)
764+
}
765+
766+
fn line_log() -> Line {
767+
Line::new(PlotPoints::from_explicit_callback(
768+
move |x| x.log10(),
769+
0.1..=1000.0,
770+
1000,
771+
))
772+
.name("y = log10(x)")
773+
.color(Color32::BLUE)
774+
}
775+
776+
fn ui(&mut self, ui: &mut egui::Ui) -> Response {
777+
let old_transforms = self.axis_transforms;
778+
self.axis_transforms.horizontal =
779+
transform_edit("horizontal axis", self.axis_transforms.horizontal, ui);
780+
self.axis_transforms.vertical =
781+
transform_edit("vertical axis", self.axis_transforms.vertical, ui);
782+
let just_changed = old_transforms != self.axis_transforms;
783+
Plot::new("log_demo")
784+
.axis_transforms(self.axis_transforms)
785+
.x_axis_label("x")
786+
.y_axis_label("y")
787+
.show_axes(Vec2b::new(true, true))
788+
.legend(Legend::default())
789+
.show(ui, |ui| {
790+
if just_changed {
791+
if let AxisTransform::Logarithmic(_) = self.axis_transforms.horizontal {
792+
ui.set_plot_bounds(PlotBounds::from_min_max([0.1, 0.1], [1e3, 1e4]));
793+
} else {
794+
ui.set_plot_bounds(PlotBounds::from_min_max([0.0, 0.0], [3.0, 1000.0]));
795+
}
796+
}
797+
ui.line(Self::line_exp());
798+
ui.line(Self::line_lin());
799+
ui.line(Self::line_log());
800+
})
801+
.response
802+
}
803+
}
804+
694805
// ----------------------------------------------------------------------------
695806

696807
#[derive(Default, PartialEq, serde::Deserialize, serde::Serialize)]

egui_plot/src/axis.rs

+5-2
Original file line numberDiff line numberDiff line change
@@ -339,8 +339,11 @@ impl<'a> AxisWidget<'a> {
339339
for step in self.steps.iter() {
340340
let text = (self.hints.formatter)(*step, &self.range);
341341
if !text.is_empty() {
342-
let spacing_in_points =
343-
(transform.dpos_dvalue()[usize::from(axis)] * step.step_size).abs() as f32;
342+
let spacing_in_points = transform.points_at_pos_range(
343+
[step.value, step.value],
344+
[step.step_size, step.step_size],
345+
)[usize::from(axis)]
346+
.abs();
344347

345348
if spacing_in_points <= label_spacing.min {
346349
// Labels are too close together - don't paint them.

egui_plot/src/items/bar.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ impl RectElement for Bar {
186186
}
187187

188188
fn default_values_format(&self, transform: &PlotTransform) -> String {
189-
let scale = transform.dvalue_dpos();
189+
let scale = transform.smallest_distance_per_point();
190190
let scale = match self.orientation {
191191
Orientation::Horizontal => scale[0],
192192
Orientation::Vertical => scale[1],

egui_plot/src/items/box_elem.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ impl RectElement for BoxElem {
271271
}
272272

273273
fn default_values_format(&self, transform: &PlotTransform) -> String {
274-
let scale = transform.dvalue_dpos();
274+
let scale = transform.smallest_distance_per_point();
275275
let scale = match self.orientation {
276276
Orientation::Horizontal => scale[0],
277277
Orientation::Vertical => scale[1],

egui_plot/src/items/mod.rs

+18-17
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ pub trait PlotItem {
4141
fn shapes(&self, ui: &Ui, transform: &PlotTransform, shapes: &mut Vec<Shape>);
4242

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

4646
fn name(&self) -> &str;
4747

@@ -228,7 +228,7 @@ impl PlotItem for HLine {
228228
style.style_line(points, *stroke, *highlight, shapes);
229229
}
230230

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

233233
fn name(&self) -> &str {
234234
&self.name
@@ -371,7 +371,7 @@ impl PlotItem for VLine {
371371
style.style_line(points, *stroke, *highlight, shapes);
372372
}
373373

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

376376
fn name(&self) -> &str {
377377
&self.name
@@ -581,8 +581,8 @@ impl PlotItem for Line {
581581
style.style_line(values_tf, *stroke, *highlight, shapes);
582582
}
583583

584-
fn initialize(&mut self, x_range: RangeInclusive<f64>) {
585-
self.series.generate_points(x_range);
584+
fn initialize(&mut self, x_range: RangeInclusive<f64>, log_base: Option<f64>) {
585+
self.series.generate_points(x_range, log_base);
586586
}
587587

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

740-
fn initialize(&mut self, x_range: RangeInclusive<f64>) {
741-
self.series.generate_points(x_range);
740+
fn initialize(&mut self, x_range: RangeInclusive<f64>, log_base: Option<f64>) {
741+
self.series.generate_points(x_range, log_base);
742742
}
743743

744744
fn name(&self) -> &str {
@@ -879,7 +879,7 @@ impl PlotItem for Text {
879879
}
880880
}
881881

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

884884
fn name(&self) -> &str {
885885
self.name.as_str()
@@ -1157,8 +1157,8 @@ impl PlotItem for Points {
11571157
});
11581158
}
11591159

1160-
fn initialize(&mut self, x_range: RangeInclusive<f64>) {
1161-
self.series.generate_points(x_range);
1160+
fn initialize(&mut self, x_range: RangeInclusive<f64>, log_base: Option<f64>) {
1161+
self.series.generate_points(x_range, log_base);
11621162
}
11631163

11641164
fn name(&self) -> &str {
@@ -1312,10 +1312,11 @@ impl PlotItem for Arrows {
13121312
});
13131313
}
13141314

1315-
fn initialize(&mut self, _x_range: RangeInclusive<f64>) {
1315+
fn initialize(&mut self, _x_range: RangeInclusive<f64>, log_base: Option<f64>) {
13161316
self.origins
1317-
.generate_points(f64::NEG_INFINITY..=f64::INFINITY);
1318-
self.tips.generate_points(f64::NEG_INFINITY..=f64::INFINITY);
1317+
.generate_points(f64::NEG_INFINITY..=f64::INFINITY, log_base);
1318+
self.tips
1319+
.generate_points(f64::NEG_INFINITY..=f64::INFINITY, log_base);
13191320
}
13201321

13211322
fn name(&self) -> &str {
@@ -1504,7 +1505,7 @@ impl PlotItem for PlotImage {
15041505
}
15051506
}
15061507

1507-
fn initialize(&mut self, _x_range: RangeInclusive<f64>) {}
1508+
fn initialize(&mut self, _x_range: RangeInclusive<f64>, _log_base: Option<f64>) {}
15081509

15091510
fn name(&self) -> &str {
15101511
self.name.as_str()
@@ -1699,7 +1700,7 @@ impl PlotItem for BarChart {
16991700
}
17001701
}
17011702

1702-
fn initialize(&mut self, _x_range: RangeInclusive<f64>) {
1703+
fn initialize(&mut self, _x_range: RangeInclusive<f64>, _log_base: Option<f64>) {
17031704
// nothing to do
17041705
}
17051706

@@ -1873,7 +1874,7 @@ impl PlotItem for BoxPlot {
18731874
}
18741875
}
18751876

1876-
fn initialize(&mut self, _x_range: RangeInclusive<f64>) {
1877+
fn initialize(&mut self, _x_range: RangeInclusive<f64>, _log_base: Option<f64>) {
18771878
// nothing to do
18781879
}
18791880

@@ -2059,7 +2060,7 @@ pub(super) fn rulers_at_value(
20592060
};
20602061

20612062
let text = {
2062-
let scale = plot.transform.dvalue_dpos();
2063+
let scale = plot.transform.smallest_distance_per_point();
20632064
let x_decimals = ((-scale[0].abs().log10()).ceil().at_least(0.0) as usize).clamp(1, 6);
20642065
let y_decimals = ((-scale[1].abs().log10()).ceil().at_least(0.0) as usize).clamp(1, 6);
20652066
if let Some(custom_label) = label_formatter {

egui_plot/src/items/values.rs

+17-4
Original file line numberDiff line numberDiff line change
@@ -276,15 +276,28 @@ impl PlotPoints {
276276

277277
/// If initialized with a generator function, this will generate `n` evenly spaced points in the
278278
/// given range.
279-
pub(super) fn generate_points(&mut self, x_range: RangeInclusive<f64>) {
279+
pub(super) fn generate_points(&mut self, x_range: RangeInclusive<f64>, log_base: Option<f64>) {
280280
if let Self::Generator(generator) = self {
281281
*self = Self::range_intersection(&x_range, &generator.x_range)
282282
.map(|intersection| {
283-
let increment =
284-
(intersection.end() - intersection.start()) / (generator.points - 1) as f64;
283+
let increment = match log_base {
284+
Some(base) => {
285+
(intersection.end().log(base) - intersection.start().log(base))
286+
/ (generator.points - 1) as f64
287+
}
288+
None => {
289+
(intersection.end() - intersection.start())
290+
/ (generator.points - 1) as f64
291+
}
292+
};
285293
(0..generator.points)
286294
.map(|i| {
287-
let x = intersection.start() + i as f64 * increment;
295+
let x = match log_base {
296+
Some(base) => {
297+
base.powf(intersection.start().log(base) + i as f64 * increment)
298+
}
299+
None => intersection.start() + i as f64 * increment,
300+
};
288301
let y = (generator.function)(x);
289302
[x, y]
290303
})

0 commit comments

Comments
 (0)