use std::fmt;
use std::path::Path;
use plotters::coord::Shift;
use plotters::drawing::DrawingArea;
use plotters::prelude::*;
use crate::stats::GenerationStats;
#[derive(Debug)]
pub enum VisualizationError {
DrawingError(String),
IoError(String),
UnsupportedFormat,
InsufficientData,
}
impl fmt::Display for VisualizationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
VisualizationError::DrawingError(msg) => write!(f, "Drawing error: {}", msg),
VisualizationError::IoError(msg) => write!(f, "I/O error: {}", msg),
VisualizationError::UnsupportedFormat => {
write!(f, "Unsupported format: path must end in .png or .svg")
}
VisualizationError::InsufficientData => {
write!(f, "Insufficient data: at least 2 data points required")
}
}
}
}
impl std::error::Error for VisualizationError {}
fn compute_y_range(stats: &[GenerationStats]) -> (f64, f64) {
let mut y_min = f64::INFINITY;
let mut y_max = f64::NEG_INFINITY;
for s in stats {
y_min = y_min
.min(s.best_fitness)
.min(s.avg_fitness)
.min(s.worst_fitness);
y_max = y_max
.max(s.best_fitness)
.max(s.avg_fitness)
.max(s.worst_fitness);
}
if (y_max - y_min).abs() < f64::EPSILON {
y_max = y_min + 1.0;
}
(y_min, y_max)
}
fn draw_fitness_chart<DB>(
root: &DrawingArea<DB, Shift>,
stats: &[GenerationStats],
) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>>
where
DB: DrawingBackend,
DB::ErrorType: std::error::Error + Send + Sync,
{
let max_gen = stats.last().map(|s| s.generation).unwrap_or(0);
let x_max = if max_gen == 0 { 1 } else { max_gen };
let (y_min, y_max) = compute_y_range(stats);
let mut chart = ChartBuilder::on(root)
.caption("Fitness over Generations", (FontFamily::SansSerif, 20))
.margin(20)
.x_label_area_size(40)
.y_label_area_size(60)
.build_cartesian_2d(0usize..x_max, y_min..y_max)?;
chart
.configure_mesh()
.x_desc("Generation")
.y_desc("Fitness")
.x_label_style((FontFamily::SansSerif, 12))
.y_label_style((FontFamily::SansSerif, 12))
.draw()?;
chart
.draw_series(LineSeries::new(
stats.iter().map(|s| (s.generation, s.best_fitness)),
&BLUE,
))?
.label("Best")
.legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], BLUE));
chart
.draw_series(LineSeries::new(
stats.iter().map(|s| (s.generation, s.avg_fitness)),
&GREEN,
))?
.label("Average")
.legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], GREEN));
chart
.draw_series(LineSeries::new(
stats.iter().map(|s| (s.generation, s.worst_fitness)),
&RED,
))?
.label("Worst")
.legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], RED));
chart
.configure_series_labels()
.background_style(WHITE.mix(0.8))
.border_style(BLACK)
.label_font((FontFamily::SansSerif, 12))
.draw()?;
Ok(())
}
fn compute_diversity_range(stats: &[GenerationStats]) -> (f64, f64) {
let mut y_min = f64::INFINITY;
let mut y_max = f64::NEG_INFINITY;
for s in stats {
y_min = y_min.min(s.diversity);
y_max = y_max.max(s.diversity);
}
if (y_max - y_min).abs() < f64::EPSILON {
y_max = y_min + 1.0;
}
(y_min, y_max)
}
fn draw_diversity_chart<DB>(
root: &DrawingArea<DB, Shift>,
stats: &[GenerationStats],
) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>>
where
DB: DrawingBackend,
DB::ErrorType: std::error::Error + Send + Sync,
{
let max_gen = stats.last().map(|s| s.generation).unwrap_or(0);
let x_max = if max_gen == 0 { 1 } else { max_gen };
let (y_min, y_max) = compute_diversity_range(stats);
let mut chart = ChartBuilder::on(root)
.caption(
"Population Diversity over Generations",
(FontFamily::SansSerif, 20),
)
.margin(20)
.x_label_area_size(40)
.y_label_area_size(60)
.build_cartesian_2d(0usize..x_max, y_min..y_max)?;
chart
.configure_mesh()
.x_desc("Generation")
.y_desc("Diversity")
.x_label_style((FontFamily::SansSerif, 12))
.y_label_style((FontFamily::SansSerif, 12))
.draw()?;
chart
.draw_series(LineSeries::new(
stats.iter().map(|s| (s.generation, s.diversity)),
&BLUE,
))?
.label("Diversity")
.legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], BLUE));
chart
.configure_series_labels()
.background_style(WHITE.mix(0.8))
.border_style(BLACK)
.label_font((FontFamily::SansSerif, 12))
.draw()?;
Ok(())
}
fn draw_histogram_chart<DB>(
root: &DrawingArea<DB, Shift>,
fitness_values: &[f64],
) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>>
where
DB: DrawingBackend,
DB::ErrorType: std::error::Error + Send + Sync,
{
const NUM_BINS: u32 = 20;
let min = fitness_values.iter().cloned().fold(f64::INFINITY, f64::min);
let max = fitness_values
.iter()
.cloned()
.fold(f64::NEG_INFINITY, f64::max);
let bin_width = if (max - min).abs() < f64::EPSILON {
1.0
} else {
(max - min) / NUM_BINS as f64
};
let max_count = fitness_values.len() as u32;
let mut chart = ChartBuilder::on(root)
.caption("Fitness Distribution", (FontFamily::SansSerif, 20))
.margin(20)
.x_label_area_size(50)
.y_label_area_size(60)
.build_cartesian_2d((0u32..NUM_BINS).into_segmented(), 0u32..max_count)?;
chart
.configure_mesh()
.x_desc("Fitness bin")
.y_desc("Count")
.x_label_formatter(&|v| match v {
SegmentValue::Exact(b) => {
let val = min + (*b as f64) * bin_width;
format!("{:.2}", val)
}
SegmentValue::CenterOf(b) => {
let val = min + (*b as f64 + 0.5) * bin_width;
format!("{:.2}", val)
}
SegmentValue::Last => format!("{:.2}", max),
})
.x_label_style((FontFamily::SansSerif, 10))
.y_label_style((FontFamily::SansSerif, 12))
.draw()?;
chart.draw_series(
Histogram::vertical(&chart)
.style(BLUE.mix(0.5).filled())
.margin(1)
.data(fitness_values.iter().map(|&v| {
let bin = ((v - min) / bin_width).min((NUM_BINS - 1) as f64).max(0.0) as u32;
(bin, 1u32)
})),
)?;
Ok(())
}
pub fn plot_fitness(stats: &[GenerationStats], path: &str) -> Result<(), VisualizationError> {
if stats.len() < 2 {
return Err(VisualizationError::InsufficientData);
}
match Path::new(path).extension().and_then(|e| e.to_str()) {
Some("png") => {
#[cfg(not(target_arch = "wasm32"))]
{
let root = BitMapBackend::new(path, (800, 600)).into_drawing_area();
root.fill(&WHITE)
.map_err(|e| VisualizationError::DrawingError(format!("{:?}", e)))?;
draw_fitness_chart(&root, stats)
.map_err(|e| VisualizationError::DrawingError(format!("{:?}", e)))?;
root.present()
.map_err(|e| VisualizationError::DrawingError(format!("{:?}", e)))?;
}
#[cfg(target_arch = "wasm32")]
{
return Err(VisualizationError::UnsupportedFormat);
}
}
Some("svg") => {
#[cfg(not(target_arch = "wasm32"))]
{
let root = SVGBackend::new(path, (800, 600)).into_drawing_area();
root.fill(&WHITE)
.map_err(|e| VisualizationError::DrawingError(format!("{:?}", e)))?;
draw_fitness_chart(&root, stats)
.map_err(|e| VisualizationError::DrawingError(format!("{:?}", e)))?;
root.present()
.map_err(|e| VisualizationError::DrawingError(format!("{:?}", e)))?;
}
#[cfg(target_arch = "wasm32")]
{
return Err(VisualizationError::UnsupportedFormat);
}
}
_ => return Err(VisualizationError::UnsupportedFormat),
}
Ok(())
}
pub fn plot_diversity(stats: &[GenerationStats], path: &str) -> Result<(), VisualizationError> {
if stats.len() < 2 {
return Err(VisualizationError::InsufficientData);
}
match Path::new(path).extension().and_then(|e| e.to_str()) {
Some("png") => {
#[cfg(not(target_arch = "wasm32"))]
{
let root = BitMapBackend::new(path, (800, 600)).into_drawing_area();
root.fill(&WHITE)
.map_err(|e| VisualizationError::DrawingError(format!("{:?}", e)))?;
draw_diversity_chart(&root, stats)
.map_err(|e| VisualizationError::DrawingError(format!("{:?}", e)))?;
root.present()
.map_err(|e| VisualizationError::DrawingError(format!("{:?}", e)))?;
}
#[cfg(target_arch = "wasm32")]
{
return Err(VisualizationError::UnsupportedFormat);
}
}
Some("svg") => {
#[cfg(not(target_arch = "wasm32"))]
{
let root = SVGBackend::new(path, (800, 600)).into_drawing_area();
root.fill(&WHITE)
.map_err(|e| VisualizationError::DrawingError(format!("{:?}", e)))?;
draw_diversity_chart(&root, stats)
.map_err(|e| VisualizationError::DrawingError(format!("{:?}", e)))?;
root.present()
.map_err(|e| VisualizationError::DrawingError(format!("{:?}", e)))?;
}
#[cfg(target_arch = "wasm32")]
{
return Err(VisualizationError::UnsupportedFormat);
}
}
_ => return Err(VisualizationError::UnsupportedFormat),
}
Ok(())
}
pub fn plot_histogram(fitness_values: &[f64], path: &str) -> Result<(), VisualizationError> {
if fitness_values.len() < 2 {
return Err(VisualizationError::InsufficientData);
}
match Path::new(path).extension().and_then(|e| e.to_str()) {
Some("png") => {
#[cfg(not(target_arch = "wasm32"))]
{
let root = BitMapBackend::new(path, (800, 600)).into_drawing_area();
root.fill(&WHITE)
.map_err(|e| VisualizationError::DrawingError(format!("{:?}", e)))?;
draw_histogram_chart(&root, fitness_values)
.map_err(|e| VisualizationError::DrawingError(format!("{:?}", e)))?;
root.present()
.map_err(|e| VisualizationError::DrawingError(format!("{:?}", e)))?;
}
#[cfg(target_arch = "wasm32")]
{
return Err(VisualizationError::UnsupportedFormat);
}
}
Some("svg") => {
#[cfg(not(target_arch = "wasm32"))]
{
let root = SVGBackend::new(path, (800, 600)).into_drawing_area();
root.fill(&WHITE)
.map_err(|e| VisualizationError::DrawingError(format!("{:?}", e)))?;
draw_histogram_chart(&root, fitness_values)
.map_err(|e| VisualizationError::DrawingError(format!("{:?}", e)))?;
root.present()
.map_err(|e| VisualizationError::DrawingError(format!("{:?}", e)))?;
}
#[cfg(target_arch = "wasm32")]
{
return Err(VisualizationError::UnsupportedFormat);
}
}
_ => return Err(VisualizationError::UnsupportedFormat),
}
Ok(())
}
fn compute_pareto_range(iter: impl Iterator<Item = f64>) -> (f64, f64) {
let mut min = f64::INFINITY;
let mut max = f64::NEG_INFINITY;
for v in iter {
if v < min {
min = v;
}
if v > max {
max = v;
}
}
if (max - min).abs() < f64::EPSILON {
max = min + 1.0;
}
(min, max)
}
fn draw_pareto_2d_chart<DB>(
root: &DrawingArea<DB, Shift>,
points: &[(f64, f64)],
) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>>
where
DB: DrawingBackend,
DB::ErrorType: std::error::Error + Send + Sync,
{
let (x_min, x_max) = compute_pareto_range(points.iter().map(|p| p.0));
let (y_min, y_max) = compute_pareto_range(points.iter().map(|p| p.1));
let mut chart = ChartBuilder::on(root)
.caption("Pareto Front", (FontFamily::SansSerif, 20))
.margin(20)
.x_label_area_size(40)
.y_label_area_size(60)
.build_cartesian_2d(x_min..x_max, y_min..y_max)?;
chart
.configure_mesh()
.x_desc("f1")
.y_desc("f2")
.x_label_style((FontFamily::SansSerif, 12))
.y_label_style((FontFamily::SansSerif, 12))
.draw()?;
let mut sorted: Vec<(f64, f64)> = points.to_vec();
sorted.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
chart.draw_series(LineSeries::new(
sorted.iter().copied(),
BLUE.mix(0.3).stroke_width(1),
))?;
chart
.draw_series(
points
.iter()
.map(|&(x, y)| Circle::new((x, y), 3, BLUE.filled())),
)?
.label("Pareto front")
.legend(|(x, y)| Circle::new((x + 10, y), 3, BLUE.filled()));
chart
.configure_series_labels()
.background_style(WHITE.mix(0.8))
.border_style(BLACK)
.label_font((FontFamily::SansSerif, 12))
.draw()?;
Ok(())
}
pub fn plot_pareto_front_2d(points: &[(f64, f64)], path: &str) -> Result<(), VisualizationError> {
if points.len() < 2 {
return Err(VisualizationError::InsufficientData);
}
match Path::new(path).extension().and_then(|e| e.to_str()) {
Some("png") => {
#[cfg(not(target_arch = "wasm32"))]
{
let root = BitMapBackend::new(path, (800, 600)).into_drawing_area();
root.fill(&WHITE)
.map_err(|e| VisualizationError::DrawingError(format!("{:?}", e)))?;
draw_pareto_2d_chart(&root, points)
.map_err(|e| VisualizationError::DrawingError(format!("{:?}", e)))?;
root.present()
.map_err(|e| VisualizationError::DrawingError(format!("{:?}", e)))?;
}
#[cfg(target_arch = "wasm32")]
{
return Err(VisualizationError::UnsupportedFormat);
}
}
Some("svg") => {
#[cfg(not(target_arch = "wasm32"))]
{
let root = SVGBackend::new(path, (800, 600)).into_drawing_area();
root.fill(&WHITE)
.map_err(|e| VisualizationError::DrawingError(format!("{:?}", e)))?;
draw_pareto_2d_chart(&root, points)
.map_err(|e| VisualizationError::DrawingError(format!("{:?}", e)))?;
root.present()
.map_err(|e| VisualizationError::DrawingError(format!("{:?}", e)))?;
}
#[cfg(target_arch = "wasm32")]
{
return Err(VisualizationError::UnsupportedFormat);
}
}
_ => return Err(VisualizationError::UnsupportedFormat),
}
Ok(())
}
fn draw_pareto_3d_chart<DB>(
root: &DrawingArea<DB, Shift>,
points: &[(f64, f64, f64)],
) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>>
where
DB: DrawingBackend,
DB::ErrorType: std::error::Error + Send + Sync,
{
let panels = root.split_evenly((1, 3));
let panel_axes: [(usize, usize, &str, &str, &str); 3] = [
(0, 1, "f1 vs f2", "f1", "f2"),
(0, 2, "f1 vs f3", "f1", "f3"),
(1, 2, "f2 vs f3", "f2", "f3"),
];
for i in 0..panels.len() {
let (xi, yi, title, x_label, y_label) = panel_axes[i];
let (x_min, x_max) = compute_pareto_range(points.iter().map(|p| match xi {
0 => p.0,
1 => p.1,
_ => p.2,
}));
let (y_min, y_max) = compute_pareto_range(points.iter().map(|p| match yi {
0 => p.0,
1 => p.1,
_ => p.2,
}));
let mut chart = ChartBuilder::on(&panels[i])
.caption(title, (FontFamily::SansSerif, 16))
.margin(15)
.x_label_area_size(40)
.y_label_area_size(55)
.build_cartesian_2d(x_min..x_max, y_min..y_max)?;
chart
.configure_mesh()
.x_desc(x_label)
.y_desc(y_label)
.x_label_style((FontFamily::SansSerif, 11))
.y_label_style((FontFamily::SansSerif, 11))
.draw()?;
chart.draw_series(points.iter().map(|p| {
let px = match xi {
0 => p.0,
1 => p.1,
_ => p.2,
};
let py = match yi {
0 => p.0,
1 => p.1,
_ => p.2,
};
Circle::new((px, py), 3, BLUE.filled())
}))?;
}
Ok(())
}
pub fn plot_pareto_front_3d(
points: &[(f64, f64, f64)],
path: &str,
) -> Result<(), VisualizationError> {
if points.len() < 2 {
return Err(VisualizationError::InsufficientData);
}
match Path::new(path).extension().and_then(|e| e.to_str()) {
Some("png") => {
#[cfg(not(target_arch = "wasm32"))]
{
let root = BitMapBackend::new(path, (1200, 400)).into_drawing_area();
root.fill(&WHITE)
.map_err(|e| VisualizationError::DrawingError(format!("{:?}", e)))?;
draw_pareto_3d_chart(&root, points)
.map_err(|e| VisualizationError::DrawingError(format!("{:?}", e)))?;
root.present()
.map_err(|e| VisualizationError::DrawingError(format!("{:?}", e)))?;
}
#[cfg(target_arch = "wasm32")]
{
return Err(VisualizationError::UnsupportedFormat);
}
}
Some("svg") => {
#[cfg(not(target_arch = "wasm32"))]
{
let root = SVGBackend::new(path, (1200, 400)).into_drawing_area();
root.fill(&WHITE)
.map_err(|e| VisualizationError::DrawingError(format!("{:?}", e)))?;
draw_pareto_3d_chart(&root, points)
.map_err(|e| VisualizationError::DrawingError(format!("{:?}", e)))?;
root.present()
.map_err(|e| VisualizationError::DrawingError(format!("{:?}", e)))?;
}
#[cfg(target_arch = "wasm32")]
{
return Err(VisualizationError::UnsupportedFormat);
}
}
_ => return Err(VisualizationError::UnsupportedFormat),
}
Ok(())
}
fn draw_true_fitness_calls_chart<DB>(
root: &DrawingArea<DB, Shift>,
data: &[(usize, u64)],
) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>>
where
DB: DrawingBackend,
DB::ErrorType: std::error::Error + Send + Sync,
{
let max_gen = data.iter().map(|&(g, _)| g).max().unwrap_or(0);
let x_max = if max_gen == 0 { 1 } else { max_gen };
let (y_min, y_max) = compute_pareto_range(data.iter().map(|&(_, v)| v as f64));
let mut chart = ChartBuilder::on(root)
.caption(
"True Fitness Calls over Generations",
(FontFamily::SansSerif, 20),
)
.margin(20)
.x_label_area_size(40)
.y_label_area_size(60)
.build_cartesian_2d(0usize..x_max + 1, y_min..y_max)?;
chart
.configure_mesh()
.x_desc("Generation")
.y_desc("True fitness calls")
.x_label_style((FontFamily::SansSerif, 12))
.y_label_style((FontFamily::SansSerif, 12))
.draw()?;
chart
.draw_series(LineSeries::new(
data.iter().map(|&(g, v)| (g, v as f64)),
&MAGENTA,
))?
.label("True fitness calls")
.legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], MAGENTA));
chart
.configure_series_labels()
.background_style(WHITE.mix(0.8))
.border_style(BLACK)
.label_font((FontFamily::SansSerif, 12))
.draw()?;
Ok(())
}
pub fn plot_true_fitness_calls(
stats: &[GenerationStats],
path: &str,
) -> Result<(), VisualizationError> {
let data: Vec<(usize, u64)> = stats
.iter()
.filter_map(|s| s.true_fitness_calls.map(|v| (s.generation, v)))
.collect();
if data.len() < 2 {
return Err(VisualizationError::InsufficientData);
}
match Path::new(path).extension().and_then(|e| e.to_str()) {
Some("png") => {
#[cfg(not(target_arch = "wasm32"))]
{
let root = BitMapBackend::new(path, (800, 600)).into_drawing_area();
root.fill(&WHITE)
.map_err(|e| VisualizationError::DrawingError(format!("{:?}", e)))?;
draw_true_fitness_calls_chart(&root, &data)
.map_err(|e| VisualizationError::DrawingError(format!("{:?}", e)))?;
root.present()
.map_err(|e| VisualizationError::DrawingError(format!("{:?}", e)))?;
}
#[cfg(target_arch = "wasm32")]
{
return Err(VisualizationError::UnsupportedFormat);
}
}
Some("svg") => {
#[cfg(not(target_arch = "wasm32"))]
{
let root = SVGBackend::new(path, (800, 600)).into_drawing_area();
root.fill(&WHITE)
.map_err(|e| VisualizationError::DrawingError(format!("{:?}", e)))?;
draw_true_fitness_calls_chart(&root, &data)
.map_err(|e| VisualizationError::DrawingError(format!("{:?}", e)))?;
root.present()
.map_err(|e| VisualizationError::DrawingError(format!("{:?}", e)))?;
}
#[cfg(target_arch = "wasm32")]
{
return Err(VisualizationError::UnsupportedFormat);
}
}
_ => return Err(VisualizationError::UnsupportedFormat),
}
Ok(())
}