DiagnostikApps/STAXI-2/app.R
2026-09-22 18:35:43 +02:00

1209 lines
47 KiB
R

# Präambel ####
PFAD_DOWNLOAD_SKRIPT = "../API/get_data_staxi2.R"
PFAD_PSEUDONYM_SKRIPT = "../get_pseudo.R"
PFAD_NORMTABELLEN = "normtabellen"
AKZENT_FARBE = "#8B2635"
STAXI2_DISCLAIMER = paste0(
"Diese Auswertung ist ein Hilfsmittel fuer klinisches Fachpersonal und ersetzt ",
"keine klinische Diagnose. Die Interpretation obliegt der behandelnden Person."
)
# 5 Klassifikationsstufen fuer T-Werte (Abschnitt 9): <30, 30-39, 40-60, 61-70, >70.
STAXI2_STUFEN_FARBEN = c(
"0" = "#1B5E20",
"1" = "#81C784",
"2" = "#9E9E9E",
"3" = "#FFB74D",
"4" = "#C62828"
)
STAXI2_STUFEN_TEXT_FARBEN = c(
"0" = "white", "1" = "#222222", "2" = "white", "3" = "#222222", "4" = "white"
)
# Farben fuer Einzelitem-Antwortbadges (Rohcode 1-4, unabhaengig von der
# jeweiligen Antwortskala aus Teil 1 bzw. Teil 2/3).
STAXI2_ANTWORT_FARBEN = c(
"1" = "#4CAF50", "2" = "#F48FB1", "3" = "#EF5350", "4" = "#B71C1C"
)
STAXI2_ANTWORT_TEXT_FARBEN = c(
"1" = "white", "2" = "#333333", "3" = "white", "4" = "white"
)
library(shiny)
library(dplyr)
library(ggplot2)
library(haven)
library(officer)
library(DBI)
library(RSQLite)
# Infrastruktur ####
APP_VERZEICHNIS = normalizePath(getwd())
absPath = function(pfad) {
if (grepl("^([A-Za-z]:[/\\\\]|/)", pfad)) return(pfad)
file.path(APP_VERZEICHNIS, pfad)
}
PFAD_DOWNLOAD_SKRIPT = normalizePath(absPath(PFAD_DOWNLOAD_SKRIPT), mustWork = FALSE)
PFAD_PSEUDONYM_SKRIPT = normalizePath(absPath(PFAD_PSEUDONYM_SKRIPT), mustWork = FALSE)
PFAD_NORMTABELLEN = normalizePath(absPath(PFAD_NORMTABELLEN), mustWork = FALSE)
# Helper ####
labels_teil1 = c("ueberhaupt nicht" = 1, "ein wenig" = 2, "ziemlich" = 3, "sehr" = 4)
labels_teil23 = c("fast nie" = 1, "manchmal" = 2, "oft" = 3, "fast immer" = 4)
# Normalisiert Labeltext fuer den Vergleich: Markdown-Sternchen und Umlaute
# (formr liefert echte Umlaute wie "ueberhaupt", waehrend hier aus
# Kodierungssicherheitsgruenden ASCII-Text hinterlegt ist) sowie
# Interpunktion/Gross-Kleinschreibung/Mehrfach-Leerzeichen vereinheitlichen.
staxi2_normalisiere_label = function(x) {
x = gsub("\\*\\*", "", x)
x = tolower(trimws(x))
x = gsub("[[:punct:]]", "", x)
x = gsub("\\s+", " ", x)
x = trimws(x)
x = gsub("ä", "ae", x, fixed = TRUE)
x = gsub("ö", "oe", x, fixed = TRUE)
x = gsub("ü", "ue", x, fixed = TRUE)
x = gsub("ß", "ss", x, fixed = TRUE)
x
}
# Validiert das labels-Attribut der ORIGINAL-Spalte gegen die erwartete Kodierung,
# bevor der numerische Rohwert uebernommen wird. Nie den Antwortcode ungeprueft
# als Positionsnummer annehmen.
item_wert = function(spalte, erwartete_labels) {
labels_attr = attr(spalte, "labels")
if (is.null(labels_attr)) {
stop("Kein labels-Attribut gefunden, Itemkodierung kann nicht validiert werden.")
}
namen_normalisiert = sort(sapply(names(labels_attr), staxi2_normalisiere_label, USE.NAMES = FALSE))
namen_erwartet = sort(sapply(names(erwartete_labels), staxi2_normalisiere_label, USE.NAMES = FALSE))
passt = identical(namen_normalisiert, namen_erwartet)
if (!passt) {
stop(paste0(
"Itemlabels weichen von der erwarteten Kodierung ab, Rohwertberechnung abgebrochen. ",
"Gefunden: ", paste(namen_normalisiert, collapse = " | "),
" Erwartet: ", paste(namen_erwartet, collapse = " | ")))
}
as.numeric(spalte)
}
# Loest den Antworttext einer labelled Spalte ueber deren labels-Attribut auf,
# nie ueber den Rohwert direkt (z.B. bei staxi2_geschlecht, dessen 1/2-Kodierung
# nicht hartkodiert angenommen werden darf).
staxi2_label_text = function(spalte_original, wert) {
if (is.null(wert) || length(wert) == 0 || is.na(wert[1])) return(NA_character_)
labels_attr = attr(spalte_original, "labels")
if (is.null(labels_attr) || length(labels_attr) == 0) return(NA_character_)
pos = which(as.vector(labels_attr) == suppressWarnings(as.numeric(wert[1])))
if (length(pos) == 0) return(NA_character_)
trimws(names(labels_attr)[pos[1]])
}
# Entfernt Markdown-Reste (Fettschrift-Sternchen, escapte Punkte) und die
# fuehrende Itemnummer aus Item-/Antworttexten fuer die Anzeige (formr
# liefert z.B. "3\\. Es aergert mich..." statt "Es aergert mich...";
# die Itemnummer wird ohnehin separat als eigenes Badge angezeigt).
staxi2_bereinige_text = function(x) {
if (is.null(x) || length(x) == 0 || is.na(x[1])) return(NA_character_)
text = as.character(x[1])
text = gsub("\\*\\*", "", text)
text = gsub("\\.", ".", text, fixed = TRUE)
text = sub("^\\s*\\d+\\.\\s*", "", text)
trimws(text)
}
altersgruppe_von = function(alter) {
alter = as.numeric(alter)
if (is.na(alter)) return(NA_character_)
if (alter < 16) return(NA_character_)
if (alter <= 39) return("16_39")
if (alter <= 59) return("40_59")
return("60plus")
}
# Missing-Value-Regel je Skala: >= 2 fehlende Items -> nicht auswertbar;
# 1 fehlendes Item -> Ersatzwert aus dem gerundeten Mittelwert der uebrigen
# Items derselben Skala; 0 fehlende Items -> normale Summe.
staxi2_score_skala = function(werte) {
n_missing = sum(is.na(werte))
if (n_missing >= 2) {
return(list(rohwert = NA_real_, missing_n = n_missing, auswertbar = FALSE))
}
if (n_missing == 1) {
ersatz = round(mean(werte, na.rm = TRUE))
werte[is.na(werte)] = ersatz
}
list(rohwert = sum(werte), missing_n = n_missing, auswertbar = TRUE)
}
# Exakter Rohwert-Lookup in einer Normtabelle. Kein Treffer oder leeres Feld
# (Rohwert ausserhalb der Eichstichprobe) -> naechstgelegenen verfuegbaren Wert
# verwenden und das sichtbar als Notloesung kennzeichnen.
norm_lookup = function(tabelle, rohwert) {
zeile = tabelle[tabelle$rohwert == rohwert, ]
if (nrow(zeile) == 1 && !is.na(zeile$t_wert)) {
return(list(t_wert = zeile$t_wert, prozentrang = zeile$prozentrang, ausserhalb_eichstichprobe = FALSE))
}
gueltig = tabelle[!is.na(tabelle$t_wert), ]
if (nrow(gueltig) == 0) stop("Keine gueltigen Normwerte in dieser Tabelle.")
index_naechster = which.min(abs(gueltig$rohwert - rohwert))
list(
t_wert = gueltig$t_wert[index_naechster],
prozentrang = gueltig$prozentrang[index_naechster],
ausserhalb_eichstichprobe = TRUE
)
}
staxi2_klassifiziere = function(t_wert) {
if (is.null(t_wert) || length(t_wert) == 0 || is.na(t_wert)) {
return(list(stufe = NA_integer_, text = "nicht bestimmbar", farbe = "#9E9E9E"))
}
if (t_wert < 30) return(list(stufe = 0L, text = "stark unterdurchschnittlich", farbe = unname(STAXI2_STUFEN_FARBEN["0"])))
if (t_wert <= 39) return(list(stufe = 1L, text = "unterdurchschnittlich", farbe = unname(STAXI2_STUFEN_FARBEN["1"])))
if (t_wert <= 60) return(list(stufe = 2L, text = "durchschnittlich", farbe = unname(STAXI2_STUFEN_FARBEN["2"])))
if (t_wert <= 70) return(list(stufe = 3L, text = "ueberdurchschnittlich", farbe = unname(STAXI2_STUFEN_FARBEN["3"])))
list(stufe = 4L, text = "stark ueberdurchschnittlich", farbe = unname(STAXI2_STUFEN_FARBEN["4"]))
}
# Zeile aus Tabelle 6 (Standardmessfehler/Konfidenzintervalle/kritische Differenzen)
# nachschlagen. Die bekannte Luecke AC-I/frauen/40_59 wird explizit als solche
# markiert (kein Ersatzwert, keine Nachbarzeile), siehe Abschnitt 8.
tabelle6_lookup = function(tabelle6, skala, geschlecht, altersgruppe) {
zeile = tabelle6[
tabelle6$skala == skala &
tabelle6$geschlecht == geschlecht &
tabelle6$altersgruppe == altersgruppe, , drop = FALSE
]
if (nrow(zeile) == 0) {
return(list(gefunden = FALSE, luecke = FALSE))
}
if (identical(trimws(as.character(zeile$alpha[1])), "FEHLT_IM_SCAN")) {
return(list(gefunden = FALSE, luecke = TRUE))
}
list(
gefunden = TRUE,
luecke = FALSE,
alpha = suppressWarnings(as.numeric(zeile$alpha[1])),
se = suppressWarnings(as.numeric(zeile$se[1])),
ci90 = suppressWarnings(as.numeric(zeile$ci90[1])),
ci95 = suppressWarnings(as.numeric(zeile$ci95[1])),
ci99 = suppressWarnings(as.numeric(zeile$ci99[1])),
kritdiff_zweiseitig_5 = suppressWarnings(as.numeric(zeile$kritdiff_zweiseitig_5[1]))
)
}
# Optionale, sichtbar gekennzeichnete Notloesung fuer AC-I/frauen/40_59, nur
# auf explizite Nutzeranforderung eingeblendet (siehe Abschnitt 8).
STAXI2_ACI_NOTLOESUNG = list(
list(bezeichnung = "AC-I, Frauen, 60+", alpha = 0.79, se = 4.58),
list(bezeichnung = "AC-I, Gesamt, 40-59", alpha = 0.80, se = 4.47)
)
make_gauge_staxi2 = function(t_wert) {
zonen = data.frame(
xmin = c(20, 30, 40, 60, 70),
xmax = c(30, 40, 60, 70, 80),
farbe = c(unname(STAXI2_STUFEN_FARBEN["0"]), unname(STAXI2_STUFEN_FARBEN["1"]),
unname(STAXI2_STUFEN_FARBEN["2"]), unname(STAXI2_STUFEN_FARBEN["3"]),
unname(STAXI2_STUFEN_FARBEN["4"])),
stringsAsFactors = FALSE
)
p = ggplot() +
geom_rect(data = zonen, aes(xmin = xmin, xmax = xmax, ymin = 0, ymax = 1, fill = farbe),
color = "white", linewidth = 0.6) +
scale_fill_identity() +
scale_x_continuous(limits = c(20, 80), breaks = c(20, 30, 40, 50, 60, 70, 80)) +
scale_y_continuous(limits = c(-0.35, 1.35)) +
labs(x = "T-Wert", y = NULL) +
theme_minimal(base_size = 11) +
theme(
axis.text.y = element_blank(),
axis.ticks.y = element_blank(),
panel.grid = element_blank(),
plot.background = element_rect(fill = "white", colour = NA),
panel.background = element_rect(fill = "white", colour = NA),
plot.margin = margin(t = 14, r = 8, b = 4, l = 8)
)
if (!is.na(t_wert)) {
t_geklemmt = max(20, min(80, t_wert))
p = p +
geom_segment(aes(x = t_geklemmt, xend = t_geklemmt, y = -0.15, yend = 1.15),
color = AKZENT_FARBE, linewidth = 2.2, lineend = "round") +
annotate("text", x = t_geklemmt, y = 1.27, label = paste0("T = ", round(t_wert)),
color = AKZENT_FARBE, fontface = "bold", size = 3.6)
}
p
}
make_profil_plot_staxi2 = function(werte) {
skalen = names(werte)
n = length(skalen)
df_punkte = data.frame(x = seq_len(n), t_wert = as.numeric(werte))
zonen = data.frame(
ymin = c(20, 30, 40, 60, 70),
ymax = c(30, 40, 60, 70, 80),
farbe = c(unname(STAXI2_STUFEN_FARBEN["0"]), unname(STAXI2_STUFEN_FARBEN["1"]),
unname(STAXI2_STUFEN_FARBEN["2"]), unname(STAXI2_STUFEN_FARBEN["3"]),
unname(STAXI2_STUFEN_FARBEN["4"])),
stringsAsFactors = FALSE
)
ggplot() +
geom_rect(data = zonen,
aes(xmin = 0.5, xmax = n + 0.5, ymin = ymin, ymax = ymax, fill = farbe),
alpha = 0.55) +
scale_fill_identity() +
geom_line(data = df_punkte, aes(x = x, y = t_wert), color = AKZENT_FARBE,
linewidth = 1.1, na.rm = TRUE) +
geom_point(data = df_punkte, aes(x = x, y = t_wert), color = AKZENT_FARBE,
size = 3.2, na.rm = TRUE) +
geom_text(data = df_punkte,
aes(x = x, y = t_wert, label = ifelse(is.na(t_wert), "", as.character(round(t_wert)))),
vjust = -1.1, color = AKZENT_FARBE, fontface = "bold", size = 3.6) +
scale_x_continuous(breaks = seq_len(n), labels = skalen, limits = c(0.5, n + 0.5)) +
scale_y_continuous(limits = c(18, 82), breaks = c(20, 30, 40, 50, 60, 70, 80)) +
labs(x = NULL, y = "T-Wert") +
theme_minimal(base_size = 12) +
theme(
panel.grid.minor = element_blank(),
plot.background = element_rect(fill = "white", colour = NA),
panel.background = element_rect(fill = "white", colour = NA),
axis.text.x = element_text(face = "bold", color = "#333333")
)
}
# Datenaufbereitung ####
alle_s_cols = sprintf("staxi2_s_%02d", 1:15)
alle_t_cols = sprintf("staxi2_t_%02d", 1:10)
alle_e_cols = sprintf("staxi2_e_%02d", 1:26)
STAXI2_SKALEN = list(
"S-Ang/F" = paste0("staxi2_s_", sprintf("%02d", c(1, 4, 6, 9, 11))),
"S-Ang/V" = paste0("staxi2_s_", sprintf("%02d", c(2, 5, 8, 13, 15))),
"S-Ang/P" = paste0("staxi2_s_", sprintf("%02d", c(3, 7, 10, 12, 14))),
"T-Ang/T" = paste0("staxi2_t_", sprintf("%02d", c(1, 2, 5, 8, 10))),
"T-Ang/R" = paste0("staxi2_t_", sprintf("%02d", c(3, 4, 6, 7))),
"AX-O" = paste0("staxi2_e_", sprintf("%02d", c(3, 6, 12, 14, 16, 18, 24, 25))),
"AX-I" = paste0("staxi2_e_", sprintf("%02d", c(2, 8, 11, 13, 15, 19, 22, 26))),
"AC-O" = paste0("staxi2_e_", sprintf("%02d", c(1, 5, 9, 20, 23))),
"AC-I" = paste0("staxi2_e_", sprintf("%02d", c(4, 7, 10, 17, 21)))
)
STAXI2_SKALEN[["S-Ang"]] = c(STAXI2_SKALEN[["S-Ang/F"]], STAXI2_SKALEN[["S-Ang/V"]], STAXI2_SKALEN[["S-Ang/P"]])
STAXI2_SKALEN[["T-Ang"]] = c(STAXI2_SKALEN[["T-Ang/T"]], STAXI2_SKALEN[["T-Ang/R"]], "staxi2_t_09")
STAXI2_SKALEN[["AC"]] = c(STAXI2_SKALEN[["AC-O"]], STAXI2_SKALEN[["AC-I"]])
STAXI2_WERTEBEREICH = list(
"S-Ang/F" = c(5, 20), "S-Ang/V" = c(5, 20), "S-Ang/P" = c(5, 20), "S-Ang" = c(15, 60),
"T-Ang/T" = c(5, 20), "T-Ang/R" = c(4, 16), "T-Ang" = c(10, 40),
"AX-O" = c(8, 32), "AX-I" = c(8, 32),
"AC-O" = c(5, 20), "AC-I" = c(5, 20), "AC" = c(10, 40)
)
STAXI2_STATE_SKALEN = c("S-Ang", "S-Ang/F", "S-Ang/V", "S-Ang/P")
STAXI2_NORMIERTE_SKALEN = c("T-Ang", "T-Ang/T", "T-Ang/R", "AX-O", "AX-I", "AC", "AC-O", "AC-I")
STAXI2_HAUPTSKALEN_PROFIL = c("T-Ang", "AX-O", "AX-I", "AC")
STAXI2_SKALA_DATEIPRAEFIX = c(
"T-Ang" = "t_ang",
"T-Ang/T" = "t_ang_t",
"T-Ang/R" = "t_ang_r",
"AX-O" = "ax_o",
"AX-I" = "ax_i",
"AC" = "ac",
"AC-O" = "ac_o",
"AC-I" = "ac_i"
)
STAXI2_ALTERSGRUPPEN = c("16_39", "40_59", "60plus")
STAXI2_ALTERSGRUPPEN_LABEL = c("16_39" = "16-39 Jahre", "40_59" = "40-59 Jahre", "60plus" = "60+ Jahre")
STAXI2_GESCHLECHTER = c("gesamt", "maenner", "frauen")
if (!dir.exists(PFAD_NORMTABELLEN)) {
stop(paste0("Normtabellen-Ordner nicht gefunden: ", PFAD_NORMTABELLEN,
". Bitte den Ordner 'normtabellen/' mit den 73 CSV-Dateien neben app.R ablegen."))
}
staxi2_normtabellen = list()
for (praefix in STAXI2_SKALA_DATEIPRAEFIX) {
staxi2_normtabellen[[praefix]] = list()
for (ag in STAXI2_ALTERSGRUPPEN) {
staxi2_normtabellen[[praefix]][[ag]] = list()
for (gs in STAXI2_GESCHLECHTER) {
dateiname = paste0(praefix, "_", ag, "_", gs, ".csv")
pfad = file.path(PFAD_NORMTABELLEN, dateiname)
if (!file.exists(pfad)) {
stop(paste0("Normtabelle nicht gefunden: ", pfad))
}
tab = tryCatch(
read.csv(pfad, stringsAsFactors = FALSE, na.strings = c("", "NA")),
error = function(e) stop(paste0("Fehler beim Einlesen von '", dateiname, "': ", e$message))
)
for (spalte in c("rohwert", "t_wert", "prozentrang")) {
if (!(spalte %in% names(tab))) {
stop(paste0("Normtabelle '", dateiname, "' hat keine Spalte '", spalte, "'."))
}
}
staxi2_normtabellen[[praefix]][[ag]][[gs]] = tab
}
}
}
PFAD_TABELLE6 = file.path(PFAD_NORMTABELLEN, "tabelle6_standardmessfehler_konfidenzintervalle.csv")
if (!file.exists(PFAD_TABELLE6)) {
stop(paste0("Tabelle 6 (Standardmessfehler/Konfidenzintervalle) nicht gefunden: ", PFAD_TABELLE6))
}
staxi2_tabelle6 = tryCatch(
read.csv(PFAD_TABELLE6, stringsAsFactors = FALSE, na.strings = c("", "NA")),
error = function(e) stop(paste0("Fehler beim Einlesen von Tabelle 6: ", e$message))
)
# UI ####
app_css = "
body { font-family: 'Segoe UI', Arial, sans-serif; background: #f5f5f5; }
.container-fluid { max-width: 1150px; }
.app-header {
background: #8B2635; color: white; padding: 18px 24px 14px;
margin-bottom: 20px; border-radius: 0 0 6px 6px;
}
.app-header h2 { margin: 0; font-size: 1.5rem; font-weight: 600; }
.app-header p { margin: 4px 0 0; opacity: 0.85; font-size: 0.9rem; }
.input-panel {
background: white; border-radius: 6px; padding: 16px 20px;
margin-bottom: 16px; box-shadow: 0 1px 3px rgba(0,0,0,.12);
display: flex; align-items: flex-end; gap: 12px; flex-wrap: wrap;
}
.input-panel .form-group { margin-bottom: 0; }
.input-panel label { font-weight: 600; color: #333; }
.btn-laden {
background: #8B2635 !important; color: white !important;
border: none !important; border-radius: 4px !important;
padding: 8px 20px !important; font-weight: 600 !important; cursor: pointer;
}
.btn-laden:hover { background: #6d1e29 !important; }
.alert-fehler {
background: #FFEBEE; border-left: 5px solid #C62828;
padding: 12px 16px; border-radius: 4px; color: #B71C1C;
margin-bottom: 12px; font-weight: 500;
}
.alert-warnung {
background: #FFF3E0; border-left: 5px solid #E65100;
padding: 10px 16px; border-radius: 4px; color: #BF360C;
margin-bottom: 12px; font-size: 0.93em; font-weight: 500;
}
.abschnitt-karte {
background: white; border-radius: 6px; padding: 20px 24px;
margin-bottom: 16px; box-shadow: 0 1px 3px rgba(0,0,0,.12);
}
.abschnitt-titel {
color: #8B2635; font-size: 1.15rem; font-weight: 700;
border-bottom: 2px solid #8B2635; padding-bottom: 8px; margin-bottom: 14px;
}
.meta-block { margin-bottom: 10px; color: #555; font-size: 0.95em; }
.meta-block strong { color: #222; }
.hinweis-block {
font-size: 0.86em; color: #777; font-style: italic; margin-bottom: 12px;
}
.item-zeile {
display: flex; align-items: flex-start; gap: 10px;
padding: 7px 0; border-bottom: 1px solid #F0F0F0;
}
.item-nr { font-weight: 600; color: #8B2635; min-width: 26px; flex-shrink: 0; }
.item-text { flex: 1; color: #333; font-size: 0.92em; }
.stufe-badge {
border-radius: 4px; padding: 2px 9px; font-weight: 700;
font-size: 0.82em; white-space: nowrap; display: inline-block; flex-shrink: 0;
}
.stufe-badge-0 { background: #1B5E20; color: white; }
.stufe-badge-1 { background: #81C784; color: #222222; }
.stufe-badge-2 { background: #9E9E9E; color: white; }
.stufe-badge-3 { background: #FFB74D; color: #222222; }
.stufe-badge-4 { background: #C62828; color: white; }
.skalen-grid { display: flex; gap: 16px; flex-wrap: wrap; }
.skala-block {
flex: 1; min-width: 300px; border: 1px solid #eee; border-radius: 6px;
padding: 12px 14px; margin-bottom: 12px;
}
.skala-titel { font-weight: 700; color: #333; margin-bottom: 6px; }
.rohwert-anzeige { font-size: 1.7rem; font-weight: 800; color: #222; }
.rohwert-hinweis { color: #777; font-size: 0.86em; margin-top: 2px; }
.ci-info { color: #555; font-size: 0.88em; margin-top: 6px; }
.state-grid { display: flex; gap: 14px; flex-wrap: wrap; margin-bottom: 10px; }
.state-block { flex: 1; min-width: 160px; text-align: center; padding: 8px; }
.state-label { color: #777; font-size: 0.85em; }
.item-liste { margin-top: 10px; border-top: 1px solid #eee; padding-top: 4px; }
.antwort-badge {
border-radius: 3px; padding: 2px 8px; font-weight: 700;
font-size: 0.8em; white-space: nowrap; display: inline-block;
flex-shrink: 0; min-width: 70px; text-align: center;
}
.antwort-badge-1 { background: #4CAF50; color: white; }
.antwort-badge-2 { background: #F48FB1; color: #333333; }
.antwort-badge-3 { background: #EF5350; color: white; }
.antwort-badge-4 { background: #B71C1C; color: white; }
.antwort-badge-fehlend { background: #BDBDBD; color: white; }
"
app_css = gsub("#8B2635", AKZENT_FARBE, app_css, fixed = TRUE)
ui = fluidPage(
tags$head(
tags$meta(charset = "UTF-8"),
tags$style(HTML(app_css))
),
div(class = "app-header",
tags$h2("STAXI-2 - State-Trait-Aergerausdrucks-Inventar 2"),
tags$p("Deutsche Version, Verlag Hans Huber/Hogrefe | Einzelfall-Auswertung")
),
div(class = "container-fluid",
div(class = "input-panel",
div(style = "min-width: 360px; white-space: nowrap;",
textInput("pseudonym",
label = tagList(
"Pseudonym",
tags$span(style = "font-weight: normal; font-style: italic; font-size: 0.78em; color: #888; margin-left: 4px; white-space: nowrap;",
"optional, hat Vorrang vor Chiffre")
),
placeholder = "optional", width = "340px")
),
div(style = "min-width: 200px;",
textInput("chiffre", label = "Patientenchiffre",
placeholder = "z.B. P000123", width = "100%")
),
actionButton("btn_suchen", "Auswerten", class = "btn btn-primary btn-laden"),
div(style = "margin-left: auto;",
downloadButton("download_word", "Word-Export (.docx)")
)
),
uiOutput("fehler_ui"),
uiOutput("warnung_ui"),
uiOutput("ergebnis_ui")
)
)
# Word-Export ####
erstelle_staxi2_docx = function(erg) {
doc = read_docx()
fp_titel = fp_text(color = AKZENT_FARBE, bold = TRUE, font.size = 18)
fp_abschnitt = fp_text(color = AKZENT_FARBE, bold = TRUE, font.size = 13)
fp_label = fp_text(bold = TRUE, font.size = 11)
fp_normal = fp_text(font.size = 11)
fp_warnung = fp_text(font.size = 10, italic = TRUE, color = "#BF360C")
fp_disclaimer = fp_text(font.size = 9, italic = TRUE, color = "#777777")
# Einzelitems einer Skala als eigene Zeilen mit farbigem Antwortbadge,
# gleiche Reihenfolge/Aufbereitung wie in der UI (e$items ist bereits
# nach Rohwert absteigend sortiert, Markdown/Nummer bereits bereinigt).
fuege_item_liste_hinzu = function(doc, items) {
if (is.null(items) || length(items) == 0) return(doc)
for (it in items) {
item_text = if (is.na(it$text) || nchar(it$text) == 0) paste0("Item ", it$nr) else it$text
if (is.na(it$wert)) {
badge_farbe = "#BDBDBD"
badge_text_farbe = "white"
badge_text = "fehlend"
} else {
wert_key = as.character(it$wert)
badge_farbe = unname(STAXI2_ANTWORT_FARBEN[wert_key])
badge_text_farbe = unname(STAXI2_ANTWORT_TEXT_FARBEN[wert_key])
badge_text = if (!is.na(it$antwort_text) && nchar(it$antwort_text) > 0) {
paste0(it$wert, " - ", it$antwort_text)
} else {
as.character(it$wert)
}
}
doc = body_add_fpar(doc, fpar(
ftext(sprintf(" %2d. ", it$nr), fp_text(bold = TRUE, font.size = 9, color = "#888888")),
ftext(paste0(item_text, " "), fp_text(font.size = 9, color = "#333333")),
ftext(paste0(" ", badge_text, " "),
fp_text(color = badge_text_farbe, bold = TRUE, font.size = 9, shading.color = badge_farbe))
))
}
doc
}
doc = body_add_fpar(doc, fpar(ftext("STAXI-2", fp_titel)))
doc = body_add_fpar(doc, fpar(
ftext("Chiffre: ", fp_label), ftext(erg$chiffre, fp_normal),
ftext(" Ausfuelldatum: ", fp_label), ftext(erg$ausfuelldatum, fp_normal),
ftext(" Alter: ", fp_label),
ftext(if (is.na(erg$alter)) "k. A." else as.character(erg$alter), fp_normal),
ftext(" Geschlecht: ", fp_label),
ftext(if (is.na(erg$geschlecht_gruppe)) "nicht zuordenbar" else erg$geschlecht_gruppe, fp_normal)
))
if (!is.null(erg$mehrfach_warnung)) {
doc = body_add_fpar(doc, fpar(ftext(
paste0("Mehrere Ausfuellungen gefunden (", erg$mehrfach_warnung$n,
" Eintraege), es wird die neueste angezeigt."), fp_warnung
)))
}
if (isTRUE(erg$altersgruppe_zu_jung)) {
doc = body_add_fpar(doc, fpar(ftext(
"Alter unter 16 Jahren, das Instrument ist ab 16 Jahren normiert. Keine Normwerte.",
fp_warnung
)))
}
if (isTRUE(erg$geschlecht_unbekannt)) {
doc = body_add_fpar(doc, fpar(ftext(
"Geschlecht nicht eindeutig zuordenbar, Normwerte konnten nicht berechnet werden.",
fp_warnung
)))
}
doc = body_add_par(doc, "", style = "Normal")
doc = body_add_fpar(doc, fpar(ftext("Teil 1 - State-Aerger (S-Ang)", fp_abschnitt)))
doc = body_add_fpar(doc, fpar(ftext(
"Fuer diese Skala werden laut Testmanual keine Normwerte ausgewiesen (nur Rohwerte).",
fp_disclaimer
)))
for (sk in STAXI2_STATE_SKALEN) {
e = erg$skalen[[sk]]
txt = if (isTRUE(e$auswertbar)) paste0(sk, ": ", e$rohwert) else paste0(sk, ": nicht auswertbar (zu viele fehlende Werte)")
doc = body_add_fpar(doc, fpar(ftext(txt, fp_normal)))
doc = fuege_item_liste_hinzu(doc, e$items)
}
doc = body_add_par(doc, "", style = "Normal")
fuege_normierte_skala_hinzu = function(doc, sk) {
e = erg$skalen[[sk]]
if (!isTRUE(e$auswertbar)) {
doc = body_add_fpar(doc, fpar(
ftext(paste0(sk, ": "), fp_label),
ftext("nicht auswertbar (zu viele fehlende Werte)", fp_normal)
))
doc = fuege_item_liste_hinzu(doc, e$items)
return(doc)
}
if (!isTRUE(e$normierbar)) {
doc = body_add_fpar(doc, fpar(
ftext(paste0(sk, ": Rohwert ", e$rohwert, " - "), fp_normal),
ftext(paste0("kein Normwert (", e$norm_grund, ")"), fp_warnung)
))
doc = fuege_item_liste_hinzu(doc, e$items)
return(doc)
}
klass_farbe = STAXI2_STUFEN_FARBEN[[as.character(e$klass$stufe)]]
klass_text_farbe = STAXI2_STUFEN_TEXT_FARBEN[[as.character(e$klass$stufe)]]
fp_badge = fp_text(color = klass_text_farbe, bold = TRUE, font.size = 10, shading.color = klass_farbe)
aussen_txt = if (isTRUE(e$ausserhalb_eichstichprobe)) " (ausserhalb Eichstichprobe, naechstgelegener Wert)" else ""
doc = body_add_fpar(doc, fpar(
ftext(paste0(sk, ": Rohwert ", e$rohwert, " T-Wert ", round(e$t_wert),
" PR ", e$prozentrang, aussen_txt, " "), fp_normal),
ftext(paste0(" ", e$klass$text, " "), fp_badge)
))
if (!is.null(e$tabelle6) && isTRUE(e$tabelle6$gefunden)) {
doc = body_add_fpar(doc, fpar(ftext(
sprintf(" 95%%-Konfidenzintervall: T = %s +/- %s (%.1f - %.1f)",
round(e$t_wert), round(e$tabelle6$ci95, 1),
e$t_wert - e$tabelle6$ci95, e$t_wert + e$tabelle6$ci95),
fp_text(font.size = 10, color = "#555555")
)))
} else if (!is.null(e$tabelle6) && isTRUE(e$tabelle6$luecke)) {
doc = body_add_fpar(doc, fpar(ftext(
paste0(" Fuer ", sk, ", ", erg$geschlecht_gruppe, ", ",
STAXI2_ALTERSGRUPPEN_LABEL[[erg$altersgruppe]],
" liegen keine Werte fuer Standardmessfehler und Konfidenzintervall vor (Luecke im Quellscan des Manuals)."),
fp_warnung
)))
}
doc = fuege_item_liste_hinzu(doc, e$items)
doc
}
doc = body_add_fpar(doc, fpar(ftext("Teil 2 - Trait-Aerger (T-Ang)", fp_abschnitt)))
for (sk in c("T-Ang", "T-Ang/T", "T-Ang/R")) doc = fuege_normierte_skala_hinzu(doc, sk)
doc = body_add_par(doc, "", style = "Normal")
doc = body_add_fpar(doc, fpar(ftext("Teil 3 - Aergerausdruck und -kontrolle", fp_abschnitt)))
for (sk in c("AX-O", "AX-I", "AC", "AC-O", "AC-I")) doc = fuege_normierte_skala_hinzu(doc, sk)
doc = body_add_par(doc, "", style = "Normal")
doc = body_add_fpar(doc, fpar(ftext(STAXI2_DISCLAIMER, fp_disclaimer)))
doc
}
# Server ####
server = function(input, output, session) {
observe({
query = parseQueryString(session$clientData$url_search)
if (!is.null(query$pseudonym) && nchar(trimws(query$pseudonym)) > 0) {
updateTextInput(session, "pseudonym", value = trimws(query$pseudonym))
}
})
observe({
query = parseQueryString(session$clientData$url_search)
if (!is.null(query$chiffre) && nchar(trimws(query$chiffre)) > 0) {
updateTextInput(session, "chiffre", value = toupper(trimws(query$chiffre)))
}
})
ergebnis_r = eventReactive(input$btn_suchen, {
chiffre = toupper(trimws(input$chiffre))
pseudonym_eingabe = trimws(input$pseudonym)
if (nchar(pseudonym_eingabe) == 0 && nchar(chiffre) == 0) {
return(list(typ = "format_fehler",
meldung = "Bitte eine Patientenchiffre eingeben."))
}
if (!(nchar(pseudonym_eingabe) > 0 || grepl("^[A-Z][0-9]{6}$", chiffre))) {
return(list(typ = "format_fehler",
meldung = "Ungueltige Chiffre. Erwartet: ein Grossbuchstabe + 6 Ziffern (z.B. P000123)."))
}
fehlende_skripte = c(
if (!file.exists(PFAD_DOWNLOAD_SKRIPT)) PFAD_DOWNLOAD_SKRIPT,
if (!file.exists(PFAD_PSEUDONYM_SKRIPT)) PFAD_PSEUDONYM_SKRIPT
)
if (length(fehlende_skripte) > 0) {
return(list(typ = "pfad_fehler",
meldung = paste0("Skript(e) nicht gefunden:\n", paste(fehlende_skripte, collapse = "\n"))))
}
ok_dl = tryCatch({
source(PFAD_DOWNLOAD_SKRIPT, local = FALSE)
list(ok = TRUE)
}, error = function(e) list(ok = FALSE, msg = e$message))
if (!ok_dl$ok) {
return(list(typ = "skript_fehler",
meldung = paste0("Fehler im Download-Skript: ", ok_dl$msg)))
}
if (!exists("daten_staxi2", envir = .GlobalEnv) ||
!is.data.frame(get("daten_staxi2", envir = .GlobalEnv))) {
return(list(typ = "daten_fehler",
meldung = "Objekt 'daten_staxi2' nach dem Sourcen nicht gefunden oder kein Dataframe."))
}
daten_staxi2 = get("daten_staxi2", envir = .GlobalEnv)
db_ordner = local({
ordner = dirname(normalizePath(PFAD_PSEUDONYM_SKRIPT, mustWork = FALSE))
gefunden = NULL
for (i in 1:5) {
if (file.exists(file.path(ordner, "pseudonyme.db"))) {
gefunden = ordner
break
}
elternteil = dirname(ordner)
if (elternteil == ordner) break
ordner = elternteil
}
gefunden
})
if (is.null(db_ordner)) {
return(list(typ = "db_fehler",
meldung = paste0("pseudonyme.db nicht gefunden (bis 5 Ebenen oberhalb von ",
dirname(normalizePath(PFAD_PSEUDONYM_SKRIPT, mustWork = FALSE)), " gesucht).")))
}
alter_wd = getwd()
setwd(db_ordner)
on.exit(setwd(alter_wd), add = TRUE)
ok_ps = tryCatch({
source(PFAD_PSEUDONYM_SKRIPT, local = FALSE)
list(ok = TRUE)
}, error = function(e) list(ok = FALSE, msg = e$message))
if (!ok_ps$ok) {
return(list(typ = "db_fehler",
meldung = paste0("Fehler im Pseudonym-Skript: ", ok_ps$msg)))
}
if (!exists("pseudo", envir = .GlobalEnv) || !is.data.frame(get("pseudo", envir = .GlobalEnv))) {
return(list(typ = "db_fehler",
meldung = "Objekt 'pseudo' nach dem Sourcen nicht gefunden oder kein Dataframe."))
}
pseudo = get("pseudo", envir = .GlobalEnv)
if (nchar(pseudonym_eingabe) > 0) {
treffer_pw = pseudo[trimws(as.character(pseudo$pseudonym)) == pseudonym_eingabe, , drop = FALSE]
if (nrow(treffer_pw) > 0) chiffre = toupper(trimws(as.character(treffer_pw$chiffre[1])))
}
treffer_ps = pseudo[toupper(trimws(as.character(pseudo$chiffre))) == chiffre, , drop = FALSE]
if (nrow(treffer_ps) == 0) {
meldung = if (nchar(chiffre) == 0) {
paste0("Pseudonym '", pseudonym_eingabe, "' wurde in der Pseudonym-Datenbank nicht gefunden.")
} else {
paste0("Chiffre '", chiffre, "' wurde in der Pseudonym-Datenbank nicht gefunden.")
}
return(list(typ = "chiffre_nicht_gefunden", meldung = meldung))
}
session_ids = unique(as.character(treffer_ps$pseudonym))
if (nchar(pseudonym_eingabe) > 0) session_ids = pseudonym_eingabe
idx_kandidaten = which(as.character(daten_staxi2$session) %in% session_ids)
if (length(idx_kandidaten) == 0) {
return(list(typ = "session_nicht_gefunden",
meldung = paste0("Kein STAXI-2-Datensatz fuer Chiffre '", chiffre, "' gefunden. (",
length(session_ids), " Pseudonym(e) geprueft)")))
}
mehrfach_warnung = NULL
if (length(idx_kandidaten) > 1) {
zeitstempel = suppressWarnings(as.POSIXct(daten_staxi2$created[idx_kandidaten]))
idx_final = idx_kandidaten[which.max(zeitstempel)]
mehrfach_warnung = list(n = length(idx_kandidaten))
} else {
idx_final = idx_kandidaten[1]
}
zeile = daten_staxi2[idx_final, , drop = FALSE]
ausfuelldatum = tryCatch(
format(as.POSIXct(zeile[["created"]][1]), "%d.%m.%Y"),
error = function(e) "unbekannt"
)
item_extraktion = tryCatch({
werte_liste = list()
for (col in alle_s_cols) {
werte_liste[[col]] = item_wert(daten_staxi2[[col]], labels_teil1)
}
for (col in c(alle_t_cols, alle_e_cols)) {
werte_liste[[col]] = item_wert(daten_staxi2[[col]], labels_teil23)
}
list(ok = TRUE, werte_liste = werte_liste)
}, error = function(e) list(ok = FALSE, msg = e$message))
if (!item_extraktion$ok) {
return(list(typ = "itemfehler",
meldung = paste0(
"Fehler bei der Itemkodierung: ", item_extraktion$msg,
" Die gesamte Auswertung wird abgebrochen, da ein Kodierungsfehler alle Items ",
"derselben Antwortskala betreffen kann.")))
}
werte_liste = item_extraktion$werte_liste
item_wert_person = function(col) unname(werte_liste[[col]][idx_final])
alter_roh = suppressWarnings(as.numeric(trimws(as.character(zeile[["staxi2_alter"]][1]))))
alter = if (!is.na(alter_roh) && alter_roh > 0) alter_roh else NA_real_
geschlecht_text = staxi2_label_text(daten_staxi2[["staxi2_geschlecht"]], zeile[["staxi2_geschlecht"]])
geschlecht_gruppe = if (!is.na(geschlecht_text) && grepl("weiblich", geschlecht_text, ignore.case = TRUE)) {
"frauen"
} else if (!is.na(geschlecht_text) && grepl("nnlich", geschlecht_text, ignore.case = TRUE)) {
"maenner"
} else {
NA_character_
}
geschlecht_unbekannt = is.na(geschlecht_gruppe)
altersgruppe = altersgruppe_von(alter)
altersgruppe_zu_jung = !is.na(alter) && alter < 16
skalen_ergebnisse = list()
for (sk in names(STAXI2_SKALEN)) {
cols = STAXI2_SKALEN[[sk]]
werte = sapply(cols, item_wert_person)
sc = staxi2_score_skala(werte)
bereich = STAXI2_WERTEBEREICH[[sk]]
unplausibel = isTRUE(sc$auswertbar) && !is.na(sc$rohwert) &&
(sc$rohwert < bereich[1] || sc$rohwert > bereich[2])
items = lapply(seq_along(cols), function(i) {
col = cols[i]
list(
nr = as.integer(sub(".*_(\\d+)$", "\\1", col)),
text = staxi2_bereinige_text(attr(daten_staxi2[[col]], "label")),
wert = unname(werte[i]),
antwort_text = staxi2_bereinige_text(staxi2_label_text(daten_staxi2[[col]], zeile[[col]]))
)
})
# Sortierung nach Rohwert absteigend (hoechste Auspraegung zuerst),
# fehlende Werte ans Ende, bei Gleichstand nach Itemnummer aufsteigend.
item_werte = sapply(items, function(x) x$wert)
item_nrn = sapply(items, function(x) x$nr)
reihenfolge = order(is.na(item_werte), -ifelse(is.na(item_werte), 0, item_werte), item_nrn)
items = items[reihenfolge]
skalen_ergebnisse[[sk]] = c(sc, list(unplausibel = unplausibel, items = items))
}
for (sk in STAXI2_NORMIERTE_SKALEN) {
e = skalen_ergebnisse[[sk]]
if (!isTRUE(e$auswertbar)) {
e$normierbar = FALSE
e$norm_grund = "Skala nicht auswertbar (zu viele fehlende Werte)"
} else if (is.na(altersgruppe) || is.na(geschlecht_gruppe)) {
e$normierbar = FALSE
e$norm_grund = if (is.na(altersgruppe)) {
"Alter nicht auswertbar oder unter 16 Jahren"
} else {
"Geschlecht nicht eindeutig zuordenbar"
}
} else {
praefix = STAXI2_SKALA_DATEIPRAEFIX[[sk]]
tab = staxi2_normtabellen[[praefix]][[altersgruppe]][[geschlecht_gruppe]]
lk = norm_lookup(tab, e$rohwert)
e$normierbar = TRUE
e$t_wert = lk$t_wert
e$prozentrang = lk$prozentrang
e$ausserhalb_eichstichprobe = lk$ausserhalb_eichstichprobe
e$klass = staxi2_klassifiziere(lk$t_wert)
e$tabelle6 = tabelle6_lookup(staxi2_tabelle6, sk, geschlecht_gruppe, altersgruppe)
}
skalen_ergebnisse[[sk]] = e
}
list(
typ = "ok",
chiffre = chiffre,
ausfuelldatum = ausfuelldatum,
alter = alter,
geschlecht_gruppe = geschlecht_gruppe,
geschlecht_unbekannt = geschlecht_unbekannt,
altersgruppe = altersgruppe,
altersgruppe_zu_jung = altersgruppe_zu_jung,
mehrfach_warnung = mehrfach_warnung,
skalen = skalen_ergebnisse
)
})
notloesung_sichtbar = reactiveVal(FALSE)
observeEvent(input$btn_notloesung, {
notloesung_sichtbar(!notloesung_sichtbar())
})
output$fehler_ui = renderUI({
req(input$btn_suchen)
d = ergebnis_r()
if (!identical(d$typ, "ok")) div(class = "alert-fehler", d$meldung)
})
output$warnung_ui = renderUI({
req(input$btn_suchen)
d = ergebnis_r()
if (!identical(d$typ, "ok")) return(NULL)
meldungen = list()
if (!is.null(d$mehrfach_warnung)) {
meldungen = c(meldungen, paste0(
"Mehrere Ausfuellungen gefunden (", d$mehrfach_warnung$n,
" Eintraege). Angezeigt wird die neueste."))
}
if (isTRUE(d$altersgruppe_zu_jung)) {
meldungen = c(meldungen,
"Alter unter 16 Jahren: Das Instrument ist ab 16 Jahren normiert, es koennen keine Normwerte berechnet werden.")
}
if (isTRUE(d$geschlecht_unbekannt)) {
meldungen = c(meldungen,
"Geschlecht nicht eindeutig zuordenbar: Normwerte konnten nicht berechnet werden.")
}
unplausibel_skalen = names(Filter(function(x) isTRUE(x$unplausibel), d$skalen))
if (length(unplausibel_skalen) > 0) {
meldungen = c(meldungen, paste0(
"Rohwert ausserhalb des erwarteten Wertebereichs bei: ",
paste(unplausibel_skalen, collapse = ", "), "."))
}
if (length(meldungen) == 0) return(NULL)
tagList(lapply(meldungen, function(m) div(class = "alert-warnung", m)))
})
# Einzelitems einer Skala, absteigend nach Rohwert sortiert (Aufbereitung
# bereits im eventReactive erfolgt), mit farbigem Antwortbadge je Item.
item_liste_div = function(items) {
if (is.null(items) || length(items) == 0) return(NULL)
div(class = "item-liste",
lapply(items, function(it) {
wert_key = if (is.na(it$wert)) "fehlend" else as.character(it$wert)
item_text = if (is.na(it$text) || nchar(it$text) == 0) paste0("Item ", it$nr) else it$text
badge_text = if (is.na(it$wert)) {
"fehlend"
} else if (!is.na(it$antwort_text) && nchar(it$antwort_text) > 0) {
paste0(it$wert, " - ", it$antwort_text)
} else {
as.character(it$wert)
}
div(class = "item-zeile",
span(class = "item-nr", it$nr),
span(class = "item-text", item_text),
span(class = paste0("antwort-badge antwort-badge-", wert_key), badge_text)
)
})
)
}
skala_block_ui = function(sk, e, geschlecht_gruppe, altersgruppe) {
if (!isTRUE(e$auswertbar)) {
return(div(class = "skala-block",
div(class = "skala-titel", sk),
div(class = "rohwert-anzeige", "n. a."),
div(class = "rohwert-hinweis", "nicht auswertbar (zu viele fehlende Werte)"),
item_liste_div(e$items)
))
}
if (!isTRUE(e$normierbar)) {
return(div(class = "skala-block",
div(class = "skala-titel", sk),
div(class = "rohwert-anzeige", e$rohwert),
div(class = "rohwert-hinweis", paste0("kein Normwert: ", e$norm_grund)),
item_liste_div(e$items)
))
}
klass = e$klass
plot_id = paste0("gauge_", gsub("[^A-Za-z0-9]", "_", sk))
ci_block = if (!is.null(e$tabelle6) && isTRUE(e$tabelle6$gefunden)) {
div(class = "ci-info",
sprintf("95%%-KI: %.0f +/- %.1f (%.1f - %.1f)",
round(e$t_wert), e$tabelle6$ci95,
e$t_wert - e$tabelle6$ci95, e$t_wert + e$tabelle6$ci95)
)
} else if (!is.null(e$tabelle6) && isTRUE(e$tabelle6$luecke)) {
tagList(
div(class = "alert-warnung",
paste0("Fuer ", sk, ", ",
if (identical(geschlecht_gruppe, "frauen")) "Frauen" else "Maenner", ", ",
STAXI2_ALTERSGRUPPEN_LABEL[[altersgruppe]],
" liegen keine Werte fuer Standardmessfehler und Konfidenzintervall vor ",
"(Luecke im Quellscan des Manuals).")
),
actionLink("btn_notloesung", "Notloesungs-Vergleichswerte anzeigen/ausblenden"),
if (isTRUE(notloesung_sichtbar())) {
tagList(lapply(STAXI2_ACI_NOTLOESUNG, function(nl) {
div(style = "font-size:0.85em; color:#777; margin-top:4px;",
paste0(nl$bezeichnung, ": alpha = ", nl$alpha, " S_e = ", nl$se,
" (Notloesung, nicht die eigentlich fehlende Zelle)")
)
}))
}
)
} else NULL
div(class = "skala-block",
div(class = "skala-titel", sk),
div(class = "rohwert-anzeige", e$rohwert),
div(class = "rohwert-hinweis",
paste0("T = ", round(e$t_wert), " PR = ", e$prozentrang,
if (isTRUE(e$ausserhalb_eichstichprobe)) " (ausserhalb Eichstichprobe, naechstgelegener Wert)" else "")
),
span(class = paste0("stufe-badge stufe-badge-", klass$stufe), klass$text),
ci_block,
plotOutput(plot_id, height = "70px"),
item_liste_div(e$items)
)
}
output$ergebnis_ui = renderUI({
req(input$btn_suchen)
d = ergebnis_r()
if (!identical(d$typ, "ok")) return(NULL)
kopfzeile = div(class = "meta-block",
tags$strong("Chiffre: "), d$chiffre,
tags$span(" | ", style = "color:#ccc;"),
tags$strong("Ausfuelldatum: "), d$ausfuelldatum,
tags$span(" | ", style = "color:#ccc;"),
tags$strong("Alter: "), if (is.na(d$alter)) "k. A." else d$alter,
tags$span(" | ", style = "color:#ccc;"),
tags$strong("Geschlecht: "), if (is.na(d$geschlecht_gruppe)) "nicht zuordenbar" else d$geschlecht_gruppe
)
teil1_karte = div(class = "abschnitt-karte",
div(class = "abschnitt-titel", "Teil 1 - State-Aerger (S-Ang)"),
div(class = "hinweis-block",
"Fuer diese Skala werden laut Testmanual keine Normwerte ausgewiesen (nur Rohwerte, keine T-Werte, keine Klassifikation)."),
div(class = "state-grid",
lapply(STAXI2_STATE_SKALEN, function(sk) {
e = d$skalen[[sk]]
div(class = "state-block",
div(class = "state-label", sk),
div(class = "rohwert-anzeige",
if (isTRUE(e$auswertbar)) e$rohwert else "n. a."
)
)
})
),
div(class = "skalen-grid",
lapply(STAXI2_STATE_SKALEN, function(sk) {
e = d$skalen[[sk]]
div(class = "skala-block",
div(class = "skala-titel", sk),
item_liste_div(e$items)
)
})
)
)
profil_karte = div(class = "abschnitt-karte",
div(class = "abschnitt-titel", "Profil (T-Werte)"),
plotOutput("profil_plot", height = "220px")
)
teil2_karte = div(class = "abschnitt-karte",
div(class = "abschnitt-titel", "Teil 2 - Trait-Aerger (T-Ang)"),
div(class = "skalen-grid",
lapply(c("T-Ang", "T-Ang/T", "T-Ang/R"), function(sk) {
skala_block_ui(sk, d$skalen[[sk]], d$geschlecht_gruppe, d$altersgruppe)
})
)
)
teil3_karte = div(class = "abschnitt-karte",
div(class = "abschnitt-titel", "Teil 3 - Aergerausdruck und -kontrolle"),
div(class = "skalen-grid",
lapply(c("AX-O", "AX-I", "AC", "AC-O", "AC-I"), function(sk) {
skala_block_ui(sk, d$skalen[[sk]], d$geschlecht_gruppe, d$altersgruppe)
})
)
)
tagList(
kopfzeile,
teil1_karte,
profil_karte,
teil2_karte,
teil3_karte,
uiOutput("vergleich_ui")
)
})
output$profil_plot = renderPlot({
d = ergebnis_r()
req(identical(d$typ, "ok"))
werte = sapply(STAXI2_HAUPTSKALEN_PROFIL, function(sk) {
e = d$skalen[[sk]]
if (isTRUE(e$normierbar)) e$t_wert else NA_real_
})
names(werte) = STAXI2_HAUPTSKALEN_PROFIL
make_profil_plot_staxi2(werte)
}, bg = "transparent")
for (sk in STAXI2_NORMIERTE_SKALEN) {
local({
sk_lokal = sk
plot_id = paste0("gauge_", gsub("[^A-Za-z0-9]", "_", sk_lokal))
output[[plot_id]] = renderPlot({
d = ergebnis_r()
req(identical(d$typ, "ok"))
e = d$skalen[[sk_lokal]]
req(isTRUE(e$normierbar))
make_gauge_staxi2(e$t_wert)
}, bg = "transparent")
})
}
output$vergleich_ui = renderUI({
d = ergebnis_r()
req(identical(d$typ, "ok"))
verfuegbar = names(Filter(function(x) isTRUE(x$normierbar) && !is.na(x$t_wert), d$skalen[STAXI2_NORMIERTE_SKALEN]))
if (length(verfuegbar) < 2) return(NULL)
div(class = "abschnitt-karte",
div(class = "abschnitt-titel", "Vergleich zweier Skalen (optional)"),
div(class = "hinweis-block",
"Beide Skalen stammen aus derselben Alters-/Geschlechtsgruppe dieser Person; kritische Differenz auf Basis Tabelle 6, zweiseitig, alpha = .05."),
fluidRow(
column(5, selectInput("vergleich_skala1", "Skala 1", choices = verfuegbar, selected = verfuegbar[1])),
column(5, selectInput("vergleich_skala2", "Skala 2", choices = verfuegbar,
selected = verfuegbar[min(2, length(verfuegbar))]))
),
uiOutput("vergleich_ergebnis_ui")
)
})
output$vergleich_ergebnis_ui = renderUI({
d = ergebnis_r()
req(identical(d$typ, "ok"))
req(input$vergleich_skala1, input$vergleich_skala2)
if (identical(input$vergleich_skala1, input$vergleich_skala2)) {
return(div(class = "alert-warnung", "Bitte zwei unterschiedliche Skalen waehlen."))
}
e1 = d$skalen[[input$vergleich_skala1]]
e2 = d$skalen[[input$vergleich_skala2]]
if (is.null(e1$tabelle6) || is.null(e2$tabelle6) ||
!isTRUE(e1$tabelle6$gefunden) || !isTRUE(e2$tabelle6$gefunden)) {
return(div(class = "alert-warnung",
"Fuer mindestens eine der beiden Skalen liegt keine kritische Differenz vor (siehe Luecken-Hinweis oben)."))
}
kritdiff = e1$tabelle6$kritdiff_zweiseitig_5
diff = abs(e1$t_wert - e2$t_wert)
signifikant = !is.na(kritdiff) && diff >= kritdiff
div(
div(paste0("Differenz der T-Werte: ", round(diff, 1))),
div(paste0("Kritische Differenz (zweiseitig, alpha = .05): ", round(kritdiff, 1))),
div(style = paste0("font-weight:700; margin-top:6px; color:",
if (isTRUE(signifikant)) "#B71C1C" else "#2E7D32", ";"),
if (isTRUE(signifikant)) "Unterschied statistisch bedeutsam" else "Unterschied nicht statistisch bedeutsam")
)
})
output$download_word = downloadHandler(
filename = function() {
d = tryCatch(ergebnis_r(), error = function(e) NULL)
chiffre_esc = if (is.list(d) && identical(d$typ, "ok") && nchar(d$chiffre) > 0) {
gsub("[^A-Za-z0-9_-]", "_", d$chiffre)
} else {
"export"
}
ausfuelldatum_fn = tryCatch(
format(as.Date(d$ausfuelldatum, "%d.%m.%Y"), "%Y%m%d"),
error = function(e) format(Sys.Date(), "%Y%m%d")
)
if (is.na(ausfuelldatum_fn) || length(ausfuelldatum_fn) == 0) {
ausfuelldatum_fn = format(Sys.Date(), "%Y%m%d")
}
paste0("STAXI2_", chiffre_esc, "_", ausfuelldatum_fn, ".docx")
},
content = function(file) {
d = tryCatch(ergebnis_r(), error = function(e) NULL)
daten_ok = is.list(d) && identical(d$typ, "ok")
if (!daten_ok) {
doc = read_docx()
doc = body_add_par(doc,
"Kein Datensatz geladen. Bitte zuerst Chiffre eingeben und 'Auswerten' klicken.",
style = "Normal")
print(doc, target = file)
return()
}
doc = tryCatch(
erstelle_staxi2_docx(d),
error = function(e) {
err_doc = read_docx()
body_add_par(err_doc,
paste0("Fehler beim Erstellen des Word-Dokuments: ", e$message),
style = "Normal")
}
)
print(doc, target = file)
}
)
}
# Start ####
shinyApp(ui, server)