use super::*;
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"));
}
if is_grouped(spec.encoding.color.as_ref(), xf) {
return compile_bar_grouped(spec, table, opts, plot_w, plot_h);
}
let scan = scan_bars(rows, xf, yf, None, agg);
let (mut cats, mut cells, dropped) = (scan.cats, scan.cells, scan.dropped);
if cats.is_empty() {
return Err(no_rows_error(yf, xf));
}
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)?;
let values: Vec<f64> = agg_cells
.iter()
.map(|r| r[0].expect("plain bars: a scanned category has values"))
.collect();
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();
for (i, v) in values.iter().enumerate() {
let center = (i as f64 + 0.5) * step;
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,
});
anchors.push((
(center.round() as usize).min(plot_w - 1),
truncate(&cats[i], label_max),
));
}
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),
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 resolved_type(&spec.encoding.x, table) == FieldType::Ordinal {
(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)?;
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),
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 resolved_type(&spec.encoding.y, 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)?;
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),
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 resolved_type(&spec.encoding.y, table) == FieldType::Ordinal {
(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)?;
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),
series_points,
data_source: table.provenance.source,
truncated: table.provenance.truncated,
total_rows: table.provenance.total_rows,
},
})
}