Skip to content

Commit b9acc20

Browse files
authored
Internal code cleanup: use, mod and cyclic deps resolution (#212)
1 parent 3259adb commit b9acc20

27 files changed

Lines changed: 739 additions & 766 deletions

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ check_license:
152152

153153
# Checks for dependency cycles between modules.
154154
check_cycles:
155-
cargo-cycles
155+
cargo-cycles --check-public-api --check-absolute-paths
156156

157157
check_linter:
158158
python3 ./scripts/lint.py

egui_plot/src/axis.rs

Lines changed: 279 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,12 @@ use egui::WidgetText;
1515
use egui::emath::Rot2;
1616
use egui::emath::remap_clamp;
1717
use egui::epaint::TextShape;
18+
use emath::Vec2b;
19+
use emath::pos2;
20+
use emath::remap;
1821

19-
use super::transform::PlotTransform;
22+
use crate::bounds::PlotBounds;
23+
use crate::bounds::PlotPoint;
2024
use crate::colors;
2125
use crate::grid::GridMark;
2226
use crate::placement::HPlacement;
@@ -329,3 +333,277 @@ impl<'a> AxisWidget<'a> {
329333
thickness
330334
}
331335
}
336+
337+
/// Contains the screen rectangle and the plot bounds and provides methods to
338+
/// transform between them.
339+
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
340+
#[derive(Clone, Copy, Debug)]
341+
pub struct PlotTransform {
342+
/// The screen rectangle.
343+
frame: Rect,
344+
345+
/// The plot bounds.
346+
bounds: PlotBounds,
347+
348+
/// Whether to always center the x-range or y-range of the bounds.
349+
centered: Vec2b,
350+
351+
/// Whether to always invert the x and/or y axis
352+
inverted_axis: Vec2b,
353+
}
354+
355+
impl PlotTransform {
356+
pub fn new(frame: Rect, bounds: PlotBounds, center_axis: impl Into<Vec2b>) -> Self {
357+
debug_assert!(
358+
0.0 <= frame.width() && 0.0 <= frame.height(),
359+
"Bad plot frame: {frame:?}"
360+
);
361+
let center_axis = center_axis.into();
362+
363+
// Since the current Y bounds an affect the final X bounds and vice versa, we
364+
// need to keep the original version of the `bounds` before we start
365+
// modifying it.
366+
let mut new_bounds = bounds;
367+
368+
// Sanitize bounds.
369+
//
370+
// When a given bound axis is "thin" (e.g. width or height is 0) but finite, we
371+
// center the bounds around that value. If the other axis is "fat", we
372+
// reuse its extent for the thin axis, and default to +/- 1.0 otherwise.
373+
if !bounds.is_finite_x() {
374+
new_bounds.set_x(&PlotBounds::new_symmetrical(1.0));
375+
} else if bounds.width() <= 0.0 {
376+
new_bounds.set_x_center_width(
377+
bounds.center().x,
378+
if bounds.is_valid_y() { bounds.height() } else { 1.0 },
379+
);
380+
}
381+
382+
if !bounds.is_finite_y() {
383+
new_bounds.set_y(&PlotBounds::new_symmetrical(1.0));
384+
} else if bounds.height() <= 0.0 {
385+
new_bounds.set_y_center_height(
386+
bounds.center().y,
387+
if bounds.is_valid_x() { bounds.width() } else { 1.0 },
388+
);
389+
}
390+
391+
// Scale axes so that the origin is in the center.
392+
if center_axis.x {
393+
new_bounds.make_x_symmetrical();
394+
}
395+
if center_axis.y {
396+
new_bounds.make_y_symmetrical();
397+
}
398+
399+
debug_assert!(new_bounds.is_valid(), "Bad final plot bounds: {new_bounds:?}");
400+
401+
Self {
402+
frame,
403+
bounds: new_bounds,
404+
centered: center_axis,
405+
inverted_axis: Vec2b::new(false, false),
406+
}
407+
}
408+
409+
pub fn new_with_invert_axis(
410+
frame: Rect,
411+
bounds: PlotBounds,
412+
center_axis: impl Into<Vec2b>,
413+
invert_axis: impl Into<Vec2b>,
414+
) -> Self {
415+
let mut new = Self::new(frame, bounds, center_axis);
416+
new.inverted_axis = invert_axis.into();
417+
new
418+
}
419+
420+
/// ui-space rectangle.
421+
#[inline]
422+
pub fn frame(&self) -> &Rect {
423+
&self.frame
424+
}
425+
426+
/// Plot-space bounds.
427+
#[inline]
428+
pub fn bounds(&self) -> &PlotBounds {
429+
&self.bounds
430+
}
431+
432+
#[inline]
433+
pub fn set_bounds(&mut self, bounds: PlotBounds) {
434+
self.bounds = bounds;
435+
}
436+
437+
pub fn translate_bounds(&mut self, mut delta_pos: (f64, f64)) {
438+
if self.centered.x {
439+
delta_pos.0 = 0.;
440+
}
441+
if self.centered.y {
442+
delta_pos.1 = 0.;
443+
}
444+
delta_pos.0 *= self.dvalue_dpos()[0];
445+
delta_pos.1 *= self.dvalue_dpos()[1];
446+
self.bounds.translate((delta_pos.0, delta_pos.1));
447+
}
448+
449+
/// Zoom by a relative factor with the given screen position as center.
450+
pub fn zoom(&mut self, zoom_factor: Vec2, center: Pos2) {
451+
let center = self.value_from_position(center);
452+
453+
let mut new_bounds = self.bounds;
454+
new_bounds.zoom(zoom_factor, center);
455+
456+
if new_bounds.is_valid() {
457+
self.bounds = new_bounds;
458+
}
459+
}
460+
461+
pub fn position_from_point_x(&self, value: f64) -> f32 {
462+
remap(
463+
value,
464+
self.bounds.min[0]..=self.bounds.max[0],
465+
if self.inverted_axis[0] {
466+
(self.frame.right() as f64)..=(self.frame.left() as f64)
467+
} else {
468+
(self.frame.left() as f64)..=(self.frame.right() as f64)
469+
},
470+
) as f32
471+
}
472+
473+
pub fn position_from_point_y(&self, value: f64) -> f32 {
474+
remap(
475+
value,
476+
self.bounds.min[1]..=self.bounds.max[1],
477+
// negated y axis by default
478+
if self.inverted_axis[1] {
479+
(self.frame.top() as f64)..=(self.frame.bottom() as f64)
480+
} else {
481+
(self.frame.bottom() as f64)..=(self.frame.top() as f64)
482+
},
483+
) as f32
484+
}
485+
486+
/// Screen/ui position from point on plot.
487+
pub fn position_from_point(&self, value: &PlotPoint) -> Pos2 {
488+
pos2(self.position_from_point_x(value.x), self.position_from_point_y(value.y))
489+
}
490+
491+
/// Plot point from screen/ui position.
492+
pub fn value_from_position(&self, pos: Pos2) -> PlotPoint {
493+
let x = remap(
494+
pos.x as f64,
495+
if self.inverted_axis[0] {
496+
(self.frame.right() as f64)..=(self.frame.left() as f64)
497+
} else {
498+
(self.frame.left() as f64)..=(self.frame.right() as f64)
499+
},
500+
self.bounds.range_x(),
501+
);
502+
let y = remap(
503+
pos.y as f64,
504+
// negated y axis by default
505+
if self.inverted_axis[1] {
506+
(self.frame.top() as f64)..=(self.frame.bottom() as f64)
507+
} else {
508+
(self.frame.bottom() as f64)..=(self.frame.top() as f64)
509+
},
510+
self.bounds.range_y(),
511+
);
512+
513+
PlotPoint::new(x, y)
514+
}
515+
516+
/// Transform a rectangle of plot values to a screen-coordinate rectangle.
517+
///
518+
/// This typically means that the rect is mirrored vertically (top becomes
519+
/// bottom and vice versa), since the plot's coordinate system has +Y
520+
/// up, while egui has +Y down.
521+
pub fn rect_from_values(&self, value1: &PlotPoint, value2: &PlotPoint) -> Rect {
522+
let pos1 = self.position_from_point(value1);
523+
let pos2 = self.position_from_point(value2);
524+
525+
let mut rect = Rect::NOTHING;
526+
rect.extend_with(pos1);
527+
rect.extend_with(pos2);
528+
rect
529+
}
530+
531+
/// delta position / delta value = how many ui points per step in the X axis
532+
/// in "plot space"
533+
pub fn dpos_dvalue_x(&self) -> f64 {
534+
let flip = if self.inverted_axis[0] { -1.0 } else { 1.0 };
535+
flip * (self.frame.width() as f64) / self.bounds.width()
536+
}
537+
538+
/// delta position / delta value = how many ui points per step in the Y axis
539+
/// in "plot space"
540+
pub fn dpos_dvalue_y(&self) -> f64 {
541+
let flip = if self.inverted_axis[1] { 1.0 } else { -1.0 };
542+
flip * (self.frame.height() as f64) / self.bounds.height()
543+
}
544+
545+
/// delta position / delta value = how many ui points per step in "plot
546+
/// space"
547+
pub fn dpos_dvalue(&self) -> [f64; 2] {
548+
[self.dpos_dvalue_x(), self.dpos_dvalue_y()]
549+
}
550+
551+
/// delta value / delta position = how much ground do we cover in "plot
552+
/// space" per ui point?
553+
pub fn dvalue_dpos(&self) -> [f64; 2] {
554+
[1.0 / self.dpos_dvalue_x(), 1.0 / self.dpos_dvalue_y()]
555+
}
556+
557+
/// scale.x/scale.y ratio.
558+
///
559+
/// If 1.0, it means the scale factor is the same in both axes.
560+
fn aspect(&self) -> f64 {
561+
let rw = self.frame.width() as f64;
562+
let rh = self.frame.height() as f64;
563+
(self.bounds.width() / rw) / (self.bounds.height() / rh)
564+
}
565+
566+
/// Sets the aspect ratio by expanding the x- or y-axis.
567+
///
568+
/// This never contracts, so we don't miss out on any data.
569+
pub(crate) fn set_aspect_by_expanding(&mut self, aspect: f64) {
570+
let current_aspect = self.aspect();
571+
572+
let epsilon = 1e-5;
573+
if (current_aspect - aspect).abs() < epsilon {
574+
// Don't make any changes when the aspect is already almost correct.
575+
return;
576+
}
577+
578+
if current_aspect < aspect {
579+
self.bounds
580+
.expand_x((aspect / current_aspect - 1.0) * self.bounds.width() * 0.5);
581+
} else {
582+
self.bounds
583+
.expand_y((current_aspect / aspect - 1.0) * self.bounds.height() * 0.5);
584+
}
585+
}
586+
587+
/// Sets the aspect ratio by changing either the X or Y axis (callers
588+
/// choice).
589+
pub(crate) fn set_aspect_by_changing_axis(&mut self, aspect: f64, axis: Axis) {
590+
let current_aspect = self.aspect();
591+
592+
let epsilon = 1e-5;
593+
if (current_aspect - aspect).abs() < epsilon {
594+
// Don't make any changes when the aspect is already almost correct.
595+
return;
596+
}
597+
598+
match axis {
599+
Axis::X => {
600+
self.bounds
601+
.expand_x((aspect / current_aspect - 1.0) * self.bounds.width() * 0.5);
602+
}
603+
Axis::Y => {
604+
self.bounds
605+
.expand_y((current_aspect / aspect - 1.0) * self.bounds.height() * 0.5);
606+
}
607+
}
608+
}
609+
}

egui_plot/src/bounds.rs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,50 @@ use std::ops::RangeInclusive;
22

33
use ahash::HashMap;
44
use egui::Id;
5+
use emath::Pos2;
56
use emath::Vec2;
67
use emath::Vec2b;
78

8-
use crate::PlotPoint;
9+
/// A point coordinate in the plot.
10+
///
11+
/// Uses f64 for improved accuracy to enable plotting
12+
/// large values (e.g. unix time on x axis).
13+
#[derive(Clone, Copy, Debug, PartialEq)]
14+
pub struct PlotPoint {
15+
/// This is often something monotonically increasing, such as time, but
16+
/// doesn't have to be. Goes from left to right.
17+
pub x: f64,
18+
19+
/// Goes from bottom to top (inverse of everything else in egui!).
20+
pub y: f64,
21+
}
22+
23+
impl From<[f64; 2]> for PlotPoint {
24+
#[inline]
25+
fn from([x, y]: [f64; 2]) -> Self {
26+
Self { x, y }
27+
}
28+
}
29+
30+
impl PlotPoint {
31+
#[inline(always)]
32+
pub fn new(x: impl Into<f64>, y: impl Into<f64>) -> Self {
33+
Self {
34+
x: x.into(),
35+
y: y.into(),
36+
}
37+
}
38+
39+
#[inline(always)]
40+
pub fn to_pos2(self) -> Pos2 {
41+
Pos2::new(self.x as f32, self.y as f32)
42+
}
43+
44+
#[inline(always)]
45+
pub fn to_vec2(self) -> Vec2 {
46+
Vec2::new(self.x as f32, self.y as f32)
47+
}
48+
}
949

1050
/// 2D bounding box of f64 precision.
1151
///

0 commit comments

Comments
 (0)