use plotters::element::{Drawable, PointCollection};
use plotters::style::RGBColor;
use plotters_backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
use crate::stats::StatsError;
use crate::style::{fill_style, palette_color, stroke_style};
#[derive(Debug, Clone)]
pub struct RegLine {
points: Vec<(f64, f64)>,
n_line: usize,
has_zero: bool,
color: RGBColor,
stroke_width: u32,
marker_radius: u32,
show_marker: bool,
name: Option<String>,
}
impl RegLine {
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
pub fn color(&self) -> RGBColor {
self.color
}
}
impl<'a> PointCollection<'a, (f64, f64)> for &'a RegLine {
type Point = &'a (f64, f64);
type IntoIter = &'a [(f64, f64)];
fn point_iter(self) -> &'a [(f64, f64)] {
&self.points
}
}
impl<DB: DrawingBackend> Drawable<DB> for RegLine {
fn draw<I: Iterator<Item = BackendCoord>>(
&self,
points: I,
backend: &mut DB,
_parent_dim: (u32, u32),
) -> Result<(), DrawingErrorKind<DB::ErrorType>> {
let pix: Vec<BackendCoord> = points.collect();
if pix.len() < self.n_line {
return Ok(());
}
backend.draw_path(
pix[..self.n_line].iter().copied(),
&stroke_style(&self.color, self.stroke_width),
)?;
if self.show_marker && self.has_zero && pix.len() > self.n_line {
let marker = pix[self.n_line];
backend.draw_circle(marker, self.marker_radius, &fill_style(&self.color), true)?;
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct RegularizationPath {
lines: Vec<RegLine>,
}
impl RegularizationPath {
pub fn new(strengths: &[f64], coefficients: &[Vec<f64>]) -> Result<Self, StatsError> {
if strengths.is_empty() || coefficients.is_empty() {
return Err(StatsError::EmptyInput);
}
if coefficients.len() != strengths.len() {
return Err(StatsError::LengthMismatch {
scores: strengths.len(),
labels: coefficients.len(),
});
}
let n_features = coefficients[0].len();
if coefficients.iter().any(|r| r.len() != n_features) {
return Err(StatsError::LengthMismatch {
scores: n_features,
labels: coefficients.iter().map(|r| r.len()).max().unwrap_or(0),
});
}
let mut lines = Vec::with_capacity(n_features);
for j in 0..n_features {
let series: Vec<(f64, f64)> = strengths
.iter()
.zip(coefficients.iter())
.map(|(&s, row)| (s, row[j]))
.collect();
let zero = first_zero_crossing(&series);
let n_line = series.len();
let mut points = series;
let has_zero = zero.is_some();
if let Some(z) = zero {
points.push(z);
}
lines.push(RegLine {
points,
n_line,
has_zero,
color: palette_color(j),
stroke_width: 2,
marker_radius: 4,
show_marker: true,
name: None,
});
}
Ok(Self { lines })
}
pub fn feature_names<S: Into<String>, I: IntoIterator<Item = S>>(mut self, names: I) -> Self {
for (line, name) in self.lines.iter_mut().zip(names) {
line.name = Some(name.into());
}
self
}
pub fn zero_markers(mut self, show: bool) -> Self {
for line in &mut self.lines {
line.show_marker = show;
}
self
}
pub fn stroke_width(mut self, width: u32) -> Self {
for line in &mut self.lines {
line.stroke_width = width;
}
self
}
pub fn lines(&self) -> Vec<RegLine> {
self.lines.clone()
}
}
impl IntoIterator for RegularizationPath {
type Item = RegLine;
type IntoIter = std::vec::IntoIter<RegLine>;
fn into_iter(self) -> Self::IntoIter {
self.lines.into_iter()
}
}
fn first_zero_crossing(series: &[(f64, f64)]) -> Option<(f64, f64)> {
for w in series.windows(2) {
let (s0, c0) = w[0];
let (s1, c1) = w[1];
if c0 == 0.0 {
return Some((s0, 0.0));
}
if c1 == 0.0 {
return Some((s1, 0.0));
}
if c0 * c1 < 0.0 {
let t = c0 / (c0 - c1); return Some((s0 + t * (s1 - s0), 0.0));
}
}
match series.first() {
Some(&(s, 0.0)) => Some((s, 0.0)),
_ => None,
}
}