glacier-web-cli 0.5.0

Command-line tool for creating and running Glacier applications.
{% props label: String, values: Vec<(String, f64)> = Vec::new(), name: String = "Value", labels: Vec<String> = Vec::new(), series: Vec<(String, Vec<f64>)> = Vec::new(), variant: String = "", category: String = "" %}
{#- One series as `values` (x label, value), named `name`; or several as `labels` plus `series` (name, one value per label). variant: stacked, expanded (stacked to 100%) or step. `label` is the chart's accessible name; `category` heads the table's first column. -#}
{%- let (labels, series) = if values.is_empty() { (labels, series) } else { (values.iter().map(|v| v.0.clone()).collect(), vec![(name.clone(), values.iter().map(|v| v.1).collect())]) } %}
{%- let stacked = variant == "stacked" || variant == "expanded" %}
{%- let (y_ticks, x_ticks, layers) = {
  let n = labels.len();
  let x = |i: usize| if n < 2 { 186.0 } else { 56.0 + i as f64 * 260.0 / (n - 1) as f64 };
  let totals: Vec<f64> = (0..n).map(|i| series.iter().map(|s| s.1.get(i).copied().unwrap_or(0.0)).sum()).collect();
  // Each layer's top, per point: its own value, or the running sum when stacked.
  let mut below = vec![0.0; n];
  let mut tops = Vec::new();
  for (_, data) in series.iter() {
    let top: Vec<f64> = (0..n).map(|i| {
      let v = data.get(i).copied().unwrap_or(0.0);
      if !stacked { v } else if variant == "expanded" { below[i] + if totals[i] > 0.0 { v / totals[i] * 100.0 } else { 0.0 } } else { below[i] + v }
    }).collect();
    tops.push((if stacked { below.clone() } else { vec![0.0; n] }, top.clone()));
    if stacked { below = top; }
  }
  let max = tops.iter().flat_map(|t| t.1.iter().copied()).fold(0.0_f64, f64::max);
  let step = if variant == "expanded" { 25.0 } else {
    let raw = if max > 0.0 { max / 4.0 } else { 1.0 };
    let exp = 10f64.powf(raw.log10().floor());
    let f = raw / exp;
    exp * if f <= 1.0 { 1.0 } else if f <= 2.0 { 2.0 } else if f <= 2.5 { 2.5 } else if f <= 5.0 { 5.0 } else { 10.0 }
  };
  let y = |v: f64| 192.0 - v / (step * 4.0) * 176.0;
  let suffix = if variant == "expanded" { "%" } else { "" };
  let y_ticks: Vec<(String, String)> = (0..=4).map(|i| (format!("{:.1}", y(step * i as f64)), format!("{}{suffix}", step * i as f64))).collect();
  let x_ticks: Vec<(String, String)> = labels.iter().enumerate().map(|(i, l)| (format!("{:.1}", x(i)), l.clone())).collect();
  let points = |vals: &Vec<f64>| -> Vec<(f64, f64)> {
    let mut pts = Vec::new();
    for (i, v) in vals.iter().enumerate() {
      if variant == "step" && i > 0 { pts.push((x(i), y(vals[i - 1]))); }
      pts.push((x(i), y(*v)));
    }
    pts
  };
  let path = |pts: &[(f64, f64)]| pts.iter().enumerate().map(|(i, (px, py))| format!("{}{px:.1},{py:.1}", if i == 0 { "M" } else { " L" })).collect::<String>();
  let layers: Vec<(usize, String, String)> = tops.iter().enumerate().map(|(i, (low, top))| {
    let upper = points(top);
    let mut lower = points(low);
    lower.reverse();
    let line = path(&upper);
    let area = format!("{line}{} Z", lower.iter().map(|(px, py)| format!(" L{px:.1},{py:.1}")).collect::<String>());
    (i + 1, area, line)
  }).collect();
  (y_ticks, x_ticks, layers)
} %}
{%- if series.len() > 1 %}
<ul class="ui-chart-legend">
  {%- for (i, (name, _)) in series.iter().enumerate() %}
  <li class="s{{ i + 1 }}"><span class="ui-chart-swatch"></span>{{ name }}</li>
  {%- end %}
</ul>
{%- end %}
<div class="ui-chart">
  <svg viewBox="0 0 330 220" role="img" aria-label="{{ label }}">
    <g class="grid">{% for (y, _) in y_ticks.iter().skip(1) %}<line x1="40" x2="320" y1="{{ y }}" y2="{{ y }}"/>{% end %}</g>
    <line class="baseline" x1="40" x2="320" y1="192" y2="192"/>
    <g class="axis">{% for (y, text) in y_ticks.iter() %}<text x="32" y="{{ y }}" dy="4" text-anchor="end">{{ text }}</text>{% end %}{% for (x, text) in x_ticks.iter() %}<text x="{{ x }}" y="210" text-anchor="middle">{{ text }}</text>{% end %}</g>
    {%- for (class, area, line) in layers.iter() %}
    <g class="s{{ class }}"><path class="area" d="{{ area }}"/><path class="line" d="{{ line }}"/></g>
    {%- end %}
  </svg>
</div>
<details class="ui-details">
  <summary><svg class="ui-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m9 18 6-6-6-6"/></svg>View as table</summary>
  <div class="ui-table-wrap">
    <table class="ui-table">
      <thead><tr><th>{{ category }}</th>{% for (name, _) in series.iter() %}<th class="ui-num">{{ name }}</th>{% end %}{% if stacked && series.len() > 1 %}<th class="ui-num">Total</th>{% end %}</tr></thead>
      <tbody>
        {%- for (i, text) in labels.iter().enumerate() %}
        <tr><td>{{ text }}</td>{% for (_, data) in series.iter() %}<td class="ui-num">{{ data.get(i).copied().unwrap_or(0.0) }}</td>{% end %}{% if stacked && series.len() > 1 %}<td class="ui-num">{{ series.iter().map(|s| s.1.get(i).copied().unwrap_or(0.0)).sum::<f64>() }}</td>{% end %}</tr>
        {%- end %}
      </tbody>
    </table>
  </div>
</details>