Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 80 additions & 57 deletions egui_plot/src/axis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,18 @@ impl<'a> AxisHints<'a> {

fn default_formatter(mark: GridMark, _range: &RangeInclusive<f64>) -> String {
// Example: If the step to the next tick is `0.01`, we should use 2 decimals of precision:
let num_decimals = -mark.step_size.log10().round() as usize;

emath::format_with_decimals_in_range(mark.value, num_decimals..=num_decimals)
let mut num_decimals = (-mark.step_size.log10().round()) as isize;
#[allow(clippy::manual_clamp)]
if num_decimals < 0 {
num_decimals = 0;
}
if num_decimals > 10 {
num_decimals = 10;
}
emath::format_with_decimals_in_range(
mark.value,
num_decimals as usize..=num_decimals as usize,
)
}

/// Specify axis label.
Expand Down Expand Up @@ -306,22 +315,20 @@ impl<'a> AxisWidget<'a> {
// Add tick labels:
if axis == Axis::X {
if let Some(bx) = transform.segment_xaxis() {
let text_color = ui.visuals().text_color();

let step_hint = estimate_step_hint_data_units(transform);

let raw_ticks = compute_segmented_x_ticks(transform, bx, step_hint);
const DESIRED_PX: f32 = 80.0;
let raw_ticks = compute_segmented_x_ticks_per_segment(transform, bx, DESIRED_PX);

const CLUSTER_PX_THRESHOLD: f32 = 6.0;
let clusters = cluster_overlapping_ticks(raw_ticks, CLUSTER_PX_THRESHOLD);

let text_color = ui.visuals().text_color();
let mut last_drawn_center_x: Option<f32> = None;
let mut thickness: f32 = 0.0;

for cluster in clusters {
if !cluster.has_edge {
if let Some(prev_cx) = last_drawn_center_x {
if (cluster.screen_x - prev_cx).abs() < self.hints.label_spacing.min {
if let Some(prev) = last_drawn_center_x {
if (cluster.screen_x - prev).abs() < self.hints.label_spacing.min {
continue;
}
}
Expand All @@ -346,7 +353,7 @@ impl<'a> AxisWidget<'a> {
for (tick, side) in to_draw {
let gm = GridMark {
value: tick.world_x,
step_size: step_hint,
step_size: tick.step_size,
};
let txt = (self.hints.formatter)(gm, &self.range);
if txt.is_empty() {
Expand All @@ -368,7 +375,6 @@ impl<'a> AxisWidget<'a> {
};

let label_pos = Pos2::new(label_pos_x, y);

if label_pos.x + galley_size.x < self.rect.min.x {
continue;
}
Expand All @@ -377,7 +383,6 @@ impl<'a> AxisWidget<'a> {
}

painter.add(TextShape::new(label_pos, galley, text_color));

thickness = thickness.max(galley_size.y);
}

Expand Down Expand Up @@ -466,58 +471,15 @@ impl<'a> AxisWidget<'a> {
thickness
}
}
fn estimate_step_hint_data_units(transform: &PlotTransform) -> f64 {
let desired_px_spacing: f32 = 80.0;

let units_per_px = transform.dvalue_dpos()[0] as f32;
(units_per_px.abs() * desired_px_spacing) as f64
}
#[derive(Clone, Copy, Debug)]
struct ScreenTick {
world_x: f64,
screen_x: f32,
step_size: f64,
is_segment_edge: bool,
}

fn compute_segmented_x_ticks(
tf: &PlotTransform,
bx: &crate::SegmentedAxis,
step_hint: f64,
) -> Vec<ScreenTick> {
let per_seg_ticks = bx.segment_ticks(step_hint);

let mut out = Vec::new();

for (seg_idx, ticks_for_seg) in per_seg_ticks.iter().enumerate() {
let seg = &bx.segments[seg_idx];

for &world_x in ticks_for_seg {
if !world_x.is_finite() {
continue;
}

let screen_x = tf.position_from_point_x(world_x);

if !screen_x.is_finite() {
continue;
}

out.push(ScreenTick {
world_x,
screen_x,
is_segment_edge: (world_x == seg.start) || (world_x == seg.end),
});
}
}

out.sort_by(|a, b| {
a.screen_x
.partial_cmp(&b.screen_x)
.unwrap_or(std::cmp::Ordering::Equal)
});

out
}
#[derive(Clone)]
struct TickCluster {
pub screen_x: f32,
Expand Down Expand Up @@ -567,3 +529,64 @@ enum TickSide {
Right,
Center,
}
fn compute_segmented_x_ticks_per_segment(
tf: &PlotTransform,
bx: &crate::SegmentedAxis,
desired_px: f32,
) -> Vec<ScreenTick> {
let mut out = Vec::new();

for seg in &bx.segments {
let sx0 = tf.position_from_point_x(seg.start);
let sx1 = tf.position_from_point_x(seg.end);
let seg_px = (sx1 - sx0).abs().max(1.0);
let seg_len = seg.end - seg.start;

let n_steps = (seg_px / desired_px).ceil().max(1.0) as i32;

if n_steps <= 1 {
let step_size = seg_len.abs().max(1e-6);
out.push(ScreenTick {
world_x: seg.start,
screen_x: sx0,
step_size,
is_segment_edge: true,
});
out.push(ScreenTick {
world_x: seg.end,
screen_x: sx1,
step_size,
is_segment_edge: true,
});
continue;
}

let step = (seg_len / n_steps as f64).abs().max(1e-6);

for i in 0..=n_steps {
let world_x = seg.start + (step * i as f64).copysign(seg_len);
let screen_x = tf.position_from_point_x(world_x);
if !screen_x.is_finite() {
continue;
}

let is_edge = (world_x - seg.start).abs() < f64::EPSILON
|| (world_x - seg.end).abs() < f64::EPSILON;

out.push(ScreenTick {
world_x,
screen_x,
step_size: step,
is_segment_edge: is_edge,
});
}
}

out.sort_by(|a, b| {
a.screen_x
.partial_cmp(&b.screen_x)
.unwrap_or(std::cmp::Ordering::Equal)
});

out
}
68 changes: 65 additions & 3 deletions egui_plot/src/items/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1068,11 +1068,73 @@ impl PlotItem for Line<'_> {

let draw_stroke = final_stroke.width > 0.0
&& final_stroke.color != egui::epaint::ColorMode::Solid(Color32::TRANSPARENT);
if !draw_stroke {
return;
}


let dx_per_px = transform.dvalue_dpos()[0].abs() as f32;

let dx_per_px = if dx_per_px > 0.0 { dx_per_px } else { 1.0 };


let max_jump_px = bx.gap_px;

let mut scratch: Vec<Pos2> = Vec::new();
let mut current: Vec<Pos2> = Vec::new();


let mut last_screen: Option<Pos2> = None;
let mut last_x: Option<f64> = None;

for idx in i0..=i1 {
// screen pos
let p = get_pos(idx);


let x_val = match src {
Src::Col { xs, .. } => xs[idx],
Src::Legacy { pts } => pts[idx].x,
Src::Empty => unreachable!(),
};


let mut break_here = false;
if let (Some(prev_p), Some(prev_x)) = (last_screen, last_x) {

let screen_jump = (p.x - prev_p.x).abs();


let data_gap = (x_val - prev_x).abs() as f32;
let natural_px_gap = data_gap / dx_per_px;

if screen_jump > max_jump_px * 0.9 || natural_px_gap > max_jump_px * 0.9 {
break_here = true;
}
}

if break_here {
if current.len() > 1 {
style.style_line_iter(
current.iter().copied(),
final_stroke.clone(),
base.highlight,
shapes,
&mut scratch,
);
}
current.clear();
}

current.push(p);
last_screen = Some(p);
last_x = Some(x_val);
}

if draw_stroke {
let mut scratch: Vec<Pos2> = Vec::new();

if current.len() > 1 {
style.style_line_iter(
(i0..=i1).map(&get_pos),
current.into_iter(),
final_stroke.clone(),
base.highlight,
shapes,
Expand Down
17 changes: 14 additions & 3 deletions egui_plot/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -951,9 +951,10 @@ impl<'a> Plot<'a> {
last_click_pos_for_zoom: None,
x_axis_thickness: Default::default(),
y_axis_thickness: Default::default(),
segmented_x_offset: 0.0,
});

let last_plot_transform = mem.transform;
let last_plot_transform = mem.transform.clone();
// Call the plot build function.
let mut plot_ui = PlotUi {
ctx: ui.ctx().clone(),
Expand Down Expand Up @@ -1112,10 +1113,11 @@ impl<'a> Plot<'a> {
}

// Build transform
mem.transform = PlotTransform::new(plot_rect, bounds, center_axis);

mem.transform.set_segment_xaxis(segmented_x_axis);
let mut new_tf = PlotTransform::new(plot_rect, bounds, center_axis);
new_tf.set_segment_xaxis(segmented_x_axis);

mem.set_transform(new_tf);
// Aspect
if let Some(data_aspect) = data_aspect {
if let Some((_, linked_axes)) = &linked_axes {
Expand Down Expand Up @@ -1167,6 +1169,9 @@ impl<'a> Plot<'a> {

mem.transform
.translate_bounds((delta.x as f64, delta.y as f64));
if mem.transform.segment_xaxis().is_some() {
mem.segmented_x_offset = mem.transform.segment_x_offset();
}
mem.auto_bounds = mem.auto_bounds.and(!allow_drag);
last_user_cause = Some(BoundsChangeCause::Pan);

Expand Down Expand Up @@ -1497,6 +1502,12 @@ impl<'a> Plot<'a> {
}

let transform = mem.transform.clone();

// if segmented, remember the offset we ended up with this frame
if mem.transform.segment_xaxis().is_some() {
mem.segmented_x_offset = mem.transform.segment_x_offset();
}

mem.store(ui.ctx(), plot_id);

response = if show_x || show_y {
Expand Down
14 changes: 12 additions & 2 deletions egui_plot/src/memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,29 @@ pub struct PlotMemory {
/// in order to fit the labels, if necessary.
pub(crate) x_axis_thickness: BTreeMap<usize, f32>,
pub(crate) y_axis_thickness: BTreeMap<usize, f32>,

pub(crate) segmented_x_offset: f64,
}

impl PlotMemory {
#[inline]
pub fn transform(&self) -> &PlotTransform {
&self.transform
}

#[inline]
pub fn set_transform(&mut self, t: PlotTransform) {
pub fn set_transform(&mut self, mut t: PlotTransform) {
if t.segment_xaxis().is_some() {
t.set_segment_x_offset(self.segmented_x_offset);
}
self.transform = t;
}

#[inline]
pub fn update_segmented_x_offset_from_transform(&mut self) {
if self.transform.segment_xaxis().is_some() {
self.segmented_x_offset = self.transform.segment_x_offset();
}
}
/// Plot-space bounds.
#[inline]
pub fn bounds(&self) -> &PlotBounds {
Expand Down
Loading
Loading