use super::*;
use crate::spec::TimeUnit;
use crate::time;
use serde_json::Value;
fn bucket_rows(rows: &[Row], xf: &str, unit: TimeUnit) -> Result<(Vec<Row>, f64, f64), Error> {
let mut out = Vec::with_capacity(rows.len());
let (mut min_ms, mut max_ms) = (f64::INFINITY, f64::NEG_INFINITY);
for (ri, row) in rows.iter().enumerate() {
let mut r = row.clone();
match row.get(xf) {
Some(v) if !v.is_null() => {
let text = data::text(v);
match time::parse_temporal(&text) {
Some(ms) => {
min_ms = min_ms.min(ms);
max_ms = max_ms.max(ms);
r.insert(xf.to_string(), Value::String(time::bucket_key(ms, unit)));
}
None => return Err(temporal_parse_error(ri, &text, xf)),
}
}
_ => {
r.remove(xf);
}
}
out.push(r);
}
Ok((out, min_ms, max_ms))
}
fn densify(
cats: Vec<String>,
cells: Vec<Vec<Vec<f64>>>,
min_ms: f64,
max_ms: f64,
unit: TimeUnit,
) -> (Vec<String>, Vec<Vec<Vec<f64>>>, Vec<f64>) {
let mut by_key: HashMap<String, Vec<Vec<f64>>> = cats.into_iter().zip(cells).collect();
let (mut dense_cats, mut dense_cells, mut dense_ms) = (Vec::new(), Vec::new(), Vec::new());
let mut ms = time::bucket_start(min_ms, unit);
let last = time::bucket_start(max_ms, unit);
loop {
let key = time::bucket_key(ms, unit);
let cell = by_key.remove(&key).unwrap_or_else(|| vec![Vec::new()]);
dense_cats.push(key);
dense_cells.push(cell);
dense_ms.push(ms);
if ms >= last {
break;
}
ms = time::next_bucket(ms, unit);
}
debug_assert!(
by_key.is_empty(),
"every data bucket lies within [bucket_start(min), bucket_start(max)]"
);
(dense_cats, dense_cells, dense_ms)
}
fn is_grouped(color: Option<&Channel>, category_field: &str) -> bool {
color.is_some_and(|c| c.field != category_field)
}
pub(super) fn compile_bar(
spec: &Spec,
table: &Table,
opts: &CompileOptions,
plot_w: usize,
plot_h: usize,
) -> Result<Scene, Error> {
let rows = &table.rows;
let xf = &spec.encoding.x.field;
let yf = &spec.encoding.y.field;
let agg = spec.encoding.y.aggregate.unwrap_or(Aggregate::Sum);
let theme = &opts.theme;
if spec.encoding.x.aggregate.is_some() {
return Err(bar_aggregate_error("y"));
}
let time_unit = spec.encoding.x.time_unit;
if is_grouped(spec.encoding.color.as_ref(), xf) {
if let Some(unit_color) = spec.encoding.color.as_ref().filter(|_| time_unit.is_some()) {
return Err(timeunit_grouped_error(&unit_color.field));
}
return compile_bar_grouped(spec, table, opts, plot_w, plot_h);
}
let (scan, dense_range) = match time_unit {
Some(unit) => {
let (trows, min_ms, max_ms) = bucket_rows(rows, xf, unit)?;
(scan_bars(&trows, xf, yf, None, agg), Some((min_ms, max_ms)))
}
None => (scan_bars(rows, xf, yf, None, agg), None),
};
let (mut cats, mut cells, dropped) = (scan.cats, scan.cells, scan.dropped);
if cats.is_empty() {
return Err(no_rows_error(yf, xf));
}
let mut dense_ms: Vec<f64> = Vec::new();
if let (Some(unit), Some((min_ms, max_ms))) = (time_unit, dense_range) {
let n = time::bucket_count(min_ms, max_ms, unit);
if n > plot_w {
return Err(timeunit_overflow_error(n, plot_w, unit));
}
(cats, cells, dense_ms) = densify(cats, cells, min_ms, max_ms, unit);
} else if resolved_type(&spec.encoding.x, table) == FieldType::Ordinal {
(cats, cells) = sort_cats(cats, cells);
}
let series_points: Vec<usize> = cells.iter().map(|r| r[0].len()).collect();
let (agg_cells, vmax) = aggregate_cells(&cells, agg, time_unit.is_some())?;
let y = Linear::row_aligned(0.0, vmax, plot_h.clamp(3, 6), plot_h, true);
let categorical = spec.encoding.color.is_some();
let gutter = tick_gutter(&y);
let columns = gutter + 1 + plot_w;
let (title, title_rows) = place_title(spec, gutter, plot_w);
let total_rows = title_rows + plot_h + 2;
let top = title_rows;
let ticks = y_ticks(&y, plot_h, top);
let n = cats.len();
let step = plot_w as f64 / n as f64;
let bar_w = ((step * 0.7).floor() as usize).clamp(1, plot_w);
let label_max = (step.floor() as usize).saturating_sub(1).max(1);
let mut bars: Vec<Bar> = Vec::new();
let mut anchors: Vec<(usize, String)> = Vec::new();
let mut prev_ms: Option<f64> = None;
for (i, cell) in agg_cells.iter().enumerate() {
let center = (i as f64 + 0.5) * step;
if let Some(v) = cell[0] {
let x0 = ((center - bar_w as f64 / 2.0).round().max(0.0) as usize).min(plot_w - bar_w);
let color = if categorical {
theme.series(i)
} else {
theme.grad(y.norm(v))
};
bars.push(Bar {
x0: x0 as f64 / plot_w as f64,
y0: 1.0 - y.norm(v),
w: bar_w as f64 / plot_w as f64,
h: y.norm(v),
color,
});
}
let label = match time_unit {
Some(unit) => {
let ms = dense_ms[i];
let label = time::bucket_display(ms, unit, prev_ms);
prev_ms = Some(ms);
label
}
None => truncate(&cats[i], label_max),
};
anchors.push(((center.round() as usize).min(plot_w - 1), label));
}
let labels = place_x_labels(&anchors, gutter, columns, top + plot_h + 1);
Ok(Scene {
size: Size {
columns,
rows: total_rows,
},
plot: Rect {
x: gutter + 1,
y: top,
w: plot_w,
h: plot_h,
},
chrome: Chrome {
axis: theme.axis,
title: theme.title,
},
title,
legend: Vec::<LegendEntry>::new(),
y_axis: YAxis {
domain: [y.min, y.max],
step: y.step,
categories: None,
ticks,
},
x_axis: XAxis {
categories: Some(cats),
domain: None,
tick_cols: Vec::new(),
labels,
},
marks: vec![SceneMark::Bars {
bars,
direction: BarDirection::Vertical,
}],
dropped_rows: dropped,
source: Source {
mark: Mark::Bar,
x_field: xf.clone(),
y_field: yf.clone(),
aggregate: Some(agg),
x_type: None,
time_unit,
series_points,
data_source: table.provenance.source,
truncated: table.provenance.truncated,
total_rows: table.provenance.total_rows,
},
})
}
fn compile_bar_grouped(
spec: &Spec,
table: &Table,
opts: &CompileOptions,
plot_w: usize,
plot_h: usize,
) -> Result<Scene, Error> {
let rows = &table.rows;
let xf = &spec.encoding.x.field;
let yf = &spec.encoding.y.field;
let cf = &spec
.encoding
.color
.as_ref()
.expect("grouped bars require a color channel")
.field;
let agg = spec.encoding.y.aggregate.unwrap_or(Aggregate::Sum);
let theme = &opts.theme;
let scan = scan_bars(rows, xf, yf, Some(cf), agg);
let (mut cats, mut raw_cells, dropped) = (scan.cats, scan.cells, scan.dropped);
let series_names = scan.series;
if cats.is_empty() {
return Err(no_rows_error(yf, xf));
}
if matches!(
resolved_type(&spec.encoding.x, table),
FieldType::Ordinal | FieldType::Temporal
) {
(cats, raw_cells) = sort_cats(cats, raw_cells);
}
let n_cats = cats.len();
let n_series = series_names.len();
if n_series > theme.palette.len() {
return Err(palette_cap_error(n_series, theme.palette.len(), cf));
}
let req = n_cats * (n_series + 1);
if plot_w < req {
return Err(Error::Data(format!(
"{n_cats} categories × {n_series} series need width ≥ {req}; \
raise --width, or filter/aggregate"
)));
}
let (cells, vmax) = aggregate_cells(&raw_cells, agg, false)?;
let y = Linear::row_aligned(0.0, vmax, plot_h.clamp(3, 6), plot_h, true);
let gutter = tick_gutter(&y);
let columns = gutter + 1 + plot_w;
let (title, title_rows) = place_title(spec, gutter, plot_w);
let top = title_rows;
let ticks = y_ticks(&y, plot_h, top);
let step = plot_w as f64 / n_cats as f64;
let group_w = ((step * 0.7).round() as usize).clamp(n_series, step.floor() as usize - 1);
let bar_w = (group_w / n_series).max(1);
let group_span = bar_w * n_series;
let label_max = (step.floor() as usize).saturating_sub(1).max(1);
let mut bars: Vec<Bar> = Vec::new();
let mut anchors: Vec<(usize, String)> = Vec::new();
for (ci, cell_row) in cells.iter().enumerate() {
let center = (ci as f64 + 0.5) * step;
let group_left =
((center - group_span as f64 / 2.0).round().max(0.0) as usize).min(plot_w - group_span);
for (si, cell) in cell_row.iter().enumerate() {
if let Some(v) = cell {
let x0 = group_left + si * bar_w;
bars.push(Bar {
x0: x0 as f64 / plot_w as f64,
y0: 1.0 - y.norm(*v),
w: bar_w as f64 / plot_w as f64,
h: y.norm(*v),
color: theme.series(si),
});
}
}
anchors.push((
(center.round() as usize).min(plot_w - 1),
truncate(&cats[ci], label_max),
));
}
let (legend, legend_rows) = legend_below(&series_names, theme, gutter, columns, top, plot_h);
let total_rows = top + plot_h + 2 + legend_rows;
let labels = place_x_labels(&anchors, gutter, columns, top + plot_h + 1);
let series_points: Vec<usize> = (0..n_series)
.map(|si| cells.iter().filter(|r| r[si].is_some()).count())
.collect();
Ok(Scene {
size: Size {
columns,
rows: total_rows,
},
plot: Rect {
x: gutter + 1,
y: top,
w: plot_w,
h: plot_h,
},
chrome: Chrome {
axis: theme.axis,
title: theme.title,
},
title,
legend,
y_axis: YAxis {
domain: [y.min, y.max],
step: y.step,
categories: None,
ticks,
},
x_axis: XAxis {
categories: Some(cats),
domain: None,
tick_cols: Vec::new(),
labels,
},
marks: vec![SceneMark::Bars {
bars,
direction: BarDirection::Vertical,
}],
dropped_rows: dropped,
source: Source {
mark: Mark::Bar,
x_field: xf.clone(),
y_field: yf.clone(),
aggregate: Some(agg),
x_type: None,
time_unit: None,
series_points,
data_source: table.provenance.source,
truncated: table.provenance.truncated,
total_rows: table.provenance.total_rows,
},
})
}
pub(super) fn compile_bar_h(
spec: &Spec,
table: &Table,
opts: &CompileOptions,
plot_w: usize,
raw_height: Option<usize>,
) -> Result<Scene, Error> {
let rows = &table.rows;
let xf = &spec.encoding.x.field; let yf = &spec.encoding.y.field; let agg = spec.encoding.x.aggregate.unwrap_or(Aggregate::Sum);
let theme = &opts.theme;
if spec.encoding.y.aggregate.is_some() {
return Err(bar_aggregate_error("x"));
}
if is_grouped(spec.encoding.color.as_ref(), yf) {
return compile_bar_h_grouped(spec, table, opts, plot_w, raw_height);
}
let scan = scan_bars(rows, yf, xf, None, agg);
let (mut cats, mut cells, dropped) = (scan.cats, scan.cells, scan.dropped);
if cats.is_empty() {
return Err(no_rows_error(xf, yf));
}
if matches!(
resolved_type(&spec.encoding.y, table),
FieldType::Ordinal | FieldType::Temporal
) {
(cats, cells) = sort_cats(cats, cells);
}
let series_points: Vec<usize> = cells.iter().map(|r| r[0].len()).collect();
let (agg_cells, vmax) = aggregate_cells(&cells, agg, false)?;
let values: Vec<f64> = agg_cells
.iter()
.map(|r| r[0].expect("plain bars: a scanned category has values"))
.collect();
let xscale = Linear::nice_from(0.0, vmax, (plot_w / 10).clamp(2, 7), true);
let n = cats.len();
let content = n * 2 - 1;
let ceiling = raw_height.unwrap_or(40);
if content > ceiling {
return Err(height_ceiling_error(n, content));
}
let plot_h = content;
let categorical = spec.encoding.color.is_some();
let (names, gutter) = name_gutter(&cats);
let cat_scale = Linear::indices(n);
let columns = gutter + 1 + plot_w;
let (title, title_rows) = place_title(spec, gutter, plot_w);
let total_rows = title_rows + plot_h + 2;
let top = title_rows;
let rows_f = plot_h as f64;
let mut bars: Vec<Bar> = Vec::new();
let mut ticks: Vec<YTick> = Vec::new();
for (i, v) in values.iter().enumerate() {
let plot_row = i * 2;
let color = if categorical {
theme.series(i)
} else {
theme.grad(xscale.norm(*v))
};
bars.push(Bar {
x0: 0.0,
y0: plot_row as f64 / rows_f,
w: xscale.norm(*v),
h: 1.0 / rows_f,
color,
});
ticks.push(YTick {
value: i as f64,
frac: cat_scale.norm(i as f64),
label: names[i].clone(),
row: top + plot_row,
});
}
let (tick_cols, labels) = value_axis_x(&xscale, plot_w, gutter, columns, top + plot_h + 1);
Ok(Scene {
size: Size {
columns,
rows: total_rows,
},
plot: Rect {
x: gutter + 1,
y: top,
w: plot_w,
h: plot_h,
},
chrome: Chrome {
axis: theme.axis,
title: theme.title,
},
title,
legend: Vec::<LegendEntry>::new(),
y_axis: YAxis {
domain: [cat_scale.min, cat_scale.max],
step: cat_scale.step,
categories: Some(cats),
ticks,
},
x_axis: XAxis {
categories: None,
domain: Some([xscale.min, xscale.max]),
tick_cols,
labels,
},
marks: vec![SceneMark::Bars {
bars,
direction: BarDirection::Horizontal,
}],
dropped_rows: dropped,
source: Source {
mark: Mark::Bar,
x_field: xf.clone(),
y_field: yf.clone(),
aggregate: Some(agg),
x_type: None,
time_unit: None,
series_points,
data_source: table.provenance.source,
truncated: table.provenance.truncated,
total_rows: table.provenance.total_rows,
},
})
}
fn compile_bar_h_grouped(
spec: &Spec,
table: &Table,
opts: &CompileOptions,
plot_w: usize,
raw_height: Option<usize>,
) -> Result<Scene, Error> {
let rows = &table.rows;
let xf = &spec.encoding.x.field; let yf = &spec.encoding.y.field; let cf = &spec
.encoding
.color
.as_ref()
.expect("grouped bars require a color channel")
.field;
let agg = spec.encoding.x.aggregate.unwrap_or(Aggregate::Sum);
let theme = &opts.theme;
let scan = scan_bars(rows, yf, xf, Some(cf), agg);
let (mut cats, mut raw_cells, dropped) = (scan.cats, scan.cells, scan.dropped);
let series_names = scan.series;
if cats.is_empty() {
return Err(no_rows_error(xf, yf));
}
if matches!(
resolved_type(&spec.encoding.y, table),
FieldType::Ordinal | FieldType::Temporal
) {
(cats, raw_cells) = sort_cats(cats, raw_cells);
}
let n_cats = cats.len();
let n_series = series_names.len();
if n_series > theme.palette.len() {
return Err(palette_cap_error(n_series, theme.palette.len(), cf));
}
let (cells, vmax) = aggregate_cells(&raw_cells, agg, false)?;
let xscale = Linear::nice_from(0.0, vmax, (plot_w / 10).clamp(2, 7), true);
let content = n_cats * (n_series + 1) - 1;
let ceiling = raw_height.unwrap_or(40);
if content > ceiling {
return Err(height_ceiling_error(n_cats * n_series, content));
}
let plot_h = content;
let (names, gutter) = name_gutter(&cats);
let cat_scale = Linear::indices(n_cats);
let columns = gutter + 1 + plot_w;
let (title, title_rows) = place_title(spec, gutter, plot_w);
let top = title_rows;
let rows_f = plot_h as f64;
let mut bars: Vec<Bar> = Vec::new();
let mut ticks: Vec<YTick> = Vec::new();
for (ci, cell_row) in cells.iter().enumerate() {
let block_start = ci * (n_series + 1);
for (si, cell) in cell_row.iter().enumerate() {
if let Some(v) = cell {
let plot_row = block_start + si;
bars.push(Bar {
x0: 0.0,
y0: plot_row as f64 / rows_f,
w: xscale.norm(*v),
h: 1.0 / rows_f,
color: theme.series(si),
});
}
}
ticks.push(YTick {
value: ci as f64,
frac: cat_scale.norm(ci as f64),
label: names[ci].clone(),
row: top + block_start + (n_series - 1) / 2,
});
}
let (legend, legend_rows) = legend_below(&series_names, theme, gutter, columns, top, plot_h);
let total_rows = title_rows + plot_h + 2 + legend_rows;
let (tick_cols, labels) = value_axis_x(&xscale, plot_w, gutter, columns, top + plot_h + 1);
let series_points: Vec<usize> = (0..n_series)
.map(|si| cells.iter().filter(|r| r[si].is_some()).count())
.collect();
Ok(Scene {
size: Size {
columns,
rows: total_rows,
},
plot: Rect {
x: gutter + 1,
y: top,
w: plot_w,
h: plot_h,
},
chrome: Chrome {
axis: theme.axis,
title: theme.title,
},
title,
legend,
y_axis: YAxis {
domain: [cat_scale.min, cat_scale.max],
step: cat_scale.step,
categories: Some(cats),
ticks,
},
x_axis: XAxis {
categories: None,
domain: Some([xscale.min, xscale.max]),
tick_cols,
labels,
},
marks: vec![SceneMark::Bars {
bars,
direction: BarDirection::Horizontal,
}],
dropped_rows: dropped,
source: Source {
mark: Mark::Bar,
x_field: xf.clone(),
y_field: yf.clone(),
aggregate: Some(agg),
x_type: None,
time_unit: None,
series_points,
data_source: table.provenance.source,
truncated: table.provenance.truncated,
total_rows: table.provenance.total_rows,
},
})
}