use super::*;
impl Plot {
pub(super) fn calculate_total_points(&self) -> usize {
Self::calculate_total_points_for_series(&self.series_mgr.series)
}
pub(super) fn calculate_total_points_for_series(series_list: &[PlotSeries]) -> usize {
series_list
.iter()
.map(|series| match &series.series_type {
SeriesType::Line { x_data, .. }
| SeriesType::Scatter { x_data, .. }
| SeriesType::ErrorBars { x_data, .. }
| SeriesType::ErrorBarsXY { x_data, .. } => x_data.len(),
SeriesType::Bar { categories, .. } => categories.len(),
SeriesType::Histogram { data, .. } => data.len(),
SeriesType::BoxPlot { data, .. } => data.len(),
SeriesType::Heatmap { data } => data.n_rows * data.n_cols,
SeriesType::Kde { data } => data.x.len(),
SeriesType::Ecdf { data } => data.x.len(),
SeriesType::Violin { data } => data.data.len(),
SeriesType::Boxen { data } => data.boxes.len() * 4, SeriesType::Contour { data } => data.x.len() * data.y.len(),
SeriesType::Pie { data } => data.values.len(),
SeriesType::Radar { data } => data.series.iter().map(|s| s.values.len()).sum(),
SeriesType::Polar { data } => data.points.len(),
})
.sum()
}
pub(super) fn should_auto_use_datashader(
series_list: &[PlotSeries],
total_points: usize,
) -> bool {
DataShader::should_activate(total_points)
&& series_list
.iter()
.all(Self::series_supports_auto_datashader)
}
pub(super) fn series_supports_auto_datashader(series: &PlotSeries) -> bool {
matches!(
series.series_type,
SeriesType::Scatter { .. } | SeriesType::Histogram { .. }
)
}
pub(super) fn is_non_cartesian_series(series: &PlotSeries) -> bool {
matches!(
series.series_type,
SeriesType::Pie { .. } | SeriesType::Radar { .. } | SeriesType::Polar { .. }
)
}
pub(super) fn is_cartesian_series(series: &PlotSeries) -> bool {
!Self::is_non_cartesian_series(series)
}
pub(super) fn has_cartesian_series(series_list: &[PlotSeries]) -> bool {
series_list.iter().any(Self::is_cartesian_series)
}
pub(super) fn has_non_cartesian_series(series_list: &[PlotSeries]) -> bool {
series_list.iter().any(Self::is_non_cartesian_series)
}
pub(super) fn has_mixed_coordinate_series(series_list: &[PlotSeries]) -> bool {
Self::has_cartesian_series(series_list) && Self::has_non_cartesian_series(series_list)
}
pub(super) fn needs_cartesian_axes_for_series(series_list: &[PlotSeries]) -> bool {
series_list.is_empty() || Self::has_cartesian_series(series_list)
}
pub(super) fn empty_cartesian_bounds(&self) -> (f64, f64, f64, f64) {
self.apply_manual_axis_limits((0.0, 1.0, 0.0, 1.0))
}
pub(super) fn effective_main_panel_bounds_for_series(
&self,
series_list: &[PlotSeries],
) -> Result<(f64, f64, f64, f64)> {
if series_list.is_empty() {
return Ok(self.empty_cartesian_bounds());
}
if Self::has_mixed_coordinate_series(series_list) {
let cartesian_series: Vec<PlotSeries> = series_list
.iter()
.filter(|series| Self::is_cartesian_series(series))
.cloned()
.collect();
self.effective_data_bounds_for_series(&cartesian_series)
} else {
self.effective_data_bounds_for_series(series_list)
}
}
pub(super) fn raw_bounds_for_single_series(
&self,
series: &PlotSeries,
) -> Result<(f64, f64, f64, f64)> {
self.calculate_data_bounds_for_series(std::slice::from_ref(series))
}
pub(super) fn clamp_inset_rect(
plot_area: tiny_skia::Rect,
x: f32,
y: f32,
width: f32,
height: f32,
) -> Result<tiny_skia::Rect> {
let clamped_width = width.max(1.0).min(plot_area.width());
let clamped_height = height.max(1.0).min(plot_area.height());
let max_x = (plot_area.x() + plot_area.width() - clamped_width).max(plot_area.x());
let max_y = (plot_area.y() + plot_area.height() - clamped_height).max(plot_area.y());
let clamped_x = x.clamp(plot_area.x(), max_x);
let clamped_y = y.clamp(plot_area.y(), max_y);
tiny_skia::Rect::from_ltrb(
clamped_x,
clamped_y,
clamped_x + clamped_width,
clamped_y + clamped_height,
)
.ok_or(PlottingError::InvalidData {
message: "Invalid inset plot area".to_string(),
position: None,
})
}
pub(super) fn explicit_inset_rect(
plot_area: tiny_skia::Rect,
layout: InsetLayout,
render_scale: RenderScale,
) -> Result<tiny_skia::Rect> {
let layout = layout.normalized();
let margin_px = render_scale.points_to_pixels(layout.margin_pt);
let width_px = plot_area.width() * layout.width_frac;
let height_px = plot_area.height() * layout.height_frac;
let left = plot_area.x();
let top = plot_area.y();
let right = plot_area.x() + plot_area.width();
let bottom = plot_area.y() + plot_area.height();
let (x, y) = match layout.anchor {
InsetAnchor::Auto => (right - width_px - margin_px, top + margin_px),
InsetAnchor::TopLeft => (left + margin_px, top + margin_px),
InsetAnchor::TopRight => (right - width_px - margin_px, top + margin_px),
InsetAnchor::BottomLeft => (left + margin_px, bottom - height_px - margin_px),
InsetAnchor::BottomRight => {
(right - width_px - margin_px, bottom - height_px - margin_px)
}
InsetAnchor::TopCenter => {
(left + (plot_area.width() - width_px) * 0.5, top + margin_px)
}
InsetAnchor::BottomCenter => (
left + (plot_area.width() - width_px) * 0.5,
bottom - height_px - margin_px,
),
InsetAnchor::CenterLeft => (
left + margin_px,
top + (plot_area.height() - height_px) * 0.5,
),
InsetAnchor::CenterRight => (
right - width_px - margin_px,
top + (plot_area.height() - height_px) * 0.5,
),
InsetAnchor::Center => (
left + (plot_area.width() - width_px) * 0.5,
top + (plot_area.height() - height_px) * 0.5,
),
InsetAnchor::Custom { x_frac, y_frac } => (
left + x_frac.clamp(0.0, 1.0) * plot_area.width() - width_px * 0.5,
top + y_frac.clamp(0.0, 1.0) * plot_area.height() - height_px * 0.5,
),
};
Self::clamp_inset_rect(plot_area, x, y, width_px, height_px)
}
pub(super) fn inset_rects_for_series(
&self,
series_list: &[PlotSeries],
plot_area: tiny_skia::Rect,
render_scale: RenderScale,
) -> Result<Vec<Option<tiny_skia::Rect>>> {
let mut rects = vec![None; series_list.len()];
if !Self::has_mixed_coordinate_series(series_list) {
return Ok(rects);
}
let mut auto_series = Vec::new();
let mut auto_cell_height = 0.0_f32;
let mut auto_gap = 0.0_f32;
for (idx, series) in series_list.iter().enumerate() {
if !Self::is_non_cartesian_series(series) {
continue;
}
let layout = series.inset_layout.unwrap_or_default().normalized();
if matches!(layout.anchor, InsetAnchor::Auto) {
let width_px = plot_area.width() * layout.width_frac;
let height_px = plot_area.height() * layout.height_frac;
auto_cell_height = auto_cell_height.max(height_px);
auto_gap = auto_gap.max(render_scale.points_to_pixels(layout.margin_pt));
auto_series.push((idx, layout, width_px, height_px));
} else {
rects[idx] = Some(Self::explicit_inset_rect(plot_area, layout, render_scale)?);
}
}
if auto_series.is_empty() {
return Ok(rects);
}
let cols = if auto_series.len() <= 1 { 1 } else { 2 };
let gap = auto_gap.max(4.0);
for (row, row_series) in auto_series.chunks(cols).enumerate() {
let x = plot_area.x() + plot_area.width() - gap;
let y = plot_area.y() + gap + row as f32 * (auto_cell_height + gap);
let mut right_edge = x;
for (idx, _layout, width_px, height_px) in row_series.iter().copied() {
let inset_x = right_edge - width_px;
rects[idx] = Some(Self::clamp_inset_rect(
plot_area, inset_x, y, width_px, height_px,
)?);
right_edge = inset_x - gap;
}
}
Ok(rects)
}
pub(super) fn radar_plot_area(
plot_area: tiny_skia::Rect,
x_min: f64,
x_max: f64,
y_min: f64,
y_max: f64,
) -> crate::plots::PlotArea {
let size = plot_area.width().min(plot_area.height());
let x_offset = (plot_area.width() - size) * 0.5;
let y_offset = (plot_area.height() - size) * 0.5;
let title_clearance = size * 0.20;
let adjusted_size = (size - title_clearance).max(1.0);
crate::plots::PlotArea::new(
plot_area.x() + x_offset,
plot_area.y() + y_offset + title_clearance,
adjusted_size,
adjusted_size,
x_min,
x_max,
y_min,
y_max,
)
}
pub(super) fn render_series_collection_normal(
&self,
series_list: &[PlotSeries],
renderer: &mut SkiaRenderer,
plot_area: tiny_skia::Rect,
x_min: f64,
x_max: f64,
y_min: f64,
y_max: f64,
render_scale: RenderScale,
) -> Result<()> {
let inset_rects = self.inset_rects_for_series(series_list, plot_area, render_scale)?;
for (idx, series) in series_list.iter().enumerate() {
let (series_area, series_bounds) = if let Some(inset_rect) = inset_rects[idx] {
(inset_rect, self.raw_bounds_for_single_series(series)?)
} else {
(plot_area, (x_min, x_max, y_min, y_max))
};
self.render_series_normal(
series,
renderer,
series_area,
series_bounds.0,
series_bounds.1,
series_bounds.2,
series_bounds.3,
)?;
}
Ok(())
}
pub(super) fn render_series_svg(
&self,
svg: &mut crate::export::SvgRenderer,
series: &PlotSeries,
default_color: Color,
plot_area: tiny_skia::Rect,
x_min: f64,
x_max: f64,
y_min: f64,
y_max: f64,
) -> Result<()> {
let color = series.color.unwrap_or(default_color);
let render_scale = self.render_scale();
let line_width = render_scale
.points_to_pixels(series.line_width.unwrap_or(self.display.theme.line_width));
let line_style = series.line_style.clone().unwrap_or(LineStyle::Solid);
match &series.series_type {
SeriesType::Line { x_data, y_data } => {
let x_data = x_data.resolve(0.0);
let y_data = y_data.resolve(0.0);
let points: Vec<(f32, f32)> = x_data
.iter()
.zip(y_data.iter())
.map(|(&x, &y)| {
crate::render::skia::map_data_to_pixels(
x, y, x_min, x_max, y_min, y_max, plot_area,
)
})
.collect();
svg.draw_polyline(&points, color, line_width, line_style);
if let Some(marker_style) = series.marker_style {
let marker_size =
render_scale.points_to_pixels(series.marker_size.unwrap_or(6.0));
for &(px, py) in &points {
svg.draw_marker(px, py, marker_size, marker_style, color);
}
}
}
SeriesType::Scatter { x_data, y_data } => {
let x_data = x_data.resolve(0.0);
let y_data = y_data.resolve(0.0);
let marker_style = series.marker_style.unwrap_or(MarkerStyle::Circle);
let marker_size = render_scale.points_to_pixels(series.marker_size.unwrap_or(6.0));
for (&x, &y) in x_data.iter().zip(y_data.iter()) {
let (px, py) = crate::render::skia::map_data_to_pixels(
x, y, x_min, x_max, y_min, y_max, plot_area,
);
svg.draw_marker(px, py, marker_size, marker_style, color);
}
}
SeriesType::Bar { categories, values } => {
let values = values.resolve(0.0);
let num_bars = categories.len();
let bar_width = plot_area.width() / num_bars as f32 * 0.7;
for (i, &value) in values.iter().enumerate() {
let bar_x = plot_area.x()
+ (i as f32 + 0.5) * (plot_area.width() / num_bars as f32)
- bar_width / 2.0;
let (_, py) = crate::render::skia::map_data_to_pixels(
0.0, value, x_min, x_max, y_min, y_max, plot_area,
);
let (_, py_zero) = crate::render::skia::map_data_to_pixels(
0.0, 0.0, x_min, x_max, y_min, y_max, plot_area,
);
let bar_height = (py - py_zero).abs();
let bar_y = py.min(py_zero);
svg.draw_rectangle(bar_x, bar_y, bar_width, bar_height, color, true);
}
}
SeriesType::Pie { data } => {
self.render_pie_series_svg(svg, data, plot_area)?;
}
SeriesType::Radar { data } => {
self.render_radar_series_svg(svg, data, plot_area)?;
}
SeriesType::Polar { data } => {
self.render_polar_series_svg(
svg, data, plot_area, x_min, x_max, y_min, y_max, color,
)?;
}
SeriesType::Histogram { .. } => {}
_ => {}
}
Ok(())
}
pub(super) fn render_pie_series_svg(
&self,
svg: &mut crate::export::SvgRenderer,
data: &crate::plots::composition::pie::PieData,
plot_area: tiny_skia::Rect,
) -> Result<()> {
if data.wedges.is_empty() {
return Ok(());
}
let size = plot_area.width().min(plot_area.height());
let cx = plot_area.x() + plot_area.width() * 0.5;
let cy = plot_area.y() + plot_area.height() * 0.5;
let radius = size * 0.45;
let screen_data = crate::plots::composition::pie::PieData::from_values(
&data.values,
cx as f64,
cy as f64,
radius as f64,
&data.config,
);
let colors = if let Some(ref colors) = data.config.colors {
colors.clone()
} else {
let palette = self.display.theme.color_palette.clone();
(0..screen_data.wedges.len())
.map(|i| palette[i % palette.len()])
.collect()
};
let segments = 64;
if data.config.shadow > 0.0 {
let shadow_color = Color::new(100, 100, 100).with_alpha(0.3);
let offset = data.config.shadow;
for wedge in &screen_data.wedges {
let polygon: Vec<(f32, f32)> = wedge
.as_polygon(segments)
.iter()
.map(|(x, y)| ((x + offset) as f32, (y + offset) as f32))
.collect();
svg.draw_filled_polygon(&polygon, shadow_color);
}
}
for (idx, wedge) in screen_data.wedges.iter().enumerate() {
let polygon: Vec<(f32, f32)> = wedge
.as_polygon(segments)
.iter()
.map(|(x, y)| (*x as f32, *y as f32))
.collect();
svg.draw_filled_polygon(&polygon, colors[idx % colors.len()]);
if let Some(edge_color) = data.config.edge_color {
let scaled_edge_width = svg.render_scale().points_to_pixels(data.config.edge_width);
svg.draw_polygon_outline(&polygon, edge_color, scaled_edge_width);
}
}
if data.config.show_labels || data.config.show_percentages || data.config.show_values {
for (idx, wedge) in screen_data.wedges.iter().enumerate() {
let label_parts: Vec<String> = [
if data.config.show_labels && idx < data.config.labels.len() {
Some(data.config.labels[idx].clone())
} else {
None
},
if data.config.show_percentages {
Some(format!("{:.1}%", screen_data.percentages[idx]))
} else {
None
},
if data.config.show_values {
Some(format!("{:.1}", screen_data.values[idx]))
} else {
None
},
]
.into_iter()
.flatten()
.collect();
if !label_parts.is_empty() {
let label = label_parts.join("\n");
let label_r = if data.config.inner_radius > 0.0 {
radius as f64 * (1.0 + data.config.inner_radius) / 2.0
* data.config.label_distance
} else {
radius as f64 * data.config.label_distance
};
let mid_angle = (wedge.start_angle + wedge.end_angle) / 2.0;
let label_x = cx as f64 + label_r * mid_angle.cos();
let label_y = cy as f64 + label_r * mid_angle.sin();
svg.draw_text_centered(
&label,
label_x as f32,
label_y as f32,
data.config.label_font_size,
data.config.text_color,
)?;
}
}
}
Ok(())
}
pub(super) fn render_radar_series_svg(
&self,
svg: &mut crate::export::SvgRenderer,
data: &crate::plots::polar::radar::RadarPlotData,
plot_area: tiny_skia::Rect,
) -> Result<()> {
if data.series.is_empty() {
return Ok(());
}
let area = Self::radar_plot_area(plot_area, -1.25, 1.25, -1.25, 1.25);
if data.config.show_grid {
let grid_color = self.display.theme.grid_color;
let grid_line_width = svg.render_scale().logical_pixels_to_pixels(0.5);
for ring in &data.grid_rings {
if ring.len() < 2 {
continue;
}
for idx in 0..ring.len() {
let (x1, y1) = ring[idx];
let (x2, y2) = ring[(idx + 1) % ring.len()];
let (sx1, sy1) = area.data_to_screen(x1, y1);
let (sx2, sy2) = area.data_to_screen(x2, y2);
svg.draw_line(
sx1,
sy1,
sx2,
sy2,
grid_color,
grid_line_width,
LineStyle::Solid,
);
}
}
for &((x1, y1), (x2, y2)) in &data.axes {
let (sx1, sy1) = area.data_to_screen(x1, y1);
let (sx2, sy2) = area.data_to_screen(x2, y2);
svg.draw_line(
sx1,
sy1,
sx2,
sy2,
grid_color,
grid_line_width,
LineStyle::Solid,
);
}
}
if data.config.show_axis_labels {
for (label, x, y) in &data.axis_labels {
let (sx, sy) = area.data_to_screen(*x, *y);
svg.draw_text_centered(
label,
sx,
sy,
data.config.label_font_size,
self.display.theme.foreground,
)?;
}
}
let scaled_line_width = svg.render_scale().points_to_pixels(data.config.line_width);
let scaled_marker_size = svg.render_scale().points_to_pixels(data.config.marker_size);
for (series_idx, series_data) in data.series.iter().enumerate() {
let series_color = data
.config
.colors
.as_ref()
.and_then(|colors| colors.get(series_idx).copied())
.unwrap_or_else(|| self.display.theme.get_color(series_idx));
if data.config.fill && !series_data.polygon.is_empty() {
let polygon: Vec<(f32, f32)> = series_data
.polygon
.iter()
.map(|(x, y)| area.data_to_screen(*x, *y))
.collect();
svg.draw_filled_polygon(&polygon, series_color.with_alpha(data.config.fill_alpha));
}
if series_data.polygon.len() > 1 {
let polygon: Vec<(f32, f32)> = series_data
.polygon
.iter()
.map(|(x, y)| area.data_to_screen(*x, *y))
.collect();
svg.draw_polygon_outline(&polygon, series_color, scaled_line_width);
}
if data.config.marker_size > 0.0 {
for (x, y) in &series_data.markers {
let (sx, sy) = area.data_to_screen(*x, *y);
svg.draw_marker(
sx,
sy,
scaled_marker_size,
MarkerStyle::Circle,
series_color,
);
}
}
}
Ok(())
}
pub(super) fn render_polar_series_svg(
&self,
svg: &mut crate::export::SvgRenderer,
data: &crate::plots::polar::polar_plot::PolarPlotData,
plot_area: tiny_skia::Rect,
x_min: f64,
x_max: f64,
y_min: f64,
y_max: f64,
default_color: Color,
) -> Result<()> {
if data.points.is_empty() {
return Ok(());
}
let size = plot_area.width().min(plot_area.height());
let x_offset = (plot_area.width() - size) * 0.5;
let y_offset = (plot_area.height() - size) * 0.5;
let area = crate::plots::PlotArea::new(
plot_area.x() + x_offset,
plot_area.y() + y_offset,
size,
size,
x_min,
x_max,
y_min,
y_max,
);
let line_color = data.config.color.unwrap_or(default_color);
if data.config.fill && !data.fill_polygon.is_empty() {
let polygon: Vec<(f32, f32)> = data
.fill_polygon
.iter()
.map(|(x, y)| area.data_to_screen(*x, *y))
.collect();
svg.draw_filled_polygon(&polygon, line_color.with_alpha(data.config.fill_alpha));
}
if data.points.len() > 1 {
let points: Vec<(f32, f32)> = data
.points
.iter()
.map(|point| area.data_to_screen(point.x, point.y))
.collect();
let scaled_line_width = svg.render_scale().points_to_pixels(data.config.line_width);
svg.draw_polyline(&points, line_color, scaled_line_width, LineStyle::Solid);
}
if data.config.marker_size > 0.0 {
let scaled_marker_size = svg.render_scale().points_to_pixels(data.config.marker_size);
for point in &data.points {
let (sx, sy) = area.data_to_screen(point.x, point.y);
svg.draw_marker(sx, sy, scaled_marker_size, MarkerStyle::Circle, line_color);
}
}
for label in &data.theta_labels {
let (sx, sy) = area.data_to_screen(label.x, label.y);
svg.draw_text_centered(
&label.text,
sx,
sy,
data.config.label_font_size,
self.display.theme.foreground,
)?;
}
for label in &data.r_labels {
let (sx, sy) = area.data_to_screen(label.x, label.y);
svg.draw_text_centered(
&label.text,
sx,
sy,
data.config.label_font_size,
self.display.theme.foreground,
)?;
}
Ok(())
}
pub(super) fn needs_cartesian_axes(&self) -> bool {
Self::needs_cartesian_axes_for_series(&self.series_mgr.series)
}
pub(super) fn apply_manual_axis_limits(
&self,
bounds: (f64, f64, f64, f64),
) -> (f64, f64, f64, f64) {
let (mut x_min, mut x_max, mut y_min, mut y_max) = bounds;
if let Some((x_min_manual, x_max_manual)) = self.layout.x_limits {
x_min = x_min_manual;
x_max = x_max_manual;
}
if let Some((y_min_manual, y_max_manual)) = self.layout.y_limits {
y_min = y_min_manual;
y_max = y_max_manual;
}
if (x_max - x_min).abs() < f64::EPSILON {
x_min -= 1.0;
x_max += 1.0;
}
if (y_max - y_min).abs() < f64::EPSILON {
y_min -= 1.0;
y_max += 1.0;
}
(x_min, x_max, y_min, y_max)
}
pub(super) fn effective_data_bounds(&self) -> Result<(f64, f64, f64, f64)> {
if self.series_mgr.series.is_empty() {
return Ok(self.empty_cartesian_bounds());
}
self.calculate_data_bounds()
.map(|bounds| self.apply_manual_axis_limits(bounds))
}
pub(super) fn effective_data_bounds_for_series(
&self,
series_list: &[PlotSeries],
) -> Result<(f64, f64, f64, f64)> {
if series_list.is_empty() {
return Ok(self.empty_cartesian_bounds());
}
self.calculate_data_bounds_for_series(series_list)
.map(|bounds| self.apply_manual_axis_limits(bounds))
}
pub(super) fn effective_data_bounds_from_resolved(
&self,
resolved_series: &[ResolvedSeries<'_>],
) -> Result<(f64, f64, f64, f64)> {
if resolved_series.is_empty() {
return Ok(self.empty_cartesian_bounds());
}
self.calculate_data_bounds_from_resolved(resolved_series)
.map(|bounds| self.apply_manual_axis_limits(bounds))
}
pub(super) fn apply_auto_padding_to_bounds(
&self,
bounds: (f64, f64, f64, f64),
fraction: f64,
) -> (f64, f64, f64, f64) {
let (mut x_min, mut x_max, mut y_min, mut y_max) = bounds;
if self.layout.x_limits.is_none() {
let x_range = x_max - x_min;
x_min -= x_range * fraction;
x_max += x_range * fraction;
}
if self.layout.y_limits.is_none() {
let y_range = y_max - y_min;
y_min -= y_range * fraction;
y_max += y_range * fraction;
}
self.apply_manual_axis_limits((x_min, x_max, y_min, y_max))
}
#[allow(clippy::too_many_arguments)]
pub(super) fn render_attached_error_bars(
renderer: &mut SkiaRenderer,
x_data: &[f64],
y_data: &[f64],
y_errors: Option<&ErrorValues>,
x_errors: Option<&ErrorValues>,
error_config: Option<&ErrorBarConfig>,
series_color: Color,
x_min: f64,
x_max: f64,
y_min: f64,
y_max: f64,
plot_area: tiny_skia::Rect,
default_line_width: f32,
render_scale: RenderScale,
) -> Result<()> {
let config = error_config.cloned().unwrap_or_default();
let bar_color = config
.color
.unwrap_or(series_color)
.with_alpha(config.alpha);
let scaled_config_width = render_scale.logical_pixels_to_pixels(config.line_width);
let line_width = scaled_config_width.max(default_line_width * 0.75);
let cap_size_px = render_scale.logical_pixels_to_pixels(config.cap_size);
let half_cap = cap_size_px / 2.0;
let n = x_data.len().min(y_data.len());
for i in 0..n {
let x_val = x_data[i];
let y_val = y_data[i];
if !x_val.is_finite() || !y_val.is_finite() {
continue;
}
let (y_lower, y_upper) = if let Some(yerr) = y_errors {
if let Some((lo, hi)) = yerr.bounds_at(i) {
let lo_abs = lo.abs();
let hi_abs = hi.abs();
if lo_abs.is_finite() && hi_abs.is_finite() {
(lo_abs, hi_abs)
} else {
(0.0, 0.0) }
} else {
(0.0, 0.0)
}
} else {
(0.0, 0.0)
};
let (x_lower, x_upper) = if let Some(xerr) = x_errors {
if let Some((lo, hi)) = xerr.bounds_at(i) {
let lo_abs = lo.abs();
let hi_abs = hi.abs();
if lo_abs.is_finite() && hi_abs.is_finite() {
(lo_abs, hi_abs)
} else {
(0.0, 0.0) }
} else {
(0.0, 0.0)
}
} else {
(0.0, 0.0)
};
if y_lower > 0.0 || y_upper > 0.0 {
let (px, py_top_raw) = map_data_to_pixels(
x_val,
y_val + y_upper,
x_min,
x_max,
y_min,
y_max,
plot_area,
);
let (_, py_bottom_raw) = map_data_to_pixels(
x_val,
y_val - y_lower,
x_min,
x_max,
y_min,
y_max,
plot_area,
);
let plot_top = plot_area.y();
let plot_bottom = plot_area.y() + plot_area.height();
let plot_left = plot_area.x();
let plot_right = plot_area.x() + plot_area.width();
let py_top = py_top_raw.max(plot_top).min(plot_bottom);
let py_bottom = py_bottom_raw.max(plot_top).min(plot_bottom);
if (py_bottom - py_top).abs() > 0.5 {
renderer.draw_line(
px,
py_top,
px,
py_bottom,
bar_color,
line_width,
LineStyle::Solid,
)?;
let cap_left_x = (px - half_cap).max(plot_left);
let cap_right_x = (px + half_cap).min(plot_right);
if py_top_raw >= plot_top && py_top_raw <= plot_bottom {
renderer.draw_line(
cap_left_x,
py_top,
cap_right_x,
py_top,
bar_color,
line_width,
LineStyle::Solid,
)?;
}
if py_bottom_raw >= plot_top && py_bottom_raw <= plot_bottom {
renderer.draw_line(
cap_left_x,
py_bottom,
cap_right_x,
py_bottom,
bar_color,
line_width,
LineStyle::Solid,
)?;
}
}
}
if x_lower > 0.0 || x_upper > 0.0 {
let (_, py) =
map_data_to_pixels(x_val, y_val, x_min, x_max, y_min, y_max, plot_area);
let (px_left_raw, _) = map_data_to_pixels(
x_val - x_lower,
y_val,
x_min,
x_max,
y_min,
y_max,
plot_area,
);
let (px_right_raw, _) = map_data_to_pixels(
x_val + x_upper,
y_val,
x_min,
x_max,
y_min,
y_max,
plot_area,
);
let plot_top = plot_area.y();
let plot_bottom = plot_area.y() + plot_area.height();
let plot_left = plot_area.x();
let plot_right = plot_area.x() + plot_area.width();
let px_left = px_left_raw.max(plot_left).min(plot_right);
let px_right = px_right_raw.max(plot_left).min(plot_right);
if (px_right - px_left).abs() > 0.5 {
renderer.draw_line(
px_left,
py,
px_right,
py,
bar_color,
line_width,
LineStyle::Solid,
)?;
let cap_top_y = (py - half_cap).max(plot_top);
let cap_bottom_y = (py + half_cap).min(plot_bottom);
if px_left_raw >= plot_left && px_left_raw <= plot_right {
renderer.draw_line(
px_left,
cap_top_y,
px_left,
cap_bottom_y,
bar_color,
line_width,
LineStyle::Solid,
)?;
}
if px_right_raw >= plot_left && px_right_raw <= plot_right {
renderer.draw_line(
px_right,
cap_top_y,
px_right,
cap_bottom_y,
bar_color,
line_width,
LineStyle::Solid,
)?;
}
}
}
}
Ok(())
}
}