use core::fmt;
use std::{
collections::{HashMap, HashSet},
sync::{
Arc, RwLock,
atomic::{AtomicBool, AtomicU64, Ordering},
},
};
use glam::{DVec2, Vec2};
use iced::{
Color, Element, Length, Rectangle, Theme,
alignment::{self, Horizontal, Vertical},
keyboard,
mouse::{self, Interaction},
padding::{self, Padding},
wgpu::TextureFormat,
widget::{
self, container,
shader::{self, Pipeline, Viewport},
stack,
},
};
use indexmap::IndexMap;
use crate::{
AxisScale, DragEvent, Fill, HLine, HoverPickEvent, KeyAction, MarkerStyle, PlotUiMessage,
PointId, Series, Size, TooltipContext, Transform, VLine, axes_labels,
axis_link::AxisLink,
axis_scale::plot_point_to_data,
camera::Camera,
controls::PlotControls,
default_style,
legend::{self, LegendEntry},
message::{CursorPositionUiPayload, PlotRenderUpdate, TooltipUiPayload},
picking, plot_overlay,
plot_renderer::{PlotRenderStrategy, PlotRenderer, RenderParams},
plot_state::PlotState,
series::{SeriesError, ShapeId},
style::{PlotStyle, StyleFn},
ticks::{self, PositionedTick, TickFormatter, TickProducer},
transform::{PositionTransform, data_point_to_plot_with_transform},
};
const PLOT_CONTENT_PADDING: f32 = 2.0;
pub(crate) type CursorProvider = Arc<dyn Fn(f64, f64) -> String + Send + Sync>;
pub(crate) type HighlightPointProvider =
Arc<dyn Fn(TooltipContext<'_>, &mut HighlightPoint) -> Option<String> + Send + Sync>;
pub struct PlotWidget {
pub(crate) instance_id: u64,
pub(crate) series: IndexMap<ShapeId, Series>,
pub(crate) fills: IndexMap<ShapeId, Fill>,
pub(crate) vlines: IndexMap<ShapeId, VLine>,
pub(crate) hlines: IndexMap<ShapeId, HLine>,
pub(crate) hidden_shapes: HashSet<ShapeId>,
pub(crate) data_version: u64,
pub(crate) autoscale_on_updates: bool,
pub(crate) controls: PlotControls,
pub(crate) highlight_on_hover: bool,
pub(crate) show_controls_help: bool,
pub(crate) legend_enabled: bool,
pub(crate) legend_collapsed: bool,
pub(crate) x_axis_label: String,
pub(crate) y_axis_label: String,
pub(crate) x_lim: Option<(f64, f64)>,
pub(crate) y_lim: Option<(f64, f64)>,
pub(crate) x_axis_scale: AxisScale,
pub(crate) y_axis_scale: AxisScale,
pub(crate) x_axis_link: Option<AxisLink>,
pub(crate) y_axis_link: Option<AxisLink>,
pub(crate) hover_radius_px: f32,
pub(crate) pick_highlight_provider: Option<HighlightPointProvider>,
pub(crate) hover_highlight_provider: Option<HighlightPointProvider>,
pub(crate) cursor_overlay: bool,
pub(crate) cursor_provider: Option<CursorProvider>,
pub(crate) crosshairs_enabled: bool,
pub(crate) render_strategy: PlotRenderStrategy,
pub(crate) controls_overlay_open: bool,
#[cfg(feature = "canvas")]
pub(crate) canvas_caches: crate::plot_renderer::canvas::CanvasCaches,
pub(crate) x_axis_formatter: Option<TickFormatter>,
pub(crate) y_axis_formatter: Option<TickFormatter>,
pub(crate) x_tick_producer: Option<TickProducer>,
pub(crate) y_tick_producer: Option<TickProducer>,
pub(crate) tick_label_size: f32,
pub(crate) axis_label_size: f32,
pub(crate) data_aspect: Option<f64>,
pub(crate) style: StyleFn,
pub(crate) resolved_style: RwLock<PlotStyle>,
pub(crate) picked_points: IndexMap<PointId, (HighlightPoint, Option<TooltipUiPayload>)>,
pub(crate) hovered_points: IndexMap<PointId, (HighlightPoint, Option<TooltipUiPayload>)>,
pub(crate) cursor_ui: Option<CursorPositionUiPayload>,
pub(crate) x_ticks: Vec<PositionedTick>,
pub(crate) y_ticks: Vec<PositionedTick>,
pub(crate) shape_overlays_enabled: AtomicBool,
pub(crate) camera_bounds: Option<(Camera, Rectangle)>,
}
impl Default for PlotWidget {
fn default() -> Self {
Self::new()
}
}
impl PlotWidget {
pub fn new() -> Self {
Self {
instance_id: NEXT_ID.fetch_add(1, Ordering::Relaxed),
series: IndexMap::new(),
fills: IndexMap::new(),
vlines: IndexMap::new(),
hlines: IndexMap::new(),
hidden_shapes: HashSet::new(),
data_version: 1,
autoscale_on_updates: false,
controls: PlotControls::default(),
highlight_on_hover: true,
show_controls_help: true,
legend_enabled: true,
legend_collapsed: false,
x_axis_label: String::new(),
y_axis_label: String::new(),
x_lim: None,
y_lim: None,
x_axis_scale: AxisScale::Linear,
y_axis_scale: AxisScale::Linear,
x_axis_link: None,
y_axis_link: None,
hover_radius_px: 8.0,
pick_highlight_provider: None,
hover_highlight_provider: None,
cursor_overlay: true,
cursor_provider: None,
crosshairs_enabled: false,
render_strategy: PlotRenderStrategy::default(),
controls_overlay_open: false,
#[cfg(feature = "canvas")]
canvas_caches: crate::plot_renderer::canvas::CanvasCaches::default(),
x_axis_formatter: Some(Arc::new(ticks::default_formatter)),
y_axis_formatter: Some(Arc::new(ticks::default_formatter)),
x_tick_producer: Some(Arc::new(ticks::default_tick_producer)),
y_tick_producer: Some(Arc::new(ticks::default_tick_producer)),
tick_label_size: 10.0,
axis_label_size: 16.0,
data_aspect: None,
style: Arc::new(default_style),
resolved_style: RwLock::new(PlotStyle::default()),
x_ticks: Vec::new(),
y_ticks: Vec::new(),
picked_points: IndexMap::new(),
hovered_points: IndexMap::new(),
cursor_ui: None,
shape_overlays_enabled: AtomicBool::new(false),
camera_bounds: None,
}
}
pub fn add_series(&mut self, item: Series) -> Result<(), SeriesError> {
item.validate()?;
self.series.insert(item.id, item);
self.data_version += 1;
Ok(())
}
pub fn set_data_aspect(&mut self, aspect: f64) {
if aspect.is_finite() && aspect > 0.0 {
self.data_aspect = Some(aspect);
} else {
self.data_aspect = None;
}
self.data_version = self.data_version.wrapping_add(1);
}
pub fn set_style<F>(&mut self, style: F)
where
F: Fn(&Theme) -> PlotStyle + Send + Sync + 'static,
{
self.style = Arc::new(style);
}
pub fn remove_series(&mut self, id: &ShapeId) -> Result<(), SeriesError> {
if self.series.shift_remove(id).is_some() {
self.hidden_shapes.remove(id);
self.data_version += 1;
Ok(())
} else {
Err(SeriesError::NotFound(*id))
}
}
pub fn remove_fill(&mut self, id: &ShapeId) -> Result<(), SeriesError> {
if self.fills.shift_remove(id).is_some() {
self.hidden_shapes.remove(id);
self.data_version += 1;
Ok(())
} else {
Err(SeriesError::NotFound(*id))
}
}
pub fn update_series<F: FnMut(&mut Series)>(
&mut self,
id: &ShapeId,
mut f: F,
) -> Result<(), SeriesError> {
if let Some(series) = self.series.get_mut(id) {
f(series);
self.data_version += 1;
Ok(())
} else {
Err(SeriesError::NotFound(*id))
}
}
pub fn add_vline(&mut self, vline: VLine) {
self.vlines.insert(vline.id, vline);
self.data_version += 1;
}
pub fn add_fill(&mut self, fill: Fill) -> Result<(), SeriesError> {
if fill.begin == fill.end {
return Err(SeriesError::InvalidFillEndpoints);
}
if !self.is_fill_endpoint_available(fill.begin) {
return Err(SeriesError::FillEndpointNotFound(fill.begin));
}
if !self.is_fill_endpoint_available(fill.end) {
return Err(SeriesError::FillEndpointNotFound(fill.end));
}
self.fills.insert(fill.id, fill);
self.data_version = self.data_version.wrapping_add(1);
Ok(())
}
pub fn add_hline(&mut self, hline: HLine) {
self.hlines.insert(hline.id, hline);
self.data_version += 1;
}
pub fn set_x_axis_label(&mut self, label: impl Into<String>) {
self.x_axis_label = label.into();
}
pub fn set_y_axis_label(&mut self, label: impl Into<String>) {
self.y_axis_label = label.into();
}
pub fn set_x_lim(&mut self, min: f64, max: f64) {
self.x_lim = Some((min, max));
}
pub fn set_x_axis_scale(&mut self, scale: AxisScale) {
self.x_axis_scale = scale;
self.data_version = self.data_version.wrapping_add(1);
}
pub fn set_y_lim(&mut self, min: f64, max: f64) {
self.y_lim = Some((min, max));
}
pub fn set_y_axis_scale(&mut self, scale: AxisScale) {
self.y_axis_scale = scale;
self.data_version = self.data_version.wrapping_add(1);
}
pub fn set_x_axis_link(&mut self, link: AxisLink) {
self.x_axis_link = Some(link);
}
pub fn set_y_axis_link(&mut self, link: AxisLink) {
self.y_axis_link = Some(link);
}
pub(crate) fn update_style(&self, theme: &Theme) -> PlotStyle {
let style = (self.style)(theme);
*self
.resolved_style
.write()
.expect("plot style lock poisoned") = style;
style
}
fn world_to_screen_position(
world: [f64; 2],
camera_bounds: &(Camera, Rectangle),
x_axis_scale: AxisScale,
y_axis_scale: AxisScale,
transform: &PositionTransform,
) -> Option<[f32; 2]> {
let (camera, bounds) = camera_bounds;
let world = data_point_to_plot_with_transform(
world,
x_axis_scale,
y_axis_scale,
transform,
Some(camera.axis_ranges()),
)?;
if let (Some(screen_x), Some(screen_y)) = (
world_to_screen_position_x(world[0], camera, bounds),
world_to_screen_position_y(world[1], camera, bounds),
) {
Some([screen_x, screen_y])
} else {
None
}
}
fn world_to_screen_position_unclipped(
world: [f64; 2],
camera_bounds: &(Camera, Rectangle),
x_axis_scale: AxisScale,
y_axis_scale: AxisScale,
transform: &PositionTransform,
) -> Option<[f32; 2]> {
let (camera, bounds) = camera_bounds;
let world = data_point_to_plot_with_transform(
world,
x_axis_scale,
y_axis_scale,
transform,
Some(camera.axis_ranges()),
)?;
let ndc_x = (world[0] - camera.position.x) / camera.half_extents.x;
let ndc_y = (world[1] - camera.position.y) / camera.half_extents.y;
let screen_x = (ndc_x as f32 + 1.0) * 0.5 * bounds.width;
let screen_y = (1.0 - ndc_y as f32) * 0.5 * bounds.height;
(screen_x.is_finite() && screen_y.is_finite()).then_some([screen_x, screen_y])
}
fn tooltip_anchor_world(point: &HighlightPoint) -> [f64; 2] {
if let Some(marker_style) = point.marker_style
&& let Size::World(size) = marker_style.size
{
let half = size * 0.5;
[point.x + half, point.y + half]
} else {
[point.x, point.y]
}
}
fn update_tooltip_positions(&mut self) {
if let Some(camera_bounds) = &self.camera_bounds {
for (highlight_point, tooltip) in self
.hovered_points
.values_mut()
.chain(self.picked_points.values_mut())
{
if let Some(tooltip) = tooltip {
tooltip.screen_xy = Self::world_to_screen_position(
Self::tooltip_anchor_world(highlight_point),
camera_bounds,
self.x_axis_scale,
self.y_axis_scale,
&highlight_point.transform,
);
}
}
}
}
pub fn series_ids(&self) -> Vec<ShapeId> {
self.series.keys().copied().collect()
}
pub fn point_position(&self, point_id: PointId) -> Option<[f64; 2]> {
self.series
.get(&point_id.series_id)?
.positions
.get(point_id.point_index)
.copied()
}
pub fn nearest_point(&self, series_id: ShapeId, x: f64, y: f64) -> Option<PointId> {
let series = self.series.get(&series_id)?;
let mut min_distance = f64::INFINITY;
let mut nearest_point = None;
for (i, position) in series.positions.iter().enumerate() {
let distance = (position[0] - x).powi(2) + (position[1] - y).powi(2);
if distance < min_distance {
min_distance = distance;
nearest_point = Some(PointId {
series_id,
point_index: i,
});
}
}
nearest_point
}
pub fn nearest_point_horizontal(&self, series_id: ShapeId, x: f64) -> Option<PointId> {
let series = self.series.get(&series_id)?;
let mut min_distance = f64::INFINITY;
let mut nearest_point = None;
for (i, position) in series.positions.iter().enumerate() {
let distance = (position[0] - x).abs();
if distance < min_distance {
min_distance = distance;
nearest_point = Some(PointId {
series_id,
point_index: i,
});
}
}
nearest_point
}
pub fn nearest_point_vertical(&self, series_id: ShapeId, y: f64) -> Option<PointId> {
let series = self.series.get(&series_id)?;
let mut min_distance = f64::INFINITY;
let mut nearest_point = None;
for (i, position) in series.positions.iter().enumerate() {
let distance = (position[1] - y).abs();
if distance < min_distance {
min_distance = distance;
nearest_point = Some(PointId {
series_id,
point_index: i,
});
}
}
nearest_point
}
pub fn add_hover_point(&mut self, point_id: PointId) {
self.handle_hover_pick::<false>(point_id);
}
pub fn add_pick_point(&mut self, point_id: PointId) {
self.handle_hover_pick::<true>(point_id);
}
pub fn clear_hover(&mut self) {
if !self.hovered_points.is_empty() {
self.hovered_points.clear();
}
}
pub fn clear_pick(&mut self) {
if !self.picked_points.is_empty() {
self.picked_points.clear();
}
}
fn cached_style(&self) -> PlotStyle {
*self
.resolved_style
.read()
.expect("plot style lock poisoned")
}
fn handle_hover_pick<const PICK: bool>(&mut self, point_id: PointId) -> bool {
let mut changed = false;
let (highlight_provider, points) = if PICK {
if self.picked_points.shift_remove(&point_id).is_some() {
return true;
}
changed |= self.hovered_points.shift_remove(&point_id).is_some();
(&self.pick_highlight_provider, &mut self.picked_points)
} else {
if self.picked_points.contains_key(&point_id) {
return false;
}
(&self.hover_highlight_provider, &mut self.hovered_points)
};
if let Some(highlight_provider) = highlight_provider
&& let Some(series) = self.series.get(&point_id.series_id)
&& let Some(position) = series.positions.get(point_id.point_index)
&& let Some(camera_bounds) = &self.camera_bounds
{
let mut highlight_point = HighlightPoint {
x: position[0],
y: position[1],
transform: series.transform.clone(),
color: series
.point_colors
.as_ref()
.map(|colors| colors[point_id.point_index])
.unwrap_or(series.color),
marker_style: series.marker_style,
mask_padding: Some(3.0),
};
let tooltip_text = highlight_provider(
TooltipContext {
series_id: series.id,
series_label: series.label.as_deref().unwrap_or(""),
point_index: point_id.point_index,
},
&mut highlight_point,
);
let tooltip = tooltip_text.map(|text| TooltipUiPayload {
screen_xy: Self::world_to_screen_position(
Self::tooltip_anchor_world(&highlight_point),
camera_bounds,
self.x_axis_scale,
self.y_axis_scale,
&highlight_point.transform,
),
text,
});
let new_payload = (highlight_point, tooltip);
match points.entry(point_id) {
indexmap::map::Entry::Occupied(mut occupied) => {
if PartialEq::ne(occupied.get(), &new_payload) {
occupied.insert(new_payload);
changed = true;
}
}
indexmap::map::Entry::Vacant(vacant) => {
vacant.insert(new_payload);
changed = true;
}
}
}
changed
}
pub fn update(&mut self, msg: PlotUiMessage) {
match msg {
PlotUiMessage::ToggleLegend => {
self.legend_collapsed = !self.legend_collapsed;
}
PlotUiMessage::ToggleControlsOverlay => {
self.controls_overlay_open = !self.controls_overlay_open;
}
PlotUiMessage::ToggleSeriesVisibility(id) => {
self.toggle_visibility(&id);
}
PlotUiMessage::RenderUpdate(payload) => {
if let Some(camera_bounds) = payload.camera_bounds
&& self.camera_bounds != Some(*camera_bounds)
{
self.camera_bounds = Some(*camera_bounds);
self.update_tooltip_positions();
}
match payload.hover_pick {
Some(HoverPickEvent::Hover(point_id)) => {
self.hovered_points.clear();
self.handle_hover_pick::<false>(point_id);
}
Some(HoverPickEvent::Pick(point_id)) => {
self.handle_hover_pick::<true>(point_id);
}
Some(HoverPickEvent::ClearHover) => {
self.hovered_points.clear();
}
Some(HoverPickEvent::ClearPick) => {
self.picked_points.clear();
}
_ => {}
};
if payload.clear_cursor_position {
self.cursor_ui = None;
}
if let Some(c) = payload.cursor_position_ui {
self.cursor_ui = Some(c);
}
if let Some(ticks) = payload.x_ticks {
self.x_ticks = ticks;
}
if let Some(ticks) = payload.y_ticks {
self.y_ticks = ticks;
}
}
}
}
pub fn view<'a>(&'a self) -> iced::Element<'a, PlotUiMessage> {
self.shape_overlays_enabled.store(false, Ordering::Relaxed);
self.view_with_shape_elements(std::iter::empty(), std::iter::empty(), |message| message)
}
pub fn view_with_shapes<'a, Message>(
&'a self,
shapes_bottom: impl Iterator<Item = plot_overlay::PlotOverlay<'a, Message>> + 'a,
shapes_top: impl Iterator<Item = plot_overlay::PlotOverlay<'a, Message>> + 'a,
map_plot: impl Fn(PlotUiMessage) -> Message + Copy + 'a,
) -> iced::Element<'a, Message>
where
Message: 'a,
{
self.shape_overlays_enabled.store(true, Ordering::Relaxed);
let shapes_bottom = shapes_bottom.filter_map(|shape| self.view_shape_overlay(shape));
let shapes_top = shapes_top.filter_map(|shape| self.view_shape_overlay(shape));
self.view_with_shape_elements(shapes_bottom, shapes_top, map_plot)
}
fn view_with_shape_elements<'a, Message, MapPlot>(
&'a self,
shapes_bottom: impl Iterator<Item = Element<'a, Message>>,
shapes_top: impl Iterator<Item = Element<'a, Message>>,
map_plot: MapPlot,
) -> iced::Element<'a, Message>
where
Message: 'a,
MapPlot: Fn(PlotUiMessage) -> Message + Copy + 'a,
{
let style = self.cached_style();
let tooltip_overlays = self
.visible_highlighted_points()
.filter_map(|(_, tooltip)| self.view_tooltip_overlay(tooltip, &self.camera_bounds))
.map(|element| element.map(map_plot));
let shapes_top = shapes_top.chain(tooltip_overlays);
let inner_container = self.view_plot_area(shapes_bottom, shapes_top, map_plot);
let legend = if self.legend_enabled {
legend::legend(self, self.legend_collapsed)
} else {
None
};
let has_legend = legend.is_some();
let mut layers = Vec::new();
layers.push(inner_container);
layers.push(self.view_top_right_overlay(has_legend).map(map_plot));
if let Some(tick_labels) = self.view_tick_labels() {
layers.push(tick_labels.map(map_plot));
}
if let Some(legend) = legend {
layers.push(legend.map(map_plot));
}
let elements: Element<'a, Message> = stack(layers)
.width(Length::Fill)
.height(Length::Fill)
.into();
container(axes_labels::stack_with_labels(
elements,
&self.x_axis_label,
&self.y_axis_label,
self.axis_label_size,
style.axis_label_color,
))
.padding(3.0)
.style(|theme: &Theme| self.update_style(theme).frame)
.into()
}
fn view_plot_area<'a, Message, MapPlot>(
&'a self,
shapes_bottom: impl Iterator<Item = Element<'a, Message>>,
shapes_top: impl Iterator<Item = Element<'a, Message>>,
map_plot: MapPlot,
) -> Element<'a, Message>
where
Message: 'a,
MapPlot: Fn(PlotUiMessage) -> Message + Copy + 'a,
{
let plot: Element<'a, PlotUiMessage> = match self.render_strategy {
PlotRenderStrategy::Shader => widget::shader(self)
.width(Length::Fill)
.height(Length::Fill)
.into(),
#[cfg(feature = "canvas")]
PlotRenderStrategy::Canvas => widget::canvas(self)
.width(Length::Fill)
.height(Length::Fill)
.into(),
};
let plot = plot.map(map_plot);
let plot_layers = shapes_bottom.chain(std::iter::once(plot)).chain(shapes_top);
container(stack(plot_layers).width(Length::Fill).height(Length::Fill))
.clip(true)
.padding(PLOT_CONTENT_PADDING)
.style(|theme: &Theme| self.update_style(theme).plot_area)
.into()
}
fn view_shape_overlay<'a, Message>(
&'a self,
shape: plot_overlay::PlotOverlay<'a, Message>,
) -> Option<Element<'a, Message>>
where
Message: 'a,
{
let camera_bounds = self.camera_bounds.as_ref()?;
let mut screen_xy = Self::world_to_screen_position_unclipped(
shape.anchor_position,
camera_bounds,
self.x_axis_scale,
self.y_axis_scale,
&shape.anchor_position_transform,
)?;
screen_xy[0] += shape.anchor_offset[0];
screen_xy[1] -= shape.anchor_offset[1];
Some(plot_overlay::positioned_overlay(
shape.element,
screen_xy,
shape.align_to_anchor_horizontal,
shape.align_to_anchor_vertical,
))
}
pub fn autoscale_on_updates(&mut self, enabled: bool) {
self.autoscale_on_updates = enabled;
}
pub fn hover_radius_px(&mut self, radius: f32) {
self.hover_radius_px = radius.max(0.0);
}
pub fn set_pick_highlight_provider(&mut self, provider: HighlightPointProvider) {
self.pick_highlight_provider = Some(provider);
}
pub fn set_hover_highlight_provider(&mut self, provider: HighlightPointProvider) {
self.hover_highlight_provider = Some(provider);
}
pub fn set_cursor_overlay(&mut self, enabled: bool) {
self.cursor_overlay = enabled;
}
pub fn set_cursor_provider(&mut self, provider: CursorProvider) {
self.cursor_provider = Some(provider);
}
pub fn set_crosshairs(&mut self, enabled: bool) {
self.crosshairs_enabled = enabled;
}
pub fn set_render_strategy(&mut self, strategy: PlotRenderStrategy) {
self.render_strategy = strategy;
#[cfg(feature = "canvas")]
{
self.canvas_caches.static_layer.clear();
self.canvas_caches.overlay_layer.clear();
}
}
pub fn set_controls(&mut self, controls: PlotControls) {
self.controls = controls;
}
pub fn get_controls(&self) -> &PlotControls {
&self.controls
}
pub fn get_controls_mut(&mut self) -> &mut PlotControls {
&mut self.controls
}
pub fn set_highlight_on_hover(&mut self, enabled: bool) {
self.highlight_on_hover = enabled;
}
pub fn highlight_on_hover(&self) -> bool {
self.highlight_on_hover
}
pub fn set_controls_help(&mut self, enabled: bool) {
self.show_controls_help = enabled;
}
pub fn controls_help_enabled(&self) -> bool {
self.show_controls_help
}
pub fn set_x_axis_formatter(&mut self, formatter: TickFormatter) {
self.x_axis_formatter = Some(formatter);
}
pub fn set_y_axis_formatter(&mut self, formatter: TickFormatter) {
self.y_axis_formatter = Some(formatter);
}
pub fn set_x_tick_producer(&mut self, producer: TickProducer) {
self.x_tick_producer = Some(producer);
}
pub fn set_y_tick_producer(&mut self, producer: TickProducer) {
self.y_tick_producer = Some(producer);
}
pub fn set_series_positions(&mut self, id: &ShapeId, positions: &[[f64; 2]]) {
if let Some(series) = self.series.get_mut(id) {
series.positions = positions.to_vec();
if let Some(colors) = &mut series.point_colors
&& colors.len() != series.positions.len()
{
colors.resize(series.positions.len(), series.color);
}
self.data_version += 1;
}
}
pub fn set_series_point_colors(&mut self, id: &ShapeId, mut colors: Vec<Color>) {
if let Some(series) = self.series.get_mut(id) {
if colors.len() != series.positions.len() {
colors.resize(series.positions.len(), series.color);
}
series.point_colors = Some(colors);
self.data_version += 1;
}
}
pub(crate) fn legend_entries(&self) -> Vec<LegendEntry> {
let mut out = Vec::new();
for (id, s) in &self.series {
if let Some(ref label) = s.label {
if label.is_empty() {
continue;
}
if s.positions.is_empty() {
continue;
}
if s.marker_style.is_some() || s.line_style.is_some() {
let marker = if let Some(ref marker_style) = s.marker_style {
marker_style.marker_type as u32
} else {
u32::MAX
};
out.push(LegendEntry {
id: *id,
label: label.clone(),
color: s.color,
_marker: marker,
_line_style: s.line_style,
hidden: self.hidden_shapes.contains(id),
});
}
}
}
for (id, vline) in &self.vlines {
if let Some(ref label) = vline.label
&& !label.is_empty()
{
out.push(LegendEntry {
id: *id,
label: label.clone(),
color: vline.color,
_marker: u32::MAX,
_line_style: Some(vline.line_style),
hidden: self.hidden_shapes.contains(id),
});
}
}
for (id, hline) in &self.hlines {
if let Some(ref label) = hline.label
&& !label.is_empty()
{
out.push(LegendEntry {
id: *id,
label: label.clone(),
color: hline.color,
_marker: u32::MAX,
_line_style: Some(hline.line_style),
hidden: self.hidden_shapes.contains(id),
});
}
}
for (id, fill) in &self.fills {
if let Some(ref label) = fill.label
&& !label.is_empty()
{
out.push(LegendEntry {
id: *id,
label: label.clone(),
color: fill.color,
_marker: u32::MAX,
_line_style: None,
hidden: self.hidden_shapes.contains(id),
});
}
}
out
}
fn view_tooltip_overlay<'a>(
&'a self,
payload: &'a Option<TooltipUiPayload>,
camera_bounds: &Option<(Camera, Rectangle)>,
) -> Option<Element<'a, PlotUiMessage>> {
const OFFSET: f32 = 8.0;
let payload = payload.as_ref()?;
let [screen_x, screen_y] = payload.screen_xy?;
let mut anchor = [screen_x + OFFSET, screen_y + OFFSET];
let mut horizontal_position = Horizontal::Right;
let mut vertical_position = Vertical::Bottom;
const FLIP_PCT: f32 = 0.8;
if let Some((_, bounds)) = &camera_bounds {
if screen_y > bounds.height * FLIP_PCT {
anchor[1] = screen_y - OFFSET;
vertical_position = Vertical::Top;
}
if screen_x > bounds.width * FLIP_PCT {
anchor[0] = screen_x - OFFSET;
horizontal_position = Horizontal::Left;
}
}
let tooltip_bubble = container(
widget::text(&payload.text)
.size(14.0)
.wrapping(widget::text::Wrapping::None),
)
.padding(6.0)
.style(|theme| self.update_style(theme).tooltip);
Some(plot_overlay::positioned_overlay(
tooltip_bubble.into(),
anchor,
horizontal_position,
vertical_position,
))
}
fn view_cursor_overlay(&self) -> Option<Element<'_, PlotUiMessage>> {
if !self.cursor_overlay {
return None;
}
let Some(payload) = &self.cursor_ui else {
return None;
};
let bubble = container(widget::text(payload.text.clone()).size(12.0))
.padding(6.0)
.style(|theme| self.update_style(theme).cursor_overlay);
Some(bubble.into())
}
fn view_top_right_overlay(&self, has_legend: bool) -> Element<'_, PlotUiMessage> {
let help_btn = self.show_controls_help.then(|| {
let help_label = if self.controls_overlay_open {
"×"
} else {
"?"
};
widget::button(widget::text(help_label).size(12.0))
.padding(6.0)
.on_press(PlotUiMessage::ToggleControlsOverlay)
});
let top_row = widget::row![self.view_cursor_overlay(), help_btn].spacing(6.0);
let col = widget::column![top_row, self.view_controls_overlay_panel(has_legend)]
.spacing(6.0)
.width(Length::Shrink)
.height(Length::Shrink)
.align_x(Horizontal::Right);
container(col)
.width(Length::Fill)
.height(Length::Fill)
.padding(Padding {
top: 4.0,
right: 4.0,
..Padding::ZERO
})
.align_x(Horizontal::Right)
.align_y(Vertical::Top)
.style(container::transparent)
.into()
}
fn view_controls_overlay_panel(&self, has_legend: bool) -> Option<Element<'_, PlotUiMessage>> {
if !self.controls_overlay_open {
return None;
}
Some(
container(self.controls.view_controls_overlay_panel(has_legend))
.padding(8.0)
.style(|theme| self.update_style(theme).controls_panel)
.into(),
)
}
fn view_tick_labels(&self) -> Option<Element<'_, PlotUiMessage>> {
if self.x_ticks.is_empty() && self.y_ticks.is_empty() {
return None;
}
let mut tick_elements = Vec::with_capacity(self.x_ticks.len() + self.y_ticks.len());
let tick_label_color = self.cached_style().tick_label_color;
let tick_text = |text| {
widget::text(text)
.size(self.tick_label_size)
.color(tick_label_color)
};
if let Some(formatter) = &self.x_axis_formatter {
for tick in &self.x_ticks {
let label_text = formatter(tick.tick);
let centering_offset = 2.0 * (label_text.len() as f32); let text_widget = tick_text(label_text);
let positioned_label = container(text_widget)
.width(Length::Fill)
.height(Length::Fill)
.padding(padding::left(tick.screen_pos - centering_offset))
.align_x(Horizontal::Left)
.align_y(Vertical::Bottom)
.style(container::transparent);
tick_elements.push(positioned_label.into());
}
}
if let Some(formatter) = &self.y_axis_formatter {
for tick in &self.y_ticks {
let label_text = formatter(tick.tick);
let text_widget = tick_text(label_text);
let positioned_label = widget::container(text_widget)
.width(Length::Fill)
.height(Length::Fill)
.padding(padding::top(tick.screen_pos - 5.0))
.align_x(alignment::Horizontal::Left)
.align_y(Vertical::Top)
.style(container::transparent);
tick_elements.push(positioned_label.into());
}
}
if tick_elements.is_empty() {
return None;
}
Some(stack(tick_elements).into())
}
pub(crate) fn visible_highlighted_points(
&self,
) -> impl Iterator<Item = &(HighlightPoint, Option<TooltipUiPayload>)> {
self.hovered_points
.iter()
.chain(self.picked_points.iter())
.filter_map(|(point_id, point_ctx)| {
(!self.hidden_shapes.contains(&point_id.series_id)).then_some(point_ctx)
})
}
fn toggle_visibility(&mut self, id: &ShapeId) {
let exists = self.series.contains_key(id)
|| self.fills.contains_key(id)
|| self.vlines.contains_key(id)
|| self.hlines.contains_key(id);
if !exists {
println!("Toggle visibility: series not found: {id}");
return;
}
if !self.hidden_shapes.remove(id) {
self.hidden_shapes.insert(*id);
}
self.data_version += 1;
}
fn is_fill_endpoint_available(&self, id: ShapeId) -> bool {
self.series.contains_key(&id)
|| self.vlines.contains_key(&id)
|| self.hlines.contains_key(&id)
}
fn shape_uses_axes_transform(&self, id: ShapeId) -> bool {
self.series
.get(&id)
.map(|series| &series.transform)
.is_some_and(PositionTransform::uses_axes_coordinates)
|| self
.vlines
.get(&id)
.and_then(|vline| vline.transform.as_ref())
.is_some_and(|transform| transform.uses_axes_coordinates())
|| self
.hlines
.get(&id)
.and_then(|hline| hline.transform.as_ref())
.is_some_and(|transform| transform.uses_axes_coordinates())
}
fn has_visible_dynamic_geometry_transforms(&self) -> bool {
self.series.iter().any(|(id, series)| {
!self.hidden_shapes.contains(id) && series.transform.uses_axes_coordinates()
}) || self.fills.iter().any(|(id, fill)| {
!self.hidden_shapes.contains(id)
&& !self.hidden_shapes.contains(&fill.begin)
&& !self.hidden_shapes.contains(&fill.end)
&& (self.shape_uses_axes_transform(fill.begin)
|| self.shape_uses_axes_transform(fill.end))
})
}
}
#[doc(hidden)]
pub struct Primitive {
instance_id: u64,
plot_widget: PlotState,
}
impl std::fmt::Debug for Primitive {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Primitive")
.field("instance_id", &self.instance_id)
.finish_non_exhaustive()
}
}
#[derive(Default, Debug)]
struct UpdateEffects {
needs_redraw: bool,
hover_pick: Option<HoverPickEvent>,
drag_event: Option<DragEvent>,
cursor_ui: Option<CursorPositionUiPayload>,
clear_cursor_position: bool,
publish_camera_bounds: bool,
}
#[derive(Default, Debug)]
struct CanvasInvalidation {
static_layer: bool,
overlay_layer: bool,
}
impl CanvasInvalidation {
fn static_layer(&mut self) {
self.static_layer = true;
}
fn overlay_layer(&mut self) {
self.overlay_layer = true;
}
fn all(&mut self) {
self.static_layer = true;
self.overlay_layer = true;
}
fn apply(self, widget: &PlotWidget) {
if self.static_layer {
invalidate_static_canvas(widget);
}
if self.overlay_layer {
invalidate_overlay_canvas(widget);
}
}
}
fn widget_has_any_tooltips(widget: &PlotWidget) -> bool {
widget
.hovered_points
.values()
.chain(widget.picked_points.values())
.any(|(_, tooltip)| tooltip.is_some())
}
fn widget_needs_camera_bounds(widget: &PlotWidget) -> bool {
widget_has_any_tooltips(widget) || widget.shape_overlays_enabled.load(Ordering::Relaxed)
}
fn clear_hover_effect(widget: &PlotWidget, state: &mut PlotState, effects: &mut UpdateEffects) {
let should_clear_hover =
state.picking.last_hover_cache.is_some() || !widget.hovered_points.is_empty();
if effects.hover_pick.is_none() && should_clear_hover {
state.picking.last_hover_cache = None;
effects.hover_pick = Some(HoverPickEvent::ClearHover);
}
}
fn maybe_submit_hover_request(
widget: &PlotWidget,
state: &mut PlotState,
effects: &mut UpdateEffects,
) {
if !state.hover_enabled || state.pan.active || state.selection.active {
return;
}
if !state.cursor_inside() {
clear_hover_effect(widget, state, effects);
return;
}
if effects.hover_pick.is_some() {
return;
}
let PlotState {
picking: pick_state,
cursor_position,
hover_radius_px,
points,
series,
camera,
bounds,
..
} = state;
let force_cpu_picking = widget.render_strategy.force_cpu_picking();
match pick_state.request_hover(
widget.instance_id,
*cursor_position,
*hover_radius_px,
force_cpu_picking,
points.as_ref(),
series.as_ref(),
camera,
bounds,
|pid| widget.valid_point_id(pid),
) {
picking::HoverRequest::CpuHit(point) => {
effects.hover_pick = Some(HoverPickEvent::Hover(point));
}
picking::HoverRequest::CpuMiss => {
clear_hover_effect(widget, state, effects);
}
picking::HoverRequest::RequestedGpu => {
effects.needs_redraw = true;
}
}
}
fn update_cursor_overlay_on_move(
widget: &PlotWidget,
state: &PlotState,
effects: &mut UpdateEffects,
) {
if !widget.cursor_overlay {
return;
}
if state.cursor_inside() {
let viewport = Vec2::new(state.bounds.width, state.bounds.height);
let plot = state.camera.screen_to_world(
DVec2::new(
state.cursor_position.x as f64,
state.cursor_position.y as f64,
),
DVec2::new(viewport.x as f64, viewport.y as f64),
);
let Some(world) =
plot_point_to_data([plot.x, plot.y], widget.x_axis_scale, widget.y_axis_scale)
else {
effects.clear_cursor_position = true;
return;
};
let text = if let Some(p) = &widget.cursor_provider {
(p)(world[0], world[1])
} else {
format!("{:.4}, {:.4}", world[0], world[1])
};
effects.cursor_ui = Some(CursorPositionUiPayload {
x: world[0],
y: world[1],
text,
});
} else {
effects.clear_cursor_position = true;
}
}
fn invalidate_static_canvas(widget: &PlotWidget) {
_ = widget;
#[cfg(feature = "canvas")]
{
widget.canvas_caches.static_layer.clear();
}
}
fn invalidate_overlay_canvas(widget: &PlotWidget) {
_ = widget;
#[cfg(feature = "canvas")]
{
widget.canvas_caches.overlay_layer.clear();
}
}
fn consume_gpu_pick_results(
widget: &PlotWidget,
state: &mut PlotState,
effects: &mut UpdateEffects,
) {
if !(state.hover_enabled || state.pick_enabled)
|| state.points.len() < picking::CPU_PICK_THRESHOLD
{
return;
}
if effects.hover_pick.is_some() {
return;
}
match state
.picking
.consume_gpu_result(widget.instance_id, |pid| widget.valid_point_id(pid))
{
Some(picking::GpuResultEvent::Pick(point)) => {
effects.hover_pick = Some(HoverPickEvent::Pick(point));
}
Some(picking::GpuResultEvent::Hover(point)) => {
effects.hover_pick = Some(HoverPickEvent::Hover(point));
}
Some(picking::GpuResultEvent::HoverMiss) => {
clear_hover_effect(widget, state, effects);
}
None => {}
}
}
fn update_ticks_and_build_payload(
widget: &PlotWidget,
state: &mut PlotState,
effects: &mut UpdateEffects,
first_time_widget_view: bool,
) -> (Option<Vec<PositionedTick>>, Option<Vec<PositionedTick>>) {
if !effects.needs_redraw {
return (None, None);
}
let old_x = state.x_ticks.clone();
let old_y = state.y_ticks.clone();
state.update_ticks(
widget.x_tick_producer.as_ref(),
widget.y_tick_producer.as_ref(),
);
let publish_x =
(first_time_widget_view || (state.x_ticks != old_x)).then(|| state.x_ticks.clone());
let publish_y =
(first_time_widget_view || (state.y_ticks != old_y)).then(|| state.y_ticks.clone());
if publish_x.is_none()
&& publish_y.is_none()
&& widget_needs_camera_bounds(widget)
&& widget.camera_bounds != Some((state.camera, state.bounds))
{
effects.publish_camera_bounds = true;
}
(publish_x, publish_y)
}
fn update_plot_program<const IS_CANVAS: bool>(
widget: &PlotWidget,
state: &mut PlotState,
event: &iced::Event,
bounds: Rectangle,
cursor: mouse::Cursor,
) -> Option<shader::Action<PlotUiMessage>> {
let mut effects = UpdateEffects::default();
let mut invalidation = CanvasInvalidation::default();
let prev_camera = state.camera;
let prev_bounds = state.bounds;
state.bounds = bounds;
state.hover_enabled = widget.highlight_on_hover
&& (widget.hover_highlight_provider.is_some() || widget.pick_highlight_provider.is_some());
state.pick_enabled = widget.controls.has_pick_action();
state.hover_radius_px = widget.hover_radius_px;
state.crosshairs_enabled = widget.crosshairs_enabled;
if IS_CANVAS {
let grid_style = widget.cached_style().grid;
if state.grid_style != grid_style {
state.grid_style = grid_style;
effects.needs_redraw = true;
invalidation.static_layer();
}
}
if state.sync_highlighted_points_from_widget(widget) {
effects.needs_redraw = true;
invalidation.overlay_layer();
}
let limits_changed = widget.x_lim != state.x_lim || widget.y_lim != state.y_lim;
let instance_switched = state.source_instance_id != Some(widget.instance_id);
let first_time_widget_view = instance_switched && widget.camera_bounds.is_none();
if widget.data_version != state.data_src_version || instance_switched {
state.rebuild_from_widget(widget);
if instance_switched && let Some((camera, _)) = widget.camera_bounds {
state.camera = camera;
}
maybe_submit_hover_request(widget, state, &mut effects);
if widget.autoscale_on_updates || limits_changed || first_time_widget_view {
state.autoscale(!first_time_widget_view);
}
state.data_src_version = widget.data_version;
state.source_instance_id = Some(widget.instance_id);
effects.needs_redraw = true;
invalidation.all();
} else if limits_changed {
state.x_lim = widget.x_lim;
state.y_lim = widget.y_lim;
state.autoscale(true);
effects.needs_redraw = true;
invalidation.all();
}
if let Some(ref link) = state.x_axis_link {
let link_version = link.version();
if link_version != state.x_link_version {
let (position, half_extent, version) = link.get();
state.camera.position.x = position;
state.camera.half_extents.x = half_extent;
state.x_link_version = version;
effects.needs_redraw = true;
invalidation.all();
}
}
if let Some(ref link) = state.y_axis_link {
let link_version = link.version();
if link_version != state.y_link_version {
let (position, half_extent, version) = link.get();
state.camera.position.y = position;
state.camera.half_extents.y = half_extent;
state.y_link_version = version;
effects.needs_redraw = true;
invalidation.all();
}
}
match event {
iced::Event::Mouse(mouse_event) => {
let mouse_redraw = state.handle_mouse_event(
*mouse_event,
cursor,
widget,
&mut effects.hover_pick,
&mut effects.drag_event,
);
effects.needs_redraw |= mouse_redraw;
if mouse_redraw {
invalidation.overlay_layer();
}
match mouse_event {
iced::mouse::Event::CursorMoved { .. } => {
if state.available_cursor_is_inside(cursor) {
maybe_submit_hover_request(widget, state, &mut effects);
update_cursor_overlay_on_move(widget, state, &mut effects);
} else {
clear_hover_effect(widget, state, &mut effects);
if widget.cursor_overlay {
effects.clear_cursor_position = true;
}
}
invalidation.overlay_layer();
}
iced::mouse::Event::CursorLeft => {
clear_hover_effect(widget, state, &mut effects);
invalidation.overlay_layer();
}
_ => {}
}
}
iced::Event::Keyboard(keyboard_event) => {
if let keyboard::Event::KeyPressed { key, .. } = keyboard_event
&& state.available_cursor_is_inside(cursor)
&& widget.controls.key_action(key) == Some(KeyAction::ClearPick)
{
effects.hover_pick = Some(HoverPickEvent::ClearPick);
invalidation.overlay_layer();
}
effects.needs_redraw |= state.handle_keyboard_event(keyboard_event, widget, cursor);
}
_ => {}
}
if let Some(aspect) = widget.data_aspect
&& apply_data_aspect(&mut state.camera, &state.bounds, aspect)
{
effects.needs_redraw = true;
invalidation.all();
}
if (state.camera != prev_camera || state.bounds != prev_bounds)
&& widget.has_visible_dynamic_geometry_transforms()
{
state.rebuild_from_widget(widget);
effects.needs_redraw = true;
invalidation.all();
}
if !state.highlighted_points.is_empty()
&& (state.camera != prev_camera || state.bounds != prev_bounds)
{
state.highlight_version = state.highlight_version.wrapping_add(1);
effects.needs_redraw = true;
invalidation.overlay_layer();
}
if state.camera != prev_camera || state.bounds != prev_bounds {
effects.needs_redraw = true;
effects.publish_camera_bounds = true;
invalidation.all();
}
let had_hover_pick = effects.hover_pick.is_some();
consume_gpu_pick_results(widget, state, &mut effects);
if !had_hover_pick && effects.hover_pick.is_some() {
invalidation.overlay_layer();
}
if !IS_CANVAS {
effects.needs_redraw |= state.picking.has_outstanding_gpu_request();
}
let (publish_x_ticks, publish_y_ticks) =
update_ticks_and_build_payload(widget, state, &mut effects, first_time_widget_view);
if publish_x_ticks.is_some() || publish_y_ticks.is_some() {
invalidation.static_layer();
}
let needs_publish = effects.hover_pick.is_some()
|| effects.drag_event.is_some()
|| effects.cursor_ui.is_some()
|| publish_x_ticks.is_some()
|| publish_y_ticks.is_some()
|| effects.clear_cursor_position
|| effects.publish_camera_bounds;
let camera_bounds = if effects.hover_pick.is_some()
|| publish_x_ticks.is_some()
|| publish_y_ticks.is_some()
|| effects.publish_camera_bounds
{
Some((state.camera, state.bounds))
} else {
None
};
if IS_CANVAS {
invalidation.apply(widget);
}
if needs_publish {
Some(shader::Action::publish(PlotUiMessage::RenderUpdate(
PlotRenderUpdate {
hover_pick: effects.hover_pick,
drag_event: effects.drag_event,
clear_cursor_position: effects.clear_cursor_position,
cursor_position_ui: effects.cursor_ui,
x_ticks: publish_x_ticks,
y_ticks: publish_y_ticks,
camera_bounds: camera_bounds.map(Box::new),
},
)))
} else {
effects.needs_redraw.then(shader::Action::request_redraw)
}
}
fn plot_mouse_interaction(state: &PlotState) -> Interaction {
if state.pan.active {
Interaction::Grabbing
} else if state.selection.active {
Interaction::Crosshair
} else if state.picking.last_hover_cache.is_some() {
Interaction::Pointer
} else {
Interaction::None
}
}
impl shader::Program<PlotUiMessage> for PlotWidget {
type State = PlotState;
type Primitive = Primitive;
fn draw(
&self,
state: &Self::State,
_cursor: mouse::Cursor,
_bounds: Rectangle,
) -> Self::Primitive {
let mut plot_widget = state.clone();
plot_widget.grid_style = self.cached_style().grid;
Primitive {
instance_id: self.instance_id,
plot_widget,
}
}
fn update(
&self,
state: &mut Self::State,
event: &iced::Event,
bounds: Rectangle,
cursor: mouse::Cursor,
) -> Option<shader::Action<PlotUiMessage>> {
update_plot_program::<false>(self, state, event, bounds, cursor)
}
fn mouse_interaction(
&self,
state: &Self::State,
_bounds: Rectangle,
_cursor: mouse::Cursor,
) -> Interaction {
plot_mouse_interaction(state)
}
}
#[cfg(feature = "canvas")]
impl iced::widget::canvas::Program<PlotUiMessage> for PlotWidget {
type State = PlotState;
fn update(
&self,
state: &mut Self::State,
event: &iced::Event,
bounds: Rectangle,
cursor: mouse::Cursor,
) -> Option<iced::widget::canvas::Action<PlotUiMessage>> {
update_plot_program::<true>(self, state, event, bounds, cursor)
}
fn draw(
&self,
state: &Self::State,
renderer: &iced::Renderer,
_theme: &Theme,
bounds: Rectangle,
_cursor: mouse::Cursor,
) -> Vec<iced::widget::canvas::Geometry> {
crate::plot_renderer::canvas::draw(renderer, &self.canvas_caches, state, bounds)
}
fn mouse_interaction(
&self,
state: &Self::State,
_bounds: Rectangle,
_cursor: mouse::Cursor,
) -> Interaction {
plot_mouse_interaction(state)
}
}
#[doc(hidden)]
pub struct PlotRendererState {
renderers: HashMap<u64, PlotRenderer>,
format: TextureFormat,
}
impl shader::Primitive for Primitive {
type Pipeline = PlotRendererState;
fn prepare(
&self,
renderer_state: &mut Self::Pipeline,
device: &iced::wgpu::Device,
queue: &iced::wgpu::Queue,
bounds: &Rectangle,
viewport: &Viewport,
) {
let renderer = renderer_state
.renderers
.entry(self.instance_id)
.or_insert_with(|| PlotRenderer::new(device, queue, renderer_state.format));
renderer.prepare_frame(device, queue, viewport, bounds, &self.plot_widget);
renderer.service_picking(self.instance_id, device, queue, &self.plot_widget);
}
fn render(
&self,
renderer_state: &Self::Pipeline,
encoder: &mut iced::wgpu::CommandEncoder,
target: &iced::wgpu::TextureView,
clip_bounds: &Rectangle<u32>,
) {
if let Some(renderer) = renderer_state.renderers.get(&self.instance_id) {
renderer.encode(RenderParams {
encoder,
target,
clip_bounds,
});
}
}
}
impl PlotWidget {
fn valid_point_id(&self, point_id: &PointId) -> bool {
self.series
.get(&point_id.series_id)
.map(|series| series.pickable && point_id.point_index < series.positions.len())
.unwrap_or(false)
}
}
fn apply_data_aspect(camera: &mut Camera, bounds: &Rectangle, aspect: f64) -> bool {
let width = bounds.width.max(1.0) as f64;
let height = bounds.height.max(1.0) as f64;
let target_half_y = aspect * camera.half_extents.x * (height / width);
if (camera.half_extents.y - target_half_y).abs() > f64::EPSILON {
camera.half_extents.y = target_half_y;
return true;
}
false
}
impl PlotWidget {
pub(crate) fn pick_hit(&self, state: &mut PlotState) -> Option<PointId> {
let PlotState {
picking: pick_state,
cursor_position,
hover_radius_px,
points,
series,
camera,
bounds,
..
} = state;
let force_cpu_picking = self.render_strategy.force_cpu_picking();
pick_state.request_pick_hit(
self.instance_id,
*cursor_position,
*hover_radius_px,
force_cpu_picking,
points.as_ref(),
series.as_ref(),
camera,
bounds,
|pid| self.valid_point_id(pid),
)
}
}
impl Pipeline for PlotRendererState {
fn new(
_device: &iced::wgpu::Device,
_queue: &iced::wgpu::Queue,
format: iced::wgpu::TextureFormat,
) -> Self
where
Self: Sized,
{
PlotRendererState {
renderers: HashMap::new(),
format,
}
}
}
static NEXT_ID: AtomicU64 = AtomicU64::new(1);
#[derive(Debug, Clone, PartialEq)]
pub struct HighlightPoint {
pub x: f64,
pub y: f64,
pub transform: PositionTransform,
pub color: Color,
pub marker_style: Option<MarkerStyle>,
pub mask_padding: Option<f32>,
}
pub struct PositionDisplay<'a> {
pos: f64,
transform: &'a Option<Transform>,
}
impl fmt::Display for PositionDisplay<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(transform) = self.transform
&& transform.uses_axes_coordinates()
{
write!(f, "{:.1}%", self.pos * 100.0)
} else {
write!(f, "{:.2}", self.pos)
}
}
}
impl HighlightPoint {
pub fn resize_marker(&mut self, factor: f64) {
if let Some(marker_style) = &mut self.marker_style {
match &mut marker_style.size {
Size::Pixels(size) => {
*size *= factor as f32;
}
Size::World(size) => {
*size *= factor;
}
}
}
}
pub fn display_x(&self) -> PositionDisplay<'_> {
PositionDisplay {
pos: self.x,
transform: &self.transform.x,
}
}
pub fn display_y(&self) -> PositionDisplay<'_> {
PositionDisplay {
pos: self.y,
transform: &self.transform.y,
}
}
}
pub(crate) fn world_to_screen_position_x(
x: f64,
camera: &Camera,
bounds: &Rectangle,
) -> Option<f32> {
let ndc_x = (x - camera.position.x) / camera.half_extents.x;
let screen_x = (ndc_x as f32 + 1.0) * 0.5 * bounds.width;
if screen_x < 0.0 || screen_x > bounds.width {
None
} else {
Some(screen_x)
}
}
pub(crate) fn world_to_screen_position_y(
y: f64,
camera: &Camera,
bounds: &Rectangle,
) -> Option<f32> {
let ndc_y = (y - camera.position.y) / camera.half_extents.y;
let screen_y = (1.0 - ndc_y as f32) * 0.5 * bounds.height;
if screen_y < 0.0 || screen_y > bounds.height {
None
} else {
Some(screen_y)
}
}