Return plot transforms (#2935)

* Expose the plot transform to users

* Rename plot::ScreenTransform to PlotTransform

* Plot: return a PlotResponse with a transform member
This commit is contained in:
Emil Ernerfeldt 2023-04-19 17:01:16 +02:00 committed by GitHub
parent ce761e548f
commit d46cf067ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 85 additions and 58 deletions

View File

@ -2,7 +2,7 @@ use crate::emath::NumExt;
use crate::epaint::{Color32, RectShape, Rounding, Shape, Stroke};
use super::{add_rulers_and_text, highlighted_color, Orientation, PlotConfig, RectElement};
use crate::plot::{BarChart, Cursor, PlotPoint, ScreenTransform};
use crate::plot::{BarChart, Cursor, PlotPoint, PlotTransform};
/// One bar in a [`BarChart`]. Potentially floating, allowing stacked bar charts.
/// Width can be changed to allow variable-width histograms.
@ -116,7 +116,7 @@ impl Bar {
pub(super) fn add_shapes(
&self,
transform: &ScreenTransform,
transform: &PlotTransform,
highlighted: bool,
shapes: &mut Vec<Shape>,
) {
@ -183,7 +183,7 @@ impl RectElement for Bar {
self.orientation
}
fn default_values_format(&self, transform: &ScreenTransform) -> String {
fn default_values_format(&self, transform: &PlotTransform) -> String {
let scale = transform.dvalue_dpos();
let scale = match self.orientation {
Orientation::Horizontal => scale[0],

View File

@ -2,7 +2,7 @@ use crate::emath::NumExt;
use crate::epaint::{Color32, RectShape, Rounding, Shape, Stroke};
use super::{add_rulers_and_text, highlighted_color, Orientation, PlotConfig, RectElement};
use crate::plot::{BoxPlot, Cursor, PlotPoint, ScreenTransform};
use crate::plot::{BoxPlot, Cursor, PlotPoint, PlotTransform};
/// Contains the values of a single box in a box plot.
#[derive(Clone, Debug, PartialEq)]
@ -136,7 +136,7 @@ impl BoxElem {
pub(super) fn add_shapes(
&self,
transform: &ScreenTransform,
transform: &PlotTransform,
highlighted: bool,
shapes: &mut Vec<Shape>,
) {
@ -267,7 +267,7 @@ impl RectElement for BoxElem {
self.point_at(self.argument, self.spread.upper_whisker)
}
fn default_values_format(&self, transform: &ScreenTransform) -> String {
fn default_values_format(&self, transform: &PlotTransform) -> String {
let scale = transform.dvalue_dpos();
let scale = match self.orientation {
Orientation::Horizontal => scale[0],

View File

@ -7,7 +7,7 @@ use epaint::Mesh;
use crate::*;
use super::{Cursor, LabelFormatter, PlotBounds, ScreenTransform};
use super::{Cursor, LabelFormatter, PlotBounds, PlotTransform};
use rect_elem::*;
use values::{ClosestElem, PlotGeometry};
@ -25,14 +25,14 @@ const DEFAULT_FILL_ALPHA: f32 = 0.05;
/// Container to pass-through several parameters related to plot visualization
pub(super) struct PlotConfig<'a> {
pub ui: &'a Ui,
pub transform: &'a ScreenTransform,
pub transform: &'a PlotTransform,
pub show_x: bool,
pub show_y: bool,
}
/// Trait shared by things that can be drawn in the plot.
pub(super) trait PlotItem {
fn shapes(&self, ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>);
fn shapes(&self, ui: &mut Ui, transform: &PlotTransform, shapes: &mut Vec<Shape>);
/// For plot-items which are generated based on x values (plotting functions).
fn initialize(&mut self, x_range: RangeInclusive<f64>);
@ -49,7 +49,7 @@ pub(super) trait PlotItem {
fn bounds(&self) -> PlotBounds;
fn find_closest(&self, point: Pos2, transform: &ScreenTransform) -> Option<ClosestElem> {
fn find_closest(&self, point: Pos2, transform: &PlotTransform) -> Option<ClosestElem> {
match self.geometry() {
PlotGeometry::None => None,
@ -177,7 +177,7 @@ impl HLine {
}
impl PlotItem for HLine {
fn shapes(&self, ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) {
fn shapes(&self, ui: &mut Ui, transform: &PlotTransform, shapes: &mut Vec<Shape>) {
let HLine {
y,
stroke,
@ -293,7 +293,7 @@ impl VLine {
}
impl PlotItem for VLine {
fn shapes(&self, ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) {
fn shapes(&self, ui: &mut Ui, transform: &PlotTransform, shapes: &mut Vec<Shape>) {
let VLine {
x,
stroke,
@ -423,7 +423,7 @@ fn y_intersection(p1: &Pos2, p2: &Pos2, y: f32) -> Option<f32> {
}
impl PlotItem for Line {
fn shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) {
fn shapes(&self, _ui: &mut Ui, transform: &PlotTransform, shapes: &mut Vec<Shape>) {
let Self {
series,
stroke,
@ -584,7 +584,7 @@ impl Polygon {
}
impl PlotItem for Polygon {
fn shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) {
fn shapes(&self, _ui: &mut Ui, transform: &PlotTransform, shapes: &mut Vec<Shape>) {
let Self {
series,
stroke,
@ -696,7 +696,7 @@ impl Text {
}
impl PlotItem for Text {
fn shapes(&self, ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) {
fn shapes(&self, ui: &mut Ui, transform: &PlotTransform, shapes: &mut Vec<Shape>) {
let color = if self.color == Color32::TRANSPARENT {
ui.style().visuals.text_color()
} else {
@ -836,7 +836,7 @@ impl Points {
}
impl PlotItem for Points {
fn shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) {
fn shapes(&self, _ui: &mut Ui, transform: &PlotTransform, shapes: &mut Vec<Shape>) {
let sqrt_3 = 3_f32.sqrt();
let frac_sqrt_3_2 = 3_f32.sqrt() / 2.0;
let frac_1_sqrt_2 = 1.0 / 2_f32.sqrt();
@ -1039,7 +1039,7 @@ impl Arrows {
}
impl PlotItem for Arrows {
fn shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) {
fn shapes(&self, _ui: &mut Ui, transform: &PlotTransform, shapes: &mut Vec<Shape>) {
use crate::emath::*;
let Self {
origins,
@ -1178,7 +1178,7 @@ impl PlotImage {
}
impl PlotItem for PlotImage {
fn shapes(&self, ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) {
fn shapes(&self, ui: &mut Ui, transform: &PlotTransform, shapes: &mut Vec<Shape>) {
let Self {
position,
texture_id,
@ -1369,7 +1369,7 @@ impl BarChart {
}
impl PlotItem for BarChart {
fn shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) {
fn shapes(&self, _ui: &mut Ui, transform: &PlotTransform, shapes: &mut Vec<Shape>) {
for b in &self.bars {
b.add_shapes(transform, self.highlight, shapes);
}
@ -1407,7 +1407,7 @@ impl PlotItem for BarChart {
bounds
}
fn find_closest(&self, point: Pos2, transform: &ScreenTransform) -> Option<ClosestElem> {
fn find_closest(&self, point: Pos2, transform: &PlotTransform) -> Option<ClosestElem> {
find_closest_rect(&self.bars, point, transform)
}
@ -1512,7 +1512,7 @@ impl BoxPlot {
}
impl PlotItem for BoxPlot {
fn shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) {
fn shapes(&self, _ui: &mut Ui, transform: &PlotTransform, shapes: &mut Vec<Shape>) {
for b in &self.boxes {
b.add_shapes(transform, self.highlight, shapes);
}
@ -1550,7 +1550,7 @@ impl PlotItem for BoxPlot {
bounds
}
fn find_closest(&self, point: Pos2, transform: &ScreenTransform) -> Option<ClosestElem> {
fn find_closest(&self, point: Pos2, transform: &PlotTransform) -> Option<ClosestElem> {
find_closest_rect(&self.boxes, point, transform)
}
@ -1582,7 +1582,7 @@ pub(crate) fn rulers_color(ui: &Ui) -> Color32 {
pub(crate) fn vertical_line(
pointer: Pos2,
transform: &ScreenTransform,
transform: &PlotTransform,
line_color: Color32,
) -> Shape {
let frame = transform.frame();
@ -1597,7 +1597,7 @@ pub(crate) fn vertical_line(
pub(crate) fn horizontal_line(
pointer: Pos2,
transform: &ScreenTransform,
transform: &PlotTransform,
line_color: Color32,
) -> Shape {
let frame = transform.frame();
@ -1731,7 +1731,7 @@ pub(super) fn rulers_at_value(
fn find_closest_rect<'a, T>(
rects: impl IntoIterator<Item = &'a T>,
point: Pos2,
transform: &ScreenTransform,
transform: &PlotTransform,
) -> Option<ClosestElem>
where
T: 'a + RectElement,

View File

@ -1,5 +1,5 @@
use super::{Orientation, PlotPoint};
use crate::plot::transform::{PlotBounds, ScreenTransform};
use crate::plot::transform::{PlotBounds, PlotTransform};
use epaint::emath::NumExt;
use epaint::{Color32, Rgba, Stroke};
@ -48,7 +48,7 @@ pub(super) trait RectElement {
}
/// Debug formatting for hovered-over value, if none is specified by the user
fn default_values_format(&self, transform: &ScreenTransform) -> String;
fn default_values_format(&self, transform: &PlotTransform) -> String;
}
// ----------------------------------------------------------------------------

View File

@ -9,14 +9,13 @@ use epaint::Hsva;
use items::PlotItem;
use legend::LegendWidget;
use transform::ScreenTransform;
pub use items::{
Arrows, Bar, BarChart, BoxElem, BoxPlot, BoxSpread, HLine, Line, LineStyle, MarkerShape,
Orientation, PlotImage, PlotPoint, PlotPoints, Points, Polygon, Text, VLine,
};
pub use legend::{Corner, Legend};
pub use transform::PlotBounds;
pub use transform::{PlotBounds, PlotTransform};
use self::items::{horizontal_line, rulers_color, vertical_line};
@ -104,7 +103,7 @@ struct PlotMemory {
bounds_modified: AxisBools,
hovered_entry: Option<String>,
hidden_items: ahash::HashSet<String>,
last_screen_transform: ScreenTransform,
last_plot_transform: PlotTransform,
/// Allows to remember the first click position when performing a boxed zoom
last_click_pos_for_zoom: Option<Pos2>,
}
@ -149,6 +148,20 @@ struct BoundsLinkGroups(HashMap<Id, LinkedBounds>);
// ----------------------------------------------------------------------------
/// What [`Plot::show`] returns.
pub struct PlotResponse<R> {
/// What the user closure returned.
pub inner: R,
/// The response of the plot.
pub response: Response,
/// The transform between screen coordinates and plot coordinates.
pub transform: PlotTransform,
}
// ----------------------------------------------------------------------------
/// A 2D plot, e.g. a graph of a function.
///
/// [`Plot`] supports multiple lines and points.
@ -571,7 +584,7 @@ impl Plot {
}
/// Interact with and add items to the plot and finally draw it.
pub fn show<R>(self, ui: &mut Ui, build_fn: impl FnOnce(&mut PlotUi) -> R) -> InnerResponse<R> {
pub fn show<R>(self, ui: &mut Ui, build_fn: impl FnOnce(&mut PlotUi) -> R) -> PlotResponse<R> {
self.show_dyn(ui, Box::new(build_fn))
}
@ -579,7 +592,7 @@ impl Plot {
self,
ui: &mut Ui,
build_fn: Box<dyn FnOnce(&mut PlotUi) -> R + 'a>,
) -> InnerResponse<R> {
) -> PlotResponse<R> {
let Self {
id_source,
center_x_axis,
@ -661,7 +674,7 @@ impl Plot {
bounds_modified: false.into(),
hovered_entry: None,
hidden_items: Default::default(),
last_screen_transform: ScreenTransform::new(
last_plot_transform: PlotTransform::new(
rect,
min_auto_bounds,
center_x_axis,
@ -674,7 +687,7 @@ impl Plot {
mut bounds_modified,
mut hovered_entry,
mut hidden_items,
last_screen_transform,
last_plot_transform,
mut last_click_pos_for_zoom,
} = memory;
@ -682,7 +695,7 @@ impl Plot {
let mut plot_ui = PlotUi {
items: Vec::new(),
next_auto_color_idx: 0,
last_screen_transform,
last_plot_transform,
response,
bounds_modifications: Vec::new(),
ctx: ui.ctx().clone(),
@ -691,7 +704,7 @@ impl Plot {
let PlotUi {
mut items,
mut response,
last_screen_transform,
last_plot_transform,
bounds_modifications,
..
} = plot_ui;
@ -727,7 +740,7 @@ impl Plot {
items.sort_by_key(|item| item.highlighted());
// --- Bound computation ---
let mut bounds = *last_screen_transform.bounds();
let mut bounds = *last_plot_transform.bounds();
// Find the cursors from other plots we need to draw
let draw_cursors: Vec<Cursor> = if let Some((id, _)) = linked_cursors.as_ref() {
@ -826,7 +839,7 @@ impl Plot {
}
}
let mut transform = ScreenTransform::new(rect, bounds, center_x_axis, center_y_axis);
let mut transform = PlotTransform::new(rect, bounds, center_x_axis, center_y_axis);
// Enforce aspect ratio
if let Some(data_aspect) = data_aspect {
@ -947,7 +960,7 @@ impl Plot {
coordinates_formatter,
axis_formatters,
show_axes,
transform: transform.clone(),
transform,
draw_cursor_x: linked_cursors.as_ref().map_or(false, |(_, group)| group.x),
draw_cursor_y: linked_cursors.as_ref().map_or(false, |(_, group)| group.y),
draw_cursors,
@ -999,7 +1012,7 @@ impl Plot {
bounds_modified,
hovered_entry,
hidden_items,
last_screen_transform: transform,
last_plot_transform: transform,
last_click_pos_for_zoom,
};
memory.store(ui.ctx(), plot_id);
@ -1010,7 +1023,11 @@ impl Plot {
response
};
InnerResponse { inner, response }
PlotResponse {
inner,
response,
transform,
}
}
}
@ -1026,7 +1043,7 @@ enum BoundsModification {
pub struct PlotUi {
items: Vec<Box<dyn PlotItem>>,
next_auto_color_idx: usize,
last_screen_transform: ScreenTransform,
last_plot_transform: PlotTransform,
response: Response,
bounds_modifications: Vec<BoundsModification>,
ctx: Context,
@ -1049,7 +1066,7 @@ impl PlotUi {
/// further specified in the plot builder, this will return bounds centered on the origin. The bounds do
/// not change until the plot is drawn.
pub fn plot_bounds(&self) -> PlotBounds {
*self.last_screen_transform.bounds()
*self.last_plot_transform.bounds()
}
/// Set the plot bounds. Can be useful for implementing alternative plot navigation methods.
@ -1090,18 +1107,23 @@ impl PlotUi {
/// The pointer drag delta in plot coordinates.
pub fn pointer_coordinate_drag_delta(&self) -> Vec2 {
let delta = self.response.drag_delta();
let dp_dv = self.last_screen_transform.dpos_dvalue();
let dp_dv = self.last_plot_transform.dpos_dvalue();
Vec2::new(delta.x / dp_dv[0] as f32, delta.y / dp_dv[1] as f32)
}
/// Read the transform netween plot coordinates and screen coordinates.
pub fn transform(&self) -> &PlotTransform {
&self.last_plot_transform
}
/// Transform the plot coordinates to screen coordinates.
pub fn screen_from_plot(&self, position: PlotPoint) -> Pos2 {
self.last_screen_transform.position_from_point(&position)
self.last_plot_transform.position_from_point(&position)
}
/// Transform the screen coordinates to plot coordinates.
pub fn plot_from_screen(&self, position: Pos2) -> PlotPoint {
self.last_screen_transform.value_from_position(position)
self.last_plot_transform.value_from_position(position)
}
/// Add a data line.
@ -1299,7 +1321,7 @@ struct PreparedPlot {
coordinates_formatter: Option<(Corner, CoordinatesFormatter)>,
axis_formatters: [AxisFormatter; 2],
show_axes: [bool; 2],
transform: ScreenTransform,
transform: PlotTransform,
draw_cursor_x: bool,
draw_cursor_y: bool,
draw_cursors: Vec<Cursor>,

View File

@ -181,10 +181,10 @@ impl PlotBounds {
}
}
/// Contains the screen rectangle and the plot bounds and provides methods to transform them.
/// Contains the screen rectangle and the plot bounds and provides methods to transform between them.
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone)]
pub(crate) struct ScreenTransform {
#[derive(Clone, Copy, Debug)]
pub struct PlotTransform {
/// The screen rectangle.
frame: Rect,
@ -198,7 +198,7 @@ pub(crate) struct ScreenTransform {
y_centered: bool,
}
impl ScreenTransform {
impl PlotTransform {
pub fn new(frame: Rect, mut bounds: PlotBounds, x_centered: bool, y_centered: bool) -> Self {
// Make sure they are not empty.
if !bounds.is_valid_x() {
@ -229,15 +229,16 @@ impl ScreenTransform {
&self.frame
}
/// Plot-space bounds.
pub fn bounds(&self) -> &PlotBounds {
&self.bounds
}
pub fn set_bounds(&mut self, bounds: PlotBounds) {
pub(crate) fn set_bounds(&mut self, bounds: PlotBounds) {
self.bounds = bounds;
}
pub fn translate_bounds(&mut self, mut delta_pos: Vec2) {
pub(crate) fn translate_bounds(&mut self, mut delta_pos: Vec2) {
if self.x_centered {
delta_pos.x = 0.;
}
@ -250,7 +251,7 @@ impl ScreenTransform {
}
/// Zoom by a relative factor with the given screen position as center.
pub fn zoom(&mut self, zoom_factor: Vec2, center: Pos2) {
pub(crate) fn zoom(&mut self, zoom_factor: Vec2, center: Pos2) {
let center = self.value_from_position(center);
let mut new_bounds = self.bounds;
@ -280,6 +281,7 @@ impl ScreenTransform {
) as f32
}
/// Screen/ui position from point on plot.
pub fn position_from_point(&self, value: &PlotPoint) -> Pos2 {
pos2(
self.position_from_point_x(value.x),
@ -287,6 +289,7 @@ impl ScreenTransform {
)
}
/// Plot point from screen/ui position.
pub fn value_from_position(&self, pos: Pos2) -> PlotPoint {
let x = remap(
pos.x as f64,
@ -335,6 +338,7 @@ impl ScreenTransform {
[1.0 / self.dpos_dvalue_x(), 1.0 / self.dpos_dvalue_y()]
}
/// width / height aspect ratio
fn aspect(&self) -> f64 {
let rw = self.frame.width() as f64;
let rh = self.frame.height() as f64;
@ -344,7 +348,7 @@ impl ScreenTransform {
/// Sets the aspect ratio by expanding the x- or y-axis.
///
/// This never contracts, so we don't miss out on any data.
pub fn set_aspect_by_expanding(&mut self, aspect: f64) {
pub(crate) fn set_aspect_by_expanding(&mut self, aspect: f64) {
let current_aspect = self.aspect();
let epsilon = 1e-5;
@ -363,7 +367,7 @@ impl ScreenTransform {
}
/// Sets the aspect ratio by changing either the X or Y axis (callers choice).
pub fn set_aspect_by_changing_axis(&mut self, aspect: f64, change_x: bool) {
pub(crate) fn set_aspect_by_changing_axis(&mut self, aspect: f64, change_x: bool) {
let current_aspect = self.aspect();
let epsilon = 1e-5;

View File

@ -1,7 +1,7 @@
use std::f64::consts::TAU;
use std::ops::RangeInclusive;
use egui::plot::{AxisBools, GridInput, GridMark};
use egui::plot::{AxisBools, GridInput, GridMark, PlotResponse};
use egui::*;
use plot::{
Arrows, Bar, BarChart, BoxElem, BoxPlot, BoxSpread, CoordinatesFormatter, Corner, HLine,
@ -751,9 +751,10 @@ impl InteractionDemo {
fn ui(&mut self, ui: &mut Ui) -> Response {
let plot = Plot::new("interaction_demo").height(300.0);
let InnerResponse {
let PlotResponse {
response,
inner: (screen_pos, pointer_coordinate, pointer_coordinate_drag_delta, bounds, hovered),
..
} = plot.show(ui, |plot_ui| {
(
plot_ui.screen_from_plot(PlotPoint::new(0.0, 0.0)),