use std::fmt::Write;
use super::{
escape, number,
table::{data_table, table_extra},
};
use crate::{
scene::Hook,
spec::{ChartSpec, ChartType, Mark},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Hooks {
Off,
Attributes,
WithData,
}
pub(super) fn emit_hook(hook: &Hook, spec: &ChartSpec, output: &mut String) {
let pairs = |pairs: &[(f64, f64)]| {
let (values, pixels): (Vec<String>, Vec<String>) = pairs
.iter()
.map(|(value, pixel)| (number(*value), number(*pixel)))
.unzip();
(values.join(" "), pixels.join(" "))
};
match hook {
Hook::Plot { pane, x, y, log } => {
let (x_domain, x_range) = pairs(x);
let (y_domain, y_range) = pairs(y);
let scale = if *log { " data-y-scale=\"log\"" } else { "" };
write!(
output,
"<g data-chartlet-plot=\"\" data-pane=\"{pane}\" data-x-domain=\"{x_domain}\" data-x-range=\"{x_range}\" data-y-domain=\"{y_domain}\" data-y-range=\"{y_range}\"{scale}/>"
)
.expect("write");
}
Hook::Layer(global) => {
let entry = spec
.indexed_layers()
.nth(*global)
.expect("a hook names an existing layer");
if entry.layer.is_data() {
let series = spec
.data_layers()
.position(|data| data.global == *global)
.expect("a data layer is among the data layers");
write!(
output,
"<g data-series=\"{series}\" data-pane=\"{}\"",
entry.pane
)
.expect("write");
if let Some(name) = &entry.layer.name {
write!(output, " data-name=\"{}\"", escape(name)).expect("write");
}
output.push('>');
} else {
write!(
output,
"<g data-annotation=\"{global}\" data-pane=\"{}\">",
entry.pane
)
.expect("write");
}
}
Hook::Object(attribute, id) => {
write!(output, "<g {attribute}=\"{}\">", escape(id)).expect("write");
}
Hook::End => output.push_str("</g>"),
}
}
pub(super) struct TableHooks {
pub(super) columns: Vec<(usize, usize, &'static str, bool)>,
pub(super) x: Vec<f64>,
pub(super) values: Vec<Vec<Option<f64>>>,
}
pub(super) fn table_hooks(spec: &ChartSpec) -> TableHooks {
if matches!(
spec.chart_type,
ChartType::Sequence
| ChartType::Flow
| ChartType::State
| ChartType::Classes
| ChartType::Network
| ChartType::Architecture
| ChartType::Tree
| ChartType::Timeline
| ChartType::Scatter
| ChartType::Survival
| ChartType::Sankey
| ChartType::Treemap
| ChartType::Parliament
| ChartType::Waffle
| ChartType::Waterfall
) {
let table = data_table(spec);
return TableHooks {
columns: (1..table.columns.len())
.map(|index| (index - 1, 0, "text", false))
.collect(),
x: (0..table.rows.len())
.map(|row| f64::from(u32::try_from(row).expect("rows are limited")))
.collect(),
values: vec![vec![None; table.columns.len() - 1]; table.rows.len()],
};
}
let dataset = spec.table_dataset();
let rows = dataset.categories.len();
let extras = table_extra(spec).len();
let values = (0..rows)
.map(|row| {
dataset
.series
.iter()
.map(|series| series.values[row])
.chain(std::iter::repeat_n(None, extras))
.collect()
})
.collect();
if !matches!(spec.chart_type, ChartType::Time | ChartType::Multiples) || spec.is_bar_multiples()
{
return TableHooks {
columns: (0..dataset.series.len())
.map(|index| (index, 0, "value", false))
.chain((0..extras).map(|extra| (dataset.series.len() + extra, 0, "text", false)))
.collect(),
x: (0..rows)
.map(|row| f64::from(u32::try_from(row).expect("rows are limited")))
.collect(),
values,
};
}
let zone = spec.time_zone().unwrap_or_default();
let mut epochs: Vec<i64> = spec
.data_layers()
.flat_map(|entry| entry.layer.resolved_times(zone))
.collect();
epochs.sort_unstable();
epochs.dedup();
let mut columns = Vec::new();
for (series, entry) in spec.data_layers().enumerate() {
let parts: &[&'static str] = if entry.layer.mark == Mark::Ohlc {
&["open", "high", "low", "close"]
} else if entry.layer.has_band() {
&["value", "lower", "upper"]
} else {
&["value"]
};
let stacked = spec.stack_base(entry, zone).is_some();
columns.extend(
parts
.iter()
.map(|part| (series, entry.pane, *part, stacked)),
);
}
#[allow(clippy::cast_precision_loss)]
let x = epochs.iter().map(|epoch| *epoch as f64).collect();
TableHooks { columns, x, values }
}
pub(super) fn emit_data_block(spec: &ChartSpec, output: &mut String) {
let table = data_table(spec);
let hooks = table_hooks(spec);
let columns: Vec<serde_json::Value> = table
.columns
.iter()
.skip(1)
.zip(&hooks.columns)
.map(|(name, (series, pane, part, stacked))| {
let mut column =
serde_json::json!({ "name": name, "series": series, "pane": pane, "part": part });
if *stacked {
column["stacked"] = serde_json::Value::Bool(true);
}
column
})
.collect();
let rows: Vec<serde_json::Value> = table
.rows
.iter()
.zip(hooks.x.iter().zip(&hooks.values))
.map(|(row, (x, values))| {
serde_json::json!({ "x": x, "label": row[0], "text": row[1..], "value": values })
})
.collect();
let block = serde_json::json!({ "columns": columns, "rows": rows });
write!(
output,
"<script type=\"application/json\" data-chartlet-data=\"\">{}</script>",
escape_text(&block.to_string())
)
.expect("write");
}
fn escape_text(value: &str) -> String {
value
.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
}