///|
/// Individual observations remain visible even in an NE visit. NE visits and
/// missing observations break connecting lines; only measured/default values plot.
pub fn render_lesion_trends_svg(
  trajectory : @evidence.Trajectory,
  language? : ReportLanguage = English,
) -> String {
  let out = StringBuilder()
  let height = trajectory.study.targets.length() * 260
  out.write_string(
    "" +
    tr(language, "Individual target observations", "单病灶观察趋势") +
    "",
  )
  for index, target in trajectory.study.targets {
    let mut maximum = target.baseline_tenths_mm
    for a in trajectory.assessments {
      for m in a.measurements {
        if m.lesion_id == target.id &&
          m.effective_tenths_mm is Some(v) &&
          v > maximum {
          maximum = v
        }
      }
    }
    maximum = maximum * 5 / 4 + 1
    let top = index * 260
    let y = (value : Int) => top + 55 + (maximum - value) * 140 / maximum
    out.write_string(
      "" +
      html_escape(target.id) +
      " · " +
      axis_label(target.kind, language) +
      " / mm",
    )
    out.write_string(
      "" +
      format_diameter_mm(maximum) +
      "0.0",
    )
    out.write_string(
      "" +
      tr(language, "Baseline", "基线") +
      ": " +
      format_diameter_mm(target.baseline_tenths_mm) +
      " mm" +
      tr(language, "Baseline", "基线") +
      "",
    )
    let mut previous : (Int, Int)? = Some((70, y(target.baseline_tenths_mm)))
    for i, a in trajectory.assessments {
      let x = 70 + (i + 1) * 840 / (trajectory.assessments.length() + 1)
      for m in a.measurements {
        if m.lesion_id != target.id {
          continue
        }
        let attrs = " data-visit-id=\"" +
          html_escape(a.visit_id) +
          "\" data-state=\"" +
          state_name(m.observation) +
          "\" data-visit-response=\"" +
          a.response.code() +
          "\""
        let label = html_escape(a.visit_id) +
          " · " +
          observation_label(m.observation, language) +
          " · " +
          tr(language, "Visit target category", "随访靶病灶分类") +
          ": " +
          a.response.code()
        if m.effective_tenths_mm is Some(value) {
          let py = y(value)
          if a.response != NE && previous is Some((px, previous_y)) {
            out.write_string(
              "",
            )
          }
          let color = response_color(a.response)
          let fill = if a.response == NE { "#fffdf9" } else { color }
          let title = "" +
            label +
            ": " +
            format_diameter_mm(value) +
            " mm"
          if m.observation is TooSmallToMeasure {
            out.write_string(
              "" +
              title +
              "",
            )
          } else {
            out.write_string(
              "" +
              title +
              "",
            )
          }
          previous = if a.response == NE { None } else { Some((x, py)) }
        } else {
          out.write_string(
            "" +
            label +
            tr(language, "; no diameter", ";无直径数值") +
            "",
          )
          previous = None
        }
      }
      out.write_string(
        "V\{i + 1}" +
        a.response.code() +
        "",
      )
    }
    out.write_string("")
  }
  out.write_string("\n")
  out.to_string()
}

///|
fn lesion_trends_note(language : ReportLanguage) -> String {
  tr(
    language, "Each target uses its own vertical scale. V1, V2 follow the visit ledger. Squares mark the explicit 5.0 mm default; crosses have no diameter. Hollow points retain an observation from an NE visit. Connecting lines stop at NE or missing visits. These points do not make an NE visit eligible for the total trend or nadir.",
    "各病灶使用独立纵轴比例,V1、V2 对应随访台账顺序。方点表示明确的 5.0 mm 默认值,叉号无直径数值,空心点保留 NE 随访中的观察。NE 或缺失处断线;保留观察不意味着该次随访可进入总和趋势或更新最低值。",
  )
}

///|
fn render_lesion_section_html(
  trajectory : @evidence.Trajectory,
  language : ReportLanguage,
) -> String {
  let out = StringBuilder()
  out.write_string(
    "

" + tr(language, "Individual target trends", "单病灶趋势") + "

" + lesion_trends_note(language) + "

" + render_lesion_trends_svg(trajectory, language~), ) out.write_string( "
" + tr( language, "", "", ) + "", ) for target in trajectory.study.targets { out.write_string( "", ) } for i, a in trajectory.assessments { for m in a.measurements { out.write_string( "", ) } } out.write_string("
Visit / dateTarget / axisObservationEffective / mmVisit target category随访 / 日期病灶 / 测量轴观察状态采用直径 / mm随访靶病灶分类
" + tr(language, "Baseline", "基线") + "
" + trajectory.study.baseline_date + "
" + html_escape(target.id) + " · " + axis_label(target.kind, language) + "" + tr(language, "baseline measurement", "基线测量") + "" + format_diameter_mm(target.baseline_tenths_mm) + "—
" + html_escape(a.visit_id) + "
" + a.date + "
" + html_escape(m.lesion_id) + " · " + axis_label(m.kind, language) + "" + observation_label(m.observation, language) + "" + mm_or_missing(m.effective_tenths_mm) + "" + a.response.code() + "
") out.to_string() } ///| fn render_lesion_section_markdown( trajectory : @evidence.Trajectory, language : ReportLanguage, ) -> String { let out = StringBuilder() out.write_string( "\n## " + tr(language, "Individual target trends", "单病灶趋势") + "\n\n" + lesion_trends_note(language) + "\n\n", ) out.write_string( tr( language, "| Visit / date | Target / axis | Observation | Effective / mm | Visit target category |\n", "| 随访 / 日期 | 病灶 / 测量轴 | 观察状态 | 采用直径 / mm | 随访靶病灶分类 |\n", ) + "|---|---|---|---:|---|\n", ) for target in trajectory.study.targets { out.write_string( "| " + tr(language, "Baseline", "基线") + " / " + trajectory.study.baseline_date + " | " + markdown_escape(target.id) + " / " + axis_label(target.kind, language) + " | " + tr(language, "baseline measurement", "基线测量") + " | " + format_diameter_mm(target.baseline_tenths_mm) + " | — |\n", ) } for a in trajectory.assessments { for m in a.measurements { out.write_string( "| " + markdown_escape(a.visit_id) + " / " + a.date + " | " + markdown_escape(m.lesion_id) + " / " + axis_label(m.kind, language) + " | " + observation_label(m.observation, language) + " | " + mm_or_missing(m.effective_tenths_mm) + " | " + a.response.code() + " |\n", ) } } out.to_string() }