rust-office-charts 0.1.2

Render Office Open XML charts to SVG from native Rust or WebAssembly
Documentation
use super::val;
use crate::chart::{chart_node, child, ChartPoint, ChartSeries};
use crate::parse::chart_colors::ChartColors;
use crate::parse::chart_svg::{
    font_family, font_size, format_value, number, text, text_props, text_width,
};
use roxmltree::Node;
use std::collections::BTreeMap;
use std::fmt::Write;

pub(super) struct DataLabels<'a> {
    root_props: Option<Node<'a, 'a>>,
    defaults: [BTreeMap<&'a str, Node<'a, 'a>>; 2],
    overrides: [BTreeMap<u32, Node<'a, 'a>>; 2],
    source_format: Option<&'a str>,
    default_face: &'a str,
}

impl<'a> DataLabels<'a> {
    pub(super) fn new(
        group: Node<'a, 'a>,
        series: Node<'a, 'a>,
        default_face: &'a str,
    ) -> Option<Self> {
        let defaults = [child(series, "dLbls"), child(group, "dLbls")];
        if defaults.iter().all(Option::is_none) {
            return None;
        }
        let overrides = defaults.map(|node| {
            node.into_iter()
                .flat_map(|n| n.children())
                .filter(|n| chart_node(*n, "dLbl"))
                .filter_map(|n| Some((val(n, "idx")?.parse().ok()?, n)))
                .collect()
        });
        let source_format = child(series, "val")
            .or_else(|| child(series, "yVal"))
            .and_then(|n| n.descendants().find(|c| chart_node(*c, "formatCode")))
            .and_then(|n| n.text());
        let defaults = defaults.map(|node| {
            node.into_iter()
                .flat_map(|n| n.children())
                .filter(|n| {
                    n.is_element() && chart_node(*n, n.tag_name().name()) && !chart_node(*n, "dLbl")
                })
                .map(|n| (n.tag_name().name(), n))
                .collect()
        });
        let root_props = text_props(Some(group.document().root_element()), false);
        Some(Self {
            root_props,
            defaults,
            overrides,
            source_format,
            default_face,
        })
    }

    pub(super) fn draw(
        &self,
        svg: &mut String,
        series: &ChartSeries,
        point: &ChartPoint,
        end: (f64, f64),
        start: (f64, f64),
        default_pos: &str,
        horizontal: bool,
        marker_radius: f64,
        ordinal: usize,
        colors: &ChartColors,
    ) {
        let prop = |name| {
            (0..2).find_map(|i| {
                self.overrides[i]
                    .get(&point.index)
                    .and_then(|&n| child(n, name))
                    .or_else(|| self.defaults[i].get(name).copied())
            })
        };
        let show = |name, default| {
            prop(name).map_or(default, |n| {
                !matches!(n.attribute("val"), Some("false" | "0" | "off"))
            })
        };
        if show("delete", false) {
            return;
        }
        let rich = prop("tx");
        let label = if let Some(tx) = rich {
            if let Some(body) = child(tx, "rich") {
                body.children()
                    .filter(|n| n.tag_name().name() == "p")
                    .map(|p| {
                        p.descendants()
                            .filter(|n| n.is_element())
                            .filter_map(|n| match n.tag_name().name() {
                                "t" => n.text(),
                                "br" => Some("\n"),
                                _ => None,
                            })
                            .collect::<String>()
                    })
                    .collect::<Vec<_>>()
                    .join("\n")
            } else {
                tx.descendants()
                    .find(|n| chart_node(*n, "v"))
                    .and_then(|n| n.text())
                    .unwrap_or("")
                    .to_owned()
            }
        } else {
            let mut fields = Vec::new();
            if show("showSerName", false) {
                fields.push(
                    series
                        .name
                        .clone()
                        .unwrap_or_else(|| format!("Series {}", ordinal + 1)),
                );
            }
            if show("showCatName", false) {
                let label = point
                    .categories
                    .iter()
                    .min_by_key(|c| c.level)
                    .map(|c| c.label.clone())
                    .or_else(|| point.x.clone())
                    .unwrap_or_else(|| (u64::from(point.index) + 1).to_string());
                fields.push(label);
            }
            if show(
                "showVal",
                prop("showCatName").is_none() && prop("showSerName").is_none(),
            ) {
                let value = point.value.as_deref().or(point.y.as_deref()).unwrap_or("0");
                let code = prop("numFmt")
                    .and_then(|n| n.attribute("formatCode"))
                    .or(self.source_format);
                fields.push(
                    number(Some(value)).map_or_else(|| value.to_owned(), |v| format_value(v, code)),
                );
            }
            fields.join(prop("separator").and_then(|n| n.text()).unwrap_or(", "))
        };
        let key = show("showLegendKey", false);
        if label.is_empty() && !key {
            return;
        }
        let root_props = self.root_props;
        let text_body = prop("tx").or_else(|| prop("txPr"));
        let props = text_body.and_then(|n| {
            n.descendants()
                .find(|c| c.is_element() && matches!(c.tag_name().name(), "rPr" | "defRPr"))
        });
        let size = font_size(props, font_size(root_props, 40. / 3.));
        let family = font_family(props.or(root_props), self.default_face);
        let rows: Vec<_> = label.split('\n').collect();
        let key_width = if key { size * 0.53 + 3. } else { 0. };
        let w = rows
            .iter()
            .enumerate()
            .map(|(i, s)| text_width(s, size) + if i == 0 { key_width } else { 0. })
            .fold(0., f64::max);
        let h = size * (1. + (rows.len() - 1) as f64 * 1.16);
        let ox = w / 2. + 8. + marker_radius;
        let oy = h / 2. + 7.2 + marker_radius;
        let (dx, dy) = if horizontal {
            (if end.0 < start.0 { -ox } else { ox }, 0.)
        } else {
            (0., if end.1 > start.1 { oy } else { -oy })
        };
        let pos = prop("dLblPos")
            .and_then(|n| n.attribute("val"))
            .unwrap_or(default_pos);
        let pos = if end == start && default_pos != "r" {
            "outEnd"
        } else {
            pos
        };
        let (x, y) = match pos {
            "r" => (end.0 + ox, end.1),
            "l" => (end.0 - ox, end.1),
            "t" => (end.0, end.1 - oy),
            "b" => (end.0, end.1 + oy),
            "outEnd" => (end.0 + dx, end.1 + dy),
            "inEnd" => (end.0 - dx, end.1 - dy),
            "inBase" => (start.0 + dx, start.1 + dy),
            "ctr" if default_pos == "r" => end,
            _ => ((end.0 + start.0) / 2., (end.1 + start.1) / 2.),
        };
        let rot = text_body
            .and_then(|n| n.descendants().find(|c| c.tag_name().name() == "bodyPr"))
            .and_then(|n| number(n.attribute("rot")))
            .unwrap_or(0.)
            / 60000.;
        let paint = props
            .and_then(|n| colors.shape_fill(n))
            .or_else(|| root_props.and_then(|n| colors.shape_fill(n)))
            .unwrap_or_else(|| "#000000".into());
        write!(
            svg,
            r#"<g transform="translate({x:.2} {y:.2}) rotate({rot:.2})" fill="{paint}""#
        )
        .unwrap();
        if props
            .and_then(|n| n.attribute("b"))
            .is_some_and(|v| matches!(v, "1" | "true" | "on"))
        {
            svg.push_str(r#" font-weight="bold""#);
        }
        if props
            .and_then(|n| n.attribute("i"))
            .is_some_and(|v| matches!(v, "1" | "true" | "on"))
        {
            svg.push_str(r#" font-style="italic""#);
        }
        colors.hit_attrs(svg, "dataLabel", ordinal, Some(point.index));
        svg.push('>');
        if let Some(shape) = prop("spPr") {
            let fill = colors.shape_fill(shape).unwrap_or_else(|| "none".into());
            let stroke = colors.shape_stroke(shape).unwrap_or_else(|| "none".into());
            write!(svg,r#"<rect x="{:.2}" y="{:.2}" width="{:.2}" height="{:.2}" fill="{fill}" stroke="{stroke}"/>"#,-w/2.-2.,-h/2.-2.,w+4.,h+4.).unwrap();
        }
        let first_y = size * 0.375 - (rows.len() - 1) as f64 * size * 0.58;
        if key {
            let fill = colors.fill(ordinal, Some(point.index));
            let side = key_width - 3.;
            write!(
                svg,
                r#"<rect x="{:.2}" y="{:.2}" width="{side:.2}" height="{side:.2}" fill="{fill}"/>"#,
                -(text_width(rows[0], size) + key_width) / 2.,
                first_y - side
            )
            .unwrap();
        }
        for (i, row) in rows.iter().enumerate() {
            text(
                svg,
                if i == 0 { key_width / 2. } else { 0. },
                first_y + size * 1.16 * i as f64,
                "middle",
                size,
                &family,
                row,
            );
        }
        svg.push_str("</g>");
    }
}