emilk / egui_plot

2D plotting library in Rust for egui
Apache License 2.0
95 stars 31 forks source link

Log axes for plot #11

Open hacknus opened 1 year ago

hacknus commented 1 year ago

Is your feature request related to a problem? Please describe. I want to be able to plot data on a logarithmic scale. Either just x, just y or both axes.

Describe the solution you'd like It would be easiest to have an implementation that would only need one call in the constructor:

let my_plot = Plot::new("My Plot")
                    .height(height)
                    .width(width)
                    .log_axes([false, self.log_mode])
                    .legend(Legend::default());

to enable the log axes.

Describe alternatives you've considered None

Additional context I already started here.

Bildschirm­foto 2023-03-04 um 23 56 16

Specifically, I started by adding the function

pub fn log_axes(mut self, log_axes: [bool; 2]) -> Self {
    self.log_axes = log_axes;
    if self.log_axes[0] && self.log_axes[1] {
        let label_fmt = move |s: &str, val: &PlotPoint| {
            format!(
                "{}\n{:4.2}\n{:4.2}",
                s,
                10.0_f64.powf(val.x),
                10.0_f64.powf(val.y)
            )
        };
        let ax_fmt =
            move |x: f64, _range: &RangeInclusive<f64>| format!("{:4.2}", 10.0_f64.powf(x));
        self.label_formatter(label_fmt)
            .x_axis_formatter(ax_fmt)
            .y_axis_formatter(ax_fmt)
            .x_grid_spacer(logarithmic_grid_spacer(10))
            .y_grid_spacer(logarithmic_grid_spacer(10))
    } else if self.log_axes[0] {
        let label_fmt = move |s: &str, val: &PlotPoint| {
            format!("{}\n{:4.2}\n{:4.2}", s, 10.0_f64.powf(val.x), val.y)
        };
        let ax_fmt =
            move |y: f64, _range: &RangeInclusive<f64>| format!("{:4.2}", 10.0_f64.powf(y));
        self.label_formatter(label_fmt)
            .x_axis_formatter(ax_fmt)
            .x_grid_spacer(logarithmic_grid_spacer(10))
    } else if self.log_axes[1] {
        let label_fmt = move |s: &str, val: &PlotPoint| {
            format!("{}\n{:4.2}\n{:4.2}", s, val.x, 10.0_f64.powf(val.y),)
        };
        let ax_fmt =
            move |y: f64, _range: &RangeInclusive<f64>| format!("{:4.2}", 10.0_f64.powf(y));
        self.label_formatter(label_fmt)
            .y_axis_formatter(ax_fmt)
            .y_grid_spacer(logarithmic_grid_spacer(10))
    } else {
        self
    }
}

which adds the correct formatters and adds a logarithmic_grid_spacer:

/// Fill in all values between [min, max]
fn generate_marks_log_plot(step_sizes: [f64; 3], bounds: (f64, f64)) -> Vec<GridMark> {
    let mut steps = vec![];
    make_marks_log_plot(&mut steps, step_sizes, bounds);
    steps
}

/// Fill in all values between [min, max] which are a multiple of `step_size`
fn make_marks_log_plot(out: &mut Vec<GridMark>, step_size: [f64; 3], (min, max): (f64, f64)) {
    assert!(max > min);
    // TODO: pos/neg check
    let first = (min).floor() as i64;
    let last = (max).floor() as i64;

    let mut marks_iter = vec![];
    for i in first..=last {
        let step = (10_f64.powi(i as i32 + 1) - 10_f64.powi(i as i32)) / 9.0;
        marks_iter.push(GridMark {
            value: i as f64,
            step_size: step_size[1],
        });
        for j in 1..9 {
            let value = 10_f64.powi(i as i32) + j as f64 * step;
            marks_iter.push(GridMark {
                value: value.log(10.0),
                step_size: step_size[0],
            });
        }
    }

    out.extend(marks_iter);
}

pub fn logarithmic_grid_spacer(log_base: i64) -> GridSpacer {
    let log_base = log_base as f64;
    let step_sizes = move |input: GridInput| -> Vec<GridMark> {
        // The distance between two of the thinnest grid lines is "rounded" up
        // to the next-bigger power of base

        let smallest_visible_unit = next_power(input.base_step_size, log_base);

        let step_sizes = [
            smallest_visible_unit,
            smallest_visible_unit * log_base,
            smallest_visible_unit * log_base * log_base,
        ];

        generate_marks_log_plot(step_sizes, input.bounds)
    };

    Box::new(step_sizes)
}

Still unfinished: So far I am stuck with these things:

mkalte666 commented 4 months ago

negative values or zero values in the data are not treated/ignored.

There are three solutions to this i think

I will have a go at this based on your PR @hacknus, as i need this fairly soon^^

jordens commented 3 months ago

Over in stabilizer-stream we do this:

https://github.com/quartiq/stabilizer-stream/blob/ae9d239977f04a839c76b0d7b810444eefad5a80/src/bin/psd.rs#L492-L548