mod items;
mod legend;
mod transform;
use std::collections::HashSet;
use items::PlotItem;
pub use items::{
Arrows, HLine, Line, LineStyle, MarkerShape, PlotImage, Points, Polygon, Text, VLine, Value,
Values,
};
use legend::LegendWidget;
pub use legend::{Corner, Legend};
use transform::{Bounds, ScreenTransform};
use crate::*;
use color::Hsva;
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone)]
struct PlotMemory {
bounds: Bounds,
auto_bounds: bool,
hovered_entry: Option<String>,
hidden_items: HashSet<String>,
min_auto_bounds: Bounds,
}
pub struct Plot {
id_source: Id,
next_auto_color_idx: usize,
items: Vec<Box<dyn PlotItem>>,
center_x_axis: bool,
center_y_axis: bool,
allow_zoom: bool,
allow_drag: bool,
min_auto_bounds: Bounds,
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 {
pub fn new(id_source: impl std::hash::Hash) -> Self {
Self {
id_source: Id::new(id_source),
next_auto_color_idx: 0,
items: Default::default(),
center_x_axis: false,
center_y_axis: false,
allow_zoom: true,
allow_drag: true,
min_auto_bounds: Bounds::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],
}
}
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; let h = i as f32 * golden_ratio;
Hsva::new(h, 0.85, 0.5, 1.0).into() }
pub fn line(mut self, mut line: Line) -> Self {
if line.series.is_empty() {
return self;
};
if line.stroke.color == Color32::TRANSPARENT {
line.stroke.color = self.auto_color();
}
self.items.push(Box::new(line));
self
}
pub fn polygon(mut self, mut polygon: Polygon) -> Self {
if polygon.series.is_empty() {
return self;
};
if polygon.stroke.color == Color32::TRANSPARENT {
polygon.stroke.color = self.auto_color();
}
self.items.push(Box::new(polygon));
self
}
pub fn text(mut self, text: Text) -> Self {
if text.text.is_empty() {
return self;
};
self.items.push(Box::new(text));
self
}
pub fn points(mut self, mut points: Points) -> Self {
if points.series.is_empty() {
return self;
};
if points.color == Color32::TRANSPARENT {
points.color = self.auto_color();
}
self.items.push(Box::new(points));
self
}
pub fn arrows(mut self, mut arrows: Arrows) -> Self {
if arrows.origins.is_empty() || arrows.tips.is_empty() {
return self;
};
if arrows.color == Color32::TRANSPARENT {
arrows.color = self.auto_color();
}
self.items.push(Box::new(arrows));
self
}
pub fn image(mut self, image: PlotImage) -> Self {
self.items.push(Box::new(image));
self
}
pub fn hline(mut self, mut hline: HLine) -> Self {
if hline.stroke.color == Color32::TRANSPARENT {
hline.stroke.color = self.auto_color();
}
self.items.push(Box::new(hline));
self
}
pub fn vline(mut self, mut vline: VLine) -> Self {
if vline.stroke.color == Color32::TRANSPARENT {
vline.stroke.color = self.auto_color();
}
self.items.push(Box::new(vline));
self
}
pub fn data_aspect(mut self, data_aspect: f32) -> Self {
self.data_aspect = Some(data_aspect);
self
}
pub fn view_aspect(mut self, view_aspect: f32) -> Self {
self.view_aspect = Some(view_aspect);
self
}
pub fn width(mut self, width: f32) -> Self {
self.min_size.x = width;
self.width = Some(width);
self
}
pub fn height(mut self, height: f32) -> Self {
self.min_size.y = height;
self.height = Some(height);
self
}
pub fn min_size(mut self, min_size: Vec2) -> Self {
self.min_size = min_size;
self
}
pub fn show_x(mut self, show_x: bool) -> Self {
self.show_x = show_x;
self
}
pub fn show_y(mut self, show_y: bool) -> Self {
self.show_y = show_y;
self
}
#[deprecated = "Renamed center_x_axis"]
pub fn symmetrical_x_axis(mut self, on: bool) -> Self {
self.center_x_axis = on;
self
}
#[deprecated = "Renamed center_y_axis"]
pub fn symmetrical_y_axis(mut self, on: bool) -> Self {
self.center_y_axis = on;
self
}
pub fn center_x_axis(mut self, on: bool) -> Self {
self.center_x_axis = on;
self
}
pub fn center_y_axis(mut self, on: bool) -> Self {
self.center_y_axis = on;
self
}
pub fn allow_zoom(mut self, on: bool) -> Self {
self.allow_zoom = on;
self
}
pub fn allow_drag(mut self, on: bool) -> Self {
self.allow_drag = on;
self
}
pub fn include_x(mut self, x: impl Into<f64>) -> Self {
self.min_auto_bounds.extend_with_x(x.into());
self
}
pub fn include_y(mut self, y: impl Into<f64>) -> Self {
self.min_auto_bounds.extend_with_y(y.into());
self
}
#[deprecated = "Use `Plot::legend` instead"]
pub fn show_legend(mut self, show: bool) -> Self {
self.legend_config = show.then(Legend::default);
self
}
pub fn legend(mut self, legend: Legend) -> Self {
self.legend_config = Some(legend);
self
}
pub fn show_background(mut self, show: bool) -> Self {
self.show_background = show;
self
}
pub fn show_axes(mut self, show: [bool; 2]) -> Self {
self.show_axes = show;
self
}
}
impl Widget for Plot {
fn ui(self, ui: &mut Ui) -> Response {
let Self {
id_source,
next_auto_color_idx: _,
mut items,
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;
let plot_id = ui.make_persistent_id(id_source);
let mut memory = ui
.memory()
.id_data
.get_mut_or_insert_with(plot_id, || PlotMemory {
bounds: min_auto_bounds,
auto_bounds: !min_auto_bounds.is_valid(),
hovered_entry: None,
hidden_items: HashSet::new(),
min_auto_bounds,
})
.clone();
if min_auto_bounds != memory.min_auto_bounds {
memory = PlotMemory {
bounds: min_auto_bounds,
auto_bounds: !min_auto_bounds.is_valid(),
hovered_entry: None,
min_auto_bounds,
..memory
};
ui.memory().id_data.insert(plot_id, memory.clone());
}
let PlotMemory {
mut bounds,
mut auto_bounds,
mut hovered_entry,
mut hidden_items,
..
} = memory;
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_finite().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_finite().y
}
})
.at_least(min_size.y);
vec2(width, height)
};
let (rect, response) = ui.allocate_exact_size(size, Sense::drag());
let plot_painter = ui.painter().sub_region(rect);
if show_background {
plot_painter.add(Shape::Rect {
rect,
corner_radius: 2.0,
fill: ui.visuals().extreme_bg_color,
stroke: ui.visuals().widgets.noninteractive.bg_stroke,
});
}
let legend = legend_config
.and_then(|config| LegendWidget::try_new(rect, config, &items, &hidden_items));
if hovered_entry.is_some() {
show_x = false;
show_y = false;
}
items.retain(|item| !hidden_items.contains(item.name()));
if let Some(hovered_name) = &hovered_entry {
items
.iter_mut()
.filter(|entry| entry.name() == hovered_name)
.for_each(|entry| entry.highlight());
}
items.sort_by_key(|item| item.highlighted());
auto_bounds |= response.double_clicked_by(PointerButton::Primary);
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);
}
if !bounds.is_valid() {
bounds = Bounds::new_symmetrical(1.0);
}
if center_x_axis {
bounds.make_x_symmetrical();
};
if center_y_axis {
bounds.make_y_symmetrical()
};
let mut transform = ScreenTransform::new(rect, bounds, center_x_axis, center_y_axis);
if let Some(data_aspect) = data_aspect {
transform.set_aspect(data_aspect as f64);
}
if allow_drag && response.dragged_by(PointerButton::Primary) {
transform.translate_bounds(-response.drag_delta());
auto_bounds = false;
}
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;
}
}
}
items
.iter_mut()
.for_each(|item| item.initialize(transform.bounds().range_x()));
let bounds = *transform.bounds();
let prepared = Prepared {
items,
show_x,
show_y,
show_axes,
transform,
};
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();
}
ui.memory().id_data.insert(
plot_id,
PlotMemory {
bounds,
auto_bounds,
hovered_entry,
hidden_items,
min_auto_bounds,
},
);
if show_x || show_y {
response.on_hover_cursor(CursorIcon::Crosshair)
} else {
response
}
}
}
struct Prepared {
items: Vec<Box<dyn PlotItem>>,
show_x: bool,
show_y: bool,
show_axes: [bool; 2],
transform: ScreenTransform,
}
impl Prepared {
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; 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;
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 } else if n % base == 0 {
step_size_in_points * basef as f32 } else {
step_size_in_points };
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();
let galley = ui.fonts().layout_single_line(text_style, text);
let mut text_pos = pos_in_gui + vec2(1.0, -galley.size.y);
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::Text {
pos: text_pos,
galley,
color,
fake_italics: false,
});
}
}
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: f32 = 16.0;
let mut closest_value = None;
let mut closest_item = None;
let mut closest_dist_sq = interact_radius.powi(2);
for item in items {
if let Some(values) = item.values() {
for value in &values.values {
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;
closest_value = Some(value);
closest_item = Some(item.name());
}
}
}
}
let mut prefix = String::new();
if let Some(name) = closest_item {
if !name.is_empty() {
prefix = format!("{}\n", name);
}
}
let line_color = if ui.visuals().dark_mode {
Color32::from_gray(100).additive()
} else {
Color32::from_black_alpha(180)
};
let value = if let Some(value) = closest_value {
let position = transform.position_from_value(value);
shapes.push(Shape::circle_filled(position, 3.0, line_color));
*value
} else {
transform.value_from_position(pointer)
};
let pointer = transform.position_from_value(&value);
let rect = transform.frame();
if *show_x {
shapes.push(Shape::line_segment(
[pos2(pointer.x, rect.top()), pos2(pointer.x, rect.bottom())],
(1.0, line_color),
));
}
if *show_y {
shapes.push(Shape::line_segment(
[pos2(rect.left(), pointer.y), pos2(rect.right(), pointer.y)],
(1.0, line_color),
));
}
let text = {
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 *show_x && *show_y {
format!(
"{}x = {:.*}\ny = {:.*}",
prefix, x_decimals, value.x, y_decimals, value.y
)
} else if *show_x {
format!("{}x = {:.*}", prefix, x_decimals, value.x)
} else if *show_y {
format!("{}y = {:.*}", prefix, y_decimals, value.y)
} else {
unreachable!()
}
};
shapes.push(Shape::text(
ui.fonts(),
pointer + vec2(3.0, -2.0),
Align2::LEFT_BOTTOM,
text,
TextStyle::Body,
ui.visuals().text_color(),
));
}
}