///|
priv struct SvgChartBounds {
low : Double
high : Double
left : Int
right : Int
top : Int
bottom : Int
}
///|
fn svg_escape(value : String) -> String {
let output = StringBuilder()
for character in value.to_array() {
match character {
'&' => output.write_string("&")
'<' => output.write_string("<")
'>' => output.write_string(">")
'"' => output.write_string(""")
'\'' => output.write_string("'")
_ => output.write_char(character)
}
}
output.to_string()
}
///|
fn svg_run_x(index : Int, run_count : Int, left : Int, right : Int) -> Int {
if run_count <= 1 {
(left + right) / 2
} else {
left + (right - left) * index / (run_count - 1)
}
}
///|
fn svg_y(value : Double, bounds : SvgChartBounds) -> Int {
let span = bounds.high - bounds.low
let normalized = (value - bounds.low) / span
bounds.bottom -
(normalized * (bounds.bottom - bounds.top).to_double()).to_int()
}
///|
fn svg_color_for_level(index : Int) -> String {
let colors = ["#2563eb", "#0f766e", "#9333ea", "#ea580c"]
colors[index % colors.length()]
}
///|
fn svg_limit_color(sigma : Int) -> String {
if sigma == 0 {
"#1f2937"
} else if sigma == 3 || sigma == -3 {
"#dc2626"
} else if sigma == 2 || sigma == -2 {
"#d97706"
} else {
"#cbd5e1"
}
}
///|
fn svg_limit_label(sigma : Int) -> String {
if sigma == 0 {
"mean"
} else if sigma > 0 {
"+" + sigma.to_string() + " SD"
} else {
sigma.to_string() + " SD"
}
}
///|
fn svg_find_point(
run : @run.QCRun,
level_id : String,
precision : Int,
) -> @run.ControlPoint? {
for point in run.observations {
if point.control_level_id == level_id {
if point.precision == precision {
return Some(point)
}
return None
}
}
None
}
///|
fn svg_run_index(
runs : Array[@run.QCRun],
evidence : @evidence.EvidencePoint,
) -> Int? {
for index, run in runs {
if run.run_id == evidence.run_id && run.epoch.id == evidence.epoch_id {
return Some(index)
}
}
None
}
///|
fn svg_rule_offset(rule : @assay.Rule) -> Int {
match rule {
@assay.Rule12s => -9
@assay.Rule13s => -6
@assay.Rule22s => -3
@assay.RuleR4s => 3
@assay.Rule41s => 6
@assay.Rule10x => 9
}
}
///|
fn svg_disposition_color(disposition : @assay.RuleDisposition) -> String {
match disposition {
@assay.Disabled => "#6b7280"
@assay.Warning => "#d97706"
@assay.RequiresReview => "#dc2626"
}
}
///|
fn svg_append_limit(
output : StringBuilder,
level : @assay.ControlLevel,
bounds : SvgChartBounds,
sigma : Int,
) -> Unit {
let mean = level.mean.to_double()
let sd = level.standard_deviation.to_double()
let value = mean + sigma.to_double() * sd
let y = svg_y(value, bounds)
let color = svg_limit_color(sigma)
let dash = if sigma == 0 { "" } else { " stroke-dasharray=\"5 4\"" }
output.write_string(
"\n",
)
output.write_string(
"" +
svg_limit_label(sigma) +
"\n",
)
}
///|
fn svg_bounds(
program : @assay.AssayProgram,
level : @assay.ControlLevel,
runs : Array[@run.QCRun],
top : Int,
bottom : Int,
) -> SvgChartBounds {
let mean = level.mean.to_double()
let sd = level.standard_deviation.to_double()
let safe_sd = if sd > 0.0 { sd } else { 1.0 }
let mut low = mean - 3.0 * safe_sd
let mut high = mean + 3.0 * safe_sd
for run in runs {
match svg_find_point(run, level.id, program.precision) {
Some(point) => {
let value = point.value.to_double()
if value < low {
low = value
}
if value > high {
high = value
}
}
None => ()
}
}
let mut span = high - low
if span <= 0.0 {
low = mean - 1.0
high = mean + 1.0
span = high - low
}
let padding = span * 0.05
{
low: low - padding,
high: high + padding,
left: 92,
right: 1068,
top,
bottom,
}
}
///|
fn svg_append_epoch_boundaries(
output : StringBuilder,
runs : Array[@run.QCRun],
bounds : SvgChartBounds,
) -> Unit {
for index in 1..\n",
)
output.write_string(
"epoch: " +
svg_escape(current.epoch.id) +
"\n",
)
}
}
}
///|
fn svg_append_series_segment(
output : StringBuilder,
points : Array[String],
level_id : String,
segment_index : Int,
color : String,
) -> Unit {
if points.length() >= 2 {
output.write_string(
"\n",
)
}
}
///|
fn svg_append_series(
output : StringBuilder,
program : @assay.AssayProgram,
level : @assay.ControlLevel,
level_index : Int,
runs : Array[@run.QCRun],
bounds : SvgChartBounds,
) -> Unit {
let color = svg_color_for_level(level_index)
let mut points : Array[String] = []
let mut segment_index = 0
for index, run in runs {
if index > 0 && !runs[index - 1].epoch.has_same_metadata(run.epoch) {
segment_index = segment_index + 1
svg_append_series_segment(output, points, level.id, segment_index, color)
points = []
}
match svg_find_point(run, level.id, program.precision) {
Some(point) => {
let x = svg_run_x(index, runs.length(), bounds.left, bounds.right)
let y = svg_y(point.value.to_double(), bounds)
points.push(x.to_string() + "," + y.to_string())
output.write_string(
"" +
svg_escape(run.run_id) +
" / " +
svg_escape(level.id) +
": " +
svg_escape(format_scaled(point.value, program.precision)) +
" " +
svg_escape(program.unit) +
"\n",
)
}
None => {
segment_index = segment_index + 1
svg_append_series_segment(
output,
points,
level.id,
segment_index,
color,
)
points = []
}
}
}
segment_index = segment_index + 1
svg_append_series_segment(output, points, level.id, segment_index, color)
}
///|
fn svg_append_evidence_markers(
output : StringBuilder,
program : @assay.AssayProgram,
level : @assay.ControlLevel,
runs : Array[@run.QCRun],
trajectory : @evidence.Trajectory,
bounds : SvgChartBounds,
) -> Unit {
for assessment in trajectory.assessments {
for hit in assessment.hits {
let disposition = program.disposition_for(hit.rule)
let color = svg_disposition_color(disposition)
for evidence in hit.evidence {
if evidence.control_level_id == level.id {
match svg_run_index(runs, evidence) {
Some(index) => {
let base_x = svg_run_x(
index,
runs.length(),
bounds.left,
bounds.right,
)
let x = base_x + svg_rule_offset(hit.rule)
let y = svg_y(evidence.value.to_double(), bounds)
let common = " class=\"rule-evidence\" data-rule=\"" +
svg_escape(hit.rule.name()) +
"\" data-disposition=\"" +
audit_rule_disposition_name(disposition) +
"\""
let tooltip = "" +
svg_escape(hit.rule.name()) +
" / " +
svg_escape(assessment.run_id) +
" / " +
svg_escape(level.id) +
": " +
svg_escape(format_scaled(evidence.value, evidence.precision)) +
" " +
svg_escape(evidence.unit) +
""
match disposition {
@assay.Warning =>
output.write_string(
"" +
tooltip +
"\n",
)
@assay.RequiresReview =>
output.write_string(
"" +
tooltip +
"\n",
)
@assay.Disabled =>
output.write_string(
"" +
tooltip +
"\n",
)
}
}
None => ()
}
}
}
}
}
}
///|
fn svg_append_x_axis(
output : StringBuilder,
runs : Array[@run.QCRun],
bounds : SvgChartBounds,
) -> Unit {
let stride = if runs.length() <= 16 { 1 } else { (runs.length() + 15) / 16 }
for index, run in runs {
if index % stride == 0 || index == runs.length() - 1 {
let x = svg_run_x(index, runs.length(), bounds.left, bounds.right)
output.write_string(
"" +
svg_escape(run.sequence.to_string()) +
"\n",
)
}
}
}
///|
fn svg_append_legend(output : StringBuilder) -> Unit {
output.write_string(
"\n",
)
let mut x = 70
for sigma in [-3, -2, -1, 0, 1, 2, 3] {
let color = svg_limit_color(sigma)
let dash = if sigma == 0 { "" } else { " stroke-dasharray=\"4 3\"" }
output.write_string(
"\n" +
svg_limit_label(sigma) +
"\n",
)
x = x + 96
}
output.write_string(
"Warning\n",
)
output.write_string(
"Requires review\n",
)
output.write_string(
"Disabled\n",
)
output.write_string("\n")
}
///|
/// Render a deterministic, offline Levey–Jennings SVG with epoch breaks,
/// missing-value gaps, and rule-evidence markers.
pub fn render_levey_jennings_svg(
program : @assay.AssayProgram,
runs : Array[@run.QCRun],
trajectory : @evidence.Trajectory,
) -> String {
let panel_height = 250
let header_height = 76
let width = 1120
let height = header_height + program.levels.length() * panel_height + 20
let output = StringBuilder()
output.write_string(
"\n")
output.to_string()
}