770 lines
24 KiB
Rust
770 lines
24 KiB
Rust
//! Simple plotting library.
|
|
|
|
use crate::*;
|
|
use epaint::ahash::AHashSet;
|
|
use epaint::color::Hsva;
|
|
use epaint::util::FloatOrd;
|
|
use items::PlotItem;
|
|
use legend::LegendWidget;
|
|
use transform::{PlotBounds, ScreenTransform};
|
|
|
|
pub use items::{
|
|
Arrows, Bar, BarChart, BoxElem, BoxPlot, BoxSpread, HLine, Line, LineStyle, MarkerShape,
|
|
PlotImage, Points, Polygon, Text, VLine, Value, Values,
|
|
};
|
|
pub use legend::{Corner, Legend};
|
|
|
|
mod items;
|
|
mod legend;
|
|
mod transform;
|
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
/// Information about the plot that has to persist between frames.
|
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
|
#[derive(Clone)]
|
|
struct PlotMemory {
|
|
auto_bounds: bool,
|
|
hovered_entry: Option<String>,
|
|
hidden_items: AHashSet<String>,
|
|
min_auto_bounds: PlotBounds,
|
|
last_screen_transform: ScreenTransform,
|
|
}
|
|
|
|
impl PlotMemory {
|
|
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
|
|
ctx.memory().data.get_persisted(id)
|
|
}
|
|
|
|
pub fn store(self, ctx: &Context, id: Id) {
|
|
ctx.memory().data.insert_persisted(id, self);
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
/// A 2D plot, e.g. a graph of a function.
|
|
///
|
|
/// `Plot` supports multiple lines and points.
|
|
///
|
|
/// ```
|
|
/// # egui::__run_test_ui(|ui| {
|
|
/// use egui::plot::{Line, Plot, Value, Values};
|
|
/// let sin = (0..1000).map(|i| {
|
|
/// let x = i as f64 * 0.01;
|
|
/// Value::new(x, x.sin())
|
|
/// });
|
|
/// let line = Line::new(Values::from_values_iter(sin));
|
|
/// Plot::new("my_plot").view_aspect(2.0).show(ui, |plot_ui| plot_ui.line(line));
|
|
/// # });
|
|
/// ```
|
|
pub struct Plot {
|
|
id_source: Id,
|
|
|
|
center_x_axis: bool,
|
|
center_y_axis: bool,
|
|
allow_zoom: bool,
|
|
allow_drag: bool,
|
|
min_auto_bounds: PlotBounds,
|
|
margin_fraction: Vec2,
|
|
|
|
min_size: Vec2,
|
|
width: Option<f32>,
|
|
height: Option<f32>,
|
|
data_aspect: Option<f32>,
|
|
view_aspect: Option<f32>,
|
|
|
|
show_x: bool,
|
|
show_y: bool,
|
|
legend_config: Option<Legend>,
|
|
show_background: bool,
|
|
show_axes: [bool; 2],
|
|
}
|
|
|
|
impl Plot {
|
|
/// Give a unique id for each plot within the same `Ui`.
|
|
pub fn new(id_source: impl std::hash::Hash) -> Self {
|
|
Self {
|
|
id_source: Id::new(id_source),
|
|
|
|
center_x_axis: false,
|
|
center_y_axis: false,
|
|
allow_zoom: true,
|
|
allow_drag: true,
|
|
min_auto_bounds: PlotBounds::NOTHING,
|
|
margin_fraction: Vec2::splat(0.05),
|
|
|
|
min_size: Vec2::splat(64.0),
|
|
width: None,
|
|
height: None,
|
|
data_aspect: None,
|
|
view_aspect: None,
|
|
|
|
show_x: true,
|
|
show_y: true,
|
|
legend_config: None,
|
|
show_background: true,
|
|
show_axes: [true; 2],
|
|
}
|
|
}
|
|
|
|
/// 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.
|
|
/// By default the plot window's aspect ratio is used.
|
|
pub fn data_aspect(mut self, data_aspect: f32) -> Self {
|
|
self.data_aspect = Some(data_aspect);
|
|
self
|
|
}
|
|
|
|
/// width / height ratio of the plot region.
|
|
/// By default no fixed aspect ratio is set (and width/height will fill the ui it is in).
|
|
pub fn view_aspect(mut self, view_aspect: f32) -> Self {
|
|
self.view_aspect = Some(view_aspect);
|
|
self
|
|
}
|
|
|
|
/// Width of plot. By default a plot will fill the ui it is in.
|
|
/// If you set [`Self::view_aspect`], the width can be calculated from the height.
|
|
pub fn width(mut self, width: f32) -> Self {
|
|
self.min_size.x = width;
|
|
self.width = Some(width);
|
|
self
|
|
}
|
|
|
|
/// Height of plot. By default a plot will fill the ui it is in.
|
|
/// If you set [`Self::view_aspect`], the height can be calculated from the width.
|
|
pub fn height(mut self, height: f32) -> Self {
|
|
self.min_size.y = height;
|
|
self.height = Some(height);
|
|
self
|
|
}
|
|
|
|
/// Minimum size of the plot view.
|
|
pub fn min_size(mut self, min_size: Vec2) -> Self {
|
|
self.min_size = min_size;
|
|
self
|
|
}
|
|
|
|
/// Show the x-value (e.g. when hovering). Default: `true`.
|
|
pub fn show_x(mut self, show_x: bool) -> Self {
|
|
self.show_x = show_x;
|
|
self
|
|
}
|
|
|
|
/// Show the y-value (e.g. when hovering). Default: `true`.
|
|
pub fn show_y(mut self, show_y: bool) -> Self {
|
|
self.show_y = show_y;
|
|
self
|
|
}
|
|
|
|
/// Always keep the x-axis centered. Default: `false`.
|
|
pub fn center_x_axis(mut self, on: bool) -> Self {
|
|
self.center_x_axis = on;
|
|
self
|
|
}
|
|
|
|
/// Always keep the y-axis centered. Default: `false`.
|
|
pub fn center_y_axis(mut self, on: bool) -> Self {
|
|
self.center_y_axis = on;
|
|
self
|
|
}
|
|
|
|
/// Whether to allow zooming in the plot. Default: `true`.
|
|
pub fn allow_zoom(mut self, on: bool) -> Self {
|
|
self.allow_zoom = on;
|
|
self
|
|
}
|
|
|
|
/// Whether to allow dragging in the plot to move the bounds. Default: `true`.
|
|
pub fn allow_drag(mut self, on: bool) -> Self {
|
|
self.allow_drag = on;
|
|
self
|
|
}
|
|
|
|
/// Expand bounds to include the given x value.
|
|
/// For instance, to always show the y axis, call `plot.include_x(0.0)`.
|
|
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
|
|
}
|
|
|
|
/// Show a legend including all named items.
|
|
pub fn legend(mut self, legend: Legend) -> Self {
|
|
self.legend_config = Some(legend);
|
|
self
|
|
}
|
|
|
|
/// Whether or not to show the background `Rect`.
|
|
/// Can be useful to disable if the plot is overlaid over existing content.
|
|
/// Default: `true`.
|
|
pub fn show_background(mut self, show: bool) -> Self {
|
|
self.show_background = show;
|
|
self
|
|
}
|
|
|
|
/// Show the axes.
|
|
/// Can be useful to disable if the plot is overlaid over an existing grid or content.
|
|
/// Default: `[true; 2]`.
|
|
pub fn show_axes(mut self, show: [bool; 2]) -> Self {
|
|
self.show_axes = show;
|
|
self
|
|
}
|
|
|
|
/// 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> {
|
|
let Self {
|
|
id_source,
|
|
center_x_axis,
|
|
center_y_axis,
|
|
allow_zoom,
|
|
allow_drag,
|
|
min_auto_bounds,
|
|
margin_fraction,
|
|
width,
|
|
height,
|
|
min_size,
|
|
data_aspect,
|
|
view_aspect,
|
|
mut show_x,
|
|
mut show_y,
|
|
legend_config,
|
|
show_background,
|
|
show_axes,
|
|
} = self;
|
|
|
|
// Determine the size of the plot in the UI
|
|
let size = {
|
|
let width = width
|
|
.unwrap_or_else(|| {
|
|
if let (Some(height), Some(aspect)) = (height, view_aspect) {
|
|
height * aspect
|
|
} else {
|
|
ui.available_size_before_wrap().x
|
|
}
|
|
})
|
|
.at_least(min_size.x);
|
|
|
|
let height = height
|
|
.unwrap_or_else(|| {
|
|
if let Some(aspect) = view_aspect {
|
|
width / aspect
|
|
} else {
|
|
ui.available_size_before_wrap().y
|
|
}
|
|
})
|
|
.at_least(min_size.y);
|
|
vec2(width, height)
|
|
};
|
|
|
|
// Allocate the space.
|
|
let (rect, response) = ui.allocate_exact_size(size, Sense::drag());
|
|
|
|
// Load or initialize the memory.
|
|
let plot_id = ui.make_persistent_id(id_source);
|
|
let mut memory = PlotMemory::load(ui.ctx(), plot_id).unwrap_or_else(|| PlotMemory {
|
|
auto_bounds: !min_auto_bounds.is_valid(),
|
|
hovered_entry: None,
|
|
hidden_items: Default::default(),
|
|
min_auto_bounds,
|
|
last_screen_transform: ScreenTransform::new(
|
|
rect,
|
|
min_auto_bounds,
|
|
center_x_axis,
|
|
center_y_axis,
|
|
),
|
|
});
|
|
|
|
// If the min bounds changed, recalculate everything.
|
|
if min_auto_bounds != memory.min_auto_bounds {
|
|
memory = PlotMemory {
|
|
auto_bounds: !min_auto_bounds.is_valid(),
|
|
hovered_entry: None,
|
|
min_auto_bounds,
|
|
..memory
|
|
};
|
|
memory.clone().store(ui.ctx(), plot_id);
|
|
}
|
|
|
|
let PlotMemory {
|
|
mut auto_bounds,
|
|
mut hovered_entry,
|
|
mut hidden_items,
|
|
last_screen_transform,
|
|
..
|
|
} = memory;
|
|
|
|
// Call the plot build function.
|
|
let mut plot_ui = PlotUi {
|
|
items: Vec::new(),
|
|
next_auto_color_idx: 0,
|
|
last_screen_transform,
|
|
response,
|
|
ctx: ui.ctx().clone(),
|
|
};
|
|
let inner = build_fn(&mut plot_ui);
|
|
let PlotUi {
|
|
mut items,
|
|
mut response,
|
|
last_screen_transform,
|
|
..
|
|
} = plot_ui;
|
|
|
|
// Background
|
|
if show_background {
|
|
ui.painter().sub_region(rect).add(epaint::RectShape {
|
|
rect,
|
|
corner_radius: 2.0,
|
|
fill: ui.visuals().extreme_bg_color,
|
|
stroke: ui.visuals().widgets.noninteractive.bg_stroke,
|
|
});
|
|
}
|
|
|
|
// --- Legend ---
|
|
let legend = legend_config
|
|
.and_then(|config| LegendWidget::try_new(rect, config, &items, &hidden_items));
|
|
// Don't show hover cursor when hovering over legend.
|
|
if hovered_entry.is_some() {
|
|
show_x = false;
|
|
show_y = false;
|
|
}
|
|
// Remove the deselected items.
|
|
items.retain(|item| !hidden_items.contains(item.name()));
|
|
// Highlight the hovered items.
|
|
if let Some(hovered_name) = &hovered_entry {
|
|
items
|
|
.iter_mut()
|
|
.filter(|entry| entry.name() == hovered_name)
|
|
.for_each(|entry| entry.highlight());
|
|
}
|
|
// Move highlighted items to front.
|
|
items.sort_by_key(|item| item.highlighted());
|
|
|
|
// --- Bound computation ---
|
|
let mut bounds = *last_screen_transform.bounds();
|
|
|
|
// Allow double clicking to reset to automatic bounds.
|
|
auto_bounds |= response.double_clicked_by(PointerButton::Primary);
|
|
|
|
// Set bounds automatically based on content.
|
|
if auto_bounds || !bounds.is_valid() {
|
|
bounds = min_auto_bounds;
|
|
items
|
|
.iter()
|
|
.for_each(|item| bounds.merge(&item.get_bounds()));
|
|
bounds.add_relative_margin(margin_fraction);
|
|
}
|
|
|
|
let mut transform = ScreenTransform::new(rect, bounds, center_x_axis, center_y_axis);
|
|
|
|
// Enforce equal aspect ratio.
|
|
if let Some(data_aspect) = data_aspect {
|
|
transform.set_aspect(data_aspect as f64);
|
|
}
|
|
|
|
// Dragging
|
|
if allow_drag && response.dragged_by(PointerButton::Primary) {
|
|
response = response.on_hover_cursor(CursorIcon::Grabbing);
|
|
transform.translate_bounds(-response.drag_delta());
|
|
auto_bounds = false;
|
|
}
|
|
|
|
// Zooming
|
|
if allow_zoom {
|
|
if let Some(hover_pos) = response.hover_pos() {
|
|
let zoom_factor = if data_aspect.is_some() {
|
|
Vec2::splat(ui.input().zoom_delta())
|
|
} else {
|
|
ui.input().zoom_delta_2d()
|
|
};
|
|
if zoom_factor != Vec2::splat(1.0) {
|
|
transform.zoom(zoom_factor, hover_pos);
|
|
auto_bounds = false;
|
|
}
|
|
|
|
let scroll_delta = ui.input().scroll_delta;
|
|
if scroll_delta != Vec2::ZERO {
|
|
transform.translate_bounds(-scroll_delta);
|
|
auto_bounds = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Initialize values from functions.
|
|
items
|
|
.iter_mut()
|
|
.for_each(|item| item.initialize(transform.bounds().range_x()));
|
|
|
|
let prepared = PreparedPlot {
|
|
items,
|
|
show_x,
|
|
show_y,
|
|
show_axes,
|
|
transform: transform.clone(),
|
|
};
|
|
prepared.ui(ui, &response);
|
|
|
|
if let Some(mut legend) = legend {
|
|
ui.add(&mut legend);
|
|
hidden_items = legend.get_hidden_items();
|
|
hovered_entry = legend.get_hovered_entry_name();
|
|
}
|
|
|
|
let memory = PlotMemory {
|
|
auto_bounds,
|
|
hovered_entry,
|
|
hidden_items,
|
|
min_auto_bounds,
|
|
last_screen_transform: transform,
|
|
};
|
|
memory.store(ui.ctx(), plot_id);
|
|
|
|
let response = if show_x || show_y {
|
|
response.on_hover_cursor(CursorIcon::Crosshair)
|
|
} else {
|
|
response
|
|
};
|
|
|
|
InnerResponse { inner, response }
|
|
}
|
|
}
|
|
|
|
/// Provides methods to interact with a plot while building it. It is the single argument of the closure
|
|
/// provided to `Plot::show`. See [`Plot`] for an example of how to use it.
|
|
pub struct PlotUi {
|
|
items: Vec<Box<dyn PlotItem>>,
|
|
next_auto_color_idx: usize,
|
|
last_screen_transform: ScreenTransform,
|
|
response: Response,
|
|
ctx: CtxRef,
|
|
}
|
|
|
|
impl PlotUi {
|
|
fn auto_color(&mut self) -> Color32 {
|
|
let i = self.next_auto_color_idx;
|
|
self.next_auto_color_idx += 1;
|
|
let golden_ratio = (5.0_f32.sqrt() - 1.0) / 2.0; // 0.61803398875
|
|
let h = i as f32 * golden_ratio;
|
|
Hsva::new(h, 0.85, 0.5, 1.0).into() // TODO: OkLab or some other perspective color space
|
|
}
|
|
|
|
pub fn ctx(&self) -> &CtxRef {
|
|
&self.ctx
|
|
}
|
|
|
|
/// The plot bounds as they were in the last frame. If called on the first frame and the bounds were not
|
|
/// 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()
|
|
}
|
|
|
|
/// Returns `true` if the plot area is currently hovered.
|
|
pub fn plot_hovered(&self) -> bool {
|
|
self.response.hovered()
|
|
}
|
|
|
|
/// The pointer position in plot coordinates. Independent of whether the pointer is in the plot area.
|
|
pub fn pointer_coordinate(&self) -> Option<Value> {
|
|
// We need to subtract the drag delta to keep in sync with the frame-delayed screen transform:
|
|
let last_pos = self.ctx().input().pointer.latest_pos()? - self.response.drag_delta();
|
|
let value = self.plot_from_screen(last_pos);
|
|
Some(value)
|
|
}
|
|
|
|
/// 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();
|
|
Vec2::new(delta.x / dp_dv[0] as f32, delta.y / dp_dv[1] as f32)
|
|
}
|
|
|
|
/// Transform the plot coordinates to screen coordinates.
|
|
pub fn screen_from_plot(&self, position: Value) -> Pos2 {
|
|
self.last_screen_transform.position_from_value(&position)
|
|
}
|
|
|
|
/// Transform the screen coordinates to plot coordinates.
|
|
pub fn plot_from_screen(&self, position: Pos2) -> Value {
|
|
self.last_screen_transform.value_from_position(position)
|
|
}
|
|
|
|
/// Add a data line.
|
|
pub fn line(&mut self, mut line: Line) {
|
|
if line.series.is_empty() {
|
|
return;
|
|
};
|
|
|
|
// Give the stroke an automatic color if no color has been assigned.
|
|
if line.stroke.color == Color32::TRANSPARENT {
|
|
line.stroke.color = self.auto_color();
|
|
}
|
|
self.items.push(Box::new(line));
|
|
}
|
|
|
|
/// Add a polygon. The polygon has to be convex.
|
|
pub fn polygon(&mut self, mut polygon: Polygon) {
|
|
if polygon.series.is_empty() {
|
|
return;
|
|
};
|
|
|
|
// Give the stroke an automatic color if no color has been assigned.
|
|
if polygon.stroke.color == Color32::TRANSPARENT {
|
|
polygon.stroke.color = self.auto_color();
|
|
}
|
|
self.items.push(Box::new(polygon));
|
|
}
|
|
|
|
/// Add a text.
|
|
pub fn text(&mut self, text: Text) {
|
|
if text.text.is_empty() {
|
|
return;
|
|
};
|
|
|
|
self.items.push(Box::new(text));
|
|
}
|
|
|
|
/// Add data points.
|
|
pub fn points(&mut self, mut points: Points) {
|
|
if points.series.is_empty() {
|
|
return;
|
|
};
|
|
|
|
// Give the points an automatic color if no color has been assigned.
|
|
if points.color == Color32::TRANSPARENT {
|
|
points.color = self.auto_color();
|
|
}
|
|
self.items.push(Box::new(points));
|
|
}
|
|
|
|
/// Add arrows.
|
|
pub fn arrows(&mut self, mut arrows: Arrows) {
|
|
if arrows.origins.is_empty() || arrows.tips.is_empty() {
|
|
return;
|
|
};
|
|
|
|
// Give the arrows an automatic color if no color has been assigned.
|
|
if arrows.color == Color32::TRANSPARENT {
|
|
arrows.color = self.auto_color();
|
|
}
|
|
self.items.push(Box::new(arrows));
|
|
}
|
|
|
|
/// Add an image.
|
|
pub fn image(&mut self, image: PlotImage) {
|
|
self.items.push(Box::new(image));
|
|
}
|
|
|
|
/// Add a horizontal line.
|
|
/// Can be useful e.g. to show min/max bounds or similar.
|
|
/// Always fills the full width of the plot.
|
|
pub fn hline(&mut self, mut hline: HLine) {
|
|
if hline.stroke.color == Color32::TRANSPARENT {
|
|
hline.stroke.color = self.auto_color();
|
|
}
|
|
self.items.push(Box::new(hline));
|
|
}
|
|
|
|
/// Add a vertical line.
|
|
/// Can be useful e.g. to show min/max bounds or similar.
|
|
/// Always fills the full height of the plot.
|
|
pub fn vline(&mut self, mut vline: VLine) {
|
|
if vline.stroke.color == Color32::TRANSPARENT {
|
|
vline.stroke.color = self.auto_color();
|
|
}
|
|
self.items.push(Box::new(vline));
|
|
}
|
|
|
|
/// Add a box plot diagram.
|
|
pub fn box_plot(&mut self, mut box_plot: BoxPlot) {
|
|
if box_plot.boxes.is_empty() {
|
|
return;
|
|
}
|
|
|
|
// Give the elements an automatic color if no color has been assigned.
|
|
if box_plot.default_color == Color32::TRANSPARENT {
|
|
box_plot = box_plot.color(self.auto_color());
|
|
}
|
|
self.items.push(Box::new(box_plot));
|
|
}
|
|
|
|
/// Add a bar chart.
|
|
pub fn bar_chart(&mut self, mut chart: BarChart) {
|
|
if chart.bars.is_empty() {
|
|
return;
|
|
}
|
|
|
|
// Give the elements an automatic color if no color has been assigned.
|
|
if chart.default_color == Color32::TRANSPARENT {
|
|
chart = chart.color(self.auto_color());
|
|
}
|
|
self.items.push(Box::new(chart));
|
|
}
|
|
}
|
|
|
|
struct PreparedPlot {
|
|
items: Vec<Box<dyn PlotItem>>,
|
|
show_x: bool,
|
|
show_y: bool,
|
|
show_axes: [bool; 2],
|
|
transform: ScreenTransform,
|
|
}
|
|
|
|
impl PreparedPlot {
|
|
fn ui(self, ui: &mut Ui, response: &Response) {
|
|
let mut shapes = Vec::new();
|
|
|
|
for d in 0..2 {
|
|
if self.show_axes[d] {
|
|
self.paint_axis(ui, d, &mut shapes);
|
|
}
|
|
}
|
|
|
|
let transform = &self.transform;
|
|
|
|
let mut plot_ui = ui.child_ui(*transform.frame(), Layout::default());
|
|
plot_ui.set_clip_rect(*transform.frame());
|
|
for item in &self.items {
|
|
item.get_shapes(&mut plot_ui, transform, &mut shapes);
|
|
}
|
|
|
|
if let Some(pointer) = response.hover_pos() {
|
|
self.hover(ui, pointer, &mut shapes);
|
|
}
|
|
|
|
ui.painter().sub_region(*transform.frame()).extend(shapes);
|
|
}
|
|
|
|
fn paint_axis(&self, ui: &Ui, axis: usize, shapes: &mut Vec<Shape>) {
|
|
let Self { transform, .. } = self;
|
|
|
|
let bounds = transform.bounds();
|
|
let text_style = TextStyle::Body;
|
|
|
|
let base: i64 = 10;
|
|
let basef = base as f64;
|
|
|
|
let min_line_spacing_in_points = 6.0; // TODO: large enough for a wide label
|
|
let step_size = transform.dvalue_dpos()[axis] * min_line_spacing_in_points;
|
|
let step_size = basef.powi(step_size.abs().log(basef).ceil() as i32);
|
|
|
|
let step_size_in_points = (transform.dpos_dvalue()[axis] * step_size).abs() 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]);
|
|
|
|
for i in 0.. {
|
|
let value_main = step_size * (bounds.min[axis] / step_size + i as f64).floor();
|
|
if value_main > bounds.max[axis] {
|
|
break;
|
|
}
|
|
|
|
let value = if axis == 0 {
|
|
Value::new(value_main, value_cross)
|
|
} else {
|
|
Value::new(value_cross, value_main)
|
|
};
|
|
let pos_in_gui = transform.position_from_value(&value);
|
|
|
|
let n = (value_main / step_size).round() as i64;
|
|
let spacing_in_points = if n % (base * base) == 0 {
|
|
step_size_in_points * (basef * basef) as f32 // think line (multiple of 100)
|
|
} else if n % base == 0 {
|
|
step_size_in_points * basef as f32 // medium line (multiple of 10)
|
|
} else {
|
|
step_size_in_points // thin line
|
|
};
|
|
|
|
let line_alpha = remap_clamp(
|
|
spacing_in_points,
|
|
(min_line_spacing_in_points as f32)..=300.0,
|
|
0.0..=0.15,
|
|
);
|
|
|
|
if line_alpha > 0.0 {
|
|
let line_color = color_from_alpha(ui, line_alpha);
|
|
|
|
let mut p0 = pos_in_gui;
|
|
let mut p1 = pos_in_gui;
|
|
p0[1 - axis] = transform.frame().min[1 - axis];
|
|
p1[1 - axis] = transform.frame().max[1 - axis];
|
|
shapes.push(Shape::line_segment([p0, p1], Stroke::new(1.0, line_color)));
|
|
}
|
|
|
|
let text_alpha = remap_clamp(spacing_in_points, 40.0..=150.0, 0.0..=0.4);
|
|
|
|
if text_alpha > 0.0 {
|
|
let color = color_from_alpha(ui, text_alpha);
|
|
let text = emath::round_to_decimals(value_main, 5).to_string(); // hack
|
|
|
|
let galley = ui.painter().layout_no_wrap(text, text_style, color);
|
|
|
|
let mut text_pos = pos_in_gui + vec2(1.0, -galley.size().y);
|
|
|
|
// Make sure we see the labels, even if the axis is off-screen:
|
|
text_pos[1 - axis] = text_pos[1 - axis]
|
|
.at_most(transform.frame().max[1 - axis] - galley.size()[1 - axis] - 2.0)
|
|
.at_least(transform.frame().min[1 - axis] + 1.0);
|
|
|
|
shapes.push(Shape::galley(text_pos, galley));
|
|
}
|
|
}
|
|
|
|
fn color_from_alpha(ui: &Ui, alpha: f32) -> Color32 {
|
|
if ui.visuals().dark_mode {
|
|
Rgba::from_white_alpha(alpha).into()
|
|
} else {
|
|
Rgba::from_black_alpha((4.0 * alpha).at_most(1.0)).into()
|
|
}
|
|
}
|
|
}
|
|
|
|
fn hover(&self, ui: &Ui, pointer: Pos2, shapes: &mut Vec<Shape>) {
|
|
let Self {
|
|
transform,
|
|
show_x,
|
|
show_y,
|
|
items,
|
|
..
|
|
} = self;
|
|
|
|
if !show_x && !show_y {
|
|
return;
|
|
}
|
|
|
|
let interact_radius_sq: f32 = (16.0f32).powi(2);
|
|
|
|
let candidates = items.iter().filter_map(|item| {
|
|
let item = &**item;
|
|
let closest = item.find_closest(pointer, transform);
|
|
|
|
Some(item).zip(closest)
|
|
});
|
|
|
|
let closest = candidates
|
|
.min_by_key(|(_, elem)| elem.dist_sq.ord())
|
|
.filter(|(_, elem)| elem.dist_sq <= interact_radius_sq);
|
|
|
|
let plot = items::PlotConfig {
|
|
ui,
|
|
transform,
|
|
show_x: *show_x,
|
|
show_y: *show_y,
|
|
};
|
|
|
|
if let Some((item, elem)) = closest {
|
|
item.on_hover(elem, shapes, &plot);
|
|
} else {
|
|
let value = transform.value_from_position(pointer);
|
|
items::rulers_at_value(pointer, value, "", &plot, shapes);
|
|
}
|
|
}
|
|
}
|