use crate::hatch::Hatch;
use crate::markers::Marker;
use crate::stats::{boxplot_stats, BoxStats};
use crate::strokes::Stroke;
#[derive(Debug, Clone)]
pub(crate) enum Series {
Line(LineSeries),
Scatter(ScatterSeries),
Bar(BarSeries),
Area(AreaSeries),
Polygon(PolygonSeries),
ErrorBar(ErrorBarSeries),
BoxPlot(BoxPlotSeries),
Stem(StemSeries),
Quiver(QuiverSeries),
Contour(ContourSeries),
Histogram(HistogramSeries),
Heatmap(HeatmapSeries),
Hline(HlineSeries),
Vline(VlineSeries),
}
#[derive(Debug, Clone)]
pub(crate) struct LineSeries {
pub xs: Vec<f64>,
pub ys: Vec<f64>,
pub label: Option<String>,
pub stroke: Option<Stroke>,
pub stroke_width: Option<f64>,
pub markers: bool,
pub marker: Option<Marker>,
pub marker_size: Option<f64>,
}
#[derive(Debug, Default)]
pub struct LineOpts {
pub(crate) label: Option<String>,
pub(crate) stroke: Option<Stroke>,
pub(crate) stroke_width: Option<f64>,
pub(crate) markers: bool,
pub(crate) marker: Option<Marker>,
pub(crate) marker_size: Option<f64>,
}
impl LineOpts {
pub fn label(mut self, s: impl Into<String>) -> Self {
self.label = Some(s.into());
self
}
pub fn stroke(mut self, s: Stroke) -> Self {
self.stroke = Some(s);
self
}
pub fn stroke_width(mut self, w: f64) -> Self {
self.stroke_width = Some(w);
self
}
pub fn markers(mut self) -> Self {
self.markers = true;
self
}
pub fn marker(mut self, m: Marker) -> Self {
self.markers = true;
self.marker = Some(m);
self
}
pub fn marker_size(mut self, sz: f64) -> Self {
self.marker_size = Some(sz);
self
}
}
#[derive(Debug, Clone)]
pub(crate) struct ScatterSeries {
pub xs: Vec<f64>,
pub ys: Vec<f64>,
pub label: Option<String>,
pub marker: Option<Marker>,
pub marker_size: Option<f64>,
pub stroke_width: Option<f64>,
}
#[derive(Debug, Default)]
pub struct ScatterOpts {
pub(crate) label: Option<String>,
pub(crate) marker: Option<Marker>,
pub(crate) marker_size: Option<f64>,
pub(crate) stroke_width: Option<f64>,
}
impl ScatterOpts {
pub fn label(mut self, s: impl Into<String>) -> Self {
self.label = Some(s.into());
self
}
pub fn marker(mut self, m: Marker) -> Self {
self.marker = Some(m);
self
}
pub fn marker_size(mut self, sz: f64) -> Self {
self.marker_size = Some(sz);
self
}
pub fn stroke_width(mut self, w: f64) -> Self {
self.stroke_width = Some(w);
self
}
}
#[derive(Debug, Clone)]
pub(crate) struct BarSeries {
pub categories: Vec<String>,
pub values: Vec<f64>,
pub label: Option<String>,
pub hatch: Option<Hatch>,
pub group: u32,
pub stroke_width: Option<f64>,
}
#[derive(Debug, Default)]
pub struct BarOpts {
pub(crate) label: Option<String>,
pub(crate) hatch: Option<Hatch>,
pub(crate) group: u32,
pub(crate) stroke_width: Option<f64>,
}
impl BarOpts {
pub fn label(mut self, s: impl Into<String>) -> Self {
self.label = Some(s.into());
self
}
pub fn hatch(mut self, h: Hatch) -> Self {
self.hatch = Some(h);
self
}
pub fn group(mut self, g: u32) -> Self {
self.group = g;
self
}
pub fn stroke_width(mut self, w: f64) -> Self {
self.stroke_width = Some(w);
self
}
}
#[derive(Debug, Clone)]
pub(crate) struct AreaSeries {
pub xs: Vec<f64>,
pub ys: Vec<f64>,
pub label: Option<String>,
pub hatch: Option<Hatch>,
pub baseline: f64,
pub stroke: Option<Stroke>,
pub stroke_width: Option<f64>,
}
#[derive(Debug)]
pub struct AreaOpts {
pub(crate) label: Option<String>,
pub(crate) hatch: Option<Hatch>,
pub(crate) baseline: f64,
pub(crate) stroke: Option<Stroke>,
pub(crate) stroke_width: Option<f64>,
}
impl Default for AreaOpts {
fn default() -> Self {
Self {
label: None,
hatch: None,
baseline: 0.0,
stroke: None,
stroke_width: None,
}
}
}
impl AreaOpts {
pub fn label(mut self, s: impl Into<String>) -> Self {
self.label = Some(s.into());
self
}
pub fn hatch(mut self, h: Hatch) -> Self {
self.hatch = Some(h);
self
}
pub fn baseline(mut self, b: f64) -> Self {
self.baseline = b;
self
}
pub fn stroke(mut self, s: Stroke) -> Self {
self.stroke = Some(s);
self
}
pub fn stroke_width(mut self, w: f64) -> Self {
self.stroke_width = Some(w);
self
}
}
#[derive(Debug, Clone)]
pub(crate) struct PolygonSeries {
pub xs: Vec<f64>,
pub ys: Vec<f64>,
pub label: Option<String>,
pub hatch: Option<Hatch>,
pub stroke: Stroke,
pub stroke_width: Option<f64>,
}
#[derive(Debug)]
pub struct PolygonOpts {
pub(crate) label: Option<String>,
pub(crate) hatch: Option<Hatch>,
pub(crate) stroke: Stroke,
pub(crate) stroke_width: Option<f64>,
}
impl Default for PolygonOpts {
fn default() -> Self {
Self {
label: None,
hatch: None,
stroke: Stroke::Solid,
stroke_width: None,
}
}
}
impl PolygonOpts {
pub fn label(mut self, s: impl Into<String>) -> Self {
self.label = Some(s.into());
self
}
pub fn hatch(mut self, h: Hatch) -> Self {
self.hatch = Some(h);
self
}
pub fn stroke(mut self, s: Stroke) -> Self {
self.stroke = s;
self
}
pub fn stroke_width(mut self, w: f64) -> Self {
self.stroke_width = Some(w);
self
}
}
#[derive(Debug, Clone, Copy)]
pub enum Err {
Symmetric(f64),
Asymmetric(f64, f64),
}
#[derive(Debug, Clone)]
pub(crate) struct ErrorBarSeries {
pub xs: Vec<f64>,
pub ys: Vec<f64>,
pub yerr: Option<Vec<Err>>,
pub xerr: Option<Vec<Err>>,
pub marker: Option<Marker>,
pub marker_size: Option<f64>,
pub cap_width: f64,
pub stroke_width: Option<f64>,
pub label: Option<String>,
}
#[derive(Debug)]
pub struct ErrorBarOpts {
pub(crate) yerr: Option<Vec<Err>>,
pub(crate) xerr: Option<Vec<Err>>,
pub(crate) marker: Option<Marker>,
pub(crate) marker_size: Option<f64>,
pub(crate) cap_width: f64,
pub(crate) stroke_width: Option<f64>,
pub(crate) label: Option<String>,
}
impl Default for ErrorBarOpts {
fn default() -> Self {
Self {
yerr: None,
xerr: None,
marker: Some(Marker::CircleFilled),
marker_size: None,
cap_width: 8.0,
stroke_width: None,
label: None,
}
}
}
impl ErrorBarOpts {
pub fn label(mut self, s: impl Into<String>) -> Self {
self.label = Some(s.into());
self
}
pub fn yerr(mut self, e: &[f64]) -> Self {
self.yerr = Some(e.iter().map(|v| Err::Symmetric(*v)).collect());
self
}
pub fn yerr_asym(mut self, e: &[(f64, f64)]) -> Self {
self.yerr = Some(e.iter().map(|(l, u)| Err::Asymmetric(*l, *u)).collect());
self
}
pub fn xerr(mut self, e: &[f64]) -> Self {
self.xerr = Some(e.iter().map(|v| Err::Symmetric(*v)).collect());
self
}
pub fn xerr_asym(mut self, e: &[(f64, f64)]) -> Self {
self.xerr = Some(e.iter().map(|(l, u)| Err::Asymmetric(*l, *u)).collect());
self
}
pub fn marker(mut self, m: Marker) -> Self {
self.marker = Some(m);
self
}
pub fn no_marker(mut self) -> Self {
self.marker = None;
self
}
pub fn marker_size(mut self, sz: f64) -> Self {
self.marker_size = Some(sz);
self
}
pub fn cap_width(mut self, w: f64) -> Self {
self.cap_width = w;
self
}
pub fn stroke_width(mut self, w: f64) -> Self {
self.stroke_width = Some(w);
self
}
}
#[derive(Debug, Clone)]
pub(crate) struct BoxPlotSeries {
pub categories: Vec<String>,
pub stats: Vec<BoxStats>,
pub label: Option<String>,
pub hatch: Option<Hatch>,
pub box_width: f64,
pub stroke_width: Option<f64>,
}
#[derive(Debug)]
pub struct BoxPlotOpts {
pub(crate) label: Option<String>,
pub(crate) hatch: Option<Hatch>,
pub(crate) box_width: f64,
pub(crate) whisker_iqr: f64,
pub(crate) stroke_width: Option<f64>,
}
impl Default for BoxPlotOpts {
fn default() -> Self {
Self {
label: None,
hatch: None,
box_width: 0.6,
whisker_iqr: 1.5,
stroke_width: None,
}
}
}
impl BoxPlotOpts {
pub fn label(mut self, s: impl Into<String>) -> Self {
self.label = Some(s.into());
self
}
pub fn hatch(mut self, h: Hatch) -> Self {
self.hatch = Some(h);
self
}
pub fn box_width(mut self, w: f64) -> Self {
self.box_width = w;
self
}
pub fn whisker_iqr(mut self, k: f64) -> Self {
self.whisker_iqr = k;
self
}
pub fn stroke_width(mut self, w: f64) -> Self {
self.stroke_width = Some(w);
self
}
}
pub(crate) fn build_boxplot_series(
samples_by_category: &[(String, Vec<f64>)],
opts: BoxPlotOpts,
) -> BoxPlotSeries {
let mut categories = Vec::with_capacity(samples_by_category.len());
let mut stats = Vec::with_capacity(samples_by_category.len());
for (cat, samples) in samples_by_category {
categories.push(cat.clone());
stats.push(boxplot_stats(samples, opts.whisker_iqr));
}
BoxPlotSeries {
categories,
stats,
label: opts.label,
hatch: opts.hatch,
box_width: opts.box_width,
stroke_width: opts.stroke_width,
}
}
#[derive(Debug, Clone)]
pub(crate) struct StemSeries {
pub xs: Vec<f64>,
pub ys: Vec<f64>,
pub baseline: f64,
pub label: Option<String>,
pub stroke: Stroke,
pub stroke_width: Option<f64>,
pub marker: Option<Marker>,
pub marker_size: Option<f64>,
}
#[derive(Debug)]
pub struct StemOpts {
pub(crate) baseline: f64,
pub(crate) label: Option<String>,
pub(crate) stroke: Stroke,
pub(crate) stroke_width: Option<f64>,
pub(crate) marker: Option<Marker>,
pub(crate) marker_size: Option<f64>,
}
impl Default for StemOpts {
fn default() -> Self {
Self {
baseline: 0.0,
label: None,
stroke: Stroke::Solid,
stroke_width: None,
marker: Some(Marker::CircleFilled),
marker_size: None,
}
}
}
impl StemOpts {
pub fn label(mut self, s: impl Into<String>) -> Self {
self.label = Some(s.into());
self
}
pub fn baseline(mut self, b: f64) -> Self {
self.baseline = b;
self
}
pub fn stroke(mut self, s: Stroke) -> Self {
self.stroke = s;
self
}
pub fn stroke_width(mut self, w: f64) -> Self {
self.stroke_width = Some(w);
self
}
pub fn marker(mut self, m: Marker) -> Self {
self.marker = Some(m);
self
}
pub fn no_marker(mut self) -> Self {
self.marker = None;
self
}
pub fn marker_size(mut self, sz: f64) -> Self {
self.marker_size = Some(sz);
self
}
}
#[derive(Debug, Clone)]
pub(crate) struct QuiverSeries {
pub xs: Vec<f64>,
pub ys: Vec<f64>,
pub us: Vec<f64>,
pub vs: Vec<f64>,
pub scale: f64,
pub head_size: f64,
pub label: Option<String>,
pub stroke: Stroke,
pub stroke_width: Option<f64>,
}
#[derive(Debug)]
pub struct QuiverOpts {
pub(crate) scale: f64,
pub(crate) head_size: f64,
pub(crate) label: Option<String>,
pub(crate) stroke: Stroke,
pub(crate) stroke_width: Option<f64>,
}
impl Default for QuiverOpts {
fn default() -> Self {
Self {
scale: 1.0,
head_size: 6.0,
label: None,
stroke: Stroke::Solid,
stroke_width: None,
}
}
}
impl QuiverOpts {
pub fn label(mut self, s: impl Into<String>) -> Self {
self.label = Some(s.into());
self
}
pub fn scale(mut self, s: f64) -> Self {
self.scale = s;
self
}
pub fn head_size(mut self, h: f64) -> Self {
self.head_size = h;
self
}
pub fn stroke(mut self, s: Stroke) -> Self {
self.stroke = s;
self
}
pub fn stroke_width(mut self, w: f64) -> Self {
self.stroke_width = Some(w);
self
}
}
#[derive(Debug, Clone)]
pub(crate) struct ContourSeries {
pub data: Vec<Vec<f64>>,
pub x_edges: Vec<f64>,
pub y_edges: Vec<f64>,
pub levels: Vec<f64>,
pub origin: Origin,
pub stroke: Stroke,
pub stroke_width: Option<f64>,
pub label: Option<String>,
}
#[derive(Debug)]
pub struct ContourOpts {
pub(crate) x_edges: Option<Vec<f64>>,
pub(crate) y_edges: Option<Vec<f64>>,
pub(crate) levels: Option<Vec<f64>>,
pub(crate) origin: Origin,
pub(crate) stroke: Stroke,
pub(crate) stroke_width: Option<f64>,
pub(crate) label: Option<String>,
}
impl Default for ContourOpts {
fn default() -> Self {
Self {
x_edges: None,
y_edges: None,
levels: None,
origin: Origin::default(),
stroke: Stroke::Solid,
stroke_width: None,
label: None,
}
}
}
impl ContourOpts {
pub fn x_edges(mut self, e: Vec<f64>) -> Self {
self.x_edges = Some(e);
self
}
pub fn y_edges(mut self, e: Vec<f64>) -> Self {
self.y_edges = Some(e);
self
}
pub fn levels(mut self, l: Vec<f64>) -> Self {
self.levels = Some(l);
self
}
pub fn origin(mut self, o: Origin) -> Self {
self.origin = o;
self
}
pub fn stroke(mut self, s: Stroke) -> Self {
self.stroke = s;
self
}
pub fn stroke_width(mut self, w: f64) -> Self {
self.stroke_width = Some(w);
self
}
pub fn label(mut self, s: impl Into<String>) -> Self {
self.label = Some(s.into());
self
}
}
#[derive(Debug, Clone)]
pub(crate) struct HistogramSeries {
pub bin_edges: Vec<f64>,
pub values: Vec<f64>,
pub label: Option<String>,
pub hatch: Option<Hatch>,
pub stroke_width: Option<f64>,
}
#[derive(Debug, Default)]
pub struct HistogramOpts {
pub(crate) label: Option<String>,
pub(crate) hatch: Option<Hatch>,
pub(crate) stroke_width: Option<f64>,
pub(crate) bins: Option<BinStrategy>,
pub(crate) bin_edges: Option<Vec<f64>>,
pub(crate) normalize: Normalize,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum Normalize {
#[default]
Count,
Pmf,
Density,
Cmf,
}
#[derive(Debug, Clone, Copy)]
pub enum BinStrategy {
Fixed(usize),
Sturges,
Sqrt,
Scott,
FreedmanDiaconis,
}
impl HistogramOpts {
pub fn label(mut self, s: impl Into<String>) -> Self {
self.label = Some(s.into());
self
}
pub fn hatch(mut self, h: Hatch) -> Self {
self.hatch = Some(h);
self
}
pub fn bins(mut self, n: usize) -> Self {
self.bins = Some(BinStrategy::Fixed(n));
self
}
pub fn bin_strategy(mut self, s: BinStrategy) -> Self {
self.bins = Some(s);
self
}
pub fn bin_edges(mut self, edges: Vec<f64>) -> Self {
self.bin_edges = Some(edges);
self
}
pub fn normalize(mut self, n: Normalize) -> Self {
self.normalize = n;
self
}
pub fn density(mut self) -> Self {
self.normalize = Normalize::Density;
self
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum Origin {
#[default]
BottomLeft,
TopLeft,
}
#[derive(Debug, Clone)]
pub(crate) struct HeatmapSeries {
pub data: Vec<Vec<f64>>,
pub x_edges: Vec<f64>,
pub y_edges: Vec<f64>,
pub ramp: Vec<Hatch>,
pub range: Option<(f64, f64)>,
pub origin: Origin,
#[allow(dead_code)]
pub label: Option<String>,
}
#[derive(Debug, Clone)]
pub(crate) struct HlineSeries {
pub y: f64,
pub label: Option<String>,
pub stroke: Stroke,
pub stroke_width: f64,
}
#[derive(Debug, Default)]
pub struct HlineOpts {
pub(crate) label: Option<String>,
pub(crate) stroke: Option<Stroke>,
pub(crate) stroke_width: Option<f64>,
}
impl HlineOpts {
pub fn label(mut self, s: impl Into<String>) -> Self {
self.label = Some(s.into());
self
}
pub fn stroke(mut self, s: Stroke) -> Self {
self.stroke = Some(s);
self
}
pub fn stroke_width(mut self, w: f64) -> Self {
self.stroke_width = Some(w);
self
}
}
#[derive(Debug, Clone)]
pub(crate) struct VlineSeries {
pub x: f64,
pub label: Option<String>,
pub stroke: Stroke,
pub stroke_width: f64,
}
#[derive(Debug, Default)]
pub struct VlineOpts {
pub(crate) label: Option<String>,
pub(crate) stroke: Option<Stroke>,
pub(crate) stroke_width: Option<f64>,
}
impl VlineOpts {
pub fn label(mut self, s: impl Into<String>) -> Self {
self.label = Some(s.into());
self
}
pub fn stroke(mut self, s: Stroke) -> Self {
self.stroke = Some(s);
self
}
pub fn stroke_width(mut self, w: f64) -> Self {
self.stroke_width = Some(w);
self
}
}
pub(crate) fn bin_observations(
observations: &[f64],
opts: &HistogramOpts,
) -> (Vec<f64>, Vec<f64>) {
let edges = match &opts.bin_edges {
Some(e) if e.len() >= 2 => e.clone(),
_ => auto_edges(observations, opts.bins.unwrap_or(BinStrategy::Sturges)),
};
let counts = count_in(observations, &edges);
let values = normalize_counts(&counts, &edges, opts.normalize);
(edges, values)
}
fn auto_edges(observations: &[f64], strategy: BinStrategy) -> Vec<f64> {
if observations.is_empty() {
return vec![0.0, 1.0];
}
let (mut lo, mut hi) = min_max(observations);
if lo == hi {
if lo == 0.0 {
lo = -0.5;
hi = 0.5;
} else {
let pad = lo.abs() * 0.1;
lo -= pad;
hi += pad;
}
}
let n = observations.len();
let k = resolve_bin_count(observations, strategy, n, lo, hi).max(1);
let step = (hi - lo) / k as f64;
(0..=k).map(|i| lo + i as f64 * step).collect()
}
fn resolve_bin_count(obs: &[f64], strategy: BinStrategy, n: usize, lo: f64, hi: f64) -> usize {
match strategy {
BinStrategy::Fixed(k) => k.max(1),
BinStrategy::Sturges => ((n as f64).log2() + 1.0).ceil() as usize,
BinStrategy::Sqrt => (n as f64).sqrt().ceil() as usize,
BinStrategy::Scott => {
let sigma = stddev(obs);
if sigma <= 0.0 {
return (n as f64).sqrt().ceil() as usize;
}
let h = 3.49 * sigma * (n as f64).powf(-1.0 / 3.0);
if h <= 0.0 {
(n as f64).sqrt().ceil() as usize
} else {
((hi - lo) / h).ceil() as usize
}
}
BinStrategy::FreedmanDiaconis => {
let q = iqr(obs);
if q <= 0.0 {
return (n as f64).sqrt().ceil() as usize;
}
let h = 2.0 * q * (n as f64).powf(-1.0 / 3.0);
if h <= 0.0 {
(n as f64).sqrt().ceil() as usize
} else {
((hi - lo) / h).ceil() as usize
}
}
}
}
fn count_in(observations: &[f64], edges: &[f64]) -> Vec<usize> {
let n_bins = edges.len() - 1;
let mut counts = vec![0usize; n_bins];
let last = n_bins - 1;
for &v in observations {
if v.is_nan() {
continue;
}
for i in 0..n_bins {
let lo = edges[i];
let hi = edges[i + 1];
let in_bin = if i == last {
v >= lo && v <= hi
} else {
v >= lo && v < hi
};
if in_bin {
counts[i] += 1;
break;
}
}
}
counts
}
fn normalize_counts(counts: &[usize], edges: &[f64], mode: Normalize) -> Vec<f64> {
match mode {
Normalize::Count => counts.iter().map(|c| *c as f64).collect(),
Normalize::Pmf => {
let total: usize = counts.iter().sum();
if total == 0 {
return counts.iter().map(|c| *c as f64).collect();
}
counts.iter().map(|c| *c as f64 / total as f64).collect()
}
Normalize::Density => {
let total: usize = counts.iter().sum();
counts
.iter()
.enumerate()
.map(|(i, c)| {
let w = edges[i + 1] - edges[i];
if total == 0 || w == 0.0 {
0.0
} else {
*c as f64 / (total as f64 * w)
}
})
.collect()
}
Normalize::Cmf => {
let total: usize = counts.iter().sum();
if total == 0 {
return counts.iter().map(|c| *c as f64).collect();
}
let mut acc = 0.0;
counts
.iter()
.map(|c| {
acc += *c as f64 / total as f64;
acc
})
.collect()
}
}
}
pub fn staircase(edges: &[f64], cumulative: &[f64]) -> (Vec<f64>, Vec<f64>) {
if edges.is_empty() {
return (Vec::new(), Vec::new());
}
if cumulative.is_empty() {
let xs = edges.to_vec();
let ys = vec![0.0; edges.len()];
return (xs, ys);
}
let mut xs = Vec::with_capacity(2 * cumulative.len() + 1);
let mut ys = Vec::with_capacity(2 * cumulative.len() + 1);
xs.push(edges[0]);
ys.push(0.0);
let mut prev = 0.0;
for (i, &cum) in cumulative.iter().enumerate() {
let edge = edges[i + 1];
xs.push(edge);
ys.push(prev);
xs.push(edge);
ys.push(cum);
prev = cum;
}
(xs, ys)
}
fn min_max(values: &[f64]) -> (f64, f64) {
let mut lo = f64::INFINITY;
let mut hi = f64::NEG_INFINITY;
for &v in values {
if v < lo {
lo = v;
}
if v > hi {
hi = v;
}
}
(lo, hi)
}
fn stddev(values: &[f64]) -> f64 {
let n = values.len();
if n == 0 {
return 0.0;
}
let mean = values.iter().sum::<f64>() / n as f64;
let var = values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n as f64;
var.sqrt()
}
fn iqr(values: &[f64]) -> f64 {
let mut sorted: Vec<f64> = values.iter().copied().filter(|v| !v.is_nan()).collect();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
if sorted.len() < 2 {
return 0.0;
}
let q1 = percentile(&sorted, 0.25);
let q3 = percentile(&sorted, 0.75);
q3 - q1
}
fn percentile(sorted: &[f64], p: f64) -> f64 {
let n = sorted.len();
let rank = p * (n - 1) as f64;
let lo = rank.trunc() as usize;
let hi = (lo + 1).min(n - 1);
let frac = rank - lo as f64;
sorted[lo] + frac * (sorted[hi] - sorted[lo])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pmf_sums_to_one() {
let opts = HistogramOpts::default()
.bins(3)
.normalize(Normalize::Pmf);
let (_, vs) = bin_observations(&[1.0, 2.0, 2.0, 3.0, 3.0, 3.0], &opts);
let sum: f64 = vs.iter().sum();
assert!((sum - 1.0).abs() < 1e-9);
}
#[test]
fn fixed_bins_returns_correct_edge_count() {
let opts = HistogramOpts::default().bins(4);
let (edges, vs) = bin_observations(&[0.0, 1.0, 2.0, 3.0, 4.0], &opts);
assert_eq!(edges.len(), 5);
assert_eq!(vs.len(), 4);
}
}