use uzor::render::RenderContext;
use crate::coord::PlotArea;
use crate::scale::{BandScale, Scale};
use super::MarkStyle;
pub fn draw_bars(
ctx: &mut dyn RenderContext,
area: &PlotArea,
band: &BandScale,
y: &dyn Scale,
values: &[f64],
style: &MarkStyle,
) {
if band.is_empty() || values.is_empty() {
return;
}
let (y_min, y_max) = y.domain();
let baseline_value = 0.0_f64.clamp(y_min.min(y_max), y_min.max(y_max));
let baseline_px = area.y(y, baseline_value);
ctx.set_fill_color(&style.color);
ctx.set_global_alpha(style.fill_alpha);
for (i, &value) in values.iter().enumerate().take(band.len()) {
let (x0, x1) = area.x_band(band, i);
let value_px = area.y(y, value);
let (top, height) =
if value_px <= baseline_px { (value_px, baseline_px - value_px) } else { (baseline_px, value_px - baseline_px) };
ctx.fill_rect(x0, top, (x1 - x0).max(0.0), height);
}
ctx.set_global_alpha(1.0);
}
const SUB_BAND_PADDING: f64 = 0.15;
pub fn sub_band_range(x0: f64, x1: f64, series_count: usize, series_index: usize) -> (f64, f64) {
if series_count <= 1 {
return (x0, x1);
}
let sub = BandScale::new(vec![String::new(); series_count], SUB_BAND_PADDING);
let (t0, t1) = sub.band_range(series_index);
(x0 + t0 * (x1 - x0), x0 + t1 * (x1 - x0))
}
pub fn draw_bars_grouped(
ctx: &mut dyn RenderContext,
area: &PlotArea,
band: &BandScale,
y: &dyn Scale,
series_values: &[&[f64]],
colors: &[&str],
) {
if band.is_empty() || series_values.is_empty() {
return;
}
let (y_min, y_max) = y.domain();
let baseline_value = 0.0_f64.clamp(y_min.min(y_max), y_min.max(y_max));
let baseline_px = area.y(y, baseline_value);
let series_count = series_values.len();
ctx.set_global_alpha(1.0);
for i in 0..band.len() {
let (x0, x1) = area.x_band(band, i);
for (si, values) in series_values.iter().enumerate() {
let Some(&value) = values.get(i) else { continue };
let (sx0, sx1) = sub_band_range(x0, x1, series_count, si);
let value_px = area.y(y, value);
let (top, height) =
if value_px <= baseline_px { (value_px, baseline_px - value_px) } else { (baseline_px, value_px - baseline_px) };
ctx.set_fill_color(colors.get(si).copied().unwrap_or("#888888"));
ctx.fill_rect(sx0, top, (sx1 - sx0).max(0.0), height);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sub_band_range_single_series_fills_the_whole_band_unchanged() {
assert_eq!(sub_band_range(10.0, 90.0, 1, 0), (10.0, 90.0));
assert_eq!(sub_band_range(10.0, 90.0, 0, 0), (10.0, 90.0));
}
#[test]
fn sub_band_range_divides_band_into_equal_padded_slots() {
let (x0, x1) = (0.0, 100.0);
for i in 0..4 {
let (sx0, sx1) = sub_band_range(x0, x1, 4, i);
let raw_slot = 25.0;
assert!(sx1 - sx0 < raw_slot, "padded sub-band must be narrower than its raw equal slot");
assert!(sx1 - sx0 > 0.0);
}
}
#[test]
fn sub_band_range_slots_do_not_overlap_and_stay_in_stable_order() {
let (x0, x1) = (0.0, 300.0);
let n = 5;
let mut prev_right = x0;
for i in 0..n {
let (sx0, sx1) = sub_band_range(x0, x1, n, i);
assert!(sx0 >= prev_right - 1e-9, "series {i} slot must not start before the previous one ends");
assert!(sx1 <= x1 + 1e-9, "series {i} slot must stay inside the category band");
prev_right = sx1;
}
}
#[test]
fn draw_bars_grouped_ignores_a_series_missing_this_category_index() {
use uzor_export::{render_to_png, ExportSpec};
let spec = ExportSpec { width_px: 300, height_px: 100, dpr: 1.0, background: None };
let result = render_to_png(&spec, |ctx| {
let area = PlotArea::new(uzor::types::Rect::new(0.0, 0.0, 300.0, 100.0));
let band = BandScale::new(vec!["a".to_owned(), "b".to_owned(), "c".to_owned()], 0.2);
let y = crate::scale::LinearScale::new(0.0, 10.0);
let short: Vec<f64> = vec![5.0]; let full: Vec<f64> = vec![2.0, 4.0, 6.0];
draw_bars_grouped(ctx, &area, &band, &y, &[&short, &full], &["#111111", "#222222"]);
});
assert!(result.is_ok(), "a shorter series must be skipped past its missing index, never panic");
}
}