Drag and zoom support for plots (#317)
* drag and zoom support for plots * update doctest * use impl ToString * revert back to Into<String> until #302 is solved * Apply suggestions from code review Co-authored-by: ilya sheprut <optitel223@gmail.com> * use persistence feature for PlotMemory * rename shift -> translate * remove automatic bounds * removed unused methods * Into<String> -> ToString * Apply suggestions from code review Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com> * avoid potential invalid bounds bug * use new is_valid method * improve auto bounds behavior as suggested * use NOTHING to initialize min_auto_bounds Co-authored-by: ilya sheprut <optitel223@gmail.com> Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This commit is contained in:
parent
cb14e8571f
commit
012542d066
3 changed files with 234 additions and 105 deletions
|
@ -35,7 +35,8 @@ impl Value {
|
|||
|
||||
/// 2D bounding box of f64 precision.
|
||||
/// The range of data values we show.
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
#[derive(Clone, Copy, PartialEq, Debug)]
|
||||
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
|
||||
struct Bounds {
|
||||
min: [f64; 2],
|
||||
max: [f64; 2],
|
||||
|
@ -62,6 +63,10 @@ impl Bounds {
|
|||
&& self.max[1].is_finite()
|
||||
}
|
||||
|
||||
pub fn is_valid(&self) -> bool {
|
||||
self.is_finite() && self.width() > 0.0 && self.height() > 0.0
|
||||
}
|
||||
|
||||
pub fn extend_with(&mut self, value: &Value) {
|
||||
self.extend_with_x(value.x);
|
||||
self.extend_with_y(value.y);
|
||||
|
@ -89,12 +94,34 @@ impl Bounds {
|
|||
self.max[1] += pad;
|
||||
}
|
||||
|
||||
pub fn union_mut(&mut self, other: &Bounds) {
|
||||
pub fn merge(&mut self, other: &Bounds) {
|
||||
self.min[0] = self.min[0].min(other.min[0]);
|
||||
self.min[1] = self.min[1].min(other.min[1]);
|
||||
self.max[0] = self.max[0].max(other.max[0]);
|
||||
self.max[1] = self.max[1].max(other.max[1]);
|
||||
}
|
||||
|
||||
pub fn translate_x(&mut self, delta: f64) {
|
||||
self.min[0] += delta;
|
||||
self.max[0] += delta;
|
||||
}
|
||||
|
||||
pub fn translate_y(&mut self, delta: f64) {
|
||||
self.min[1] += delta;
|
||||
self.max[1] += delta;
|
||||
}
|
||||
|
||||
pub fn translate(&mut self, delta: Vec2) {
|
||||
self.translate_x(delta.x as f64);
|
||||
self.translate_y(delta.y as f64);
|
||||
}
|
||||
|
||||
pub fn add_relative_margin(&mut self, margin_fraction: Vec2) {
|
||||
let width = self.width();
|
||||
let height = self.height();
|
||||
self.expand_x(margin_fraction.x as f64 * width);
|
||||
self.expand_y(margin_fraction.y as f64 * height);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
@ -192,14 +219,25 @@ impl Curve {
|
|||
}
|
||||
|
||||
/// Name of this curve.
|
||||
pub fn name(mut self, name: impl Into<String>) -> Self {
|
||||
self.name = name.into();
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn name(mut self, name: impl ToString) -> Self {
|
||||
self.name = name.to_string();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Information about the plot that has to persist between frames.
|
||||
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[derive(Clone)]
|
||||
struct PlotMemory {
|
||||
bounds: Bounds,
|
||||
auto_bounds: bool,
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// A 2D plot, e.g. a graph of a function.
|
||||
///
|
||||
/// `Plot` supports multiple curves.
|
||||
|
@ -213,18 +251,18 @@ impl Curve {
|
|||
/// });
|
||||
/// let curve = Curve::from_values_iter(sin);
|
||||
/// ui.add(
|
||||
/// Plot::default().curve(curve).view_aspect(2.0)
|
||||
/// Plot::new("Test Plot").curve(curve).view_aspect(2.0)
|
||||
/// );
|
||||
/// ```
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct Plot {
|
||||
name: String,
|
||||
next_auto_color_idx: usize,
|
||||
|
||||
curves: Vec<Curve>,
|
||||
hlines: Vec<HLine>,
|
||||
vlines: Vec<VLine>,
|
||||
|
||||
bounds: Bounds,
|
||||
symmetrical_x_bounds: bool,
|
||||
symmetrical_y_bounds: bool,
|
||||
margin_fraction: Vec2,
|
||||
|
@ -235,20 +273,23 @@ pub struct Plot {
|
|||
data_aspect: Option<f32>,
|
||||
view_aspect: Option<f32>,
|
||||
|
||||
min_auto_bounds: Bounds,
|
||||
|
||||
show_x: bool,
|
||||
show_y: bool,
|
||||
}
|
||||
|
||||
impl Default for Plot {
|
||||
fn default() -> Self {
|
||||
impl Plot {
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn new(name: impl ToString) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
next_auto_color_idx: 0,
|
||||
|
||||
curves: Default::default(),
|
||||
hlines: Default::default(),
|
||||
vlines: Default::default(),
|
||||
|
||||
bounds: Bounds::NOTHING,
|
||||
symmetrical_x_bounds: false,
|
||||
symmetrical_y_bounds: false,
|
||||
margin_fraction: Vec2::splat(0.05),
|
||||
|
@ -259,13 +300,13 @@ impl Default for Plot {
|
|||
data_aspect: None,
|
||||
view_aspect: None,
|
||||
|
||||
min_auto_bounds: Bounds::NOTHING,
|
||||
|
||||
show_x: true,
|
||||
show_y: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Plot {
|
||||
fn auto_color(&mut self, color: &mut Color32) {
|
||||
if *color == Color32::TRANSPARENT {
|
||||
let i = self.next_auto_color_idx;
|
||||
|
@ -279,9 +320,10 @@ impl Plot {
|
|||
/// Add a data curve.
|
||||
/// You can add multiple curves.
|
||||
pub fn curve(mut self, mut curve: Curve) -> Self {
|
||||
self.auto_color(&mut curve.stroke.color);
|
||||
self.bounds.union_mut(&curve.bounds);
|
||||
self.curves.push(curve);
|
||||
if !curve.values.is_empty() {
|
||||
self.auto_color(&mut curve.stroke.color);
|
||||
self.curves.push(curve);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -290,7 +332,6 @@ impl Plot {
|
|||
/// Always fills the full width of the plot.
|
||||
pub fn hline(mut self, mut hline: HLine) -> Self {
|
||||
self.auto_color(&mut hline.stroke.color);
|
||||
self = self.include_y(hline.y);
|
||||
self.hlines.push(hline);
|
||||
self
|
||||
}
|
||||
|
@ -300,24 +341,10 @@ impl Plot {
|
|||
/// Always fills the full height of the plot.
|
||||
pub fn vline(mut self, mut vline: VLine) -> Self {
|
||||
self.auto_color(&mut vline.stroke.color);
|
||||
self = self.include_x(vline.x);
|
||||
self.vlines.push(vline);
|
||||
self
|
||||
}
|
||||
|
||||
/// Expand bounds to include the given x value.
|
||||
pub fn include_x(mut self, x: impl Into<f64>) -> Self {
|
||||
self.bounds.extend_with_x(x.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Expand bounds to include the given y value.
|
||||
/// For instance, to always show the x axis, call `plot.include_y(0.0)`.
|
||||
pub fn include_y(mut self, y: impl Into<f64>) -> Self {
|
||||
self.bounds.extend_with_y(y.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// If true, the x-bounds will be symmetrical, so that the x=0 zero line
|
||||
/// is always in the center.
|
||||
pub fn symmetrical_x_bounds(mut self, symmetrical_x_bounds: bool) -> Self {
|
||||
|
@ -332,6 +359,19 @@ impl Plot {
|
|||
self
|
||||
}
|
||||
|
||||
/// Expand bounds to include the given x value.
|
||||
pub fn include_x(mut self, x: impl Into<f64>) -> Self {
|
||||
self.min_auto_bounds.extend_with_x(x.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Expand bounds to include the given y value.
|
||||
/// For instance, to always show the x axis, call `plot.include_y(0.0)`.
|
||||
pub fn include_y(mut self, y: impl Into<f64>) -> Self {
|
||||
self.min_auto_bounds.extend_with_y(y.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// width / height ratio of the data.
|
||||
/// For instance, it can be useful to set this to `1.0` for when the two axes show the same unit.
|
||||
pub fn data_aspect(mut self, data_aspect: f32) -> Self {
|
||||
|
@ -384,11 +424,11 @@ impl Plot {
|
|||
impl Widget for Plot {
|
||||
fn ui(self, ui: &mut Ui) -> Response {
|
||||
let Self {
|
||||
name,
|
||||
next_auto_color_idx: _,
|
||||
curves,
|
||||
hlines,
|
||||
vlines,
|
||||
bounds,
|
||||
symmetrical_x_bounds,
|
||||
symmetrical_y_bounds,
|
||||
margin_fraction,
|
||||
|
@ -399,12 +439,25 @@ impl Widget for Plot {
|
|||
view_aspect,
|
||||
show_x,
|
||||
show_y,
|
||||
min_auto_bounds,
|
||||
} = self;
|
||||
|
||||
let size = {
|
||||
let width = width.map(|w| w.at_least(min_size.x));
|
||||
let height = height.map(|w| w.at_least(min_size.y));
|
||||
let plot_id = ui.make_persistent_id(name);
|
||||
let memory = ui
|
||||
.memory()
|
||||
.id_data
|
||||
.get_mut_or_insert_with(plot_id, || PlotMemory {
|
||||
bounds: min_auto_bounds,
|
||||
auto_bounds: true,
|
||||
})
|
||||
.clone();
|
||||
|
||||
let PlotMemory {
|
||||
mut bounds,
|
||||
mut auto_bounds,
|
||||
} = memory;
|
||||
|
||||
let size = {
|
||||
let width = width.unwrap_or_else(|| {
|
||||
if let (Some(height), Some(aspect)) = (height, view_aspect) {
|
||||
height * aspect
|
||||
|
@ -425,9 +478,17 @@ impl Widget for Plot {
|
|||
vec2(width, height)
|
||||
};
|
||||
|
||||
let (rect, response) = ui.allocate_exact_size(size, Sense::hover());
|
||||
let (rect, response) = ui.allocate_exact_size(size, Sense::drag());
|
||||
|
||||
let mut bounds = bounds;
|
||||
auto_bounds |= response.double_clicked_by(PointerButton::Primary);
|
||||
|
||||
if auto_bounds || !bounds.is_valid() {
|
||||
bounds = min_auto_bounds;
|
||||
hlines.iter().for_each(|line| bounds.extend_with_y(line.y));
|
||||
vlines.iter().for_each(|line| bounds.extend_with_x(line.x));
|
||||
curves.iter().for_each(|curve| bounds.merge(&curve.bounds));
|
||||
bounds.add_relative_margin(margin_fraction);
|
||||
}
|
||||
|
||||
if symmetrical_x_bounds {
|
||||
let x_abs = bounds.min[0].abs().max(bounds.max[0].abs());
|
||||
|
@ -440,17 +501,16 @@ impl Widget for Plot {
|
|||
bounds.max[1] = y_abs;
|
||||
};
|
||||
|
||||
bounds.expand_x(margin_fraction.x as f64 * bounds.width());
|
||||
bounds.expand_y(margin_fraction.y as f64 * bounds.height());
|
||||
|
||||
if let Some(data_aspect) = data_aspect {
|
||||
let data_aspect = data_aspect as f64;
|
||||
let rw = rect.width() as f64;
|
||||
let rh = rect.height() as f64;
|
||||
let current_data_aspect = (bounds.width() / rw) / (bounds.height() / rh);
|
||||
if current_data_aspect < data_aspect {
|
||||
|
||||
let epsilon = 1e-5;
|
||||
if current_data_aspect < data_aspect - epsilon {
|
||||
bounds.expand_x((data_aspect / current_data_aspect - 1.0) * bounds.width() * 0.5);
|
||||
} else {
|
||||
} else if current_data_aspect > data_aspect + epsilon {
|
||||
bounds.expand_y((current_data_aspect / data_aspect - 1.0) * bounds.height() * 0.5);
|
||||
}
|
||||
}
|
||||
|
@ -463,15 +523,35 @@ impl Widget for Plot {
|
|||
stroke: ui.visuals().window_stroke(),
|
||||
});
|
||||
|
||||
if bounds.is_finite() && bounds.width() > 0.0 && bounds.height() > 0.0 {
|
||||
if bounds.is_valid() {
|
||||
let mut transform = ScreenTransform { bounds, rect };
|
||||
if response.dragged_by(PointerButton::Primary) {
|
||||
transform.shift_bounds(-response.drag_delta());
|
||||
auto_bounds = false;
|
||||
}
|
||||
if let Some(hover_pos) = response.hover_pos() {
|
||||
let scroll_delta = ui.input().scroll_delta[1];
|
||||
if scroll_delta != 0. {
|
||||
transform.zoom(-0.01 * scroll_delta, hover_pos);
|
||||
auto_bounds = false;
|
||||
}
|
||||
}
|
||||
|
||||
ui.memory().id_data.insert(
|
||||
plot_id,
|
||||
PlotMemory {
|
||||
bounds: *transform.bounds(),
|
||||
auto_bounds,
|
||||
},
|
||||
);
|
||||
|
||||
let prepared = Prepared {
|
||||
curves,
|
||||
hlines,
|
||||
vlines,
|
||||
rect,
|
||||
bounds,
|
||||
show_x,
|
||||
show_y,
|
||||
transform,
|
||||
};
|
||||
prepared.ui(ui, &response);
|
||||
}
|
||||
|
@ -480,18 +560,44 @@ impl Widget for Plot {
|
|||
}
|
||||
}
|
||||
|
||||
struct Prepared {
|
||||
curves: Vec<Curve>,
|
||||
hlines: Vec<HLine>,
|
||||
vlines: Vec<VLine>,
|
||||
/// Screen space position of the plot
|
||||
/// Contains the screen rectangle and the plot bounds and provides methods to transform them.
|
||||
struct ScreenTransform {
|
||||
/// The screen rectangle.
|
||||
rect: Rect,
|
||||
/// The plot bounds.
|
||||
bounds: Bounds,
|
||||
show_x: bool,
|
||||
show_y: bool,
|
||||
}
|
||||
|
||||
impl Prepared {
|
||||
impl ScreenTransform {
|
||||
fn rect(&self) -> &Rect {
|
||||
&self.rect
|
||||
}
|
||||
|
||||
fn bounds(&self) -> &Bounds {
|
||||
&self.bounds
|
||||
}
|
||||
|
||||
fn shift_bounds(&mut self, mut delta_pos: Vec2) {
|
||||
delta_pos.x *= self.dvalue_dpos()[0] as f32;
|
||||
delta_pos.y *= self.dvalue_dpos()[1] as f32;
|
||||
self.bounds.translate(delta_pos);
|
||||
}
|
||||
|
||||
/// Zoom by a relative amount with the given screen position as center.
|
||||
fn zoom(&mut self, delta: f32, center: Pos2) {
|
||||
let delta = delta.clamp(-1., 1.);
|
||||
let rect_width = self.rect.width();
|
||||
let rect_height = self.rect.height();
|
||||
let bounds_width = self.bounds.width() as f32;
|
||||
let bounds_height = self.bounds.height() as f32;
|
||||
let t_x = (center.x - self.rect.min[0]) / rect_width;
|
||||
let t_y = (self.rect.max[1] - center.y) / rect_height;
|
||||
self.bounds.min[0] -= ((t_x * delta) * bounds_width) as f64;
|
||||
self.bounds.min[1] -= ((t_y * delta) * bounds_height) as f64;
|
||||
self.bounds.max[0] += (((1. - t_x) * delta) * bounds_width) as f64;
|
||||
self.bounds.max[1] += (((1. - t_y) * delta) * bounds_height) as f64;
|
||||
}
|
||||
|
||||
fn position_from_value(&self, value: &Value) -> Pos2 {
|
||||
let x = remap(
|
||||
value.x,
|
||||
|
@ -506,6 +612,20 @@ impl Prepared {
|
|||
pos2(x as f32, y as f32)
|
||||
}
|
||||
|
||||
fn value_from_position(&self, pos: Pos2) -> Value {
|
||||
let x = remap(
|
||||
pos.x as f64,
|
||||
(self.rect.left() as f64)..=(self.rect.right() as f64),
|
||||
self.bounds.min[0]..=self.bounds.max[0],
|
||||
);
|
||||
let y = remap(
|
||||
pos.y as f64,
|
||||
(self.rect.bottom() as f64)..=(self.rect.top() as f64), // negated y axis!
|
||||
self.bounds.min[1]..=self.bounds.max[1],
|
||||
);
|
||||
Value::new(x, y)
|
||||
}
|
||||
|
||||
/// delta position / delta value
|
||||
fn dpos_dvalue_x(&self) -> f64 {
|
||||
self.rect.width() as f64 / self.bounds.width()
|
||||
|
@ -525,23 +645,22 @@ impl Prepared {
|
|||
fn dvalue_dpos(&self) -> [f64; 2] {
|
||||
[1.0 / self.dpos_dvalue_x(), 1.0 / self.dpos_dvalue_y()]
|
||||
}
|
||||
}
|
||||
|
||||
fn value_from_position(&self, pos: Pos2) -> Value {
|
||||
let x = remap(
|
||||
pos.x as f64,
|
||||
(self.rect.left() as f64)..=(self.rect.right() as f64),
|
||||
self.bounds.min[0]..=self.bounds.max[0],
|
||||
);
|
||||
let y = remap(
|
||||
pos.y as f64,
|
||||
(self.rect.bottom() as f64)..=(self.rect.top() as f64), // negated y axis!
|
||||
self.bounds.min[1]..=self.bounds.max[1],
|
||||
);
|
||||
Value::new(x, y)
|
||||
}
|
||||
struct Prepared {
|
||||
curves: Vec<Curve>,
|
||||
hlines: Vec<HLine>,
|
||||
vlines: Vec<VLine>,
|
||||
show_x: bool,
|
||||
show_y: bool,
|
||||
transform: ScreenTransform,
|
||||
}
|
||||
|
||||
impl Prepared {
|
||||
fn ui(&self, ui: &mut Ui, response: &Response) {
|
||||
let mut shapes = Vec::with_capacity(self.hlines.len() + self.curves.len() + 2);
|
||||
let Self { transform, .. } = self;
|
||||
|
||||
let mut shapes = Vec::new();
|
||||
|
||||
for d in 0..2 {
|
||||
self.paint_axis(ui, d, &mut shapes);
|
||||
|
@ -550,8 +669,8 @@ impl Prepared {
|
|||
for &hline in &self.hlines {
|
||||
let HLine { y, stroke } = hline;
|
||||
let points = [
|
||||
self.position_from_value(&Value::new(self.bounds.min[0], y)),
|
||||
self.position_from_value(&Value::new(self.bounds.max[0], y)),
|
||||
transform.position_from_value(&Value::new(transform.bounds().min[0], y)),
|
||||
transform.position_from_value(&Value::new(transform.bounds().max[0], y)),
|
||||
];
|
||||
shapes.push(Shape::line_segment(points, stroke));
|
||||
}
|
||||
|
@ -559,8 +678,8 @@ impl Prepared {
|
|||
for &vline in &self.vlines {
|
||||
let VLine { x, stroke } = vline;
|
||||
let points = [
|
||||
self.position_from_value(&Value::new(x, self.bounds.min[1])),
|
||||
self.position_from_value(&Value::new(x, self.bounds.max[1])),
|
||||
transform.position_from_value(&Value::new(x, transform.bounds().min[1])),
|
||||
transform.position_from_value(&Value::new(x, transform.bounds().max[1])),
|
||||
];
|
||||
shapes.push(Shape::line_segment(points, stroke));
|
||||
}
|
||||
|
@ -568,39 +687,41 @@ impl Prepared {
|
|||
for curve in &self.curves {
|
||||
let stroke = curve.stroke;
|
||||
let values = &curve.values;
|
||||
if values.len() == 1 {
|
||||
let point = self.position_from_value(&values[0]);
|
||||
shapes.push(Shape::circle_filled(
|
||||
point,
|
||||
stroke.width / 2.0,
|
||||
stroke.color,
|
||||
));
|
||||
} else if values.len() > 1 {
|
||||
shapes.push(Shape::line(
|
||||
values.iter().map(|v| self.position_from_value(v)).collect(),
|
||||
let shape = if values.len() == 1 {
|
||||
let point = transform.position_from_value(&values[0]);
|
||||
Shape::circle_filled(point, stroke.width / 2.0, stroke.color)
|
||||
} else {
|
||||
Shape::line(
|
||||
values
|
||||
.iter()
|
||||
.map(|v| transform.position_from_value(v))
|
||||
.collect(),
|
||||
stroke,
|
||||
));
|
||||
}
|
||||
)
|
||||
};
|
||||
shapes.push(shape);
|
||||
}
|
||||
|
||||
if let Some(pointer) = response.hover_pos() {
|
||||
self.hover(ui, pointer, &mut shapes);
|
||||
}
|
||||
|
||||
ui.painter().sub_region(self.rect).extend(shapes);
|
||||
ui.painter().sub_region(*transform.rect()).extend(shapes);
|
||||
}
|
||||
|
||||
fn paint_axis(&self, ui: &Ui, axis: usize, shapes: &mut Vec<Shape>) {
|
||||
let bounds = self.bounds;
|
||||
let Self { transform, .. } = self;
|
||||
|
||||
let bounds = transform.bounds();
|
||||
let text_style = TextStyle::Body;
|
||||
|
||||
let base: f64 = 10.0;
|
||||
|
||||
let min_label_spacing_in_points = 60.0; // TODO: large enough for a wide label
|
||||
let step_size = self.dvalue_dpos()[axis] * min_label_spacing_in_points;
|
||||
let step_size = transform.dvalue_dpos()[axis] * min_label_spacing_in_points;
|
||||
let step_size = base.powi(step_size.abs().log(base).ceil() as i32);
|
||||
|
||||
let step_size_in_points = (self.dpos_dvalue()[axis] * step_size) as f32;
|
||||
let step_size_in_points = (transform.dpos_dvalue()[axis] * step_size) as f32;
|
||||
|
||||
// Where on the cross-dimension to show the label values
|
||||
let value_cross = 0.0_f64.clamp(bounds.min[1 - axis], bounds.max[1 - axis]);
|
||||
|
@ -616,7 +737,7 @@ impl Prepared {
|
|||
} else {
|
||||
Value::new(value_cross, value_main)
|
||||
};
|
||||
let pos_in_gui = self.position_from_value(&value);
|
||||
let pos_in_gui = transform.position_from_value(&value);
|
||||
|
||||
{
|
||||
// Grid: subdivide each label tick in `n` grid lines:
|
||||
|
@ -642,8 +763,8 @@ impl Prepared {
|
|||
pos_in_gui[axis] += step_size_in_points * (i as f32) / (n as f32);
|
||||
let mut p0 = pos_in_gui;
|
||||
let mut p1 = pos_in_gui;
|
||||
p0[1 - axis] = self.rect.min[1 - axis];
|
||||
p1[1 - axis] = self.rect.max[1 - axis];
|
||||
p0[1 - axis] = transform.rect.min[1 - axis];
|
||||
p1[1 - axis] = transform.rect.max[1 - axis];
|
||||
shapes.push(Shape::line_segment([p0, p1], Stroke::new(1.0, color)));
|
||||
}
|
||||
}
|
||||
|
@ -656,8 +777,8 @@ impl Prepared {
|
|||
|
||||
// Make sure we see the labels, even if the axis is off-screen:
|
||||
text_pos[1 - axis] = text_pos[1 - axis]
|
||||
.at_most(self.rect.max[1 - axis] - galley.size[1 - axis] - 2.0)
|
||||
.at_least(self.rect.min[1 - axis] + 1.0);
|
||||
.at_most(transform.rect.max[1 - axis] - galley.size[1 - axis] - 2.0)
|
||||
.at_least(transform.rect.min[1 - axis] + 1.0);
|
||||
|
||||
shapes.push(Shape::Text {
|
||||
pos: text_pos,
|
||||
|
@ -669,7 +790,15 @@ impl Prepared {
|
|||
}
|
||||
|
||||
fn hover(&self, ui: &Ui, pointer: Pos2, shapes: &mut Vec<Shape>) {
|
||||
if !self.show_x && !self.show_y {
|
||||
let Self {
|
||||
transform,
|
||||
show_x,
|
||||
show_y,
|
||||
curves,
|
||||
..
|
||||
} = self;
|
||||
|
||||
if !show_x && !show_y {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -677,9 +806,9 @@ impl Prepared {
|
|||
let mut closest_value = None;
|
||||
let mut closest_curve = None;
|
||||
let mut closest_dist_sq = interact_radius.powi(2);
|
||||
for curve in &self.curves {
|
||||
for curve in curves {
|
||||
for value in &curve.values {
|
||||
let pos = self.position_from_value(value);
|
||||
let pos = transform.position_from_value(value);
|
||||
let dist_sq = pointer.distance_sq(pos);
|
||||
if dist_sq < closest_dist_sq {
|
||||
closest_dist_sq = dist_sq;
|
||||
|
@ -699,24 +828,24 @@ impl Prepared {
|
|||
let line_color = line_color(ui, Strength::Strong);
|
||||
|
||||
let value = if let Some(value) = closest_value {
|
||||
let position = self.position_from_value(value);
|
||||
let position = transform.position_from_value(value);
|
||||
shapes.push(Shape::circle_filled(position, 3.0, line_color));
|
||||
*value
|
||||
} else {
|
||||
self.value_from_position(pointer)
|
||||
transform.value_from_position(pointer)
|
||||
};
|
||||
let pointer = self.position_from_value(&value);
|
||||
let pointer = transform.position_from_value(&value);
|
||||
|
||||
let rect = self.rect;
|
||||
let rect = transform.rect();
|
||||
|
||||
if self.show_x {
|
||||
if *show_x {
|
||||
// vertical line
|
||||
shapes.push(Shape::line_segment(
|
||||
[pos2(pointer.x, rect.top()), pos2(pointer.x, rect.bottom())],
|
||||
(1.0, line_color),
|
||||
));
|
||||
}
|
||||
if self.show_y {
|
||||
if *show_y {
|
||||
// horizontal line
|
||||
shapes.push(Shape::line_segment(
|
||||
[pos2(rect.left(), pointer.y), pos2(rect.right(), pointer.y)],
|
||||
|
@ -725,17 +854,17 @@ impl Prepared {
|
|||
}
|
||||
|
||||
let text = {
|
||||
let scale = self.dvalue_dpos();
|
||||
let scale = transform.dvalue_dpos();
|
||||
let x_decimals = ((-scale[0].abs().log10()).ceil().at_least(0.0) as usize).at_most(6);
|
||||
let y_decimals = ((-scale[1].abs().log10()).ceil().at_least(0.0) as usize).at_most(6);
|
||||
if self.show_x && self.show_y {
|
||||
if *show_x && *show_y {
|
||||
format!(
|
||||
"{}x = {:.*}\ny = {:.*}",
|
||||
prefix, x_decimals, value.x, y_decimals, value.y
|
||||
)
|
||||
} else if self.show_x {
|
||||
} else if *show_x {
|
||||
format!("{}x = {:.*}", prefix, x_decimals, value.x)
|
||||
} else if self.show_y {
|
||||
} else if *show_y {
|
||||
format!("{}y = {:.*}", prefix, y_decimals, value.y)
|
||||
} else {
|
||||
unreachable!()
|
||||
|
|
|
@ -139,7 +139,7 @@ impl super::View for PlotDemo {
|
|||
self.time += ui.input().unstable_dt.at_most(1.0 / 30.0) as f64;
|
||||
};
|
||||
|
||||
let mut plot = Plot::default()
|
||||
let mut plot = Plot::new("Demo Plot")
|
||||
.curve(self.circle())
|
||||
.curve(self.sin())
|
||||
.curve(self.thingy())
|
||||
|
|
|
@ -227,7 +227,7 @@ fn example_plot() -> egui::plot::Plot {
|
|||
let x = egui::remap(i as f64, 0.0..=(n as f64), -TAU..=TAU);
|
||||
egui::plot::Value::new(x, x.sin())
|
||||
}));
|
||||
egui::plot::Plot::default()
|
||||
egui::plot::Plot::new("Example Plot")
|
||||
.curve(curve)
|
||||
.height(32.0)
|
||||
.data_aspect(1.0)
|
||||
|
|
Loading…
Reference in a new issue