///|
/// A tabular report that can be exported to CSV or rendered by a dashboard.
pub(all) struct ReportTable {
title : String
columns : Array[String]
rows : Array[Array[String]]
} derive(FromJson, ToJson, Debug, Eq)
///|
/// Build a table from a headline analysis report.
pub fn build_report_table(report : AnalysisReport) -> ReportTable {
let rows = []
rows.push(["raw_count", report.raw_count.to_string(), "beats"])
rows.push([
"clean_count",
report.cleaned_intervals.length().to_string(),
"beats",
])
rows.push(["mean_rr", report.metrics.mean_rr.to_string(), "ms"])
rows.push(["mean_hr", report.metrics.mean_hr.to_string(), "bpm"])
rows.push(["sdnn", report.metrics.sdnn.to_string(), "ms"])
rows.push(["rmssd", report.metrics.rmssd.to_string(), "ms"])
rows.push(["pnn50", report.metrics.pnn50.to_string(), "%"])
rows.push(["poincare_sd1", report.poincare.sd1.to_string(), "ms"])
rows.push(["poincare_sd2", report.poincare.sd2.to_string(), "ms"])
rows.push([
"triangular_index",
report.geometric.triangular_index.to_string(),
"ratio",
])
rows.push(["tinn", report.geometric.tinn.to_string(), "ms"])
rows.push(["vlf_power", report.frequency.vlf.power.to_string(), "ms2"])
rows.push(["lf_power", report.frequency.lf.power.to_string(), "ms2"])
rows.push(["hf_power", report.frequency.hf.power.to_string(), "ms2"])
rows.push(["lf_hf_ratio", report.frequency.lf_hf_ratio.to_string(), "ratio"])
rows.push([
"sample_entropy",
report.nonlinear.sample_entropy.to_string(),
"nats",
])
rows.push(["quality_grade", quality_grade(report.raw_validation), ""])
{ title: "HRV analysis report", columns: ["metric", "value", "unit"], rows }
}
///|
/// Serialize a table with one header row.
pub fn report_table_to_csv(table : ReportTable) -> String {
let grid = [table.columns]
for row in table.rows {
grid.push(row)
}
to_csv(grid)
}
///|
/// Return the first matching row by its metric key.
pub fn report_table_find(table : ReportTable, key : String) -> Array[String] {
for row in table.rows {
if row.length() > 0 && row[0] == key {
return row
}
}
[]
}
///|
/// Return a table with rows selected by a predicate.
pub fn report_table_filter(
table : ReportTable,
predicate : (Array[String]) -> Bool,
) -> ReportTable {
let rows = []
for row in table.rows {
if predicate(row) {
rows.push(row)
}
}
{ title: table.title, columns: table.columns, rows }
}
///|
/// Add a row while preserving the table schema.
pub fn report_table_append(
table : ReportTable,
row : Array[String],
) -> ReportTable {
let rows = []
for existing in table.rows {
rows.push(existing)
}
if row.length() == table.columns.length() {
rows.push(row)
}
{ title: table.title, columns: table.columns, rows }
}
///|
/// Return the number of data rows.
pub fn report_table_size(table : ReportTable) -> Int {
table.rows.length()
}
///|
/// Convert a report table to a stable JSON-like key/value grid.
pub fn report_table_grid(table : ReportTable) -> Array[Array[String]] {
let grid = [table.columns]
for row in table.rows {
grid.push(row)
}
grid
}
///|
/// Build a table containing one row per segment.
pub fn segment_report_table(summaries : Array[SegmentSummary]) -> ReportTable {
let rows = []
for summary in summaries {
rows.push([
summary.range.ordinal.to_string(),
summary.mean_rr.to_string(),
summary.sdnn.to_string(),
summary.rmssd.to_string(),
summary.quality_ratio.to_string(),
])
}
{
title: "Segment report",
columns: ["segment", "mean_rr", "sdnn", "rmssd", "quality"],
rows,
}
}
///|
/// Return a report table summary line for logs.
pub fn report_table_summary(table : ReportTable) -> String {
"\{table.title}: \{table.rows.length()} rows, \{table.columns.length()} columns"
}