use crate::core::items::LineStyle;
use crate::widget::high_level::{ImageView, PlotWidget, ScatterView};
const LINE_STYLE_CYCLE: [LineStyle; 4] = [
LineStyle::Solid,
LineStyle::Dashed,
LineStyle::DashDot,
LineStyle::Dotted,
];
pub fn next_line_style(current: &LineStyle) -> LineStyle {
match LINE_STYLE_CYCLE.iter().position(|s| s == current) {
Some(idx) => LINE_STYLE_CYCLE[(idx + 1) % LINE_STYLE_CYCLE.len()].clone(),
None => LINE_STYLE_CYCLE[0].clone(),
}
}
pub fn show_axis_toggle(plot: &mut PlotWidget) -> bool {
let next = !plot.plot().axes_displayed();
plot.plot_mut().set_axes_displayed(next);
next
}
pub const ZOOM_STEP: f64 = 1.1;
pub fn scale_1d_range(min: f64, max: f64, center: f64, scale: f64, is_log: bool) -> (f64, f64) {
if is_log {
let (lmin, lmax, lcenter) = (min.log10(), max.log10(), center.log10());
if lmin == lmax {
return (min, max);
}
let offset = (lcenter - lmin) / (lmax - lmin);
let range = (lmax - lmin) / scale;
let new_min = lcenter - offset * range;
let new_max = lcenter + (1.0 - offset) * range;
return (10f64.powf(new_min), 10f64.powf(new_max));
}
if min == max {
return (min, max);
}
let offset = (center - min) / (max - min);
let range = (max - min) / scale;
let new_min = center - offset * range;
let new_max = center + (1.0 - offset) * range;
(new_min, new_max)
}
pub fn scale_1d_range_about_midpoint(min: f64, max: f64, scale: f64, is_log: bool) -> (f64, f64) {
let center = if is_log {
(min * max).sqrt()
} else {
0.5 * (min + max)
};
scale_1d_range(min, max, center, scale, is_log)
}
fn apply_zoom(plot: &mut PlotWidget, scale: f64) {
use crate::core::transform::Scale;
let x_log = plot.plot().x_scale == Scale::Log10;
let y_log = plot.plot().y_scale == Scale::Log10;
let (xmin, xmax) = plot.x_limits();
let (nxmin, nxmax) = scale_1d_range_about_midpoint(xmin, xmax, scale, x_log);
let y = plot.y_limits(crate::core::transform::YAxis::Left);
let (nymin, nymax) = match y {
Some((ymin, ymax)) => scale_1d_range_about_midpoint(ymin, ymax, scale, y_log),
None => {
let (_, _, ymin, ymax) = plot.plot().limits;
scale_1d_range_about_midpoint(ymin, ymax, scale, y_log)
}
};
let y2 = plot
.plot()
.y2
.map(|(y2min, y2max)| scale_1d_range_about_midpoint(y2min, y2max, scale, y_log));
plot.set_limits(nxmin, nxmax, nymin, nymax, y2);
}
pub fn zoom_in(plot: &mut PlotWidget) {
apply_zoom(plot, ZOOM_STEP);
}
pub fn zoom_out(plot: &mut PlotWidget) {
apply_zoom(plot, 1.0 / ZOOM_STEP);
}
pub fn zoom_back(plot: &mut PlotWidget) -> bool {
if plot.zoom_back() {
true
} else {
plot.reset_zoom();
false
}
}
pub fn curve_style_cycle(plot: &mut PlotWidget) -> Option<LineStyle> {
plot.cycle_active_curve_style()
}
pub fn toggle_x_autoscale(plot: &mut PlotWidget) -> bool {
let next = !plot.plot().x_autoscale();
plot.plot_mut().set_x_autoscale(next);
if next {
plot.reset_zoom();
}
next
}
pub fn toggle_y_autoscale(plot: &mut PlotWidget) -> bool {
let next = !plot.plot().y_autoscale();
plot.plot_mut().set_y_autoscale(next);
if next {
plot.reset_zoom();
}
next
}
pub fn image_colorbar_toggle(view: &mut ImageView) -> bool {
let next = !view.show_colorbar();
view.set_show_colorbar(next);
next
}
pub fn scatter_colorbar_toggle(view: &mut ScatterView) -> bool {
let next = !view.show_colorbar();
view.set_show_colorbar(next);
next
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn next_line_style_cycles_deterministically_and_wraps() {
assert_eq!(next_line_style(&LineStyle::Solid), LineStyle::Dashed);
assert_eq!(next_line_style(&LineStyle::Dashed), LineStyle::DashDot);
assert_eq!(next_line_style(&LineStyle::DashDot), LineStyle::Dotted);
assert_eq!(next_line_style(&LineStyle::Dotted), LineStyle::Solid);
assert_eq!(next_line_style(&LineStyle::None), LineStyle::Solid);
assert_eq!(
next_line_style(&LineStyle::Custom {
offset: 0.0,
pattern: vec![1.0, 2.0],
}),
LineStyle::Solid
);
}
#[test]
fn cycling_changes_stored_line_style_on_curve_data() {
use crate::render::gpu_curve::CurveData;
use egui::Color32;
let mut data = CurveData::new(vec![0.0, 1.0], vec![0.0, 1.0], Color32::WHITE)
.with_line_style(LineStyle::Solid);
assert_eq!(data.line_style, LineStyle::Solid);
data.line_style = next_line_style(&data.line_style);
assert_eq!(data.line_style, LineStyle::Dashed, "first cycle");
data.line_style = next_line_style(&data.line_style);
assert_eq!(data.line_style, LineStyle::DashDot, "second cycle");
}
#[test]
fn scale_1d_range_zoom_in_and_out_about_midpoint() {
let (zin_min, zin_max) = scale_1d_range_about_midpoint(0.0, 10.0, ZOOM_STEP, false);
assert!((zin_min - (5.0 - 10.0 / 1.1 / 2.0)).abs() < 1e-12);
assert!((zin_max - (5.0 + 10.0 / 1.1 / 2.0)).abs() < 1e-12);
assert!(zin_max - zin_min < 10.0, "zoom in shrinks the range");
let (zout_min, zout_max) = scale_1d_range_about_midpoint(0.0, 10.0, 1.0 / ZOOM_STEP, false);
assert!((zout_min - (-0.5)).abs() < 1e-12);
assert!((zout_max - 10.5).abs() < 1e-12);
assert!(zout_max - zout_min > 10.0, "zoom out grows the range");
}
#[test]
fn scale_1d_range_keeps_off_center_invariant_point() {
let (min, max) = scale_1d_range(2.0, 12.0, 2.0, 2.0, false);
assert!((min - 2.0).abs() < 1e-12, "center stays put");
assert!((max - 7.0).abs() < 1e-12, "range halved from the center");
}
#[test]
fn scale_1d_range_degenerate_is_unchanged() {
assert_eq!(scale_1d_range(3.0, 3.0, 3.0, 1.5, false), (3.0, 3.0));
}
#[test]
fn scale_1d_range_log_scales_in_log10_space() {
let (lo, hi) = scale_1d_range_about_midpoint(1.0, 1000.0, ZOOM_STEP, true);
assert!(
((lo * hi).sqrt() - (1.0_f64 * 1000.0).sqrt()).abs() < 1e-9,
"geometric center fixed"
);
let new_span = hi.log10() - lo.log10();
assert!(
(new_span - 3.0 / ZOOM_STEP).abs() < 1e-9,
"log span / scale"
);
assert!(new_span < 3.0, "zoom in shrinks the log span");
assert!(lo > 0.0 && hi > lo);
}
#[test]
fn zoom_back_restores_pushed_view_then_falls_back_when_empty() {
use crate::core::plot::Plot;
let mut plot = Plot::new(0);
plot.limits = (0.0, 10.0, 0.0, 10.0);
plot.push_limits();
plot.limits = (2.0, 4.0, 2.0, 4.0);
assert_eq!(plot.limits_history_len(), 1);
assert!(plot.zoom_back(), "restored a stored view");
assert_eq!(plot.limits, (0.0, 10.0, 0.0, 10.0));
assert_eq!(plot.limits_history_len(), 0);
assert!(!plot.zoom_back(), "empty history signals fallback");
}
#[test]
fn autoscale_toggle_reset_on_enable_refits_only_that_axis() {
use crate::core::plot::{DataRange, Plot};
let mut plot = Plot::new(0);
plot.limits = (0.0, 1.0, 0.0, 1.0);
plot.set_x_autoscale(false);
plot.set_y_autoscale(true);
let data = DataRange {
x: Some((10.0, 20.0)),
y: Some((-5.0, 5.0)),
y2: None,
};
let next = !plot.x_autoscale();
plot.set_x_autoscale(next);
assert!(next, "X autoscale enabled");
plot.reset_zoom_to_data_range(data);
assert_eq!(plot.limits, (10.0, 20.0, -5.0, 5.0));
}
#[test]
fn autoscale_toggle_disable_does_not_reset() {
use crate::core::plot::Plot;
let mut plot = Plot::new(0);
plot.limits = (3.0, 7.0, 2.0, 8.0);
assert!(plot.x_autoscale(), "default on");
let next = !plot.x_autoscale();
plot.set_x_autoscale(next);
assert!(!next, "X autoscale disabled");
assert_eq!(plot.limits, (3.0, 7.0, 2.0, 8.0));
}
#[test]
fn show_axis_toggle_flips_axes_displayed() {
use crate::core::plot::Plot;
let mut plot = Plot::new(0);
assert!(plot.axes_displayed(), "default is displayed");
let next = !plot.axes_displayed();
plot.set_axes_displayed(next);
assert!(!plot.axes_displayed(), "first toggle hides");
let next = !plot.axes_displayed();
plot.set_axes_displayed(next);
assert!(plot.axes_displayed(), "second toggle shows again");
}
}