use super::Canvas;
use crate::chart::cx_child as child;
use crate::parse::{
chart_svg::{number as numeric, on},
util::escape_xml_text,
};
use roxmltree::Node;
use std::fmt::Write;
fn style_entry(node: Node) -> Option<&'static str> {
match node.tag_name().name() {
"axis" if child(node, "catScaling").is_some() => Some("categoryAxis"),
"axis" => Some("valueAxis"),
"legend" => Some("legend"),
"dataLabels" | "dataLabel" => Some("dataLabel"),
"majorGridlines" => Some("gridlineMajor"),
"minorGridlines" => Some("gridlineMinor"),
"connectorLines" => Some("seriesLine"),
_ => None,
}
}
pub(super) struct Font {
face: String,
pub(super) size: f64,
fill: String,
bold: bool,
italic: bool,
}
impl Font {
pub(super) fn new(ctx: &Canvas, element: Option<Node>, fallback: &str) -> Self {
Self::with_defaults(ctx, element, fallback, "Cambria", 12.)
}
pub(super) fn with_defaults(
ctx: &Canvas,
element: Option<Node>,
fallback: &str,
default_face: &str,
default_size: f64,
) -> Self {
let properties = element
.and_then(|e| child(e, "txPr"))
.into_iter()
.flat_map(|n| n.descendants())
.filter(|n| matches!(n.tag_name().name(), "defRPr" | "rPr" | "endParaRPr"));
let size = properties
.clone()
.find_map(|n| numeric(n.attribute("sz")))
.map_or(default_size, |n| (n / 75.).clamp(1., 256.));
let typeface = properties.clone().find_map(|p| {
p.children()
.find(|n| n.tag_name().name() == "latin")?
.attribute("typeface")
});
let face = ctx.fonts.as_ref().map_or_else(
|| {
typeface
.filter(|s| !s.starts_with('+'))
.unwrap_or(default_face)
},
|fonts| fonts.resolve(typeface),
);
let fill = properties
.clone()
.find_map(|p| ctx.colors.shape_fill(p))
.or_else(|| {
let color = ctx.colors.style_text(style_entry(element?)?, fallback)?;
if fallback == "#fefefe" && color.eq_ignore_ascii_case("#595959") {
None
} else {
Some(color)
}
})
.unwrap_or_else(|| fallback.into());
Self {
face: face.to_owned(),
size,
fill,
bold: properties.clone().any(|p| on(p.attribute("b"))),
italic: properties.clone().any(|p| on(p.attribute("i"))),
}
}
pub(super) fn text(&self, out: &mut String, x: f64, y: f64, anchor: &str, label: &str) {
write!(out, r#"<text x="{x:.2}" y="{y:.2}" text-anchor="{anchor}" fill="{}" font-family="{}" font-size="{:.2}" font-weight="{}" font-style="{}">{}</text>"#,self.fill,crate::parse::chart_svg::font_family(None, &self.face),self.size,if self.bold {"bold"} else {"normal"},if self.italic {"italic"} else {"normal"},escape_xml_text(label)).unwrap();
}
pub(super) fn width(&self, s: &str) -> f64 {
crate::parse::font_metrics::width(s, self.size, &self.face, self.bold, self.italic)
.unwrap_or_else(|| label_width(s, self.size, &self.face))
}
pub(super) fn face(&self) -> &str {
&self.face
}
}
pub(super) struct Stroke {
color: String,
width: f64,
}
impl Stroke {
pub(super) fn new(ctx: &Canvas, node: Option<Node>, default_color: &str) -> Self {
let props = node.and_then(|n| child(n, "spPr"));
let color = props
.and_then(|p| ctx.colors.shape_stroke(p))
.or_else(|| ctx.colors.style_stroke(style_entry(node?)?, default_color))
.unwrap_or_else(|| default_color.into());
let width = props
.and_then(|p| p.children().find(|n| n.tag_name().name() == "ln"))
.and_then(|n| numeric(n.attribute("w")))
.map_or(1., |w| (w / 9525.).max(0.));
Self { color, width }
}
pub(super) fn line(&self, out: &mut String, x: f64, y: f64, x2: f64, y2: f64) {
if self.width > 0. && self.color != "none" {
write!(out,r#"<path d="M{x:.2},{y:.2} L{x2:.2},{y2:.2}" fill="none" stroke="{}" stroke-width="{:.2}"/>"#,self.color,self.width).unwrap();
}
}
}
fn label_width(s: &str, size: f64, font: &str) -> f64 {
size * s
.chars()
.map(|c| match c {
'0'..='9' if font == "Calibri" => 0.507,
'0'..='9' => 0.554,
'.' | 'i' | 'l' | ' ' => 0.24,
'I' | 'f' | 'r' | 't' | '-' => 0.35,
'+' | 'E' => 0.55,
'M' | 'W' | 'm' | 'w' => 0.81,
_ if c.is_ascii() => 0.51,
_ => 1.,
})
.sum::<f64>()
}
pub(super) use super::super::chart_svg::format_value;
pub(super) fn data_label(
labels: Option<Node>,
category: &str,
value: &str,
series: Option<&str>,
) -> Option<String> {
let node = labels?;
let v = child(node, "visibility")?;
let mut parts = Vec::new();
if on(v.attribute("seriesName")) {
parts.extend(series);
}
if on(v.attribute("categoryName")) {
parts.push(category);
}
if on(v.attribute("value")) {
parts.push(value);
}
(!parts.is_empty()).then(|| parts.join(", "))
}
pub(super) fn legend(
ctx: &Canvas,
out: &mut String,
entries: &[(&str, &str, usize)],
left: &mut f64,
top: &mut f64,
right: &mut f64,
bottom: &mut f64,
) {
let Some(node) = ctx
.root
.descendants()
.find(|n| n.has_tag_name((crate::chart::CHARTEX_NS, "legend")))
else {
return;
};
if entries.is_empty() {
return;
}
let font = Font::new(ctx, Some(node), "#595959");
let spacing = font.size;
let key = font.size * 0.775;
let widths: Vec<_> = entries
.iter()
.map(|(s, _, _)| font.width(s) + 2. * spacing)
.collect();
let mut width = widths.iter().sum::<f64>() - spacing * 2. / 3.;
let height = (entries.len() as f64) * spacing * 1.65;
let side_width = widths.iter().copied().fold(0., f64::max) + spacing * 0.75;
let pos = node.attribute("pos").unwrap_or("t");
let reserve = !on(node.attribute("overlay"));
let side = matches!(pos, "l" | "r");
let hierarchy = ctx
.data
.series
.first()
.is_some_and(|series| matches!(series.chart_type.as_str(), "treemap" | "sunburst"));
let rows = if ctx.colors.spreadsheet_defaults && hierarchy && !side {
(width / (ctx.width * 0.8).max(spacing)).ceil().max(1.) as usize
} else {
1
};
let columns = entries.len().div_ceil(rows).max(1);
let rows = entries.len().div_ceil(columns);
let mut column_widths = vec![0_f64; columns];
for (i, &value) in widths.iter().enumerate() {
column_widths[i % columns] = column_widths[i % columns].max(value);
}
if rows > 1 {
width = column_widths.iter().sum::<f64>() - spacing * 2. / 3.;
}
let extra_height = (rows - 1) as f64 * spacing * 2.;
let align = node.attribute("align").unwrap_or("ctr");
let align_at = |extent: f64, size: f64| match align {
"min" | "l" | "t" => 8.,
"max" | "r" | "b" => extent - size - 8.,
_ => (extent - size) / 2.,
};
let x = if side {
if pos == "l" {
8.
} else {
ctx.width - side_width
}
} else {
align_at(ctx.width, width)
};
let y = if side {
align_at(ctx.height, height) + spacing
} else if pos == "b" {
ctx.height - spacing * 1.5 - extra_height
} else {
ctx.title_bottom + spacing * 1.35
};
if reserve {
match pos {
"r" => *right -= side_width,
"l" => *left += side_width,
"b" => *bottom -= 2.75 * spacing + extra_height,
_ => *top += 3.2 * spacing + extra_height,
}
}
let start_x = x;
let (mut x, mut y) = (x, y);
for (i, (name, color, series)) in entries.iter().enumerate() {
ctx.colors
.begin_hit(out, "legendEntry", ctx.source_indices[*series], None);
let y0 = y - key / 2.;
write!(
out,
r#"<rect x="{x:.2}" y="{y0:.2}" width="{key:.2}" height="{key:.2}" fill="{color}"/>"#
)
.unwrap();
font.text(out, x + spacing * 1.17, y + spacing * 0.367, "start", name);
ctx.colors.end_hit(out);
if side {
y += spacing * 1.65;
} else if rows > 1 {
x += column_widths[i % columns];
if (i + 1) % columns == 0 {
x = start_x;
y += spacing * 2.;
}
} else {
x += widths[i];
}
}
}