rust-office-charts 0.1.7

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(in crate::parse) 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>,
    bubble_format: Option<&'a str>,
    default_face: &'a str,
    canvas: (f64, f64),
}

impl<'a> DataLabels<'a> {
    pub(in crate::parse) fn new(
        group: Node<'a, 'a>,
        series: Node<'a, 'a>,
        default_face: &'a str,
        canvas: (f64, f64),
    ) -> 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 bubble_format = child(series, "bubbleSize")
            .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,
            bubble_format,
            default_face,
            canvas,
        })
    }

    pub(in crate::parse) 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"))
            })
        };
        let number_format = |source: Option<&'a str>| {
            let node = prop("numFmt");
            let explicit = node.and_then(|n| n.attribute("formatCode"));
            if node
                .is_some_and(|n| matches!(n.attribute("sourceLinked"), Some("1" | "true" | "on")))
            {
                source.or(explicit)
            } else {
                explicit.or(source)
            }
        };
        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 = number_format(self.source_format);
                fields.push(
                    number(Some(value)).map_or_else(|| value.to_owned(), |v| format_value(v, code)),
                );
            }
            if show("showBubbleSize", false) {
                if let Some(value) = point.bubble_size.as_deref() {
                    let code = number_format(self.bubble_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 bubble = series.chart_type == "bubble";
        let measure = |text: &str| {
            if bubble {
                crate::parse::font_metrics::width(text, size, &family, false, false)
                    .unwrap_or_else(|| text_width(text, size))
            } else {
                text_width(text, size)
            }
        };
        let layout = prop("layout").and_then(|n| child(n, "manualLayout"));
        let extended_layout = prop("extLst")
            .and_then(|n| {
                n.descendants().find(|n| {
                    n.has_tag_name((
                        "http://schemas.microsoft.com/office/drawing/2012/chart",
                        "layout",
                    ))
                })
            })
            .and_then(|n| child(n, "manualLayout"));
        let extent = |axis, dimension| {
            number(
                extended_layout
                    .and_then(|n| val(n, axis))
                    .or_else(|| layout.and_then(|n| val(n, axis))),
            )
            .filter(|n| *n > 0.)
            .map(|n| n * dimension)
        };
        let explicit_width = extent("w", self.canvas.0);
        let explicit_height = extent("h", self.canvas.1);
        let key_width = if key {
            if bubble {
                56. / 3.
            } else {
                size * 0.53 + 3.
            }
        } else {
            0.
        };
        let mut rows = Vec::new();
        for paragraph in label.split('\n') {
            if bubble && rich.is_none() && explicit_width.is_none() {
                let mut row = String::new();
                for word in paragraph.split_whitespace() {
                    let next = if row.is_empty() {
                        word.to_owned()
                    } else {
                        format!("{row} {word}")
                    };
                    let key = if rows.is_empty() { key_width } else { 0. };
                    if !row.is_empty() && measure(&next) + key > 400. / 3. {
                        rows.push(row);
                        row = word.to_owned();
                    } else {
                        row = next;
                    }
                }
                rows.push(row);
            } else {
                rows.push(paragraph.to_owned());
            }
        }
        let w = rows
            .iter()
            .enumerate()
            .map(|(i, s)| measure(s) + if i == 0 { key_width } else { 0. })
            .fold(0., f64::max);
        let line_height = size * if bubble { 1.2205 } else { 1.16 };
        let h = if bubble {
            line_height * rows.len() as f64 + 4.
        } else {
            size + (rows.len() - 1) as f64 * line_height
        };
        let box_width = explicit_width.unwrap_or(w + if bubble { 8. } else { 0. });
        let box_height = explicit_height.unwrap_or(h);
        let ox = box_width / 2.
            + if explicit_width.is_some() {
                0.
            } else if bubble {
                8. / 3.
            } else {
                8.
            }
            + marker_radius;
        let oy = box_height / 2. + if explicit_height.is_some() { 0. } else { 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 coordinate = |axis, mode, center, extent, dimension| {
            let Some(value) = layout.and_then(|n| number(val(n, axis))) else {
                return center;
            };
            if layout.and_then(|n| val(n, mode)) == Some("edge") {
                value * dimension + extent / 2.
            } else {
                center + value * dimension
            }
        };
        let x = coordinate("x", "xMode", x, box_width, self.canvas.0);
        let y = coordinate("y", "yMode", y, box_height, self.canvas.1);
        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}"/>"#,-box_width/2.-2.,-box_height/2.-2.,box_width+4.,box_height+4.).unwrap();
        }
        let first_y = size * 0.375 - (rows.len() - 1) as f64 * line_height / 2.;
        if key {
            let fill = colors.fill(ordinal, Some(point.index));
            let side = size * 0.53;
            let x = -(measure(&rows[0]) + key_width) / 2.
                + if bubble { (key_width - side) / 2. } else { 0. };
            let y = first_y - side;
            if series.chart_type == "bubble" {
                let body = format!(
                    r#"<circle cx="{:.2}" cy="{:.2}" r="{:.2}" fill="{fill}"/>"#,
                    x + side / 2.,
                    y + side / 2.,
                    side / 2.
                );
                colors.paint_point(
                    svg,
                    ordinal,
                    Some(point.index),
                    "dataPoint",
                    &fill,
                    [x, y, side, side],
                    &body,
                );
            } else {
                write!(svg, r#"<rect x="{x:.2}" y="{y:.2}" width="{side:.2}" height="{side:.2}" fill="{fill}"/>"#).unwrap();
            }
        }
        for (i, row) in rows.iter().enumerate() {
            text(
                svg,
                if i == 0 || bubble { key_width / 2. } else { 0. },
                first_y + line_height * i as f64,
                "middle",
                size,
                &family,
                row,
            );
        }
        svg.push_str("</g>");
    }
}