use crate::core::charts::{crossplot_chart, Axis, ColorBy, Range, ScatterChart, ScatterInput};
use crate::core::facade::project::Project;
use crate::units::SrsError;
use serde_json::Value;
#[derive(Debug, Clone)]
pub struct CrossplotOpts {
pub x: String,
pub y: String,
pub wells: Option<Vec<String>>,
pub color_by: String,
pub x_log: bool,
pub y_log: bool,
pub regression: bool,
}
pub fn crossplot(proj: &Project, opts: &CrossplotOpts) -> Result<ScatterChart, SrsError> {
let cb = opts.color_by.to_ascii_lowercase();
let categorical = cb == "well" || cb == "zone";
let mut points: Vec<(f64, f64, Value)> = Vec::new();
let mut groups: Vec<String> = Vec::new(); let (mut cmin, mut cmax) = (f64::INFINITY, f64::NEG_INFINITY);
for bw in proj.wells() {
if let Some(sel) = &opts.wells {
let parent = bw.id.split(' ').next().unwrap_or(&bw.id);
if !sel.iter().any(|s| s == &bw.id || s == parent) {
continue;
}
}
let (Some(xv), Some(yv)) = (bw.log(&opts.x), bw.log(&opts.y)) else {
continue; };
let zones = if cb == "zone" { bw.zones() } else { Vec::new() };
let cv = if !categorical {
bw.log(&opts.color_by)
} else {
None
};
let mds = xv.md();
let vals = xv.values();
for (md, &xval) in mds.iter().zip(vals.iter()) {
if !xval.is_finite() {
continue;
}
let Some(yval) = yv.at_md(*md) else { continue };
if !yval.is_finite() {
continue;
}
let c = if cb == "well" {
cat_value(&bw.id, &mut groups)
} else if cb == "zone" {
let z = zone_at(*md, &zones);
cat_value(&z, &mut groups)
} else {
match cv.as_ref().and_then(|v| v.at_md(*md)) {
Some(cval) if cval.is_finite() => {
cmin = cmin.min(cval);
cmax = cmax.max(cval);
Value::from(cval)
}
_ => continue, }
};
points.push((xval, yval, c));
}
}
if points.is_empty() {
return Err(SrsError::InvalidInput(format!(
"crossplot: no paired ({}, {}) log samples found on the selected bores",
opts.x, opts.y
)));
}
let color_by = if categorical {
Some(ColorBy {
name: opts.color_by.clone(),
kind: "categorical",
range: None,
units: None,
})
} else {
Some(ColorBy {
name: opts.color_by.clone(),
kind: "continuous",
range: Some(Range {
min: cmin,
max: cmax,
}),
units: None,
})
};
Ok(crossplot_chart(ScatterInput {
title: format!("{} vs {}", opts.x, opts.y),
x: Axis {
name: opts.x.clone(),
units: String::new(),
log: opts.x_log,
},
y: Axis {
name: opts.y.clone(),
units: String::new(),
log: opts.y_log,
},
color_by,
groups,
points,
regression: opts.regression,
}))
}
fn cat_value(name: &str, groups: &mut Vec<String>) -> Value {
if !groups.iter().any(|g| g == name) {
groups.push(name.to_string());
}
Value::String(name.to_string())
}
fn zone_at(md: f64, zones: &[petekio::Interval<'_>]) -> String {
zones
.iter()
.find(|z| md >= z.top_md && md < z.base_md)
.map(|z| z.name.clone())
.unwrap_or_else(|| "unzoned".to_string())
}