use crate::Precision;
use crate::TEMP_SUFFIX;
use crate::chart::Chart;
use crate::core::context::PanelContext;
use crate::core::layer::{CircleConfig, LineConfig, MarkRenderer, RenderBackend};
use crate::error::ChartonError;
use crate::mark::errorbar::MarkErrorBar;
use crate::visual::color::SingleColor;
impl MarkRenderer for Chart<MarkErrorBar> {
fn render_marks(
&self,
backend: &mut dyn RenderBackend,
context: &PanelContext,
) -> Result<(), ChartonError> {
let ds = &self.data;
if ds.row_count == 0 {
return Ok(());
}
let x_enc = self
.encoding
.x
.as_ref()
.ok_or_else(|| ChartonError::Encoding("X-axis missing".into()))?;
let y_field = self
.encoding
.y
.as_ref()
.map(|y| y.field.as_str())
.ok_or_else(|| ChartonError::Encoding("Y-axis missing".into()))?;
let is_manual_range = self.encoding.y2.is_some();
let (y_min_field, y_max_field) = if let Some(y2) = &self.encoding.y2 {
(y_field.to_string(), y2.field.clone())
} else {
(
format!("{}_{}_min", TEMP_SUFFIX, y_field),
format!("{}_{}_max", TEMP_SUFFIX, y_field),
)
};
let mark_config = self
.mark
.as_ref()
.ok_or_else(|| ChartonError::Mark("MarkErrorBar config missing".into()))?;
let x_scale = context.coord.get_x_scale();
let y_scale = context.coord.get_y_scale();
let x_norms = x_scale
.scale_type()
.normalize_column(x_scale, ds.column(&x_enc.field)?);
let y_min_norms = y_scale
.scale_type()
.normalize_column(y_scale, ds.column(&y_min_field)?);
let y_max_norms = y_scale
.scale_type()
.normalize_column(y_scale, ds.column(&y_max_field)?);
let yc_norms = if !is_manual_range && mark_config.show_center {
Some(
y_scale
.scale_type()
.normalize_column(y_scale, ds.column(y_field)?),
)
} else {
None
};
let color_norms = if let Some(ref mapping) = context.spec.aesthetics.color {
let s_trait = mapping.scale_impl.as_ref();
Some(
s_trait
.scale_type()
.normalize_column(s_trait, ds.column(&mapping.field)?),
)
} else {
None
};
let sub_idx_col = ds.column(&format!("{}_sub_idx", TEMP_SUFFIX)).ok();
let groups_count_col = ds.column(&format!("{}_groups_count", TEMP_SUFFIX)).ok();
let is_flipped = context.coord.is_flipped();
let unit_step_norm = (x_scale.normalize(1.0) - x_scale.normalize(0.0)).abs();
if let (Some(sub_col), Some(cnt_col)) = (sub_idx_col, groups_count_col) {
for (idx, xn_opt) in x_norms.iter().enumerate().take(ds.row_count) {
let Some(xn) = *xn_opt else { continue };
let sub_idx = sub_col.get_f64(idx).unwrap_or(0.0);
let n_groups = cnt_col.get_f64(idx).unwrap_or(1.0);
self.render_errorbar_item(
idx,
xn,
sub_idx,
n_groups,
unit_step_norm,
is_flipped,
&y_min_norms,
&y_max_norms,
&yc_norms,
&color_norms,
mark_config,
context,
backend,
);
}
} else {
let color_field = self.encoding.color.as_ref().map(|c| c.field.as_str());
let grouped_data = ds.group_by(color_field);
let n_groups = grouped_data.groups.len() as f64;
for (group_idx, (_group_key, row_indices)) in grouped_data.groups.iter().enumerate() {
for &idx in row_indices {
let Some(xn) = x_norms[idx] else { continue };
let sub_idx = group_idx as f64;
self.render_errorbar_item(
idx,
xn,
sub_idx,
n_groups,
unit_step_norm,
is_flipped,
&y_min_norms,
&y_max_norms,
&yc_norms,
&color_norms,
mark_config,
context,
backend,
);
}
}
}
Ok(())
}
}
impl Chart<MarkErrorBar> {
#[allow(clippy::too_many_arguments)]
fn render_errorbar_item(
&self,
idx: usize,
xn: f64,
sub_idx: f64,
n_groups: f64,
unit_step_norm: f64,
is_flipped: bool,
y_min_norms: &[Option<f64>],
y_max_norms: &[Option<f64>],
yc_norms: &Option<Vec<Option<f64>>>,
color_norms: &Option<Vec<Option<f64>>>,
mark_config: &MarkErrorBar,
context: &PanelContext,
backend: &mut dyn RenderBackend,
) {
let offset_norm = if n_groups > 1.0 {
let actual_width =
mark_config.span / (n_groups + (n_groups - 1.0) * mark_config.spacing);
let width_norm = actual_width.min(mark_config.width) * unit_step_norm;
let spacing_norm = width_norm * mark_config.spacing;
(sub_idx - (n_groups - 1.0) / 2.0) * (width_norm + spacing_norm)
} else {
0.0
};
let x_final_n = xn + offset_norm;
let mark_color = if let Some(norms) = color_norms {
self.resolve_color_from_value(norms[idx], context, &mark_config.color)
} else {
mark_config.color
};
if let (Some(yn1), Some(yn2)) = (y_min_norms[idx], y_max_norms[idx]) {
let (x_pix1, y_pix1) = context.coord.transform(x_final_n, yn1, &context.panel);
let (x_pix2, y_pix2) = context.coord.transform(x_final_n, yn2, &context.panel);
backend.draw_line(LineConfig {
x1: x_pix1 as Precision,
y1: y_pix1 as Precision,
x2: x_pix2 as Precision,
y2: y_pix2 as Precision,
color: mark_config.color,
width: mark_config.stroke_width as Precision,
opacity: mark_config.opacity as Precision,
dash: vec![],
});
let cap_len = mark_config.cap_length as Precision;
let endpoints = [(x_pix1, y_pix1), (x_pix2, y_pix2)];
for (px, py) in endpoints {
let (x1, y1, x2, y2);
if !is_flipped {
x1 = px as Precision - cap_len;
y1 = py as Precision;
x2 = px as Precision + cap_len;
y2 = py as Precision;
} else {
x1 = px as Precision;
y1 = py as Precision - cap_len;
x2 = px as Precision;
y2 = py as Precision + cap_len;
}
backend.draw_line(LineConfig {
x1,
y1,
x2,
y2,
color: mark_config.color,
width: mark_config.stroke_width as Precision,
opacity: mark_config.opacity as Precision,
dash: vec![],
});
}
}
if let Some(center_norms) = yc_norms
&& let Some(ycn) = center_norms[idx]
{
let (cx, cy) = context.coord.transform(x_final_n, ycn, &context.panel);
backend.draw_circle(CircleConfig {
x: cx as Precision,
y: cy as Precision,
radius: 3.0,
fill: mark_color,
stroke: mark_color,
stroke_width: 0.0,
opacity: mark_config.opacity as Precision,
});
}
}
}
impl Chart<MarkErrorBar> {
fn resolve_color_from_value(
&self,
val: Option<f64>,
context: &PanelContext,
fallback: &SingleColor,
) -> SingleColor {
if let (Some(v), Some(mapping)) = (val, &context.spec.aesthetics.color) {
let s_trait = mapping.scale_impl.as_ref();
s_trait
.mapper()
.as_ref()
.map(|m| m.map_to_color(v, s_trait.logical_max()))
.unwrap_or(*fallback)
} else {
*fallback
}
}
}