mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 18:46:57 -06:00
Charts: 100 % stacked axes count in percent; linked axis formats come from the data
This commit is contained in:
@@ -314,7 +314,12 @@ impl Chart {
|
||||
/// A value axis label in the axis' number format and the chart's language.
|
||||
pub fn axis_label(&self, secondary: bool, v: f64) -> String {
|
||||
let code = if secondary { &self.y2_format } else { &self.y_format };
|
||||
crate::numfmt::format(code.as_deref().unwrap_or("General"), v, self.lang.as_deref())
|
||||
// A 100 % stacked chart's value axis counts in percent unless the file says otherwise.
|
||||
let code = match code.as_deref() {
|
||||
None | Some("General") if !secondary && self.kind.percent() => "0%",
|
||||
c => c.unwrap_or("General"),
|
||||
};
|
||||
crate::numfmt::format(code, v, self.lang.as_deref())
|
||||
}
|
||||
|
||||
/// A data label of series `s`: its number format, else its axis', in the chart's language.
|
||||
@@ -480,6 +485,16 @@ mod tests {
|
||||
assert_eq!(c.data_label(&c.series[1], 0.0), "- ₽");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_percent_axes_count_in_percent() {
|
||||
let mut c = Chart::sample(ChartType::PercentColumn);
|
||||
assert_eq!(c.axis_label(false, 0.5), "50%");
|
||||
c.y_format = Some("0.0%".into());
|
||||
assert_eq!(c.axis_label(false, 0.5), "50.0%");
|
||||
let c = Chart::sample(ChartType::Column);
|
||||
assert_eq!(c.axis_label(false, 0.5), "0.5");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn combo_axis_ranges() {
|
||||
// Spend in columns on the primary axis, clicks as a line on the secondary one.
|
||||
|
||||
@@ -249,7 +249,22 @@ pub(crate) fn chart_from(root: &El) -> Option<Chart> {
|
||||
let by_id = |id: &Option<String>| pa.children_named("valAx").find(|a| id.is_some() && a.child_val("axId") == id.as_deref());
|
||||
let val_ax = by_id(&primary_val).or_else(|| pa.children_named("valAx").find(vertical)).or_else(|| pa.children_named("valAx").last());
|
||||
let num = |a: &El, k: &str| a.child_val(k).and_then(|v| v.parse::<f64>().ok()).filter(|v| v.is_finite());
|
||||
let format_code = |a: &El| a.child("numFmt").and_then(|n| n.attr("formatCode")).map(str::to_string);
|
||||
// An axis' number format: its own unless linked to the source (`sourceLinked="1"`, or no
|
||||
// c:numFmt at all), in which case the data's format of the first series plotted on it.
|
||||
let source_format = |ax: &El| {
|
||||
let id = ax.child_val("axId");
|
||||
pa.elements()
|
||||
.filter(|g| g.children_named("axId").any(|a| a.attr("val") == id))
|
||||
.flat_map(|g| g.children_named("ser"))
|
||||
.find_map(|s| {
|
||||
s.child("val").or_else(|| s.child("yVal")).and_then(|v| v.find("numCache")).and_then(|c| c.child("formatCode")).map(|f| f.text())
|
||||
})
|
||||
.filter(|f| !f.is_empty() && f != "General")
|
||||
};
|
||||
let format_code = |a: &El| match a.child("numFmt") {
|
||||
Some(n) if n.attr("sourceLinked") != Some("1") => n.attr("formatCode").map(str::to_string),
|
||||
n => source_format(a).or_else(|| n.and_then(|n| n.attr("formatCode")).map(str::to_string)),
|
||||
};
|
||||
let deleted = |a: &El| a.child("delete").and_then(|d| d.bool("val")) == Some(true);
|
||||
if c.kind == ChartType::Combo && c.series.iter().any(|s| s.secondary_axis) {
|
||||
let primary_id = val_ax.and_then(|a| a.child_val("axId"));
|
||||
@@ -363,6 +378,20 @@ mod tests {
|
||||
assert!(c.series.iter().all(|s| !s.secondary_axis));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linked_axis_formats_come_from_the_data() {
|
||||
// No c:numFmt on the value axis, or a source-linked one: the series' own format.
|
||||
let xml = |ax_fmt: &str| {
|
||||
format!(
|
||||
r#"<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><c:chart><c:plotArea><c:barChart><c:barDir val="col"/><c:ser><c:idx val="0"/><c:val><c:numRef><c:numCache><c:formatCode>#,##0</c:formatCode><c:ptCount val="1"/><c:pt idx="0"><c:v>1200</c:v></c:pt></c:numCache></c:numRef></c:val></c:ser><c:axId val="1"/><c:axId val="2"/></c:barChart><c:catAx><c:axId val="1"/></c:catAx><c:valAx><c:axId val="2"/>{ax_fmt}<c:axPos val="l"/></c:valAx></c:plotArea></c:chart></c:chartSpace>"#
|
||||
)
|
||||
};
|
||||
let fmt = |x: &str| read_chart_xml(xml(x).as_bytes()).unwrap().y_format;
|
||||
assert_eq!(fmt("").as_deref(), Some("#,##0"));
|
||||
assert_eq!(fmt(r#"<c:numFmt formatCode="General" sourceLinked="1"/>"#).as_deref(), Some("#,##0"));
|
||||
assert_eq!(fmt(r#"<c:numFmt formatCode="0.0" sourceLinked="0"/>"#).as_deref(), Some("0.0"));
|
||||
}
|
||||
|
||||
#[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
|
||||
|
||||
@@ -289,7 +289,10 @@ fn axes(w: &mut W, c: &Chart, g: Group) {
|
||||
if let Some(t) = &c.axis_titles.1 {
|
||||
title(w, t);
|
||||
}
|
||||
w.empty("c:numFmt", A::new().a("formatCode", if c.kind.percent() { "0%" } else { "General" }).a("sourceLinked", 1));
|
||||
match c.y_format.as_deref().filter(|f| !f.is_empty()) {
|
||||
Some(f) => w.empty("c:numFmt", A::new().a("formatCode", f).a("sourceLinked", 0)),
|
||||
None => w.empty("c:numFmt", A::new().a("formatCode", if c.kind.percent() { "0%" } else { "General" }).a("sourceLinked", 1)),
|
||||
}
|
||||
w.val("c:majorTickMark", "none");
|
||||
w.val("c:minorTickMark", "none");
|
||||
w.val("c:tickLblPos", "nextTo");
|
||||
|
||||
Reference in New Issue
Block a user