Skip to content

Commit 6559a6a

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 a19562e commit 6559a6a

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+
AxisTransform::Linear => 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
@@ -321,8 +321,11 @@ impl<'a> AxisWidget<'a> {
321321
for step in self.steps.iter() {
322322
let text = (self.hints.formatter)(*step, &self.range);
323323
if !text.is_empty() {
324-
let spacing_in_points =
325-
(transform.dpos_dvalue()[usize::from(axis)] * step.step_size).abs() as f32;
324+
let spacing_in_points = transform.points_at_pos_range(
325+
[step.value, step.value],
326+
[step.step_size, step.step_size],
327+
)[usize::from(axis)]
328+
.abs();
326329

327330
if spacing_in_points <= label_spacing.min {
328331
// 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
@@ -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

+1-1
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ impl RectElement for BoxElem {
277277
}
278278

279279
fn default_values_format(&self, transform: &PlotTransform) -> String {
280-
let scale = transform.dvalue_dpos();
280+
let scale = transform.smallest_distance_per_point();
281281
let scale = match self.orientation {
282282
Orientation::Horizontal => scale[0],
283283
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 {
@@ -880,7 +880,7 @@ impl PlotItem for Text {
880880
}
881881
}
882882

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

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

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

11651165
fn name(&self) -> &str {
@@ -1313,10 +1313,11 @@ impl PlotItem for Arrows {
13131313
});
13141314
}
13151315

1316-
fn initialize(&mut self, _x_range: RangeInclusive<f64>) {
1316+
fn initialize(&mut self, _x_range: RangeInclusive<f64>, log_base: Option<f64>) {
13171317
self.origins
1318-
.generate_points(f64::NEG_INFINITY..=f64::INFINITY);
1319-
self.tips.generate_points(f64::NEG_INFINITY..=f64::INFINITY);
1318+
.generate_points(f64::NEG_INFINITY..=f64::INFINITY, log_base);
1319+
self.tips
1320+
.generate_points(f64::NEG_INFINITY..=f64::INFINITY, log_base);
13201321
}
13211322

13221323
fn name(&self) -> &str {
@@ -1505,7 +1506,7 @@ impl PlotItem for PlotImage {
15051506
}
15061507
}
15071508

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

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

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

@@ -1874,7 +1875,7 @@ impl PlotItem for BoxPlot {
18741875
}
18751876
}
18761877

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

@@ -2060,7 +2061,7 @@ pub(super) fn rulers_at_value(
20602061
};
20612062

20622063
let text = {
2063-
let scale = plot.transform.dvalue_dpos();
2064+
let scale = plot.transform.smallest_distance_per_point();
20642065
let x_decimals = ((-scale[0].abs().log10()).ceil().at_least(0.0) as usize).clamp(1, 6);
20652066
let y_decimals = ((-scale[1].abs().log10()).ceil().at_least(0.0) as usize).clamp(1, 6);
20662067
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)