systemprompt-mcp 0.43.0

Native Model Context Protocol (MCP) implementation for systemprompt.io. Orchestration, per-server OAuth2, RBAC middleware, and tool-call governance — the core of the AI governance pipeline.
Documentation
//! Pie and doughnut plots.
//!
//! Copyright (c) systemprompt.io — Business Source License 1.1.
//! See <https://systemprompt.io> for licensing details.

use super::{ChartSpec, VIEW_H, VIEW_W, scale, series_color};
use crate::services::ui_renderer::templates::html::html_escape;
use systemprompt_models::artifacts::types::ChartType;

const DOUGHNUT_INNER: f64 = 0.58;

pub(super) fn plot(spec: &ChartSpec<'_>) -> Option<String> {
    let values = spec.datasets.first().map(|set| set.data.as_slice())?;

    let total: f64 = values.iter().filter(|v| v.is_finite() && **v > 0.0).sum();
    if total <= 0.0 {
        return None;
    }

    let cx = VIEW_W / 2.0;
    let cy = VIEW_H / 2.0;
    let radius = (VIEW_H / 2.0 - 24.0).min(VIEW_W / 2.0 - 24.0);
    let inner = if matches!(spec.chart_type, ChartType::Doughnut) {
        radius * DOUGHNUT_INNER
    } else {
        0.0
    };

    let mut start = -std::f64::consts::FRAC_PI_2;
    let mut slices = Vec::with_capacity(values.len());

    for (i, &value) in values.iter().enumerate() {
        if !value.is_finite() || value <= 0.0 {
            continue;
        }
        let sweep = value / total * std::f64::consts::TAU;
        let end = start + sweep;
        let label = spec.labels.get(i).map_or("", String::as_str);
        slices.push(slice(
            Arc {
                cx,
                cy,
                radius,
                inner,
                start,
                end,
            },
            &series_color(i),
            &format!(
                "{label}: {value} ({pct:.0}%)",
                value = scale::format_value(value),
                pct = value / total * 100.0,
            ),
        ));
        start = end;
    }

    let center = if inner > 0.0 {
        format!(
            "\n{}",
            format_args!(
                r#"        <text class="chart-tick" x="{cx}" y="{cy}" text-anchor="middle" dominant-baseline="middle">{total}</text>"#,
                cx = scale::coord(cx),
                cy = scale::coord(cy),
                total = html_escape(&scale::format_value(total)),
            )
        )
    } else {
        String::new()
    };

    // Why: Every value was non-finite or non-positive despite a positive total, so
    // there is still nothing to draw.
    if slices.is_empty() {
        return None;
    }

    Some(format!(
        "        <g class=\"chart-series\">\n{}\n        </g>{center}",
        slices.join("\n")
    ))
}

#[derive(Debug, Clone, Copy)]
struct Arc {
    cx: f64,
    cy: f64,
    radius: f64,
    inner: f64,
    start: f64,
    end: f64,
}

fn slice(arc: Arc, color: &str, title: &str) -> String {
    let large = i32::from(arc.end - arc.start > std::f64::consts::PI);
    let (x0, y0) = point(arc.cx, arc.cy, arc.radius, arc.start);
    let (x1, y1) = point(arc.cx, arc.cy, arc.radius, arc.end);

    let d = if arc.inner > 0.0 {
        let (ix1, iy1) = point(arc.cx, arc.cy, arc.inner, arc.end);
        let (ix0, iy0) = point(arc.cx, arc.cy, arc.inner, arc.start);
        format!(
            "M {x0} {y0} A {r} {r} 0 {large} 1 {x1} {y1} L {ix1} {iy1} A {ri} {ri} 0 {large} 0 {ix0} {iy0} Z",
            x0 = scale::coord(x0),
            y0 = scale::coord(y0),
            x1 = scale::coord(x1),
            y1 = scale::coord(y1),
            ix0 = scale::coord(ix0),
            iy0 = scale::coord(iy0),
            ix1 = scale::coord(ix1),
            iy1 = scale::coord(iy1),
            r = scale::coord(arc.radius),
            ri = scale::coord(arc.inner),
        )
    } else {
        format!(
            "M {cx} {cy} L {x0} {y0} A {r} {r} 0 {large} 1 {x1} {y1} Z",
            cx = scale::coord(arc.cx),
            cy = scale::coord(arc.cy),
            x0 = scale::coord(x0),
            y0 = scale::coord(y0),
            x1 = scale::coord(x1),
            y1 = scale::coord(y1),
            r = scale::coord(arc.radius),
        )
    };

    format!(
        r#"            <path class="chart-slice" fill="{color}" d="{d}"><title>{title}</title></path>"#,
        title = html_escape(title),
    )
}

fn point(cx: f64, cy: f64, radius: f64, angle: f64) -> (f64, f64) {
    (
        radius.mul_add(angle.cos(), cx),
        radius.mul_add(angle.sin(), cy),
    )
}