Charts: text at the sizes the file sets, as in PowerPoint
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7d8bb05374
commit
8afaf3f90f
|
|
@ -150,6 +150,13 @@ pub struct Chart {
|
|||
pub legend: Option<String>,
|
||||
/// `c:legend/c:overlay`: the legend is drawn over the plot instead of shrinking it.
|
||||
pub legend_overlay: bool,
|
||||
/// Text size in points of the axis labels and legend (from their `c:txPr`, else the chart's);
|
||||
/// `None`: the file gives none.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub text_size: Option<f64>,
|
||||
/// Title text size in points (its rich text or `c:txPr`).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub title_size: Option<f64>,
|
||||
pub data_labels: bool,
|
||||
pub gridlines: bool,
|
||||
pub axis_titles: (Option<String>, Option<String>),
|
||||
|
|
@ -193,6 +200,8 @@ impl Default for Chart {
|
|||
series: vec![],
|
||||
legend: Some("b".into()),
|
||||
legend_overlay: false,
|
||||
text_size: None,
|
||||
title_size: None,
|
||||
data_labels: false,
|
||||
gridlines: true,
|
||||
axis_titles: (None, None),
|
||||
|
|
|
|||
|
|
@ -277,9 +277,35 @@ pub(crate) fn chart_from(root: &El) -> Option<Chart> {
|
|||
if let Some(a) = cat_ax {
|
||||
c.axis_titles.0 = a.child("title").map(|t| rich_text(t).unwrap_or_else(|| "Axis Title".into()));
|
||||
}
|
||||
// Text sizes as the file sets them (PowerPoint draws chart text at these sizes, not scaled to
|
||||
// the chart): the axes' and legend's, else the whole chart's; the title's own.
|
||||
let chart_size = text_size(root.child("txPr"));
|
||||
c.text_size = [val_ax, cat_ax, ch.child("legend")].into_iter().flatten().find_map(|e| text_size(e.child("txPr"))).or(chart_size);
|
||||
c.title_size = ch.child("title").and_then(|t| text_size(t.child("tx").and_then(|x| x.child("rich"))).or_else(|| text_size(t.child("txPr"))));
|
||||
Some(c)
|
||||
}
|
||||
|
||||
/// The size in points of the first `a:defRPr` or `a:rPr` with one under `e` (`sz` in hundredths).
|
||||
fn text_size(e: Option<&El>) -> Option<f64> {
|
||||
fn rec(e: &El, depth: usize) -> Option<f64> {
|
||||
if depth > 8 {
|
||||
return None;
|
||||
}
|
||||
for c in e.elements() {
|
||||
if (c.is("defRPr") || c.is("rPr"))
|
||||
&& let Some(v) = c.attr("sz").and_then(|v| v.parse::<f64>().ok()).filter(|v| v.is_finite() && *v > 0.0)
|
||||
{
|
||||
return Some((v / 100.0).clamp(1.0, 400.0));
|
||||
}
|
||||
if let Some(v) = rec(c, depth + 1) {
|
||||
return Some(v);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
rec(e?, 0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -336,4 +362,30 @@ mod tests {
|
|||
let c = read_chart_xml(single.as_bytes()).unwrap();
|
||||
assert!(c.series.iter().all(|s| !s.secondary_axis));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_sizes_are_read_from_the_file() {
|
||||
// PowerPoint's new charts set the axes' and legend's text to 11.97 pt and the title's to
|
||||
// 18.62 pt; a chart that only sets its overall text size uses it everywhere.
|
||||
let ns =
|
||||
r#"xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main""#;
|
||||
let tx = |sz: u32| format!(r#"<c:txPr><a:bodyPr/><a:p><a:pPr><a:defRPr sz="{sz}"/></a:pPr></a:p></c:txPr>"#);
|
||||
let xml = format!(
|
||||
r#"<c:chartSpace {ns}><c:chart><c:title><c:tx><c:rich><a:bodyPr/><a:p><a:pPr><a:defRPr sz="1862"/></a:pPr><a:r><a:t>T</a:t></a:r></a:p></c:rich></c:tx></c:title><c:plotArea><c:barChart><c:barDir val="col"/><c:axId val="1"/><c:axId val="2"/></c:barChart><c:catAx><c:axId val="1"/>{}</c:catAx><c:valAx><c:axId val="2"/>{}</c:valAx></c:plotArea></c:chart></c:chartSpace>"#,
|
||||
tx(1197),
|
||||
tx(1197)
|
||||
);
|
||||
let c = read_chart_xml(xml.as_bytes()).unwrap();
|
||||
assert_eq!((c.text_size, c.title_size), (Some(11.97), Some(18.62)));
|
||||
let xml = format!(
|
||||
r#"<c:chartSpace {ns}><c:chart><c:plotArea><c:barChart><c:barDir val="col"/></c:barChart></c:plotArea></c:chart>{}</c:chartSpace>"#,
|
||||
tx(1400)
|
||||
);
|
||||
let c = read_chart_xml(xml.as_bytes()).unwrap();
|
||||
assert_eq!((c.text_size, c.title_size), (Some(14.0), None));
|
||||
let xml = format!(
|
||||
r#"<c:chartSpace {ns}><c:chart><c:plotArea><c:barChart><c:barDir val="col"/></c:barChart></c:plotArea></c:chart></c:chartSpace>"#
|
||||
);
|
||||
assert_eq!(read_chart_xml(xml.as_bytes()).unwrap().text_size, None, "no sizes: the renderer picks one");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -139,11 +139,14 @@ pub fn draw(ctx: &mut RenderContext, rctx: &Ctx, c: &Chart, m: Affine, w: f64, h
|
|||
rctx.pres,
|
||||
);
|
||||
}
|
||||
let base = (h.min(w) / 22.0).clamp(7.0, 18.0);
|
||||
// The file's text sizes when it has them (PowerPoint draws chart text at them); otherwise a size
|
||||
// that suits the chart.
|
||||
let base = c.text_size.unwrap_or_else(|| (h.min(w) / 22.0).clamp(7.0, 18.0));
|
||||
let title_size = c.title_size.unwrap_or(base * 1.4);
|
||||
let mut area = Rect::new(w * 0.04, h * 0.04, w * 0.96, h * 0.96);
|
||||
if let Some(t) = &c.title {
|
||||
label(ctx, rctx, m, t, Rect::new(0.0, area.y0, w, area.y0 + base * 1.8), base * 1.4, Align::Center, Anchor::Top, text_col, false);
|
||||
area.y0 += base * 2.4;
|
||||
label(ctx, rctx, m, t, Rect::new(0.0, area.y0, w, area.y0 + title_size * 1.3), title_size, Align::Center, Anchor::Top, text_col, false);
|
||||
area.y0 += title_size * 1.7;
|
||||
}
|
||||
let pie = matches!(c.kind, ChartType::Pie | ChartType::Doughnut);
|
||||
// Legend placed by `c:legendPos`.
|
||||
|
|
|
|||
Loading…
Reference in New Issue