use serde_json::{json, Map, Value as Json};
use crate::manifest::FrameDataset;
use crate::output::TreeNode;
pub struct NodeCtx<'a> {
pub levels: &'a [String],
pub metric_ids: &'a [String],
pub count_id: Option<&'a str>,
pub entity_noun: &'a str,
pub is_path: bool,
}
pub fn frontend_node(t: &TreeNode, ctx: &NodeCtx, depth: usize) -> Json {
let level = if depth == 0 {
"root".to_string()
} else if ctx.is_path {
crate::path::level_col(depth - 1)
} else if depth <= ctx.levels.len() {
ctx.levels[depth - 1].clone()
} else {
ctx.entity_noun.to_string()
};
let mut node = Map::new();
node.insert("name".into(), json!(t.name));
node.insert("level".into(), json!(level));
let count = ctx
.count_id
.and_then(|cid| t.measures.get(cid))
.and_then(|v| v.as_f64())
.map(|f| f as i64)
.unwrap_or(0);
node.insert("count".into(), json!(count));
for mid in ctx.metric_ids {
node.insert(
mid.clone(),
t.measures.get(mid).cloned().unwrap_or(Json::Null),
);
}
if t.is_other {
node.insert("_others".into(), json!(t.n_folded.max(1)));
}
if t.has_more {
node.insert("has_more_children".into(), json!(true));
}
node.insert(
"children".into(),
Json::Array(
t.children
.iter()
.map(|c| frontend_node(c, ctx, depth + 1))
.collect(),
),
);
Json::Object(node)
}
pub fn boot_manifest(ds: &FrameDataset) -> Json {
let metric_ids: Vec<String> = ds.metrics.iter().map(|m| m.id.clone()).collect();
let extensive = |agg: &str| matches!(agg, "sum" | "count");
let metrics: Vec<Json> = ds
.metrics
.iter()
.map(|m| {
json!({
"id": m.id,
"label": m.label.clone().unwrap_or_else(|| m.id.clone()),
"unit": m.unit,
"default_agg": if extensive(&m.agg) { "sum" } else { "median" },
"kind": if extensive(&m.agg) { "extensive" } else { "intensive" },
"cadence": "daily",
"timeseries": true,
"notice": Json::Null,
})
})
.collect();
let axes: Vec<Json> = ds
.axes
.iter()
.map(|a| {
let mut o = json!({
"id": a.id,
"label": a.label.clone().unwrap_or_else(|| a.id.clone()),
"levels": a.levels,
"descr": "",
"row": 0,
});
if a.path.is_some() {
if let Some(obj) = o.as_object_mut() {
obj.insert("path".into(), json!(true));
}
}
if let Some(dsb) = &a.default_size_by {
if let Some(obj) = o.as_object_mut() {
obj.insert("default_size_by".into(), json!(dsb));
}
}
if let Some(sb) = &a.size_by {
if let Some(obj) = o.as_object_mut() {
obj.insert("size_by".into(), json!(sb));
}
}
o
})
.collect();
let filters: Vec<Json> = ds
.filters
.iter()
.map(|f| {
let is_range = f.r#type == "range";
let control = f
.control
.clone()
.filter(|c| matches!(c.as_str(), "select" | "multiselect" | "range"))
.unwrap_or_else(|| {
if is_range {
"range".into()
} else {
"multiselect".into()
}
});
json!({
"id": f.id,
"label": f.label.clone().unwrap_or_else(|| f.id.clone()),
"control": control,
"options": Json::Null,
"options_provider": !is_range,
"range_min": Json::Null,
"range_max": Json::Null,
"default": f.default.clone().unwrap_or(Json::Null),
})
})
.collect();
let default_axis = ds
.default_axis
.clone()
.unwrap_or_else(|| ds.axes[0].id.clone());
let default_size = ds
.default_size_by
.clone()
.unwrap_or_else(|| metric_ids[0].clone());
let default_y = metric_ids
.get(1)
.cloned()
.unwrap_or_else(|| metric_ids[0].clone());
let lookahead = ds.lookahead.map(|n| n.max(0));
let branch_cap = ds.branch_cap.max(1);
let leaf_cap = ds.leaf_cap.max(1);
let max_levels = ds
.axes
.iter()
.map(|a| a.levels.len() as i64)
.max()
.unwrap_or(4)
.max(4);
let levels_default = ds.default_levels.clamp(1, max_levels);
let treemap = json!({
"axes": axes,
"size_by": metric_ids,
"default_axis": default_axis,
"default_size_by": default_size,
"filters": filters,
"levels": {"min": 1, "max": max_levels, "default": levels_default},
"branch_cap": branch_cap,
"leaf_cap": leaf_cap,
"lookahead": lookahead,
"entity_level_label": ds.entity_noun,
});
let scatter = json!({
"metrics": metric_ids,
"default_x": metric_ids[0],
"default_y": default_y,
"color_label": "",
"log_x": true,
"log_y": true,
});
let has_series = ds.series_source.is_some() || ds.timestamp_column.is_some();
let detail_metrics: Vec<Json> = if has_series {
ds.series_metrics
.clone()
.unwrap_or_else(|| metric_ids.clone())
.into_iter()
.map(Json::String)
.collect()
} else {
vec![]
};
let detail_windows = if has_series {
json!([
{"id": "1m", "days": 31}, {"id": "1y", "days": 365},
{"id": "5y", "days": 1825}, {"id": "max", "days": 36500}
])
} else {
json!([])
};
let detail_resolutions = ds
.series_resolutions
.clone()
.unwrap_or_else(|| vec!["d".into(), "w".into(), "m".into()]);
let detail_default_resolution = ds
.series_default_resolution
.clone()
.or_else(|| detail_resolutions.first().cloned())
.unwrap_or_else(|| "d".into());
let detail = json!({
"facts_layout": [], "headline_metrics": [],
"series_metrics": detail_metrics, "windows": detail_windows,
"resolutions": detail_resolutions,
"default_resolution": detail_default_resolution,
"chart_styles": ["line", "area"],
"second_metric": true,
"supports_ohlc": false,
});
let mut views = Map::new();
views.insert("treemap".into(), treemap);
if ds.series_source.is_some() || ds.timestamp_column.is_some() {
let series_metrics = ds
.series_metrics
.clone()
.unwrap_or_else(|| metric_ids.clone());
let resolutions = ds
.series_resolutions
.clone()
.unwrap_or_else(|| vec!["d".into(), "w".into(), "m".into()]);
let default_resolution = ds
.series_default_resolution
.clone()
.or_else(|| resolutions.first().cloned())
.unwrap_or_else(|| "d".into());
views.insert(
"series".into(),
json!({
"windows": [
{"id": "1m", "days": 31}, {"id": "1y", "days": 365},
{"id": "5y", "days": 1825}, {"id": "max", "days": 36500}
],
"resolutions": resolutions,
"metrics": series_metrics,
"aggs": ["sum", "mean", "median"],
"default_window": "1y",
"default_resolution": default_resolution,
"log_scale": true,
"follows_treemap": true,
}),
);
}
views.insert("scatter".into(), scatter);
views.insert("detail".into(), detail);
json!({
"dataset_id": ds.source,
"title": ds.title,
"entity_noun": ds.entity_noun,
"entity_noun_plural": ds.entity_noun_plural,
"metrics": metrics,
"views": Json::Object(views),
"theme": {},
"fonts": Json::Null,
"warm_views": [],
})
}
pub fn frontend_tree(ds: &FrameDataset, axis: &str, tree: &TreeNode) -> Option<Json> {
let a = ds.axis(axis)?;
let levels = &a.levels;
let metric_ids: Vec<String> = ds.metrics.iter().map(|m| m.id.clone()).collect();
let count_id = ds
.metrics
.iter()
.find(|m| m.agg == "count")
.map(|m| m.id.clone());
let ctx = NodeCtx {
levels,
metric_ids: &metric_ids,
count_id: count_id.as_deref(),
entity_noun: &ds.entity_noun,
is_path: a.path.is_some(),
};
Some(frontend_node(tree, &ctx, 0))
}