use plotlars_core::components::axis::AxisType;
use plotlars_core::components::{Axis, Legend, Rgb, Text};
use plotlars_core::ir::layout::LayoutIR;
use plotlars_core::policy::report_unsupported;
const DEFAULT_LABEL_FONT: &str = "sans-serif";
const DEFAULT_AXIS_LABEL_SIZE: u32 = 15;
fn label_font(text: Option<&Text>) -> String {
text.map(|t| {
if t.font.is_empty() {
DEFAULT_LABEL_FONT.to_string()
} else {
t.font.clone()
}
})
.unwrap_or_else(|| DEFAULT_LABEL_FONT.to_string())
}
fn label_size(text: Option<&Text>) -> u32 {
text.map(|t| {
if t.size == 12 {
DEFAULT_AXIS_LABEL_SIZE
} else {
t.size as u32
}
})
.unwrap_or(DEFAULT_AXIS_LABEL_SIZE)
}
fn label_color(text: Option<&Text>) -> Rgb {
text.map(|t| t.color).unwrap_or(Rgb(0, 0, 0))
}
fn label_x(text: Option<&Text>) -> Option<f64> {
text.and_then(|t| {
if (t.x - 0.5).abs() < f64::EPSILON {
None
} else {
Some(t.x)
}
})
}
fn label_y(text: Option<&Text>) -> Option<f64> {
text.and_then(|t| {
if (t.y - 0.9).abs() < f64::EPSILON {
None
} else {
Some(t.y)
}
})
}
pub(crate) struct LayoutConfig {
pub title: Option<String>,
pub title_font_size: u32,
pub title_font: String,
pub title_color: Option<Rgb>,
pub title_x: Option<f64>,
pub title_y: Option<f64>,
pub x_label: Option<String>,
pub x_label_font: String,
pub x_label_size: u32,
pub x_label_color: Rgb,
pub x_label_x: Option<f64>,
pub x_label_y: Option<f64>,
pub y_label: Option<String>,
pub y_label_font: String,
pub y_label_size: u32,
pub y_label_color: Rgb,
pub y_label_x: Option<f64>,
pub y_label_y: Option<f64>,
pub x_axis: Option<Axis>,
pub y_axis: Option<Axis>,
pub y2_axis: Option<Axis>,
pub y2_label: Option<String>,
pub y2_label_font: String,
pub y2_label_size: u32,
pub y2_label_color: Rgb,
pub legend: Option<Legend>,
pub legend_title: Option<String>,
pub x_range: Option<(f64, f64)>,
pub y_range: Option<(f64, f64)>,
}
pub(crate) fn extract_layout_config(
layout: &LayoutIR,
unsupported: &mut Vec<String>,
) -> LayoutConfig {
let title = layout.title.as_ref().map(|t| t.content.clone());
let title_font_size = layout.title.as_ref().map(|t| t.size).unwrap_or(20) as u32;
let title_font = label_font(layout.title.as_ref());
let title_color = layout.title.as_ref().map(|t| t.color);
let title_x = label_x(layout.title.as_ref());
let title_y = label_y(layout.title.as_ref());
let x_label = layout.x_title.as_ref().map(|t| t.content.clone());
let x_label_font = label_font(layout.x_title.as_ref());
let x_label_size = label_size(layout.x_title.as_ref());
let x_label_color = label_color(layout.x_title.as_ref());
let x_label_x = label_x(layout.x_title.as_ref());
let x_label_y = label_y(layout.x_title.as_ref());
let y_label = layout.y_title.as_ref().map(|t| t.content.clone());
let y_label_font = label_font(layout.y_title.as_ref());
let y_label_size = label_size(layout.y_title.as_ref());
let y_label_color = label_color(layout.y_title.as_ref());
let y_label_x = label_x(layout.y_title.as_ref());
let y_label_y = label_y(layout.y_title.as_ref());
let (x_axis, y_axis, y2_axis) = match &layout.axes_2d {
Some(axes) => (
axes.x_axis.clone(),
axes.y_axis.clone(),
axes.y2_axis.clone(),
),
None => (None, None, None),
};
let y2_label = layout.y2_title.as_ref().map(|t| t.content.clone());
let y2_label_font = label_font(layout.y2_title.as_ref());
let y2_label_size = label_size(layout.y2_title.as_ref());
let y2_label_color = label_color(layout.y2_title.as_ref());
let legend = layout.legend.clone();
let legend_title = layout.legend_title.as_ref().map(|t| t.content.clone());
let x_range = x_axis
.as_ref()
.and_then(|a| a.value_range.as_ref())
.map(|r| (r[0], r[1]));
let y_range = y_axis
.as_ref()
.and_then(|a| a.value_range.as_ref())
.map(|r| (r[0], r[1]));
if layout.z_title.is_some() {
report_unsupported("plotters", "Layout", "z_title", unsupported);
}
if layout.scene_3d.is_some() {
report_unsupported("plotters", "Layout", "scene_3d", unsupported);
}
if layout.polar.is_some() {
report_unsupported("plotters", "Layout", "polar", unsupported);
}
if layout.mapbox.is_some() {
report_unsupported("plotters", "Layout", "mapbox", unsupported);
}
if !layout.annotations.is_empty() {
report_unsupported("plotters", "Layout", "annotations", unsupported);
}
for (axis_opt, name) in [(&x_axis, "x_axis"), (&y_axis, "y_axis")] {
if let Some(axis) = axis_opt {
report_unsupported_axis_fields(axis, name, unsupported);
}
}
LayoutConfig {
title,
title_font_size,
title_font,
title_color,
title_x,
title_y,
x_label,
x_label_font,
x_label_size,
x_label_color,
x_label_x,
x_label_y,
y_label,
y_label_font,
y_label_size,
y_label_color,
y_label_x,
y_label_y,
x_axis,
y_axis,
y2_axis,
y2_label,
y2_label_font,
y2_label_size,
y2_label_color,
legend,
legend_title,
x_range,
y_range,
}
}
fn report_unsupported_axis_fields(axis: &Axis, name: &str, unsupported: &mut Vec<String>) {
if axis.axis_position.is_some() {
report_unsupported("plotters", name, "axis_position", unsupported);
}
if axis
.axis_type
.as_ref()
.is_some_and(|t| matches!(t, AxisType::MultiCategory))
{
report_unsupported("plotters", name, "axis_type", unsupported);
}
if axis.tick_direction.is_some() {
report_unsupported("plotters", name, "tick_direction", unsupported);
}
if axis.tick_width.is_some() {
report_unsupported("plotters", name, "tick_width", unsupported);
}
if axis.tick_angle.is_some() {
report_unsupported("plotters", name, "tick_angle", unsupported);
}
if axis.tick_color.is_some() {
report_unsupported("plotters", name, "tick_color", unsupported);
}
if axis.show_zero_line.is_some() {
report_unsupported("plotters", name, "show_zero_line", unsupported);
}
if axis.zero_line_color.is_some() {
report_unsupported("plotters", name, "zero_line_color", unsupported);
}
if axis.zero_line_width.is_some() {
report_unsupported("plotters", name, "zero_line_width", unsupported);
}
}
pub(crate) fn resolve_tick_size(axis: &Axis) -> Option<i32> {
let length = axis.tick_length.unwrap_or(5) as i32;
if axis.tick_length.is_some() {
Some(length)
} else {
None
}
}
pub(crate) fn format_thousands(val: f64) -> String {
let is_negative = val < 0.0;
let abs_val = val.abs();
if abs_val.fract() == 0.0 {
let int_part = abs_val as u64;
let s = int_part.to_string();
let formatted = add_thousands_sep(&s);
if is_negative {
format!("-{formatted}")
} else {
formatted
}
} else {
let s = format!("{abs_val:.2}");
let parts: Vec<&str> = s.split('.').collect();
let formatted = add_thousands_sep(parts[0]);
if is_negative {
format!("-{formatted}.{}", parts[1])
} else {
format!("{formatted}.{}", parts[1])
}
}
}
fn add_thousands_sep(s: &str) -> String {
let bytes = s.as_bytes();
let len = bytes.len();
let mut result = String::with_capacity(len + len / 3);
for (i, &b) in bytes.iter().enumerate() {
if i > 0 && (len - i) % 3 == 0 {
result.push(',');
}
result.push(b as char);
}
result
}