use clap::Args;
use kuva::plot::line::{LinePlot, LineStyle};
use kuva::render::layout::Layout;
use kuva::render::palette::Palette;
use kuva::render::plots::Plot;
use kuva::render::render::render_multiple;
use crate::data::{ColSpec, DataTable, InputArgs};
use crate::layout_args::{
apply_axis_args, apply_base_args, apply_log_args, AxisArgs, BaseArgs, LogArgs,
};
use crate::output::write_output;
#[derive(Args, Debug)]
pub struct LineArgs {
#[arg(long)]
pub x: Option<ColSpec>,
#[arg(long, value_delimiter = ',')]
pub y: Vec<ColSpec>,
#[arg(long)]
pub color_by: Option<ColSpec>,
#[arg(long)]
pub color: Option<String>,
#[arg(long)]
pub stroke_width: Option<f64>,
#[arg(long)]
pub dashed: bool,
#[arg(long)]
pub dotted: bool,
#[arg(long)]
pub fill: bool,
#[arg(long)]
pub legend: bool,
#[command(flatten)]
pub input: InputArgs,
#[command(flatten)]
pub base: BaseArgs,
#[command(flatten)]
pub axis: AxisArgs,
#[command(flatten)]
pub log: LogArgs,
}
pub fn run(args: LineArgs) -> Result<(), String> {
let x_spec = args.x.clone().unwrap_or(ColSpec::Index(0));
let y_specs: Vec<ColSpec> = if args.y.is_empty() {
vec![ColSpec::Index(1)]
} else {
args.y.clone()
};
let mut proj: Vec<ColSpec> = std::iter::once(x_spec).chain(y_specs).collect();
if let Some(ref c) = args.color_by {
proj.push(c.clone());
}
let table = DataTable::parse(
args.input.input.as_deref(),
args.input.no_header,
args.input.delimiter,
&proj,
)?;
let x_col = args.x.unwrap_or(ColSpec::Index(0));
let y_cols: Vec<ColSpec> = if args.y.is_empty() {
vec![ColSpec::Index(1)]
} else {
args.y
};
let color = args.color.unwrap_or_else(|| "steelblue".to_string());
let stroke_width = args.stroke_width.unwrap_or(2.0);
let line_style = if args.dashed {
LineStyle::Dashed
} else if args.dotted {
LineStyle::Dotted
} else {
LineStyle::Solid
};
let fill = args.fill;
let legend = args.legend;
let plots: Vec<Plot> = if let Some(color_by) = args.color_by {
if y_cols.len() > 1 {
return Err(
"--color-by and multiple --y columns are mutually exclusive. \
Use one or the other to create multiple series."
.to_string(),
);
}
let y_col = &y_cols[0];
let groups = table.group_by(&color_by)?;
let palette = Palette::category10();
let colors: Vec<String> = (0..groups.len()).map(|i| palette[i].to_string()).collect();
groups
.into_iter()
.zip(colors)
.map(|((name, subtable), grp_color)| {
let xs = subtable.col_f64(&x_col)?;
let ys = subtable.col_f64(y_col)?;
let mut data: Vec<(f64, f64)> = xs.into_iter().zip(ys).collect();
data.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let mut plot = LinePlot::new()
.with_data(data)
.with_color(&grp_color)
.with_stroke_width(stroke_width)
.with_line_style(line_style.clone());
if fill {
plot = plot.with_fill();
}
if legend {
plot = plot.with_legend(name);
}
Ok(Plot::Line(plot))
})
.collect::<Result<Vec<_>, String>>()?
} else if y_cols.len() > 1 {
let palette = Palette::category10();
let xs = table.col_f64(&x_col)?;
y_cols
.iter()
.enumerate()
.map(|(i, y_col)| {
let series_name = col_display_name(&table, y_col);
let ys = table.col_f64(y_col)?;
let mut data: Vec<(f64, f64)> = xs.iter().copied().zip(ys).collect();
data.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let grp_color = palette[i].to_string();
let mut plot = LinePlot::new()
.with_data(data)
.with_color(&grp_color)
.with_stroke_width(stroke_width)
.with_line_style(line_style.clone());
if fill {
plot = plot.with_fill();
}
if legend {
plot = plot.with_legend(series_name);
}
Ok(Plot::Line(plot))
})
.collect::<Result<Vec<_>, String>>()?
} else {
let y_col = &y_cols[0];
let xs = table.col_f64(&x_col)?;
let ys = table.col_f64(y_col)?;
let mut data: Vec<(f64, f64)> = xs.into_iter().zip(ys).collect();
data.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let mut plot = LinePlot::new()
.with_data(data)
.with_color(&color)
.with_stroke_width(stroke_width)
.with_line_style(line_style);
if fill {
plot = plot.with_fill();
}
vec![Plot::Line(plot)]
};
let layout = Layout::auto_from_plots(&plots);
let layout = apply_base_args(layout, &args.base);
let layout = apply_axis_args(layout, &args.axis);
let layout = apply_log_args(layout, &args.log);
let scene = render_multiple(plots, layout);
write_output(scene, &args.base)
}
fn col_display_name(table: &DataTable, col: &ColSpec) -> String {
match col {
ColSpec::Name(n) => n.clone(),
ColSpec::Index(i) => table
.header
.as_ref()
.and_then(|h| h.get(*i))
.cloned()
.unwrap_or_else(|| format!("col_{i}")),
}
}