glacier-web-cli 0.5.0

Command-line tool for creating and running Glacier applications.
{% props label: String, values: Vec<(String, f64)>, unit: String = "", total: String = "", variant: String = "" %}
{#- values: (name, value), largest first, at most five, "Other" last. A donut with the total (or `total`, formatted) and `unit` in the centre; `variant="pie"` has no hole. -#}
{%- let sum: f64 = values.iter().map(|v| v.1).sum() %}
{%- let share = |v: f64| if sum > 0.0 { (v / sum * 100.0).round() } else { 0.0 } %}
{%- let total = if total.is_empty() { format!("{sum}") } else { total } %}
{%- let slices = {
  use std::f64::consts::{PI, TAU};
  let (cx, cy, outer) = (110.0, 110.0, 96.0);
  let inner = if variant == "pie" { 0.0 } else { 62.0 };
  let point = |r: f64, a: f64| (cx + r * a.sin(), cy - r * a.cos());
  let mut start = 0.0;
  values.iter().enumerate().map(|(i, (name, v))| {
    let sweep = if sum > 0.0 { (v / sum * TAU).min(TAU - 0.0001) } else { 0.0 };
    let end = start + sweep;
    // A 2px gap between slices: trim each edge by 1px of arc.
    let gap = |r: f64| if values.len() > 1 && r > 0.0 { 1.0 / r } else { 0.0 };
    let large = if sweep - 2.0 * gap(outer) > PI { 1 } else { 0 };
    let (x1, y1) = point(outer, start + gap(outer));
    let (x2, y2) = point(outer, end - gap(outer));
    let d = if inner > 0.0 {
      let (x3, y3) = point(inner, end - gap(inner));
      let (x4, y4) = point(inner, start + gap(inner));
      format!("M{x1:.2},{y1:.2} A{outer},{outer} 0 {large} 1 {x2:.2},{y2:.2} L{x3:.2},{y3:.2} A{inner},{inner} 0 {large} 0 {x4:.2},{y4:.2} Z")
    } else {
      format!("M{cx},{cy} L{x1:.2},{y1:.2} A{outer},{outer} 0 {large} 1 {x2:.2},{y2:.2} Z")
    };
    start = end;
    (i + 1, name.clone(), d, share(*v))
  }).collect::<Vec<_>>()
} %}
<div style="display:grid;grid-template-columns:180px 1fr;gap:24px;align-items:center">
  <div class="ui-chart">
    <svg viewBox="0 0 220 220" role="img" aria-label="{{ label }}: {% for (i, (_, name, _, pct)) in slices.iter().enumerate() %}{% if i > 0 %}, {% end %}{{ name }} {{ pct }}%{% end %}">
      {%- for (class, name, d, pct) in slices.iter() %}
      <g class="s{{ class }}"><path class="bar" d="{{ d }}" aria-label="{{ name }}: {{ pct }}%"/></g>
      {%- end %}
      {%- if variant != "pie" %}
      <text x="110" y="106" text-anchor="middle" style="font-size:24px;font-weight:600;fill:var(--foreground);font-family:var(--font-sans)">{{ total }}</text>
      {%- if !unit.is_empty() %}
      <text x="110" y="126" text-anchor="middle" style="font-size:12px;fill:var(--muted-foreground);font-family:var(--font-sans)">{{ unit }}</text>
      {%- end %}
      {%- end %}
    </svg>
  </div>
  <ul class="ui-chart-legend" style="flex-direction:column;gap:10px;font-size:13px">
    {%- for (class, name, _, pct) in slices.iter() %}
    <li class="s{{ class }}" style="display:flex;justify-content:space-between;width:100%"><span style="display:inline-flex;align-items:center;gap:6px"><span class="ui-chart-swatch"></span>{{ name }}</span><span style="color:var(--foreground);font-weight:500;font-variant-numeric:tabular-nums">{{ pct }}%</span></li>
    {%- end %}
  </ul>
</div>