#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| viewerHeight: 700
#| viewerWidth: "100%"
suppressPackageStartupMessages(library(shiny))
suppressPackageStartupMessages(library(bslib))
# ── Colonne di default da mantenere ──────────────────────────────────────────
DEFAULT_KEEP <- c(
"SampleId", "Analysis Date Time",
"Length(cm)", "SurfArea(cm2)",
"AvgDiam(mm)", "RootVolume(cm3)",
"Tips", "Forks", "Crossings"
)
# ── Rinomina fissa ────────────────────────────────────────────────────────────
COL_RENAME <- c(
"RHIZO 2021a" = "SampleId",
"Analysis Date Time" = "datetime"
)
# ── Dizionario UI bilingue ────────────────────────────────────────────────────
TR <- list(
it = list(
app_subtitle = "Pulizia dati di morfologia radicale da",
app_descr = "Software per l'analisi dell'architettura radicale da immagini scansionate — Regent Instruments.",
privacy_title = "100% locale — i tuoi dati non lasciano mai il browser",
privacy_body = "Questa applicazione gira interamente nel tuo browser grazie a WebR. Nessun dato viene inviato a server esterni: il file che carichi viene elaborato solo sul tuo dispositivo. Puoi usarla offline, in rete aziendale o ovunque. Al primo avvio potrebbe essere necessario qualche secondo per il caricamento dell'ambiente R.",
guide_title = "📋 Come nominare i campioni in WinRhizo",
guide_intro = "Per evitare ambiguità in fase di analisi, conviene definire una convenzione di denominazione prima di iniziare la scansione. Questa app riconosce automaticamente due schemi:",
guide_case1_title = "Caso semplice — una sola replica per campione",
guide_case1_body = "Usa il formato TRATTAMENTO_replica, con un underscore prima del numero progressivo.",
guide_case1_ex1 = "TRATTAMENTO_1, TRATTAMENTO_2, TRATTAMENTO_3 …",
guide_case2_title = "Caso con due livelli — replica biologica e tecnica",
guide_case2_body = "Se ogni vaso contiene più piante (o più sotto-campioni), conviene distinguere tra replica biologica (il vaso, cioè un individuo o gruppo indipendente) e replica tecnica (la singola pianta o misura ripetuta all'interno dello stesso vaso). Usa TRATTAMENTO.vaso.pianta, con un punto prima dei due numeri:",
guide_case2_ex1 = "TRATTAMENTO.1.1, TRATTAMENTO.1.2 → vaso 1, piante 1 e 2",
guide_case2_ex2 = "TRATTAMENTO.2.1, TRATTAMENTO.2.2 → vaso 2, piante 1 e 2",
guide_why_title = "Perché il punto e non l'underscore per vaso/pianta?",
guide_why_body = "Quando il nome del trattamento è composto da più parole unite da underscore (es. 100_PPM_UREA), l'app non ha modo di sapere a colpo d'occhio dove finisce il nome e dove iniziano i numeri di vaso e pianta: tutti gli underscore sembrano uguali. Usando il punto solo per introdurre i numeri di replica, il confine tra nome e numeri diventa sempre riconoscibile, qualunque sia la complessità del nome del trattamento.",
guide_why_ex1 = "100_PPM_UREA_1_2 → ambiguo: l'ultimo '_1' potrebbe far parte del nome o essere il vaso",
guide_why_ex2 = "100_PPM_UREA.1.2 → chiaro: trattamento 100_PPM_UREA, vaso 1, pianta 2",
guide_collapse = "Comprimi guida",
guide_expand = "Mostra guida",
status_waiting = "In attesa",
status_loaded = "✓ File caricato",
status_clean = "✓ File pulito",
status_trt = "✓ Trattamenti aggiunti",
status_final = "✓ File finale pronto",
status_error = "✗ Errore",
step1 = "1 — Carica il file",
file_placeholder = "Seleziona un file .txt esportato da WinRhizo…",
file_btn = "Sfoglia",
file_info = function(nr, nc) paste0(nr, " campioni · ", nc, " colonne rilevate"),
step2 = "2 — Seleziona le colonne da mantenere",
sel_all = "Seleziona tutte",
desel_all = "Deseleziona tutte",
col_meta = "colonne selezionate",
step3 = "3 — Genera il file pulito",
btn_clean = "Pulisci",
btn_dl_clean = "Scarica CSV pulito",
summary = "Riepilogo",
renamed_lbl = "Colonne rinominate automaticamente:",
stat_samples = "campioni",
stat_cols = "colonne",
stat_file = "nome file",
preview8 = "Anteprima dati",
step4 = "4 — Assegna i trattamenti ai campioni",
trt_hint = "WinRhizo codifica il trattamento nel nome del campione. Scegli lo schema corretto in base al tuo setup sperimentale, poi verifica o modifica i trattamenti personalizzati.",
trt_scheme_lbl = "Schema del nome campione:",
trt_scheme_1 = "Trattamento_Replica",
trt_scheme_1_sub = "es. XYZ_3 → trattamento XYZ, replica 3",
trt_scheme_2 = "Trattamento_Vaso_Pianta",
trt_scheme_2_sub = "es. X_Y_Z.1.2 → trattamento X_Y_Z, vaso 1, pianta 2",
trt_scheme_3 = "Manuale",
trt_scheme_3_sub = "assegna tu il ruolo di ogni parte del nome",
trt_manual_lbl = "Assegna un ruolo a ciascuna parte del SampleId:",
trt_manual_hint = "Basato sul primo campione del file. Riconosce underscore, punto e spazio come separatori. I segmenti consecutivi con lo stesso ruolo 'Trattamento' vengono uniti automaticamente.",
trt_manual_role_treatment = "Trattamento",
trt_manual_role_pot = "Vaso",
trt_manual_role_plant = "Pianta",
trt_manual_role_ignore = "Ignora",
trt_manual_mismatch = "Attenzione: alcuni SampleId hanno una struttura diversa dal campione di riferimento e non sono stati interpretati correttamente (mostrati con il nome originale come trattamento).",
trt_col_sample = "SampleId",
trt_col_detected = "Trattamento rilevato",
trt_col_replica = "Replica",
trt_col_vaso = "Vaso (rep. biologica)",
trt_col_pianta = "Pianta (rep. tecnica)",
trt_col_custom = "Trattamento personalizzato",
btn_trt = "Aggiungi colonna trattamento e replica",
trt_preview = "Anteprima con trattamenti",
btn_dl_trt = "Scarica CSV con trattamenti",
step5 = "5 — Formato finale e download",
pivot_label = "Trasponi parametri in formato long",
pivot_sub = "Le colonne numeriche verranno unite in una colonna 'parameter' con i valori in 'value'",
btn_final = "Genera file finale",
btn_dl_final = "Scarica CSV finale",
stat_rows = "righe",
stat_format = "formato",
final_preview = "Anteprima file finale",
step6 = "6 — Grafico esplorativo",
chart_param_lbl = "Seleziona i parametri da visualizzare:",
chart_hint = "Media ± errore standard (SE) per ciascun trattamento. Solo a scopo di controllo qualità rapido — non sostituisce l'analisi statistica.",
btn_chart = "Genera grafico",
btn_dl_png = "Scarica PNG",
btn_clear = "Cancella grafico",
chart_no_params = "Nessun parametro numerico disponibile nel file finale.",
chart_ylab = "Valore medio",
jitter_label = "Mostra punti individuali (jitter)",
jitter_sub = "Visualizza ogni singola misura come puntino, utile per individuare outlier o valutare la distribuzione",
credits = list(
pre = "Applicazione creata da ",
bug = " · Segnala problemi su ",
or = " o via ",
mail = "mail"
)
),
en = list(
app_subtitle = "Root morphology data cleaning from",
app_descr = "Software for root architecture analysis from scanned images — Regent Instruments.",
privacy_title = "100% local — your data never leaves the browser",
privacy_body = "This application runs entirely in your browser using WebR. No data is sent to external servers: the file you upload is processed only on your device. You can use it offline, on a corporate network, or anywhere. On the first load, a few seconds may be needed to initialise the R environment.",
guide_title = "📋 How to name samples in WinRhizo",
guide_intro = "To avoid ambiguity at analysis time, it helps to define a naming convention before you start scanning. This app automatically recognises two schemes:",
guide_case1_title = "Simple case — a single replicate per sample",
guide_case1_body = "Use the format TREATMENT_replicate, with an underscore before the sequential number.",
guide_case1_ex1 = "TREATMENT_1, TREATMENT_2, TREATMENT_3 …",
guide_case2_title = "Two-level case — biological and technical replicate",
guide_case2_body = "If each pot contains multiple plants (or multiple sub-samples), it helps to distinguish between the biological replicate (the pot, i.e. an independent individual or group) and the technical replicate (the single plant or repeated measurement within the same pot). Use TREATMENT.pot.plant, with a dot before the two numbers:",
guide_case2_ex1 = "TREATMENT.1.1, TREATMENT.1.2 → pot 1, plants 1 and 2",
guide_case2_ex2 = "TREATMENT.2.1, TREATMENT.2.2 → pot 2, plants 1 and 2",
guide_why_title = "Why a dot instead of an underscore for pot/plant?",
guide_why_body = "When the treatment name is made up of several words joined by underscores (e.g. 100_PPM_UREA), the app has no way to tell at a glance where the name ends and where the pot and plant numbers begin: every underscore looks the same. By using the dot only to introduce the replicate numbers, the boundary between name and numbers stays recognisable, no matter how complex the treatment name is.",
guide_why_ex1 = "100_PPM_UREA_1_2 → ambiguous: the last '_1' could be part of the name or the pot",
guide_why_ex2 = "100_PPM_UREA.1.2 → clear: treatment 100_PPM_UREA, pot 1, plant 2",
guide_collapse = "Collapse guide",
guide_expand = "Show guide",
status_waiting = "Waiting",
status_loaded = "✓ File loaded",
status_clean = "✓ File cleaned",
status_trt = "✓ Treatments added",
status_final = "✓ Final file ready",
status_error = "✗ Error",
step1 = "1 — Load file",
file_placeholder = "Select a .txt file exported from WinRhizo…",
file_btn = "Browse",
file_info = function(nr, nc) paste0(nr, " samples · ", nc, " columns detected"),
step2 = "2 — Select columns to keep",
sel_all = "Select all",
desel_all = "Deselect all",
col_meta = "columns selected",
step3 = "3 — Generate cleaned file",
btn_clean = "Clean",
btn_dl_clean = "Download cleaned CSV",
summary = "Summary",
renamed_lbl = "Automatically renamed columns:",
stat_samples = "samples",
stat_cols = "columns",
stat_file = "file name",
preview8 = "Data preview",
step4 = "4 — Assign treatments to samples",
trt_hint = "WinRhizo encodes the treatment in the sample name. Pick the scheme that matches your experimental setup, then review or override the custom treatment for each sample.",
trt_scheme_lbl = "Sample name scheme:",
trt_scheme_1 = "Treatment_Replicate",
trt_scheme_1_sub = "e.g. XYZ_3 → treatment XYZ, replicate 3",
trt_scheme_2 = "Treatment_Pot_Plant",
trt_scheme_2_sub = "e.g. X_Y_Z.1.2 → treatment X_Y_Z, pot 1, plant 2",
trt_scheme_3 = "Manual",
trt_scheme_3_sub = "assign the role of each name part yourself",
trt_manual_lbl = "Assign a role to each part of the SampleId:",
trt_manual_hint = "Based on the first sample in the file. Recognizes underscore, dot, and space as separators. Consecutive segments with the same 'Treatment' role are merged automatically.",
trt_manual_role_treatment = "Treatment",
trt_manual_role_pot = "Pot",
trt_manual_role_plant = "Plant",
trt_manual_role_ignore = "Ignore",
trt_manual_mismatch = "Warning: some SampleIds have a different structure than the reference sample and were not parsed correctly (shown with the original name as treatment).",
trt_col_sample = "SampleId",
trt_col_detected = "Detected treatment",
trt_col_replica = "Replicate",
trt_col_vaso = "Pot (biological rep.)",
trt_col_pianta = "Plant (technical rep.)",
trt_col_custom = "Custom treatment",
btn_trt = "Add treatment & replicate columns",
trt_preview = "Preview with treatments",
btn_dl_trt = "Download CSV with treatments",
step5 = "5 — Final format and download",
pivot_label = "Transpose parameters to long format",
pivot_sub = "Numeric columns will be merged into a 'parameter' column with values in 'value'",
btn_final = "Generate final file",
btn_dl_final = "Download final CSV",
stat_rows = "rows",
stat_format = "format",
final_preview = "Final file preview",
step6 = "6 — Exploratory chart",
chart_param_lbl = "Select parameters to display:",
chart_hint = "Mean ± standard error (SE) for each treatment. For quick quality control only — not a substitute for proper statistical analysis.",
btn_chart = "Generate chart",
btn_dl_png = "Download PNG",
btn_clear = "Clear chart",
chart_no_params = "No numeric parameters available in the final file.",
chart_ylab = "Mean value",
jitter_label = "Show individual data points (jitter)",
jitter_sub = "Overlay each measurement as a dot, useful to spot outliers or assess distribution",
credits = list(
pre = "Application developed by ",
bug = " · Report issues on ",
or = " or via ",
mail = "email"
)
),
es = list(
app_subtitle = "Limpieza de datos de morfología radicular desde",
app_descr = "Software para el análisis de la arquitectura radicular a partir de imágenes escaneadas — Regent Instruments.",
privacy_title = "100% local — sus datos nunca salen del navegador",
privacy_body = "Esta aplicación se ejecuta completamente en su navegador gracias a WebR. No se envían datos a servidores externos: el archivo que cargue se procesa únicamente en su dispositivo. Puede usarla sin conexión, en una red corporativa o en cualquier lugar. En el primer inicio puede ser necesario esperar unos segundos para cargar el entorno R.",
guide_title = "📋 Cómo nombrar las muestras en WinRhizo",
guide_intro = "Para evitar ambigüedades en el momento del análisis, conviene definir una convención de nombres antes de iniciar el escaneo. La app reconoce automáticamente dos esquemas:",
guide_case1_title = "Caso simple — una sola réplica por muestra",
guide_case1_body = "Usa el formato TRATAMIENTO_réplica, con un guión bajo antes del número secuencial.",
guide_case1_ex1 = "TRATAMIENTO_1, TRATAMIENTO_2, TRATAMIENTO_3 …",
guide_case2_title = "Caso con dos niveles — réplica biológica y técnica",
guide_case2_body = "Si cada maceta contiene varias plantas (o varios submuestras), conviene distinguir entre la réplica biológica (la maceta, es decir, un individuo o grupo independiente) y la réplica técnica (la planta individual o medida repetida dentro de la misma maceta). Usa TRATAMIENTO.maceta.planta, con un punto antes de los dos números:",
guide_case2_ex1 = "TRATAMIENTO.1.1, TRATAMIENTO.1.2 → maceta 1, plantas 1 y 2",
guide_case2_ex2 = "TRATAMIENTO.2.1, TRATAMIENTO.2.2 → maceta 2, plantas 1 y 2",
guide_why_title = "¿Por qué un punto y no un guión bajo para maceta/planta?",
guide_why_body = "Cuando el nombre del tratamiento está formado por varias palabras unidas con guiones bajos (p.ej. 100_PPM_UREA), la app no puede saber a simple vista dónde termina el nombre y dónde empiezan los números de maceta y planta: todos los guiones bajos son iguales. Usando el punto solo para introducir los números de réplica, el límite entre nombre y números siempre es reconocible.",
guide_why_ex1 = "100_PPM_UREA_1_2 → ambiguo: el último '_1' podría ser parte del nombre o la maceta",
guide_why_ex2 = "100_PPM_UREA.1.2 → claro: tratamiento 100_PPM_UREA, maceta 1, planta 2",
guide_collapse = "Contraer guía",
guide_expand = "Mostrar guía",
status_waiting = "Esperando",
status_loaded = "✓ Archivo cargado",
status_clean = "✓ Archivo limpio",
status_trt = "✓ Tratamientos añadidos",
status_final = "✓ Archivo final listo",
status_error = "✗ Error",
step1 = "1 — Cargar archivo",
file_placeholder = "Selecciona un archivo .txt exportado de WinRhizo…",
file_btn = "Examinar",
file_info = function(nr, nc) paste0(nr, " muestras · ", nc, " columnas detectadas"),
step2 = "2 — Seleccionar columnas a conservar",
sel_all = "Seleccionar todo",
desel_all = "Deseleccionar todo",
col_meta = "columnas seleccionadas",
step3 = "3 — Generar archivo limpio",
btn_clean = "Limpiar",
btn_dl_clean = "Descargar CSV limpio",
summary = "Resumen",
renamed_lbl = "Columnas renombradas automáticamente:",
stat_samples = "muestras",
stat_cols = "columnas",
stat_file = "nombre archivo",
preview8 = "Vista previa de datos",
step4 = "4 — Asignar tratamientos a las muestras",
trt_hint = "WinRhizo codifica el tratamiento en el nombre de la muestra. Elige el esquema que corresponda a tu diseño experimental y revisa o modifica el tratamiento personalizado para cada muestra.",
trt_scheme_lbl = "Esquema del nombre de muestra:",
trt_scheme_1 = "Tratamiento_Réplica",
trt_scheme_1_sub = "p.ej. XYZ_3 → tratamiento XYZ, réplica 3",
trt_scheme_2 = "Tratamiento_Maceta_Planta",
trt_scheme_2_sub = "p.ej. X_Y_Z.1.2 → tratamiento X_Y_Z, maceta 1, planta 2",
trt_scheme_3 = "Manual",
trt_scheme_3_sub = "asigna tú mismo el rol de cada parte del nombre",
trt_manual_lbl = "Asigna un rol a cada parte del SampleId:",
trt_manual_hint = "Basado en la primera muestra del archivo. Reconoce guión bajo, punto y espacio como separadores. Los segmentos consecutivos con el mismo rol 'Tratamiento' se fusionan automáticamente.",
trt_manual_role_treatment = "Tratamiento",
trt_manual_role_pot = "Maceta",
trt_manual_role_plant = "Planta",
trt_manual_role_ignore = "Ignorar",
trt_manual_mismatch = "Aviso: algunos SampleIds tienen una estructura diferente a la muestra de referencia y no se han interpretado correctamente (se muestra el nombre original como tratamiento).",
trt_col_sample = "SampleId",
trt_col_detected = "Tratamiento detectado",
trt_col_replica = "Réplica",
trt_col_vaso = "Maceta (rep. biológica)",
trt_col_pianta = "Planta (rep. técnica)",
trt_col_custom = "Tratamiento personalizado",
btn_trt = "Añadir columnas de tratamiento y réplica",
trt_preview = "Vista previa con tratamientos",
btn_dl_trt = "Descargar CSV con tratamientos",
step5 = "5 — Formato final y descarga",
pivot_label = "Transponer parámetros a formato largo",
pivot_sub = "Las columnas numéricas se unirán en una columna 'parameter' con los valores en 'value'",
btn_final = "Generar archivo final",
btn_dl_final = "Descargar CSV final",
stat_rows = "filas",
stat_format = "formato",
final_preview = "Vista previa del archivo final",
step6 = "6 — Gráfico exploratorio",
chart_param_lbl = "Seleccionar parámetros a visualizar:",
chart_hint = "Media ± error estándar (SE) por tratamiento. Solo para control de calidad rápido — no sustituye al análisis estadístico.",
btn_chart = "Generar gráfico",
btn_dl_png = "Descargar PNG",
btn_clear = "Borrar gráfico",
chart_no_params = "No hay parámetros numéricos disponibles en el archivo final.",
chart_ylab = "Valor medio",
jitter_label = "Mostrar puntos individuales (jitter)",
jitter_sub = "Superponer cada medición como un punto, útil para detectar valores atípicos",
credits = list(
pre = "Aplicación desarrollada por ",
bug = " · Reportar problemas en ",
or = " o por ",
mail = "correo"
)
),
fr = list(
app_subtitle = "Nettoyage des données de morphologie racinaire depuis",
app_descr = "Logiciel d'analyse de l'architecture racinaire à partir d'images numérisées — Regent Instruments.",
privacy_title = "100 % local — vos données ne quittent jamais le navigateur",
privacy_body = "Cette application s'exécute entièrement dans votre navigateur grâce à WebR. Aucune donnée n'est envoyée à des serveurs externes : le fichier que vous chargez est traité uniquement sur votre appareil. Vous pouvez l'utiliser hors ligne, sur un réseau d'entreprise ou n'importe où. Au premier démarrage, quelques secondes peuvent être nécessaires pour initialiser l'environnement R.",
guide_title = "📋 Comment nommer les échantillons dans WinRhizo",
guide_intro = "Pour éviter toute ambiguïté lors de l'analyse, il est utile de définir une convention de nommage avant de commencer le balayage. L'application reconnaît automatiquement deux schémas :",
guide_case1_title = "Cas simple — une seule réplique par échantillon",
guide_case1_body = "Utilisez le format TRAITEMENT_réplique, avec un underscore avant le numéro séquentiel.",
guide_case1_ex1 = "TRAITEMENT_1, TRAITEMENT_2, TRAITEMENT_3 …",
guide_case2_title = "Cas à deux niveaux — réplique biologique et technique",
guide_case2_body = "Si chaque pot contient plusieurs plantes (ou plusieurs sous-échantillons), il convient de distinguer la réplique biologique (le pot, c'est-à-dire un individu ou groupe indépendant) de la réplique technique (la plante individuelle ou mesure répétée dans le même pot). Utilisez TRAITEMENT.pot.plante, avec un point avant les deux chiffres :",
guide_case2_ex1 = "TRAITEMENT.1.1, TRAITEMENT.1.2 → pot 1, plantes 1 et 2",
guide_case2_ex2 = "TRAITEMENT.2.1, TRAITEMENT.2.2 → pot 2, plantes 1 et 2",
guide_why_title = "Pourquoi un point plutôt qu'un underscore pour pot/plante ?",
guide_why_body = "Lorsque le nom du traitement est composé de plusieurs mots reliés par des underscores (ex. 100_PPM_UREA), l'application ne peut pas savoir d'emblée où le nom se termine et où commencent les numéros de pot et de plante : tous les underscores se ressemblent. En utilisant le point uniquement pour introduire les numéros de réplique, la frontière entre nom et chiffres reste reconnaissable.",
guide_why_ex1 = "100_PPM_UREA_1_2 → ambigu : le dernier '_1' pourrait faire partie du nom ou être le pot",
guide_why_ex2 = "100_PPM_UREA.1.2 → clair : traitement 100_PPM_UREA, pot 1, plante 2",
guide_collapse = "Réduire le guide",
guide_expand = "Afficher le guide",
status_waiting = "En attente",
status_loaded = "✓ Fichier chargé",
status_clean = "✓ Fichier nettoyé",
status_trt = "✓ Traitements ajoutés",
status_final = "✓ Fichier final prêt",
status_error = "✗ Erreur",
step1 = "1 — Charger le fichier",
file_placeholder = "Sélectionnez un fichier .txt exporté de WinRhizo…",
file_btn = "Parcourir",
file_info = function(nr, nc) paste0(nr, " échantillons · ", nc, " colonnes détectées"),
step2 = "2 — Sélectionner les colonnes à conserver",
sel_all = "Tout sélectionner",
desel_all = "Tout désélectionner",
col_meta = "colonnes sélectionnées",
step3 = "3 — Générer le fichier nettoyé",
btn_clean = "Nettoyer",
btn_dl_clean = "Télécharger le CSV nettoyé",
summary = "Récapitulatif",
renamed_lbl = "Colonnes renommées automatiquement :",
stat_samples = "échantillons",
stat_cols = "colonnes",
stat_file = "nom du fichier",
preview8 = "Aperçu des données",
step4 = "4 — Assigner les traitements aux échantillons",
trt_hint = "WinRhizo encode le traitement dans le nom de l'échantillon. Choisissez le schéma correspondant à votre dispositif expérimental, puis vérifiez ou modifiez le traitement personnalisé pour chaque échantillon.",
trt_scheme_lbl = "Schéma du nom d'échantillon :",
trt_scheme_1 = "Traitement_Réplique",
trt_scheme_1_sub = "ex. XYZ_3 → traitement XYZ, réplique 3",
trt_scheme_2 = "Traitement_Pot_Plante",
trt_scheme_2_sub = "ex. X_Y_Z.1.2 → traitement X_Y_Z, pot 1, plante 2",
trt_scheme_3 = "Manuel",
trt_scheme_3_sub = "assignez vous-même le rôle de chaque partie du nom",
trt_manual_lbl = "Assignez un rôle à chaque partie du SampleId :",
trt_manual_hint = "Basé sur le premier échantillon du fichier. Reconnaît l'underscore, le point et l'espace comme séparateurs. Les segments consécutifs avec le même rôle 'Traitement' sont fusionnés automatiquement.",
trt_manual_role_treatment = "Traitement",
trt_manual_role_pot = "Pot",
trt_manual_role_plant = "Plante",
trt_manual_role_ignore = "Ignorer",
trt_manual_mismatch = "Avertissement : certains SampleIds ont une structure différente de l'échantillon de référence et n'ont pas été interprétés correctement (le nom original est affiché comme traitement).",
trt_col_sample = "SampleId",
trt_col_detected = "Traitement détecté",
trt_col_replica = "Réplique",
trt_col_vaso = "Pot (rép. biologique)",
trt_col_pianta = "Plante (rép. technique)",
trt_col_custom = "Traitement personnalisé",
btn_trt = "Ajouter les colonnes traitement et réplique",
trt_preview = "Aperçu avec traitements",
btn_dl_trt = "Télécharger le CSV avec traitements",
step5 = "5 — Format final et téléchargement",
pivot_label = "Transposer les paramètres en format long",
pivot_sub = "Les colonnes numériques seront fusionnées dans une colonne 'parameter' avec les valeurs dans 'value'",
btn_final = "Générer le fichier final",
btn_dl_final = "Télécharger le CSV final",
stat_rows = "lignes",
stat_format = "format",
final_preview = "Aperçu du fichier final",
step6 = "6 — Graphique exploratoire",
chart_param_lbl = "Sélectionner les paramètres à afficher :",
chart_hint = "Moyenne ± erreur standard (SE) par traitement. À des fins de contrôle qualité uniquement — ne remplace pas une analyse statistique.",
btn_chart = "Générer le graphique",
btn_dl_png = "Télécharger PNG",
btn_clear = "Effacer le graphique",
chart_no_params = "Aucun paramètre numérique disponible dans le fichier final.",
chart_ylab = "Valeur moyenne",
jitter_label = "Afficher les points individuels (jitter)",
jitter_sub = "Superposer chaque mesure sous forme de point, utile pour repérer les valeurs aberrantes",
credits = list(
pre = "Application développée par ",
bug = " · Signaler des problèmes sur ",
or = " ou via ",
mail = "e-mail"
)
)
)
# ── Parsing WinRhizo ──────────────────────────────────────────────────────────
# Il file WinRhizo ha l'header delle colonne dati in riga 1 ("RHIZO 2021a" come
# prima colonna), seguito da righe 2-5 di sub-header per altri tipi di record
# (COLOR, LINK, DEV, AXIS) che vanno scartate; le righe dati iniziano dalla 6.
parse_winrhizo <- function(filepath) {
lines <- readLines(filepath, encoding = "UTF-8", warn = FALSE)
lines <- gsub("\r", "", lines)
header <- strsplit(lines[1], "\t")[[1]]
header[header == "RHIZO 2021a"] <- "SampleId"
header <- trimws(header)
data_lines <- lines[6:length(lines)]
data_lines <- data_lines[nchar(trimws(data_lines)) > 0]
n_cols <- length(header)
split_rows <- lapply(strsplit(data_lines, "\t"), function(x) {
length(x) <- n_cols; x[is.na(x)] <- ""; x
})
df <- as.data.frame(do.call(rbind, split_rows), stringsAsFactors = FALSE)
colnames(df) <- header
df <- df[, colnames(df) != "", drop = FALSE]
df
}
# ── Estrazione trattamento/repliche dal SampleId, con livelli configurabili ──
# n_levels = 1: TRATTAMENTO_replica (es. "XYZ_3" o "XYZ.3")
# n_levels = 2: TRATTAMENTO_vaso(.|_)pianta (es. "X_Y_Z_1_2", "X_Y_Z_1.2" o "X_Y_Z.1.2")
# Il separatore puo' essere "_" oppure "." sia tra trattamento e primo numero
# sia tra i numeri successivi (entrambi accettati, anche mescolati nello
# stesso file). Convenzione consigliata: "_" per unire le parole del nome
# trattamento, "." per separare i livelli di replica, es. "X_Y_Z.1.2".
extract_treatment <- function(sample_ids, n_levels = 1) {
pattern <- paste0("[_.]\\d+([_.]\\d+){", n_levels - 1, "}$")
gsub(pattern, "", sample_ids)
}
# Estrae gli ultimi n_levels numeri come lista di vettori (uno per livello,
# in ordine: livello 1 = più esterno/biologico, livello n = più interno/tecnico)
extract_replicate_levels <- function(sample_ids, n_levels = 1) {
pattern <- paste0("[_.](\\d+)([_.](\\d+)){", n_levels - 1, "}$")
m <- regmatches(sample_ids, regexpr(pattern, sample_ids))
out <- lapply(seq_len(n_levels), function(lvl) rep(NA_character_, length(sample_ids)))
has_match <- nchar(m) > 0
if (any(has_match)) {
cleaned <- sub("^[_.]", "", m[has_match])
nums <- strsplit(cleaned, "[_.]")
for (lvl in seq_len(n_levels)) {
vals <- vapply(nums, function(x) if (length(x) >= lvl) x[lvl] else NA_character_, character(1))
out[[lvl]][has_match] <- vals
}
}
out
}
# Retro-compatibilità: singola replica (n_levels = 1)
extract_replicate <- function(sample_ids) extract_replicate_levels(sample_ids, 1)[[1]]
# ── Schema manuale: scompone un SampleId in token + separatori ──────────────
split_sampleid_tokens <- function(sample_id) {
m <- gregexpr("[^_. ]+|[_. ]", sample_id, perl = TRUE)
parts <- regmatches(sample_id, m)[[1]]
tokens <- parts[seq(1, length(parts), by = 2)]
seps <- if (length(parts) > 1) parts[seq(2, length(parts), by = 2)] else character(0)
list(tokens = tokens, seps = seps)
}
# Applica una mappatura di ruoli (uno per token, posizionale) a tutti i
# sample_id, assumendo la stessa struttura del campione di riferimento usato
# per generare i dropdown. Ruoli ammessi: "Treatment", "Pot", "Plant", "Ignore".
# Segmenti "Treatment" consecutivi vengono uniti col separatore originale.
apply_manual_scheme <- function(sample_ids, roles) {
n <- length(sample_ids)
treatment <- character(n)
pot <- rep(NA_character_, n)
plant <- rep(NA_character_, n)
ok <- rep(TRUE, n)
for (i in seq_len(n)) {
sp <- split_sampleid_tokens(sample_ids[i])
if (length(sp$tokens) != length(roles)) {
ok[i] <- FALSE
treatment[i] <- sample_ids[i]
next
}
trt_parts <- character(0)
for (j in seq_along(roles)) {
if (roles[j] == "Treatment") {
if (length(trt_parts) > 0) trt_parts <- c(trt_parts, sp$seps[j - 1])
trt_parts <- c(trt_parts, sp$tokens[j])
} else if (roles[j] == "Pot") {
pot[i] <- sp$tokens[j]
} else if (roles[j] == "Plant") {
plant[i] <- sp$tokens[j]
}
}
treatment[i] <- paste(trt_parts, collapse = "")
}
list(treatment = treatment, pot = pot, plant = plant, ok = ok)
}
apply_rename <- function(df) {
for (old in names(COL_RENAME)) {
idx <- which(colnames(df) == old)
if (length(idx)) colnames(df)[idx[1]] <- COL_RENAME[[old]]
}
df
}
pivot_long <- function(df) {
id_cols <- c("SampleId", "datetime", "treatment", "replicate", "pot", "plant")
id_present <- id_cols[id_cols %in% colnames(df)]
param_cols <- setdiff(colnames(df), id_present)
if (!length(param_cols)) return(df)
id_data <- df[, id_present, drop = FALSE]
long_rows <- do.call(rbind, lapply(param_cols, function(p) {
tmp <- id_data; tmp$parameter <- p; tmp$value <- df[[p]]; tmp
}))
long_rows <- long_rows[order(rep(seq_len(nrow(df)), length(param_cols))), ]
rownames(long_rows) <- NULL
long_rows
}
# ── Calcolo media + SE per trattamento ────────────────────────────────────────
compute_mean_se <- function(df, param, trt_col = "treatment") {
vals <- suppressWarnings(as.numeric(df[[param]]))
trt <- df[[trt_col]]
ok <- !is.na(vals) & !is.na(trt)
vals <- vals[ok]; trt <- trt[ok]
if (!length(vals)) return(NULL)
agg_mean <- tapply(vals, trt, mean, na.rm = TRUE)
agg_se <- tapply(vals, trt, function(x) {
n <- sum(!is.na(x))
if (n < 2) return(0)
sd(x, na.rm = TRUE) / sqrt(n)
})
data.frame(
treatment = names(agg_mean),
mean = as.numeric(agg_mean),
se = as.numeric(agg_se[names(agg_mean)]),
stringsAsFactors = FALSE
)
}
# ── Encoder base64 (pura base R, nessuna dipendenza esterna) ─────────────────
base64_encode_raw <- function(raw_bytes) {
alphabet <- strsplit("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", "")[[1]]
n <- length(raw_bytes)
pad <- (3 - n %% 3) %% 3
if (pad > 0) raw_bytes <- c(raw_bytes, raw(pad))
m <- matrix(as.integer(raw_bytes), nrow = 3)
idx <- rbind(
bitwShiftR(m[1, ], 2),
bitwOr(bitwShiftL(bitwAnd(m[1, ], 3L), 4), bitwShiftR(m[2, ], 4)),
bitwOr(bitwShiftL(bitwAnd(m[2, ], 15L), 2), bitwShiftR(m[3, ], 6)),
bitwAnd(m[3, ], 63L)
)
out <- alphabet[idx + 1]
out <- matrix(out, nrow = 4)
if (pad >= 1) out[4, ncol(out)] <- "="
if (pad == 2) out[3, ncol(out)] <- "="
paste(out, collapse = "")
}
# ── Rendering grafico a barre con error bar (grafica base R) ─────────────────
# ── Estrae l'unità di misura dal nome colonna, es. "Length(cm)" → "cm" ───────
extract_unit <- function(param_name) {
m <- regmatches(param_name, regexpr("\\(([^)]+)\\)\\s*$", param_name))
if (length(m) == 0 || m == "") return(NULL)
sub("^\\((.+)\\)$", "\\1", m)
}
draw_bar_chart <- function(df, params, trt_col = "treatment", ylab = "Mean value",
is_dark = FALSE, show_jitter = FALSE) {
# Stile fisso "theme_bw": sfondo sempre bianco, indipendente dal tema dell'app,
# cosi' il PNG scaricato e' sempre leggibile e stampabile.
# Palette: ggsci::pal_npg() (Nature Publishing Group), alpha = 0.8 — molto
# usata in figure di biologia/Nature, 10 colori distinti.
pal_npg_base <- c("#E64B35", "#4DBBD5", "#00A087", "#3C5488", "#F39B7F",
"#8491B4", "#91D1C2", "#DC0000", "#7E6148", "#B09C85")
pal <- adjustcolor(pal_npg_base, alpha.f = 0.8)
fg <- "#1A1A1A" # testo/assi: quasi nero, come theme_bw
major_gr <- "#E5E5E5" # griglia major (panel.grid.major)
minor_gr <- "#F0F0F0" # griglia minor (panel.grid.minor)
panel_bg <- "#FFFFFF" # panel.background: bianco puro
plot_bg <- "#FFFFFF" # plot.background: bianco puro
# Palette condivisa: stesso trattamento → stesso colore in ogni pannello
all_treatments <- sort(unique(as.character(df[[trt_col]])))
trt_colors <- setNames(pal[((seq_along(all_treatments) - 1) %% length(pal)) + 1],
all_treatments)
par(bg = plot_bg, col.axis = fg, col.lab = fg, col.main = fg, fg = fg,
font.main = 1, cex.main = 1.4, cex.lab = 1.3, cex.axis = 1.2)
n_params <- length(params)
if (n_params > 1) {
n_col <- 2
n_row <- ceiling(n_params / n_col)
par(mfrow = c(n_row, n_col), mar = c(7, 6.5, 3.2, 1.4), mgp = c(4, 0.9, 0))
} else {
par(mfrow = c(1, 1), mar = c(7, 6.5, 3.2, 1.4), mgp = c(4, 0.9, 0))
}
for (i in seq_along(params)) {
p <- params[i]
agg <- compute_mean_se(df, p, trt_col)
if (is.null(agg) || !nrow(agg)) next
bar_cols <- trt_colors[agg$treatment]
n_bars <- nrow(agg)
# Punti individuali (jitter) per trattamento, calcolati prima per includerli nello ylim
raw_vals <- suppressWarnings(as.numeric(df[[p]]))
raw_trt <- as.character(df[[trt_col]])
ok_pts <- !is.na(raw_vals) & !is.na(raw_trt)
pts_vals <- raw_vals[ok_pts]
pts_trt <- raw_trt[ok_pts]
ylim_max <- max(agg$mean + agg$se, na.rm = TRUE) * 1.18
if (show_jitter && length(pts_vals)) {
ylim_max <- max(ylim_max, max(pts_vals, na.rm = TRUE) * 1.1)
}
# 1) pannello vuoto con sfondo bianco (panel.background)
plot.new()
plot.window(xlim = c(0.2, n_bars * 1.2 + 0.2), ylim = c(0, ylim_max))
rect(par("usr")[1], par("usr")[3], par("usr")[2], par("usr")[4],
col = panel_bg, border = NA)
# 2) griglia minor + major (sotto le barre, come in ggplot)
y_major <- pretty(c(0, ylim_max))
y_major <- y_major[y_major >= 0 & y_major <= ylim_max]
y_minor <- (head(y_major, -1) + tail(y_major, -1)) / 2
abline(h = y_minor, col = minor_gr, lwd = 0.7)
abline(h = y_major, col = major_gr, lwd = 1)
# 3) barre colorate per trattamento, bordo nero sottile
bp <- barplot(agg$mean, col = bar_cols, border = "#1A1A1A", lwd = 0.9,
ylim = c(0, ylim_max), add = TRUE,
ylab = "", main = "", names.arg = rep("", n_bars),
axes = FALSE)
# 3b) jitter dei dati individuali: punti piccoli, semi-trasparenti, grigio
# scuro neutro per non competere visivamente con il colore delle barre.
if (show_jitter && length(pts_vals)) {
bar_x <- setNames(as.numeric(bp), agg$treatment)
jx <- bar_x[pts_trt] + runif(length(pts_trt), -0.13, 0.13)
points(jx, pts_vals, pch = 21, bg = adjustcolor("#1A1A1A", alpha.f = 0.45),
col = adjustcolor("#1A1A1A", alpha.f = 0.6), cex = 0.85, lwd = 0.6)
}
# 4) error bar (sopra i punti jitter, sempre ben visibili)
arrows(x0 = bp, y0 = agg$mean, x1 = bp, y1 = agg$mean + agg$se,
angle = 90, length = 0.08, col = fg, lwd = 1.6)
arrows(x0 = bp, y0 = agg$mean, x1 = bp, y1 = pmax(agg$mean - agg$se, 0),
angle = 90, length = 0.08, col = fg, lwd = 1.6)
# Etichetta asse Y con unità di misura tra parentesi quadre, se presente
unit <- extract_unit(p)
ylab_full <- if (!is.null(unit)) paste0(ylab, " [", unit, "]") else ylab
# 5) etichette asse X ruotate di 30 gradi (testo posizionato manualmente)
usr <- par("usr")
y_off <- (usr[4] - usr[3]) * 0.03
text(x = bp, y = usr[3] - y_off, labels = agg$treatment,
srt = 30, adj = c(1, 1), xpd = TRUE, cex = 1.2, col = fg)
# 6) assi e bordo pannello (panel.border)
axis(2, at = y_major, col = fg, col.axis = fg, cex.axis = 1.2, las = 1, mgp = c(4, 0.9, 0))
box(col = fg, lwd = 1)
title(ylab = ylab_full, cex.lab = 1.3, col.lab = fg)
title(main = p, cex.main = 1.4, col.main = fg, font.main = 2)
}
par(mfrow = c(1, 1))
invisible(trt_colors)
}
# ── Serializza un vettore R in array JS di stringhe (nessuna dipendenza JSON) ─
jsonlite_like_array <- function(x) {
esc <- gsub('"', '\\\\"', x)
paste0("[", paste0('"', esc, '"', collapse = ","), "]")
}
# ── Parametri numerici disponibili per il grafico (wide o long) ──────────────
get_available_chart_params <- function(df) {
excl <- c("SampleId", "datetime", "treatment", "replicate", "pot", "plant")
if ("parameter" %in% colnames(df) && "value" %in% colnames(df)) {
sort(unique(df$parameter))
} else {
cand <- setdiff(colnames(df), excl)
cand[vapply(cand, function(c) {
!all(is.na(suppressWarnings(as.numeric(df[[c]]))))
}, logical(1))]
}
}
df_to_csv_string <- function(df, sep = ";") {
con <- textConnection("out", "w")
write.table(df, con, sep = sep, row.names = FALSE, quote = FALSE, na = "")
close(con)
paste(out, collapse = "\n")
}
# ── UI ───────────────────────────────────────────────────────────────────────
ui <- page_fluid(
title = "CleanRhizo 🧹",
theme = bs_theme(
version = 5,
base_font = "system-ui",
heading_font = "system-ui",
bg = "#F5F7F5",
fg = "#1A1F1A",
primary = "#3A6B35",
secondary = "#A8C5A0",
border_radius = "6px"
),
tags$head(tags$style(HTML("
html, body { padding: 1rem 1.5rem !important; margin: 0 !important; background: #F5F7F5 !important; }
@media (prefers-color-scheme: dark) {
html, body { background: #141A13 !important; color: #D8E8D5 !important; }
}
h1.title { display: none !important; }
.wr-card { background: #fff; border: 1px solid #DDE5DB; border-radius: 10px; padding: 1.8rem 2.2rem; margin-bottom: 1.4rem; box-shadow: 0 1px 4px rgba(0,0,0,.05); }
.app-title { font-size: 1.75rem; font-weight: 800; color: #1A1F1A; margin-bottom: .15rem; letter-spacing: -.4px; }
.app-subtitle { font-size: 1.05rem; color: #5A6B58; margin-bottom: 0; }
.badge-idle { background: #EAF0E8; color: #4A5E47; }
.badge-ok { background: #D4EDDA; color: #1A5C29; }
.badge-error { background: #FEE2E2; color: #991B1B; }
.status-badge { display: inline-block; font-size: .92rem; font-weight: 600; padding: .28em .85em; border-radius: 999px; min-width: 7rem; text-align: center; white-space: nowrap; }
.shiny-html-output:has(.status-badge) { display: inline-block; flex-shrink: 0; }
.lang-switcher { display: inline-flex; align-items: center; border: 1.5px solid #C5D4C2; border-radius: 6px; overflow: hidden; font-size: .82rem; font-weight: 700; }
.lang-btn { padding: .32rem .75rem; cursor: pointer; background: #fff; color: #5A6B58; border: none; border-right: 1px solid #C5D4C2; transition: all .12s; user-select: none; }
/* scheme selector */
.scheme-grid { display: flex; flex-wrap: wrap; gap: .7rem; margin-bottom: 1.1rem; }
.scheme-card {
flex: 1; min-width: 220px; padding: .8rem 1rem;
border: 1.5px solid #C5D4C2; border-radius: 8px; background: #fff;
cursor: pointer; transition: all .12s;
}
.scheme-card.active { border-color: #3A6B35; background: #EAF4E8; box-shadow: 0 0 0 1px #3A6B35; }
.scheme-card-title { font-size: .88rem; font-weight: 700; color: #1A3D17; margin-bottom: .25rem; }
.scheme-card-sub { font-size: .78rem; color: #5A6B58; font-family: monospace; }
/* manual segment role assignment */
.segment-row { display: flex; flex-wrap: wrap; gap: .6rem; align-items: flex-end; margin-bottom: 1rem; }
.segment-block { display: flex; flex-direction: column; align-items: center; gap: .35rem; }
.segment-token {
font-family: monospace; font-size: .92rem; font-weight: 700; color: #1A3D17;
background: #EAF0E8; border: 1.5px solid #C5D4C2; border-radius: 6px;
padding: .35rem .7rem; min-width: 2.4rem; text-align: center;
}
.segment-sep { font-family: monospace; font-size: .9rem; color: #9CA3AF; padding-bottom: .55rem; }
.segment-role-select {
font-size: .76rem; border: 1.5px solid #C5D4C2; border-radius: 5px;
padding: .25rem .4rem; background: #fff; color: #374151; cursor: pointer;
}
.segment-role-select.role-treatment { border-color: #3A6B35; background: #EAF4E8; color: #1A3D17; font-weight: 600; }
.segment-role-select.role-pot { border-color: #1D4ED8; background: #EAF0FB; color: #1D3A8A; font-weight: 600; }
.segment-role-select.role-plant { border-color: #7C3AED; background: #F5F0FE; color: #4C1D95; font-weight: 600; }
.manual-warning {
font-size: .82rem; color: #92400E; background: #FFF7ED; border: 1px solid #FED7AA;
border-radius: 6px; padding: .6rem .9rem; margin-bottom: 1rem;
}
.lang-btn:last-child { border-right: none; }
.lang-btn.active { background: #3A6B35; color: #fff; }
.lang-btn:hover:not(.active) { background: #EAF0E8; }
.privacy-banner { background: #F0F9EF; border: 1px solid #B8DABB; border-radius: 10px; padding: 1rem 1.4rem; margin-bottom: 1.4rem; display: flex; align-items: flex-start; gap: .9rem; }
.privacy-icon { font-size: 1.3rem; flex-shrink: 0; line-height: 1.5; }
.privacy-title { font-size: .8rem; font-weight: 700; color: #14532D; margin-bottom: .2rem; letter-spacing: .04em; text-transform: uppercase; }
.privacy-body { font-size: .82rem; color: #166534; margin: 0; line-height: 1.55; }
/* naming guide */
.guide-banner {
background: #FFFBEB; border: 1px solid #FDE68A; border-radius: 10px;
padding: 1.1rem 1.4rem; margin-bottom: 1.4rem;
}
.guide-title { font-size: .85rem; font-weight: 700; color: #92400E; margin-bottom: .5rem; }
.guide-intro { font-size: .82rem; color: #78350F; margin-bottom: .9rem; line-height: 1.55; }
.guide-cases { display: flex; flex-wrap: wrap; gap: 1rem; margin-bottom: .9rem; }
.guide-case {
flex: 1; min-width: 260px; background: #fff; border: 1px solid #FDE68A;
border-radius: 8px; padding: .9rem 1.1rem;
}
.guide-case-title { font-size: .82rem; font-weight: 700; color: #92400E; margin-bottom: .4rem; }
.guide-case-body { font-size: .8rem; color: #57534E; margin-bottom: .6rem; line-height: 1.5; }
.guide-case-ex {
font-family: monospace; font-size: .78rem; color: #166534; background: #F0FDF4;
border: 1px solid #BBF7D0; border-radius: 5px; padding: .35rem .6rem; margin-top: .3rem;
}
.guide-case-ex-bad {
color: #9F1239; background: #FFF1F2; border-color: #FECDD3;
}
.section-label { font-size: .88rem; font-weight: 700; letter-spacing: .07em; text-transform: uppercase; color: #5A6B58; margin-bottom: .75rem; }
.file-chip { display: inline-block; font-size: .78rem; background: #EAF0E8; border: 1px solid #C5D4C2; border-radius: 4px; padding: .2rem .6rem; color: #3A6B35; font-weight: 600; margin-top: .5rem; }
.col-grid { display: flex; flex-wrap: wrap; gap: .45rem; margin-bottom: .9rem; }
.col-toggle { display: inline-flex; align-items: center; gap: .35rem; padding: .42rem .9rem; border-radius: 999px; border: 1.5px solid #C5D4C2; background: #fff; font-size: .92rem; font-weight: 500; color: #374151; cursor: pointer; transition: all .12s; user-select: none; }
.col-toggle.active { border-color: #3A6B35; background: #EAF0E8; color: #1A3D17; }
.col-toggle.active::before { content: '✓'; font-size: .82rem; color: #3A6B35; font-weight: 700; }
.col-toggle.excluded { border-color: #DDE5DB; background: #F9FAF9; color: #9CA3AF; text-decoration: line-through; }
.toggle-meta { font-size: .8rem; color: #5A6B58; margin-top: .35rem; margin-bottom: 0; }
.toggle-actions { display: flex; gap: .7rem; margin-bottom: .8rem; }
.link-btn { background: none; border: none; padding: 0; font-size: .92rem; color: #3A6B35; font-weight: 600; cursor: pointer; text-decoration: underline; }
.link-btn:hover { color: #2A5027; }
.shiny-input-container { width: 100% !important; }
.form-group { margin-bottom: 0; }
#clean_btn, #trt_btn {
border: none; color: #fff; font-weight: 700; font-size: .95rem;
padding: .58rem 1.6rem; border-radius: 6px; transition: background .15s;
}
#clean_btn { background: #3A6B35; } #clean_btn:hover { background: #2A5027; }
#trt_btn { background: #1D4ED8; } #trt_btn:hover { background: #1E40AF; }
.dl-wrap .btn { border: 1.5px solid #3A6B35; color: #3A6B35; background: transparent; font-weight: 600; font-size: 1.05rem; padding: .58rem 1.6rem; border-radius: 6px; transition: all .15s; }
.dl-wrap .btn:hover { background: #EAF0E8; }
.stats-strip { display: flex; flex-wrap: wrap; gap: 2rem; padding: .9rem 1.3rem; background: #EAF4E8; border-radius: 6px; border: 1px solid #B8DABB; margin-bottom: 1.3rem; }
.stat-item { display: flex; flex-direction: column; }
.stat-val { font-size: 1.5rem; font-weight: 800; color: #3A6B35; line-height: 1; }
.stat-lbl { font-size: .85rem; color: #5A6B58; margin-top: .2rem; }
.preview-wrap { overflow-x: auto; overflow-y: auto; max-height: 340px; border-radius: 6px; border: 1px solid #DDE5DB; }
.preview-wrap thead th { background: #EAF0E8; font-weight: 700; color: #1A3D17; border-bottom: 2px solid #C5D4C2; padding: .5rem .75rem; position: sticky; top: 0; z-index: 1; }
.preview-wrap table { font-size: .95rem !important; margin-bottom: 0; white-space: nowrap; }
.preview-wrap tbody td { padding: .4rem .75rem; color: #374151; border-bottom: 1px solid #F0F4EF; }
.preview-wrap tbody tr:last-child td { border-bottom: none; }
.rename-chips { display: flex; flex-wrap: wrap; gap: .4rem; margin-bottom: .5rem; }
.rename-chip { font-size: .78rem; padding: .22rem .6rem; border-radius: 4px; background: #FFF7ED; border: 1px solid #FED7AA; color: #92400E; font-family: monospace; }
.rename-chip .arr { color: #9CA3AF; margin: 0 .25rem; }
.rename-chip .to { color: #1A3D17; font-weight: 700; }
.trt-table { width: 100%; border-collapse: collapse; margin-bottom: 1rem; font-size: .86rem; }
.trt-table thead th { background: #EAF0E8; font-weight: 700; color: #1A3D17; border-bottom: 2px solid #C5D4C2; padding: .45rem .75rem; text-align: left; }
.trt-table tbody td { padding: .35rem .75rem; border-bottom: 1px solid #F0F4EF; vertical-align: middle; }
.trt-table tbody tr:last-child td { border-bottom: none; }
.trt-table tbody tr:hover td { background: #F5FAF4; }
.trt-input { width: 100%; border: 1.5px solid #C5D4C2; border-radius: 5px; padding: .28rem .55rem; font-size: .84rem; color: #1A1F1A; outline: none; transition: border-color .12s; background: #fff; }
.trt-input:focus { border-color: #3A6B35; }
.trt-detected { font-size: .82rem; color: #3A6B35; font-weight: 600; }
.trt-table tbody td code { color: #374151; background: #F3F4F6; padding: .1rem .4rem; border-radius: 3px; font-size: .8rem; }
.section-divider { border: none; border-top: 1px dashed #C5D4C2; margin: 1.3rem 0; }
.pivot-toggle { display: flex; align-items: center; gap: .85rem; padding: 1rem 1.2rem; border-radius: 8px; border: 1.5px solid #DDE5DB; background: #FAFAFA; margin-bottom: 1.2rem; cursor: pointer; transition: all .12s; }
.pivot-toggle:hover { border-color: #7C3AED; background: #F5F3FF; }
.pivot-toggle.on { border-color: #7C3AED; background: #F5F3FF; }
.pivot-switch { width: 2.6rem; height: 1.4rem; border-radius: 999px; background: #C5D4C2; position: relative; flex-shrink: 0; transition: background .15s; }
.pivot-toggle.on .pivot-switch { background: #7C3AED; }
.pivot-switch::after { content: ''; position: absolute; top: .18rem; left: .18rem; width: 1.04rem; height: 1.04rem; border-radius: 50%; background: #fff; transition: left .15s; }
.pivot-toggle.on .pivot-switch::after { left: 1.38rem; }
.pivot-label { font-size: 1rem; font-weight: 600; color: #374151; }
.pivot-sub { font-size: .88rem; color: #5A6B58; margin-top: .15rem; }
/* chart panel */
.chart-param-grid { display: flex; flex-wrap: wrap; gap: .5rem; margin-bottom: 1rem; }
.chart-param-toggle {
display: inline-flex; align-items: center; gap: .4rem; padding: .42rem .9rem;
border-radius: 999px; border: 1.5px solid #C5D4C2; background: #fff;
font-size: .92rem; font-weight: 500; color: #374151;
cursor: pointer; transition: all .12s; user-select: none;
}
.chart-param-toggle.active { border-color: #0E7490; background: #E0F4F8; color: #0C4A5C; }
.chart-param-toggle.active::before { content: '✓'; font-size: .82rem; color: #0E7490; font-weight: 700; }
.chart-param-toggle.excluded { border-color: #DDE5DB; background: #F9FAF9; color: #9CA3AF; }
#chart_btn {
border: none; color: #fff; font-weight: 700; font-size: .95rem;
padding: .58rem 1.6rem; border-radius: 6px; background: #0E7490; transition: background .15s;
}
#chart_btn:hover { background: #0C5E76; }
.chart-wrap {
background: #fff; border-radius: 6px; border: 1px solid #DDE5DB;
padding: 1rem; margin-top: 1rem;
}
.chart-wrap img { width: 100%; height: auto; display: block; border-radius: 4px; }
.chart-hint { font-size: .82rem; color: #5A6B58; margin-bottom: 1rem; }
/* ── dark mode ─────────────────────────────────────────────────────────── */
body.dark, body.dark html { background: #141A13 !important; color: #D8E8D5 !important; }
body.dark .wr-card { background: #1E261D; border-color: #334430; box-shadow: 0 1px 6px rgba(0,0,0,.3); }
body.dark .app-title { color: #E8F4E5; }
body.dark .app-subtitle { color: #7A9975; }
body.dark .section-label { color: #7A9975; }
body.dark .section-divider { border-color: #334430; }
body.dark .badge-idle { background: #253023; color: #7A9975; }
body.dark .badge-ok { background: #1A3D22; color: #6FCF7F; }
body.dark .badge-error { background: #3D1A1A; color: #F87171; }
body.dark .lang-switcher { border-color: #334430; }
body.dark .scheme-card { background: #1E261D; border-color: #334430; }
body.dark .scheme-card.active { border-color: #5A9B55; background: #1E3A1A; box-shadow: 0 0 0 1px #5A9B55; }
body.dark .scheme-card-title { color: #C5D8C2; }
body.dark .scheme-card-sub { color: #7A9975; }
body.dark .segment-token { background: #1E3A1A; border-color: #334430; color: #C5D8C2; }
body.dark .segment-sep { color: #5A6B58; }
body.dark .segment-role-select { background: #1E261D; border-color: #334430; color: #B8D0B5; }
body.dark .segment-role-select.role-treatment { border-color: #5A9B55; background: #1E3A1A; color: #A8E0A0; }
body.dark .segment-role-select.role-pot { border-color: #3B5FE0; background: #16203D; color: #93B4FA; }
body.dark .segment-role-select.role-plant { border-color: #9F6BF0; background: #261A3D; color: #CBB2F5; }
body.dark .manual-warning { background: #2D2010; border-color: #5A3E1A; color: #D4A050; }
body.dark .lang-btn { background: #1E261D; color: #7A9975; border-color: #334430; }
body.dark .lang-btn.active { background: #3A6B35; color: #fff; }
body.dark .lang-btn:hover:not(.active) { background: #253023; color: #D8E8D5; }
body.dark .privacy-banner { background: #1A2D1A; border-color: #2A4A2A; }
body.dark .privacy-title { color: #6FCF7F; }
body.dark .privacy-body { color: #7A9975; }
body.dark .guide-banner { background: #2D2410; border-color: #5A4A1A; }
body.dark .guide-title { color: #FBBF24; }
body.dark .guide-intro { color: #D4B870; }
body.dark .guide-case { background: #1E261D; border-color: #5A4A1A; }
body.dark .guide-case-title { color: #FBBF24; }
body.dark .guide-case-body { color: #B8D0B5; }
body.dark .guide-case-ex { background: #1A2D1A; border-color: #2A4A2A; color: #6FCF7F; }
body.dark .guide-case-ex-bad { background: #3D1A20; border-color: #5A2A35; color: #FCA5A5; }
body.dark .file-chip { background: #253023; border-color: #334430; color: #6FCF7F; }
body.dark .col-toggle { background: #1E261D; border-color: #334430; color: #C5D8C2; }
body.dark .col-toggle.active { border-color: #5A9B55; background: #1E3A1A; color: #A8E0A0; }
body.dark .col-toggle.excluded { background: #161E15; color: #4A5E47; }
body.dark .toggle-meta { color: #7A9975; }
body.dark .link-btn { color: #6FCF7F; }
body.dark .link-btn:hover { color: #A8E0A0; }
body.dark #clean_btn { background: #3A6B35; } body.dark #clean_btn:hover { background: #4A8A44; }
body.dark #trt_btn { background: #1D4ED8; } body.dark #trt_btn:hover { background: #2563EB; }
body.dark .dl-wrap .btn { border-color: #5A9B55; color: #6FCF7F; }
body.dark .dl-wrap .btn:hover { background: #1E3A1A; }
body.dark .stats-strip { background: #1A2D1A; border-color: #2A4A2A; }
body.dark .stat-val { color: #6FCF7F; }
body.dark .stat-lbl { color: #7A9975; }
body.dark .preview-wrap { border-color: #334430; background: #1E261D; }
body.dark .preview-wrap thead th { background: #253023; color: #C5D8C2; border-color: #334430; }
body.dark .preview-wrap tbody td { background: #1E261D; color: #B8D0B5; border-color: #2A3528; }
body.dark .preview-wrap tbody tr:hover td { background: #243426; }
body.dark .preview-wrap table { background: #1E261D; }
body.dark .rename-chip { background: #2D2010; border-color: #5A3E1A; color: #D4A050; }
body.dark .rename-chip .to { color: #6FCF7F; }
body.dark .trt-table thead th { background: #253023; color: #C5D8C2; border-color: #334430; }
body.dark .trt-table tbody td { border-color: #253023; color: #B8D0B5; }
body.dark .trt-table tbody tr:hover td { background: #1E2E1D; }
body.dark .trt-detected { color: #6FCF7F; }
body.dark .trt-table tbody td code { color: #C5D8C2; background: #161E15; }
body.dark .trt-input { background: #253023; border-color: #334430; color: #D8E8D5; }
body.dark .trt-input:focus { border-color: #5A9B55; }
body.dark .pivot-toggle { background: #1E261D; border-color: #334430; }
body.dark .pivot-toggle:hover, body.dark .pivot-toggle.on { border-color: #7C3AED; background: #1E1A2D; }
body.dark .pivot-switch { background: #334430; }
body.dark .pivot-label { color: #D8E8D5; }
body.dark .pivot-sub { color: #7A9975; }
body.dark .chart-param-toggle { background: #1E261D; border-color: #334430; color: #C5D8C2; }
body.dark .chart-param-toggle.active { border-color: #22B8CF; background: #122A30; color: #67E8F9; }
body.dark .chart-param-toggle.excluded { background: #161E15; color: #4A5E47; }
body.dark #chart_btn { background: #0E7490; } body.dark #chart_btn:hover { background: #155E75; }
body.dark .chart-wrap { background: #1E261D; border-color: #334430; }
body.dark .chart-hint { color: #7A9975; }
body.dark .shiny-table-output table,
body.dark table.table,
body.dark table {
background: #1E261D !important;
color: #B8D0B5 !important;
}
body.dark table.table > :not(caption) > * > * { background: #1E261D !important; color: #B8D0B5 !important; }
/* input file in dark */
body.dark .form-control, body.dark .form-select, body.dark input[type=text] {
background: #253023 !important; border-color: #334430 !important; color: #D8E8D5 !important;
}
body.dark .input-group-text { background: #334430 !important; border-color: #3D5438 !important; color: #7A9975 !important; }
"))),
tags$script(HTML("
// ── Auto-resize: comunica l'altezza reale del contenuto alla pagina
// Quarto genitore, così l'iframe shinylive si allunga invece di avere
// uno scroll interno quando il contenuto cresce con gli step.
(function() {
function sendHeight() {
var h = document.body.scrollHeight;
window.parent.postMessage({ type: 'cleanrhizo-resize', height: h }, '*');
}
var ro = new ResizeObserver(function() { sendHeight(); });
ro.observe(document.body);
// invio iniziale e dopo un breve delay per catturare il primo render completo
sendHeight();
setTimeout(sendHeight, 300);
setTimeout(sendHeight, 1000);
})();
// ── Dark mode: segue automaticamente le preferenze del sistema ───────────
(function() {
var mq = window.matchMedia('(prefers-color-scheme: dark)');
function applyDark(on) { document.body.classList.toggle('dark', on); }
applyDark(mq.matches);
mq.addEventListener('change', function(e) { applyDark(e.matches); });
})();
function setLang(lang) {
document.querySelectorAll('.lang-btn').forEach(function(b) {
b.classList.toggle('active', b.getAttribute('data-lang') === lang);
});
Shiny.setInputValue('ui_lang', lang, {priority: 'event'});
}
function setScheme(el, levels) {
document.querySelectorAll('.scheme-card').forEach(function(c) {
c.classList.toggle('active', c === el);
});
Shiny.setInputValue('sampleid_scheme', levels, {priority: 'event'});
}
function updateSegmentRole(sel) {
sel.classList.remove('role-treatment', 'role-pot', 'role-plant');
var v = sel.value;
if (v === 'Treatment') sel.classList.add('role-treatment');
if (v === 'Pot') sel.classList.add('role-pot');
if (v === 'Plant') sel.classList.add('role-plant');
var roles = [];
document.querySelectorAll('.segment-role-select').forEach(function(s) {
roles.push(s.value);
});
Shiny.setInputValue('manual_roles', roles, {priority: 'event'});
}
Shiny.addCustomMessageHandler('updateBadge', function(msg) {
var el = document.getElementById('status_badge_el');
if (!el) return;
el.textContent = msg.text;
el.className = 'status-badge ' + msg.cls;
});
Shiny.addCustomMessageHandler('downloadCSV', function(msg) {
var blob = new Blob([msg.content], {type: 'text/csv;charset=utf-8;'});
var link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = msg.filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});
Shiny.addCustomMessageHandler('updateColMeta', function(msg) {
var el = document.getElementById('col-meta');
if (el) el.textContent = msg.text;
});
function toggleCol(el) { el.classList.toggle('active'); el.classList.toggle('excluded'); updateMeta(); }
function setAll(state) {
document.querySelectorAll('.col-toggle').forEach(function(el) {
el.classList.toggle('active', state); el.classList.toggle('excluded', !state);
});
updateMeta();
}
function updateMeta() {
var active = document.querySelectorAll('.col-toggle.active').length;
var total = document.querySelectorAll('.col-toggle').length;
var sel = [];
document.querySelectorAll('.col-toggle.active').forEach(function(t) { sel.push(t.getAttribute('data-col')); });
Shiny.setInputValue('selected_cols', sel, {priority: 'event'});
Shiny.setInputValue('active_col_count', active, {priority: 'event'});
}
function togglePivot() {
var el = document.getElementById('pivot-toggle');
var on = el.classList.toggle('on');
Shiny.setInputValue('pivot_long', on, {priority: 'event'});
}
function toggleJitter() {
var el = document.getElementById('jitter-toggle');
var on = el.classList.toggle('on');
Shiny.setInputValue('show_jitter', on, {priority: 'event'});
}
function toggleChartParam(el) {
el.classList.toggle('active');
el.classList.toggle('excluded');
var sel = [];
document.querySelectorAll('.chart-param-toggle.active').forEach(function(t) {
sel.push(t.getAttribute('data-param'));
});
Shiny.setInputValue('chart_params', sel, {priority: 'event'});
}
Shiny.addCustomMessageHandler('downloadPNG', function(msg) {
var link = document.createElement('a');
link.href = 'data:image/png;base64,' + msg.data;
link.download = msg.filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});
$(document).on('shiny:value', function(e) {
if (e.name === 'col_selector_panel') setTimeout(updateMeta, 50);
});
")),
# ── Header ────────────────────────────────────────────────────────────────
div(class = "wr-card",
div(style = "display:flex; align-items:center; justify-content:space-between; flex-wrap:nowrap; gap:.5rem;",
div(class = "app-title", "CleanRhizo 🧹"),
div(style = "display:flex; align-items:center; gap:.8rem; flex-wrap:nowrap; flex-shrink:0;",
tags$span(id = "status_badge_el", class = "status-badge badge-idle", ""),
div(style = "flex-shrink:0;", class = "lang-switcher",
tags$button(class = "lang-btn active", `data-lang` = "en", onclick = "setLang('en')", "EN"),
tags$button(class = "lang-btn", `data-lang` = "it", onclick = "setLang('it')", "IT"),
tags$button(class = "lang-btn", `data-lang` = "es", onclick = "setLang('es')", "ES"),
tags$button(class = "lang-btn", `data-lang` = "fr", onclick = "setLang('fr')", "FR")
)
)
),
uiOutput("app_subtitle")
),
uiOutput("privacy_banner"),
uiOutput("guide_banner"),
uiOutput("step1_panel"),
uiOutput("col_selector_panel"),
uiOutput("step3_panel"),
uiOutput("preview_panel"),
uiOutput("treatment_panel"),
uiOutput("pivot_panel"),
uiOutput("chart_panel"),
hr(class = "section-divider"),
div(style = "text-align:center; font-size:.9rem; color:#5A6B58; padding:.4rem 0 1.2rem;",
uiOutput("app_credits")
)
)
# ── Server ───────────────────────────────────────────────────────────────────
server <- function(input, output, session) {
rv <- reactiveValues(
df_raw = NULL, df_clean = NULL, df_trt = NULL, df_final = NULL,
error = NULL, base_name = NULL, chart_png_b64 = NULL
)
lang <- reactive({ if (is.null(input$ui_lang)) "en" else input$ui_lang })
t <- reactive({ TR[[lang()]] })
output$app_subtitle <- renderUI({
tx <- t()
tagList(
p(class = "app-subtitle", tx$app_subtitle, " ",
tags$a(href = "https://www.regentinstruments.com/assets/winrhizo_mostrecent.html",
target = "_blank",
style = "color:#3A6B35;font-weight:700;text-decoration:underline;",
"WinRhizo — Regent Instruments.")),
p(class = "app-subtitle", style = "font-size:.88rem;color:#9CA3AF;margin-top:.1rem;",
tx$app_descr)
)
})
output$privacy_banner <- renderUI({
tx <- t()
div(class = "privacy-banner",
div(class = "privacy-icon", "🔒"),
div(div(class = "privacy-title", tx$privacy_title),
p(class = "privacy-body", tx$privacy_body))
)
})
output$guide_banner <- renderUI({
tx <- t()
div(class = "guide-banner",
div(class = "guide-title", tx$guide_title),
p(class = "guide-intro", tx$guide_intro),
div(class = "guide-cases",
div(class = "guide-case",
div(class = "guide-case-title", tx$guide_case1_title),
p(class = "guide-case-body", tx$guide_case1_body),
div(class = "guide-case-ex", tx$guide_case1_ex1)
),
div(class = "guide-case",
div(class = "guide-case-title", tx$guide_case2_title),
p(class = "guide-case-body", tx$guide_case2_body),
div(class = "guide-case-ex", tx$guide_case2_ex1),
div(class = "guide-case-ex", style = "margin-top:.3rem;", tx$guide_case2_ex2)
),
div(class = "guide-case",
div(class = "guide-case-title", tx$guide_why_title),
p(class = "guide-case-body", tx$guide_why_body),
div(class = "guide-case-ex guide-case-ex-bad", tx$guide_why_ex1),
div(class = "guide-case-ex", style = "margin-top:.3rem;", tx$guide_why_ex2)
)
)
)
})
output$app_credits <- renderUI({
cr <- t()$credits
tagList(
cr$pre,
tags$a(href = "https://www.mauromaver.eu", target = "_blank",
style = "color:#3A6B35; font-weight:700; text-decoration:underline;", "Mauro Maver"),
" · ",
tags$span(style = "color:#9CA3AF; font-size:.88rem;", "v.1.0"),
cr$bug,
tags$a(href = "https://github.com/Stramon1um", target = "_blank",
style = "color:#3A6B35; font-weight:600; text-decoration:underline;", "GitHub"),
cr$or,
tags$a(href = "mailto:mauro.maver@univr.it",
style = "color:#3A6B35; font-weight:600; text-decoration:underline;", cr$mail),
" 😊"
)
})
# ── Step 1 ────────────────────────────────────────────────────────────────
output$step1_panel <- renderUI({
tx <- t()
div(class = "wr-card",
p(class = "section-label", tx$step1),
fileInput("file_in", label = NULL, accept = c(".txt", "text/plain"),
placeholder = tx$file_placeholder, buttonLabel = tx$file_btn),
uiOutput("file_info_chip")
)
})
observeEvent(input$file_in, {
rv$df_raw <- rv$df_clean <- rv$df_trt <- rv$df_final <- rv$error <- rv$chart_png_b64 <- NULL
tryCatch({
rv$df_raw <- parse_winrhizo(input$file_in$datapath)
rv$base_name <- sub("\\.[^.]+$", "", input$file_in$name)
}, error = function(e) rv$error <- conditionMessage(e))
})
output$file_info_chip <- renderUI({
req(rv$df_raw)
div(class = "file-chip", t()$file_info(nrow(rv$df_raw), ncol(rv$df_raw)))
})
# ── Step 2 ────────────────────────────────────────────────────────────────
output$col_selector_panel <- renderUI({
req(rv$df_raw)
tx <- t()
all_cols <- colnames(rv$df_raw)
n_active <- sum(all_cols %in% DEFAULT_KEEP)
pills <- lapply(all_cols, function(col) {
is_active <- col %in% DEFAULT_KEEP
tags$span(class = paste("col-toggle", if (is_active) "active" else "excluded"),
`data-col` = col, onclick = "toggleCol(this);", col)
})
div(class = "wr-card",
p(class = "section-label", tx$step2),
div(class = "toggle-actions",
tags$button(class = "link-btn", onclick = "setAll(true)", tx$sel_all),
tags$button(class = "link-btn", onclick = "setAll(false)", tx$desel_all)
),
div(class = "col-grid", pills),
p(class = "toggle-meta", id = "col-meta",
paste(n_active, "of", length(all_cols), tx$col_meta))
)
})
observe({
req(rv$df_raw)
active <- if (!is.null(input$active_col_count)) input$active_col_count
else sum(colnames(rv$df_raw) %in% DEFAULT_KEEP)
session$sendCustomMessage("updateColMeta",
list(text = paste(active, "of", ncol(rv$df_raw), t()$col_meta)))
})
# ── Step 3 ────────────────────────────────────────────────────────────────
output$step3_panel <- renderUI({
req(rv$df_raw)
div(class = "wr-card",
p(class = "section-label", t()$step3),
div(style = "display:flex; gap:.8rem; align-items:center; flex-wrap:wrap;",
actionButton("clean_btn", t()$btn_clean, icon = icon("broom")),
div(class = "dl-wrap", uiOutput("dl_ui"))
)
)
})
observeEvent(input$clean_btn, {
req(rv$df_raw)
rv$df_clean <- rv$df_trt <- rv$df_final <- rv$error <- rv$chart_png_b64 <- NULL
tryCatch({
df <- rv$df_raw
keep <- input$selected_cols
if (is.null(keep) || !length(keep)) keep <- DEFAULT_KEEP
keep_ord <- colnames(df)[colnames(df) %in% keep]
rv$df_clean <- apply_rename(df[, keep_ord, drop = FALSE])
}, error = function(e) rv$error <- conditionMessage(e))
})
output$dl_ui <- renderUI({
req(rv$df_clean)
div(class = "dl-wrap", actionButton("dl_btn", t()$btn_dl_clean, icon = icon("download")))
})
observeEvent(input$dl_btn, {
req(rv$df_clean)
session$sendCustomMessage("downloadCSV", list(
filename = paste0(rv$base_name, "_clean.csv"),
content = df_to_csv_string(rv$df_clean)
))
})
# ── Anteprima step 3 ─────────────────────────────────────────────────────
output$preview_panel <- renderUI({
tx <- t()
if (!is.null(rv$error) && is.null(rv$df_clean)) {
div(class = "wr-card",
div(style = "color:#991B1B; font-size:.9rem;", icon("circle-exclamation"), " ", rv$error))
} else if (!is.null(rv$df_clean)) {
df <- rv$df_clean
applied <- COL_RENAME[names(COL_RENAME) %in% colnames(rv$df_raw)]
chips <- lapply(names(applied), function(old)
div(class = "rename-chip", span(old), span(class = "arr", "→"), span(class = "to", applied[[old]])))
div(class = "wr-card",
p(class = "section-label", tx$summary),
if (length(chips)) tagList(
p(style = "font-size:.88rem; color:#5A6B58; margin-bottom:.4rem;", tx$renamed_lbl),
div(class = "rename-chips", chips),
hr(class = "section-divider")
),
div(class = "stats-strip",
div(class="stat-item", span(class="stat-val", nrow(df)), span(class="stat-lbl", tx$stat_samples)),
div(class="stat-item", span(class="stat-val", ncol(df)), span(class="stat-lbl", tx$stat_cols)),
div(class="stat-item", span(class="stat-val", paste0(rv$base_name,"_clean.csv")), span(class="stat-lbl", tx$stat_file))
),
p(class = "section-label", tx$preview8),
div(class = "preview-wrap", tableOutput("preview_tbl"))
)
}
})
output$preview_tbl <- renderTable({ req(rv$df_clean); rv$df_clean },
striped = FALSE, hover = TRUE, bordered = FALSE, spacing = "xs", na = "")
# ── Step 4 ────────────────────────────────────────────────────────────────
output$treatment_panel <- renderUI({
req(rv$df_clean)
tx <- t()
sid_col <- if ("SampleId" %in% colnames(rv$df_clean)) "SampleId" else colnames(rv$df_clean)[1]
samples <- rv$df_clean[[sid_col]]
n_levels <- if (!is.null(input$sampleid_scheme)) input$sampleid_scheme else 1
manual_panel <- NULL
manual_warning <- NULL
if (n_levels == 3) {
# ── Schema manuale: dropdown per ogni token del primo SampleId ────────
ref_sample <- samples[1]
sp <- split_sampleid_tokens(ref_sample)
roles <- input$manual_roles
if (is.null(roles) || length(roles) != length(sp$tokens)) {
# default: tutto Treatment tranne l'ultimo (Plant)
roles <- rep("Treatment", length(sp$tokens))
if (length(roles) > 1) roles[length(roles)] <- "Plant"
}
seg_blocks <- list()
for (j in seq_along(sp$tokens)) {
sel_id <- paste0("segrole_", j)
role_cls <- switch(roles[j],
Treatment = "role-treatment", Pot = "role-pot",
Plant = "role-plant", "")
seg_blocks[[length(seg_blocks) + 1]] <- div(class = "segment-block",
div(class = "segment-token", sp$tokens[j]),
tags$select(id = sel_id, class = paste("segment-role-select", role_cls),
onchange = "updateSegmentRole(this);",
tags$option(value = "Treatment", selected = if (roles[j] == "Treatment") "selected", tx$trt_manual_role_treatment),
tags$option(value = "Pot", selected = if (roles[j] == "Pot") "selected", tx$trt_manual_role_pot),
tags$option(value = "Plant", selected = if (roles[j] == "Plant") "selected", tx$trt_manual_role_plant),
tags$option(value = "Ignore", selected = if (roles[j] == "Ignore") "selected", tx$trt_manual_role_ignore)
)
)
if (j < length(sp$tokens)) {
seg_blocks[[length(seg_blocks) + 1]] <- div(class = "segment-sep", sp$seps[j])
}
}
manual_panel <- div(
p(style = "font-size:.9rem; color:#5A6B58; margin-bottom:.4rem; font-weight:600;", tx$trt_manual_lbl),
p(style = "font-size:.86rem; color:#9CA3AF; margin-bottom:.8rem;", tx$trt_manual_hint),
div(class = "segment-row", seg_blocks)
)
result <- apply_manual_scheme(samples, roles)
detected <- result$treatment
rep1 <- result$pot
rep2 <- result$plant
if (any(!result$ok)) {
manual_warning <- div(class = "manual-warning",
icon("triangle-exclamation"), " ", tx$trt_manual_mismatch)
}
tbl_header <- tags$thead(tags$tr(
tags$th(tx$trt_col_sample), tags$th(tx$trt_col_detected),
tags$th(tx$trt_col_vaso), tags$th(tx$trt_col_pianta), tags$th(tx$trt_col_custom)
))
tbl_rows <- lapply(seq_along(samples), function(i) {
sid <- samples[i]; det <- detected[i]
inp_id <- paste0("trt_", gsub("[^A-Za-z0-9]", "_", sid))
tags$tr(
tags$td(tags$code(sid)),
tags$td(class = "trt-detected", det),
tags$td(style = "text-align:center; font-weight:600; color:#5A6B58;", rep1[i]),
tags$td(style = "text-align:center; font-weight:600; color:#5A6B58;", rep2[i]),
tags$td(tags$input(type = "text", id = inp_id, class = "trt-input",
placeholder = det,
oninput = paste0("Shiny.setInputValue('", inp_id, "', this.value, {priority:'event'})")))
)
})
} else {
detected <- extract_treatment(samples, n_levels)
rep_levels <- extract_replicate_levels(samples, n_levels)
if (n_levels == 1) {
tbl_header <- tags$thead(tags$tr(
tags$th(tx$trt_col_sample), tags$th(tx$trt_col_detected),
tags$th(tx$trt_col_replica), tags$th(tx$trt_col_custom)
))
tbl_rows <- lapply(seq_along(samples), function(i) {
sid <- samples[i]; det <- detected[i]
inp_id <- paste0("trt_", gsub("[^A-Za-z0-9]", "_", sid))
tags$tr(
tags$td(tags$code(sid)),
tags$td(class = "trt-detected", det),
tags$td(style = "text-align:center; font-weight:600; color:#5A6B58;", rep_levels[[1]][i]),
tags$td(tags$input(type = "text", id = inp_id, class = "trt-input",
placeholder = det,
oninput = paste0("Shiny.setInputValue('", inp_id, "', this.value, {priority:'event'})")))
)
})
} else {
tbl_header <- tags$thead(tags$tr(
tags$th(tx$trt_col_sample), tags$th(tx$trt_col_detected),
tags$th(tx$trt_col_vaso), tags$th(tx$trt_col_pianta), tags$th(tx$trt_col_custom)
))
tbl_rows <- lapply(seq_along(samples), function(i) {
sid <- samples[i]; det <- detected[i]
inp_id <- paste0("trt_", gsub("[^A-Za-z0-9]", "_", sid))
tags$tr(
tags$td(tags$code(sid)),
tags$td(class = "trt-detected", det),
tags$td(style = "text-align:center; font-weight:600; color:#5A6B58;", rep_levels[[1]][i]),
tags$td(style = "text-align:center; font-weight:600; color:#5A6B58;", rep_levels[[2]][i]),
tags$td(tags$input(type = "text", id = inp_id, class = "trt-input",
placeholder = det,
oninput = paste0("Shiny.setInputValue('", inp_id, "', this.value, {priority:'event'})")))
)
})
}
}
div(class = "wr-card",
p(class = "section-label", tx$step4),
p(style = "font-size:.86rem; color:#5A6B58; margin-bottom:1rem;", tx$trt_hint),
p(style = "font-size:.9rem; color:#5A6B58; margin-bottom:.5rem; font-weight:600;", tx$trt_scheme_lbl),
div(class = "scheme-grid",
div(class = paste("scheme-card", if (n_levels == 1) "active" else ""),
onclick = "setScheme(this, 1);",
div(class = "scheme-card-title", tx$trt_scheme_1),
div(class = "scheme-card-sub", tx$trt_scheme_1_sub)
),
div(class = paste("scheme-card", if (n_levels == 2) "active" else ""),
onclick = "setScheme(this, 2);",
div(class = "scheme-card-title", tx$trt_scheme_2),
div(class = "scheme-card-sub", tx$trt_scheme_2_sub)
),
div(class = paste("scheme-card", if (n_levels == 3) "active" else ""),
onclick = "setScheme(this, 3);",
div(class = "scheme-card-title", tx$trt_scheme_3),
div(class = "scheme-card-sub", tx$trt_scheme_3_sub)
)
),
manual_panel,
manual_warning,
div(style = "overflow-x:auto;",
tags$table(class = "trt-table", tbl_header, tags$tbody(tbl_rows))
),
hr(class = "section-divider"),
div(style = "display:flex; gap:.8rem; align-items:center; flex-wrap:wrap;",
actionButton("trt_btn", tx$btn_trt, icon = icon("tags")),
div(class = "dl-wrap", uiOutput("dl_trt_ui"))
),
uiOutput("trt_preview_wrap")
)
})
observeEvent(input$trt_btn, {
req(rv$df_clean)
rv$df_trt <- rv$df_final <- rv$error <- rv$chart_png_b64 <- NULL
tryCatch({
df <- rv$df_clean
sid_col <- if ("SampleId" %in% colnames(df)) "SampleId" else colnames(df)[1]
samples <- df[[sid_col]]
n_levels <- if (!is.null(input$sampleid_scheme)) input$sampleid_scheme else 1
if (n_levels == 3) {
ref_sample <- samples[1]
sp <- split_sampleid_tokens(ref_sample)
roles <- input$manual_roles
if (is.null(roles) || length(roles) != length(sp$tokens)) {
roles <- rep("Treatment", length(sp$tokens))
if (length(roles) > 1) roles[length(roles)] <- "Plant"
}
result <- apply_manual_scheme(samples, roles)
detected <- result$treatment
rep1 <- result$pot
rep2 <- result$plant
} else {
detected <- extract_treatment(samples, n_levels)
rep_levels <- extract_replicate_levels(samples, n_levels)
rep1 <- rep_levels[[1]]
rep2 <- if (n_levels >= 2) rep_levels[[2]] else NULL
}
trt_vals <- vapply(seq_along(samples), function(i) {
inp_id <- paste0("trt_", gsub("[^A-Za-z0-9]", "_", samples[i]))
val <- input[[inp_id]]
if (is.null(val) || trimws(val) == "") detected[i] else trimws(val)
}, character(1))
df$treatment <- trt_vals
if (n_levels == 1) {
df$replicate <- rep1
new_cols <- c("treatment", "replicate")
} else {
df$pot <- rep1 # vaso / replica biologica
df$plant <- rep2 # pianta / replica tecnica
new_cols <- c("treatment", "pot", "plant")
}
sid_pos <- which(colnames(df) == sid_col)
rest <- setdiff(colnames(df), c(colnames(df)[seq_len(sid_pos)], new_cols))
rv$df_trt <- df[, c(colnames(df)[seq_len(sid_pos)], new_cols, rest), drop = FALSE]
}, error = function(e) rv$error <- conditionMessage(e))
})
output$dl_trt_ui <- renderUI({
req(rv$df_trt)
div(class = "dl-wrap", actionButton("dl_trt_btn", t()$btn_dl_trt, icon = icon("download")))
})
observeEvent(input$dl_trt_btn, {
req(rv$df_trt)
session$sendCustomMessage("downloadCSV", list(
filename = paste0(rv$base_name, "_clean.csv"),
content = df_to_csv_string(rv$df_trt)
))
})
output$trt_preview_wrap <- renderUI({
req(rv$df_trt)
tagList(
hr(class = "section-divider"),
p(class = "section-label", t()$trt_preview),
div(class = "preview-wrap", tableOutput("trt_preview_tbl"))
)
})
output$trt_preview_tbl <- renderTable({ req(rv$df_trt); rv$df_trt },
striped = FALSE, hover = TRUE, bordered = FALSE, spacing = "xs", na = "")
# ── Step 5 ────────────────────────────────────────────────────────────────
output$pivot_panel <- renderUI({
req(rv$df_trt)
tx <- t()
div(class = "wr-card",
p(class = "section-label", tx$step5),
div(id = "pivot-toggle", class = "pivot-toggle", onclick = "togglePivot();",
div(class = "pivot-switch"),
div(div(class = "pivot-label", tx$pivot_label),
div(class = "pivot-sub", tx$pivot_sub))
),
div(style = "display:flex; gap:.8rem; align-items:center; flex-wrap:wrap;",
actionButton("pivot_btn", tx$btn_final, icon = icon("file-csv"),
style = "border:none; color:#fff; font-weight:700; font-size:.95rem; padding:.58rem 1.6rem; border-radius:6px; background:#7C3AED;"),
div(class = "dl-wrap", uiOutput("dl_final_ui"))
),
uiOutput("final_preview_wrap")
)
})
observeEvent(input$pivot_btn, {
req(rv$df_trt)
rv$df_final <- rv$error <- NULL
tryCatch({
rv$df_final <- if (isTRUE(input$pivot_long)) pivot_long(rv$df_trt) else rv$df_trt
}, error = function(e) rv$error <- conditionMessage(e))
})
output$dl_final_ui <- renderUI({
req(rv$df_final)
div(class = "dl-wrap", actionButton("dl_final_btn", t()$btn_dl_final, icon = icon("download")))
})
observeEvent(input$dl_final_btn, {
req(rv$df_final)
session$sendCustomMessage("downloadCSV", list(
filename = paste0(rv$base_name, "_clean.csv"),
content = df_to_csv_string(rv$df_final)
))
})
output$final_preview_wrap <- renderUI({
req(rv$df_final)
tx <- t()
tagList(
hr(class = "section-divider"),
div(class = "stats-strip",
div(class="stat-item", span(class="stat-val", nrow(rv$df_final)), span(class="stat-lbl", tx$stat_rows)),
div(class="stat-item", span(class="stat-val", ncol(rv$df_final)), span(class="stat-lbl", tx$stat_cols)),
div(class="stat-item",
span(class="stat-val", if (isTRUE(input$pivot_long)) "long" else "wide"),
span(class="stat-lbl", tx$stat_format)),
div(class="stat-item",
span(class="stat-val", paste0(rv$base_name, "_clean.csv")),
span(class="stat-lbl", tx$stat_file))
),
p(class = "section-label", tx$final_preview),
div(class = "preview-wrap", tableOutput("final_preview_tbl"))
)
})
output$final_preview_tbl <- renderTable({ req(rv$df_final); rv$df_final },
striped = FALSE, hover = TRUE, bordered = FALSE, spacing = "xs", na = "")
# ── Step 6: Grafico esplorativo ──────────────────────────────────────────
output$chart_panel <- renderUI({
req(rv$df_final)
tx <- t()
if (!"treatment" %in% colnames(rv$df_final)) return(NULL)
# parametri = colonne numeriche, escluse id/treatment/replicate/parameter/value
avail_params <- get_available_chart_params(rv$df_final)
if (!length(avail_params)) {
return(div(class = "wr-card", p(class = "section-label", tx$step6),
p(style = "font-size:.92rem; color:#9CA3AF;", tx$chart_no_params)))
}
pills <- lapply(avail_params, function(p) {
tags$span(class = "chart-param-toggle active", `data-param` = p,
onclick = "toggleChartParam(this);", p)
})
div(class = "wr-card",
p(class = "section-label", tx$step6),
p(class = "chart-hint", tx$chart_hint),
p(style = "font-size:.9rem; color:#5A6B58; margin-bottom:.5rem; font-weight:600;", tx$chart_param_lbl),
div(class = "chart-param-grid", pills),
div(id = "jitter-toggle", class = "pivot-toggle", onclick = "toggleJitter();",
div(class = "pivot-switch"),
div(
div(class = "pivot-label", tx$jitter_label),
div(class = "pivot-sub", tx$jitter_sub)
)
),
div(style = "display:flex; gap:.8rem; align-items:center; flex-wrap:wrap;",
actionButton("chart_btn", tx$btn_chart, icon = icon("chart-column")),
uiOutput("dl_png_ui"),
uiOutput("clear_chart_ui")
),
uiOutput("chart_render_wrap")
)
})
observeEvent(input$chart_btn, {
req(rv$df_final)
rv$chart_png_b64 <- NULL
sel_params <- input$chart_params
if (is.null(sel_params) || !length(sel_params)) {
sel_params <- get_available_chart_params(rv$df_final)
}
if (!length(sel_params)) return(NULL)
is_long <- "parameter" %in% colnames(rv$df_final) && "value" %in% colnames(rv$df_final)
# Se il dataset è in formato long, lo riconverte in un piccolo wide
# contenente solo i parametri selezionati, per riusare draw_bar_chart()
plot_df <- if (is_long) {
base <- rv$df_final[rv$df_final$parameter %in% sel_params, , drop = FALSE]
id_cols <- setdiff(colnames(base), c("parameter", "value"))
wide <- reshape(base, idvar = id_cols, timevar = "parameter",
direction = "wide", v.names = "value")
colnames(wide) <- sub("^value\\.", "", colnames(wide))
wide
} else rv$df_final
tmpfile <- tempfile(fileext = ".png")
n_p <- length(sel_params)
n_col <- if (n_p > 1) 2 else 1
n_row <- ceiling(n_p / n_col)
# 300 dpi: con width/height in pollici, evita la sgranatura visibile
# soprattutto quando si seleziona un solo parametro.
png(tmpfile, width = 5.2 * n_col, height = 4.6 * n_row, units = "in", res = 300)
draw_bar_chart(plot_df, sel_params, ylab = t()$chart_ylab, is_dark = FALSE,
show_jitter = isTRUE(input$show_jitter))
dev.off()
raw_bytes <- readBin(tmpfile, "raw", file.info(tmpfile)$size)
rv$chart_png_b64 <- base64_encode_raw(raw_bytes)
unlink(tmpfile)
})
output$chart_render_wrap <- renderUI({
req(rv$chart_png_b64)
div(class = "chart-wrap",
tags$img(src = paste0("data:image/png;base64,", rv$chart_png_b64))
)
})
output$dl_png_ui <- renderUI({
req(rv$chart_png_b64)
div(class = "dl-wrap", actionButton("dl_png_btn", t()$btn_dl_png, icon = icon("image")))
})
observeEvent(input$dl_png_btn, {
req(rv$chart_png_b64)
session$sendCustomMessage("downloadPNG", list(
filename = paste0(rv$base_name, "_chart.png"),
data = rv$chart_png_b64
))
})
output$clear_chart_ui <- renderUI({
req(rv$chart_png_b64)
tags$button(class = "link-btn", style = "color:#9CA3AF;",
onclick = "Shiny.setInputValue('clear_chart', Math.random(), {priority:'event'});",
icon("xmark"), " ", t()$btn_clear)
})
observeEvent(input$clear_chart, { rv$chart_png_b64 <- NULL })
# ── Badge stato ───────────────────────────────────────────────────────────
observe({
tx <- t()
cls <- if (!is.null(rv$error)) "badge-error"
else if (!is.null(rv$df_final) ||
!is.null(rv$df_trt) ||
!is.null(rv$df_clean) ||
!is.null(rv$df_raw)) "badge-ok"
else "badge-idle"
txt <- if (!is.null(rv$error)) tx$status_error
else if (!is.null(rv$df_final)) tx$status_final
else if (!is.null(rv$df_trt)) tx$status_trt
else if (!is.null(rv$df_clean)) tx$status_clean
else if (!is.null(rv$df_raw)) tx$status_loaded
else tx$status_waiting
session$sendCustomMessage("updateBadge", list(text = txt, cls = cls))
})
}
shinyApp(ui, server)