Skip to content

Commit 30f6491

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 8754770 commit 30f6491

File tree

10 files changed

+630
-116
lines changed

10 files changed

+630
-116
lines changed

demo/src/plot_demo.rs

Lines changed: 113 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1+
use egui::DragValue;
12
use std::ops::RangeInclusive;
23
use std::{f64::consts::TAU, sync::Arc};
34

45
use egui::{
56
Checkbox, Color32, ComboBox, NumExt as _, Pos2, Response, ScrollArea, Stroke, TextWrapMode,
67
Vec2b, WidgetInfo, WidgetType, remap, vec2,
78
};
8-
99
use egui_plot::{
10-
Arrows, AxisHints, Bar, BarChart, BoxElem, BoxPlot, BoxSpread, CoordinatesFormatter, Corner,
11-
GridInput, GridMark, HLine, Legend, Line, LineStyle, MarkerShape, Plot, PlotImage, PlotPoint,
12-
PlotPoints, PlotResponse, Points, Polygon, Text, VLine,
10+
Arrows, AxisHints, AxisTransform, AxisTransforms, Bar, BarChart, BoxElem, BoxPlot, BoxSpread,
11+
CoordinatesFormatter, Corner, GridInput, GridMark, HLine, Legend, Line, LineStyle, MarkerShape,
12+
Plot, PlotBounds, PlotImage, PlotPoint, PlotPoints, PlotResponse, Points, Polygon, Text, VLine,
1313
};
1414

1515
// ----------------------------------------------------------------------------
@@ -24,6 +24,7 @@ enum Panel {
2424
Interaction,
2525
CustomAxes,
2626
LinkedAxes,
27+
LogAxes,
2728
}
2829

2930
impl Default for Panel {
@@ -44,6 +45,7 @@ pub struct PlotDemo {
4445
interaction_demo: InteractionDemo,
4546
custom_axes_demo: CustomAxesDemo,
4647
linked_axes_demo: LinkedAxesDemo,
48+
log_axes_demo: LogAxesDemo,
4749
open_panel: Panel,
4850
}
4951

@@ -131,6 +133,7 @@ impl PlotDemo {
131133
ui.selectable_value(&mut self.open_panel, Panel::Interaction, "Interaction");
132134
ui.selectable_value(&mut self.open_panel, Panel::CustomAxes, "Custom Axes");
133135
ui.selectable_value(&mut self.open_panel, Panel::LinkedAxes, "Linked Axes");
136+
ui.selectable_value(&mut self.open_panel, Panel::LogAxes, "Log Axes");
134137
});
135138
});
136139
ui.separator();
@@ -160,6 +163,9 @@ impl PlotDemo {
160163
Panel::LinkedAxes => {
161164
self.linked_axes_demo.ui(ui);
162165
}
166+
Panel::LogAxes => {
167+
self.log_axes_demo.ui(ui);
168+
}
163169
}
164170
}
165171
}
@@ -774,6 +780,109 @@ impl LinkedAxesDemo {
774780
}
775781
}
776782

783+
// ----------------------------------------------------------------------------
784+
#[derive(PartialEq, serde::Deserialize, serde::Serialize, Default)]
785+
struct LogAxesDemo {
786+
axis_transforms: AxisTransforms,
787+
}
788+
789+
/// Helper function showing how to do arbitrary transform picking
790+
fn transform_edit(id: &str, old_transform: AxisTransform, ui: &mut egui::Ui) -> AxisTransform {
791+
ui.horizontal(|ui| {
792+
ui.label(format!("Transform for {id}"));
793+
if ui
794+
.radio(matches!(old_transform, AxisTransform::Linear), "Linear")
795+
.clicked()
796+
{
797+
return AxisTransform::Linear;
798+
}
799+
if ui
800+
.radio(
801+
matches!(old_transform, AxisTransform::Logarithmic(_)),
802+
"Logarithmic",
803+
)
804+
.clicked()
805+
{
806+
let reuse_base = if let AxisTransform::Logarithmic(base) = old_transform {
807+
base
808+
} else {
809+
10.0
810+
};
811+
return AxisTransform::Logarithmic(reuse_base);
812+
}
813+
814+
// no change, but perhaps additional things?
815+
match old_transform {
816+
// Nah?
817+
AxisTransform::Logarithmic(mut base) => {
818+
ui.label("Base:");
819+
ui.add(DragValue::new(&mut base).range(2.0..=100.0));
820+
AxisTransform::Logarithmic(base)
821+
}
822+
AxisTransform::Linear => old_transform,
823+
}
824+
})
825+
.inner
826+
}
827+
828+
impl LogAxesDemo {
829+
fn line_exp<'a>() -> Line<'a> {
830+
Line::new(
831+
"y = 10^(x/200)",
832+
PlotPoints::from_explicit_callback(
833+
move |x| 10.0_f64.powf(x / 200.0),
834+
0.1..=1000.0,
835+
1000,
836+
),
837+
)
838+
.color(Color32::RED)
839+
}
840+
841+
fn line_lin<'a>() -> Line<'a> {
842+
Line::new(
843+
"y = -5 + x",
844+
PlotPoints::from_explicit_callback(move |x| -5.0 + x, 0.1..=1000.0, 1000),
845+
)
846+
.color(Color32::GREEN)
847+
}
848+
849+
fn line_log<'a>() -> Line<'a> {
850+
Line::new(
851+
"y = log10(x)",
852+
PlotPoints::from_explicit_callback(move |x| x.log10(), 0.1..=1000.0, 1000),
853+
)
854+
.color(Color32::BLUE)
855+
}
856+
857+
fn ui(&mut self, ui: &mut egui::Ui) -> Response {
858+
let old_transforms = self.axis_transforms;
859+
self.axis_transforms.horizontal =
860+
transform_edit("horizontal axis", self.axis_transforms.horizontal, ui);
861+
self.axis_transforms.vertical =
862+
transform_edit("vertical axis", self.axis_transforms.vertical, ui);
863+
let just_changed = old_transforms != self.axis_transforms;
864+
Plot::new("log_demo")
865+
.axis_transforms(self.axis_transforms)
866+
.x_axis_label("x")
867+
.y_axis_label("y")
868+
.show_axes(Vec2b::new(true, true))
869+
.legend(Legend::default())
870+
.show(ui, |ui| {
871+
if just_changed {
872+
if let AxisTransform::Logarithmic(_) = self.axis_transforms.horizontal {
873+
ui.set_plot_bounds(PlotBounds::from_min_max([0.1, 0.1], [1e3, 1e4]));
874+
} else {
875+
ui.set_plot_bounds(PlotBounds::from_min_max([0.0, 0.0], [3.0, 1000.0]));
876+
}
877+
}
878+
ui.line(Self::line_exp());
879+
ui.line(Self::line_lin());
880+
ui.line(Self::line_log());
881+
})
882+
.response
883+
}
884+
}
885+
777886
// ----------------------------------------------------------------------------
778887

779888
#[derive(Default, PartialEq, serde::Deserialize, serde::Serialize)]
Lines changed: 3 additions & 0 deletions
Loading

egui_plot/src/axis.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -308,8 +308,11 @@ impl<'a> AxisWidget<'a> {
308308
for step in self.steps.iter() {
309309
let text = (self.hints.formatter)(*step, &self.range);
310310
if !text.is_empty() {
311-
let spacing_in_points =
312-
(transform.dpos_dvalue()[usize::from(axis)] * step.step_size).abs() as f32;
311+
let spacing_in_points = transform.points_at_pos_range(
312+
[step.value, step.value],
313+
[step.step_size, step.step_size],
314+
)[usize::from(axis)]
315+
.abs();
313316

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

egui_plot/src/items/bar.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ impl RectElement for Bar {
192192
}
193193

194194
fn default_values_format(&self, transform: &PlotTransform) -> String {
195-
let scale = transform.dvalue_dpos();
195+
let scale = transform.smallest_distance_per_point();
196196
let scale = match self.orientation {
197197
Orientation::Horizontal => scale[0],
198198
Orientation::Vertical => scale[1],

egui_plot/src/items/box_elem.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ impl RectElement for BoxElem {
279279
}
280280

281281
fn default_values_format(&self, transform: &PlotTransform) -> String {
282-
let scale = transform.dvalue_dpos();
282+
let scale = transform.smallest_distance_per_point();
283283
let scale = match self.orientation {
284284
Orientation::Horizontal => scale[0],
285285
Orientation::Vertical => scale[1],

egui_plot/src/items/mod.rs

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ pub trait PlotItem {
101101
fn shapes(&self, ui: &Ui, transform: &PlotTransform, shapes: &mut Vec<Shape>);
102102

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

106106
fn name(&self) -> &str {
107107
&self.base().name
@@ -269,7 +269,7 @@ impl PlotItem for HLine {
269269
);
270270
}
271271

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

274274
fn color(&self) -> Color32 {
275275
self.stroke.color
@@ -367,7 +367,7 @@ impl PlotItem for VLine {
367367
);
368368
}
369369

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

372372
fn color(&self) -> Color32 {
373373
self.stroke.color
@@ -573,8 +573,8 @@ impl PlotItem for Line<'_> {
573573
style.style_line(values_tf, final_stroke, base.highlight, shapes);
574574
}
575575

576-
fn initialize(&mut self, x_range: RangeInclusive<f64>) {
577-
self.series.generate_points(x_range);
576+
fn initialize(&mut self, x_range: RangeInclusive<f64>, log_base: Option<f64>) {
577+
self.series.generate_points(x_range, log_base);
578578
}
579579

580580
fn color(&self) -> Color32 {
@@ -683,8 +683,8 @@ impl PlotItem for Polygon<'_> {
683683
);
684684
}
685685

686-
fn initialize(&mut self, x_range: RangeInclusive<f64>) {
687-
self.series.generate_points(x_range);
686+
fn initialize(&mut self, x_range: RangeInclusive<f64>, log_base: Option<f64>) {
687+
self.series.generate_points(x_range, log_base);
688688
}
689689

690690
fn color(&self) -> Color32 {
@@ -776,7 +776,7 @@ impl PlotItem for Text {
776776
}
777777
}
778778

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

781781
fn color(&self) -> Color32 {
782782
self.color
@@ -1004,8 +1004,8 @@ impl PlotItem for Points<'_> {
10041004
});
10051005
}
10061006

1007-
fn initialize(&mut self, x_range: RangeInclusive<f64>) {
1008-
self.series.generate_points(x_range);
1007+
fn initialize(&mut self, x_range: RangeInclusive<f64>, log_base: Option<f64>) {
1008+
self.series.generate_points(x_range, log_base);
10091009
}
10101010

10111011
fn color(&self) -> Color32 {
@@ -1113,10 +1113,11 @@ impl PlotItem for Arrows<'_> {
11131113
});
11141114
}
11151115

1116-
fn initialize(&mut self, _x_range: RangeInclusive<f64>) {
1116+
fn initialize(&mut self, _x_range: RangeInclusive<f64>, log_base: Option<f64>) {
11171117
self.origins
1118-
.generate_points(f64::NEG_INFINITY..=f64::INFINITY);
1119-
self.tips.generate_points(f64::NEG_INFINITY..=f64::INFINITY);
1118+
.generate_points(f64::NEG_INFINITY..=f64::INFINITY, log_base);
1119+
self.tips
1120+
.generate_points(f64::NEG_INFINITY..=f64::INFINITY, log_base);
11201121
}
11211122

11221123
fn color(&self) -> Color32 {
@@ -1263,7 +1264,7 @@ impl PlotItem for PlotImage {
12631264
}
12641265
}
12651266

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

12681269
fn color(&self) -> Color32 {
12691270
Color32::TRANSPARENT
@@ -1410,7 +1411,7 @@ impl PlotItem for BarChart {
14101411
}
14111412
}
14121413

1413-
fn initialize(&mut self, _x_range: RangeInclusive<f64>) {
1414+
fn initialize(&mut self, _x_range: RangeInclusive<f64>, _log_base: Option<f64>) {
14141415
// nothing to do
14151416
}
14161417

@@ -1537,7 +1538,7 @@ impl PlotItem for BoxPlot {
15371538
}
15381539
}
15391540

1540-
fn initialize(&mut self, _x_range: RangeInclusive<f64>) {
1541+
fn initialize(&mut self, _x_range: RangeInclusive<f64>, _log_base: Option<f64>) {
15411542
// nothing to do
15421543
}
15431544

@@ -1714,7 +1715,7 @@ pub(super) fn rulers_and_tooltip_at_value(
17141715
} else {
17151716
format!("{name}\n")
17161717
};
1717-
let scale = plot.transform.dvalue_dpos();
1718+
let scale = plot.transform.smallest_distance_per_point();
17181719
let x_decimals = ((-scale[0].abs().log10()).ceil().at_least(0.0) as usize).clamp(1, 6);
17191720
let y_decimals = ((-scale[1].abs().log10()).ceil().at_least(0.0) as usize).clamp(1, 6);
17201721
if plot.show_x && plot.show_y {

egui_plot/src/items/values.rs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -303,15 +303,28 @@ impl<'a> PlotPoints<'a> {
303303

304304
/// If initialized with a generator function, this will generate `n` evenly spaced points in the
305305
/// given range.
306-
pub(super) fn generate_points(&mut self, x_range: RangeInclusive<f64>) {
306+
pub(super) fn generate_points(&mut self, x_range: RangeInclusive<f64>, log_base: Option<f64>) {
307307
if let Self::Generator(generator) = self {
308308
*self = Self::range_intersection(&x_range, &generator.x_range)
309309
.map(|intersection| {
310-
let increment =
311-
(intersection.end() - intersection.start()) / (generator.points - 1) as f64;
310+
let increment = match log_base {
311+
Some(base) => {
312+
(intersection.end().log(base) - intersection.start().log(base))
313+
/ (generator.points - 1) as f64
314+
}
315+
None => {
316+
(intersection.end() - intersection.start())
317+
/ (generator.points - 1) as f64
318+
}
319+
};
312320
(0..generator.points)
313321
.map(|i| {
314-
let x = intersection.start() + i as f64 * increment;
322+
let x = match log_base {
323+
Some(base) => {
324+
base.powf(intersection.start().log(base) + i as f64 * increment)
325+
}
326+
None => intersection.start() + i as f64 * increment,
327+
};
315328
let y = (generator.function)(x);
316329
[x, y]
317330
})

0 commit comments

Comments
 (0)