use crate::core::charts::bins::nice_bins;
use crate::core::charts::{CdfPoint, DistSeries, DistributionPanel, Markers};
#[derive(Debug, Clone, Copy)]
pub struct DistMarkers {
pub p90: f64,
pub p50: f64,
pub p10: f64,
}
pub fn distribution_panel(
title: &str,
units: &str,
series: &[(String, &[f64], DistMarkers)],
scale: f64,
target_bins: usize,
) -> DistributionPanel {
let s = if scale != 0.0 { scale } else { 1.0 };
let out = series
.iter()
.filter(|(_, samples, _)| !samples.is_empty())
.map(|(name, samples, mk)| {
let scaled: Vec<f64> = samples.iter().map(|v| v / s).collect();
let bins = nice_bins(&scaled, target_bins);
let cdf = exceedance(&scaled, &bins);
DistSeries {
name: name.clone(),
bins,
cdf,
markers: Markers {
p90: mk.p90 / s,
p50: mk.p50 / s,
p10: mk.p10 / s,
},
}
})
.collect();
DistributionPanel {
mark: "distribution",
title: title.to_string(),
units: units.to_string(),
series: out,
}
}
fn exceedance(scaled: &[f64], bins: &[crate::core::charts::Bin]) -> Vec<CdfPoint> {
if bins.is_empty() || scaled.is_empty() {
return Vec::new();
}
let n = scaled.len() as f64;
let mut edges: Vec<f64> = bins.iter().map(|b| b.lo).collect();
edges.push(bins.last().unwrap().hi);
edges
.into_iter()
.map(|x| {
let ge = scaled.iter().filter(|&&v| v >= x).count() as f64;
CdfPoint {
x,
exceedance: ge / n,
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn panel_bins_scales_and_orders_exceedance() {
let samples: Vec<f64> = (0..1000).map(|i| 30e6 + (i as f64) * 4e4).collect(); let mk = DistMarkers {
p90: 34e6,
p50: 50e6,
p10: 66e6,
};
let panel = distribution_panel(
"STOIIP",
"MSm³",
&[("Field".to_string(), &samples, mk)],
1e6,
24,
);
assert_eq!(panel.series.len(), 1);
let s = &panel.series[0];
assert!((s.markers.p90 - 34.0).abs() < 1e-9);
assert!(s.markers.p90 <= s.markers.p50 && s.markers.p50 <= s.markers.p10);
let total: u32 = s.bins.iter().map(|b| b.count).sum();
assert_eq!(total as usize, samples.len());
assert!((s.cdf.first().unwrap().exceedance - 1.0).abs() < 1e-9);
for w in s.cdf.windows(2) {
assert!(w[1].exceedance <= w[0].exceedance + 1e-12);
}
}
#[test]
fn empty_series_skipped() {
let empty: Vec<f64> = Vec::new();
let mk = DistMarkers {
p90: 0.0,
p50: 0.0,
p10: 0.0,
};
let panel = distribution_panel("GIIP", "bcm", &[("Gas".to_string(), &empty, mk)], 1e9, 24);
assert!(panel.series.is_empty());
}
}