use std::collections::HashMap;
use hephaestus::plot::{scale, AspectMode, Plot as HPlot, PlotComposition};
use hephaestus::scales::chrome::AxisSide;
use hephaestus::shape::ShapeRegistry;
use super::projection::apply_projection;
use super::scales::build_scale;
use super::wiring::Ctx;
use super::{channels, facet, geom, projection, scales, wiring};
use crate::naming;
use crate::plot::layer::geom::GeomType;
use crate::plot::layer::is_transposed;
use crate::plot::ParameterValue;
use crate::{DataFrame, GgsqlError, Layer, Plot, Result};
const MAP_PADDING: f64 = 0.1;
pub fn validate_plot(spec: &Plot) -> Result<()> {
if spec.layers.is_empty() {
return Err(GgsqlError::WriterError(
"a plot needs at least one layer".into(),
));
}
for layer in &spec.layers {
let geom_type = layer.geom.geom_type();
if !geom::is_supported(geom_type) {
return Err(GgsqlError::WriterError(format!(
"the plot renderer does not support the '{geom_type}' geom yet"
)));
}
}
Ok(())
}
pub fn prepare(spec: &Plot, data: &HashMap<String, DataFrame>) -> Result<PlotComposition> {
validate_plot(spec)?;
build_composition(spec, data)
}
pub fn build_composition(
spec: &Plot,
data: &HashMap<String, DataFrame>,
) -> Result<PlotComposition> {
let (composition, panels) = facet::build_panels(spec, data)?;
let mut view = PlotComposition::new(&composition)
.shape_registry(ShapeRegistry::with_builtins())
.theme(wiring::ggsql_theme());
if let Some(text) = wiring::plot_label(spec, "title") {
view = view.title(text);
}
if let Some(text) = wiring::plot_label(spec, "subtitle") {
view = view.subtitle(text);
}
if let Some(text) = wiring::plot_label(spec, "caption") {
view = view.caption(text);
}
for (side, text) in projection::composition_axis_titles(spec) {
view = view.axis_title(side, text);
}
for scale in &spec.scales {
let kind = match scale.aesthetic.as_str() {
"fill" | "stroke" => scales::RangeKind::Color,
"shape" => scales::RangeKind::Shape,
"linetype" => scales::RangeKind::Linetype,
"typeface" => scales::RangeKind::Text,
"fontweight" => scales::RangeKind::FontWeight,
"italic" => scales::RangeKind::Bool,
_ => {
if scale.aesthetic.starts_with("pos") {
scales::RangeKind::Position
} else {
scales::RangeKind::Number
}
}
};
if let Some(hs) = build_scale(scale, kind) {
view.insert_scale(scale.aesthetic.clone(), hs);
}
}
let map_bbox = map_bbox(spec, data)?;
if let Some((xmin, ymin, xmax, ymax)) = map_bbox {
view.insert_scale("pos1".to_string(), scale::continuous(map_range(xmin, xmax)));
view.insert_scale("pos2".to_string(), scale::continuous(map_range(ymin, ymax)));
}
let legend_sink = std::cell::RefCell::new(Vec::new());
let mut legends_captured = false;
for panel in &panels {
let slices: Vec<(&Layer, DataFrame)> = spec
.layers
.iter()
.enumerate()
.map(|(idx, layer)| {
Ok((
layer,
facet::panel_dataframe(layer_dataframe(layer, idx, data)?, panel)?,
))
})
.collect::<Result<_>>()?;
let empty = slices.iter().all(|(_, df)| df.height() == 0);
let mut ps = facet::PanelScales::new(spec, panel);
let layer_dfs: Vec<&DataFrame> = slices.iter().map(|(_, df)| df).collect();
if ps.free_x {
match scales::free_position_scale(spec.find_scale("pos1"), &layer_dfs, "pos1") {
Some(hs) => view.insert_scale(ps.pos1.clone(), hs),
None => ps.use_shared("pos1"),
}
}
if ps.free_y {
match scales::free_position_scale(spec.find_scale("pos2"), &layer_dfs, "pos2") {
Some(hs) => view.insert_scale(ps.pos2.clone(), hs),
None => ps.use_shared("pos2"),
}
}
let panel_legends = (!legends_captured).then_some(&legend_sink);
let mut plot = HPlot::new(&composition, panel.id.as_str())
.shape_registry(ShapeRegistry::with_builtins());
if !empty {
for (layer, df) in &slices {
let ctx = Ctx {
spec,
layer,
df,
transposed: is_transposed(layer),
pos1_scale: &ps.pos1,
pos2_scale: &ps.pos2,
legends: panel_legends,
};
geom::build_into_plot(&mut plot, &ctx)?;
}
legends_captured = true;
} else {
for (channel, name) in [("x", &ps.pos1), ("y", &ps.pos2)] {
if view.scale(name).is_some() {
plot.set_binding(channel, name.clone());
}
}
}
plot = apply_projection(plot, spec, panel, &ps);
if map_bbox.is_some() {
plot = plot.aspect_ratio(1.0).aspect_mode(AspectMode::Range);
}
if let Some(text) = &panel.strip_top {
plot = plot.strip(AxisSide::Top, text.clone());
}
if let Some(text) = &panel.strip_right {
plot = plot.strip(AxisSide::Right, text.clone());
}
view.attach_plot(plot);
}
for legend in legend_sink.into_inner() {
view.add_legend(legend);
}
let issues = view.validate();
if !issues.is_empty() {
return Err(GgsqlError::WriterError(format!(
"the plot renderer could not lay this plot out: {issues:?}"
)));
}
Ok(view)
}
fn map_bbox(
spec: &Plot,
data: &HashMap<String, DataFrame>,
) -> Result<Option<(f64, f64, f64, f64)>> {
if let Some(proj) = &spec.project {
if let Some(ParameterValue::Array(arr)) = proj.computed.get("bbox") {
let nums: Vec<f64> = arr.iter().filter_map(|e| e.to_f64()).collect();
if let [xmin, ymin, xmax, ymax] = nums[..] {
if [xmin, ymin, xmax, ymax].iter().all(|v| v.is_finite()) {
return Ok(Some((xmin, ymin, xmax, ymax)));
}
}
}
}
let is_spatial = |layer: &Layer| layer.geom.geom_type() == GeomType::Spatial;
if !spec.layers.iter().any(is_spatial) {
return Ok(None);
}
let geom_col = naming::aesthetic_column("geometry");
let (mut xmin, mut ymin, mut xmax, mut ymax) = (
f64::INFINITY,
f64::INFINITY,
f64::NEG_INFINITY,
f64::NEG_INFINITY,
);
for (idx, layer) in spec
.layers
.iter()
.enumerate()
.filter(|(_, l)| is_spatial(l))
{
let df = layer_dataframe(layer, idx, data)?;
if df.column(&geom_col).is_err() {
continue;
}
for g in channels::column_to_geometry(df, &geom_col)? {
if let Some((x0, y0, x1, y1)) = g.bounds() {
xmin = xmin.min(x0);
ymin = ymin.min(y0);
xmax = xmax.max(x1);
ymax = ymax.max(y1);
}
}
}
Ok(
(xmin.is_finite() && ymin.is_finite() && xmax.is_finite() && ymax.is_finite())
.then_some((xmin, ymin, xmax, ymax)),
)
}
pub(super) fn map_range(min: f64, max: f64) -> std::ops::RangeInclusive<f64> {
let (min, max) = if min <= max { (min, max) } else { (max, min) };
let span = max - min;
if span > f64::EPSILON {
let pad = span * MAP_PADDING / 2.0;
(min - pad)..=(max + pad)
} else {
(min - 0.5)..=(max + 0.5)
}
}
pub(super) fn layer_dataframe<'a>(
layer: &Layer,
idx: usize,
data: &'a HashMap<String, DataFrame>,
) -> Result<&'a DataFrame> {
let key = layer
.data_key
.clone()
.unwrap_or_else(|| naming::layer_key(idx));
data.get(&key)
.ok_or_else(|| GgsqlError::WriterError(format!("no data found for layer key '{key}'")))
}