###############################################################################
# Script CHD - version beta 0.2 - 09-03-2026 #
# A partir d'un corpus texte formaté aux exigences IRAMUTEQ #
# Stéphane Meurisse #
# wwww.codeandcortex.fr #
# DEV UI #
###############################################################################
# Augmente la limite d'upload Shiny (défaut ~5 Mo), utile pour les corpus .txt volumineux.
options(shiny.maxRequestSize = 30 * 1024^2)
# En environnement non interactif (containers, services), certains appels graphiques
# sans device explicite peuvent tenter d'écrire "Rplots.pdf" dans le répertoire courant.
# Si ce répertoire n'est pas inscriptible, cela provoque l'erreur:
# "cannot open file 'Rplots.pdf'".
# On force donc un device PDF de repli dans tempdir(), toujours inscriptible.
if (!interactive()) {
options(device = function(...) {
grDevices::pdf(file = file.path(tempdir(), "Rplots.pdf"), ...)
})
}
if (file.exists("help/help.md")) {
ui_aide_huggingface <- function() {
tagList(
tags$h2("Aide"),
includeMarkdown("help/help.md")
)
}
} else {
ui_aide_huggingface <- function() {
tagList(
tags$h2("Aide"),
tags$p("Le fichier help/help.md est introuvable. Vérifie le dossier d'aide du projet.")
)
}
}
generer_table_html_afc_mots <- function(df) {
if (is.null(df) || nrow(df) == 0) {
return(tags$p("Aucun mot disponible pour cette classe."))
}
surligner_segment_afc <- function(segment, terme) {
segment <- ifelse(is.na(segment), "", as.character(segment))
terme <- ifelse(is.na(terme), "", as.character(terme))
if (!nzchar(trimws(segment)) || !nzchar(trimws(terme))) {
return(htmltools::htmlEscape(segment))
}
motifs <- preparer_motifs_surlignage_nfd(terme, taille_lot = 1)
if (!length(motifs)) return(htmltools::htmlEscape(segment))
segment_hl <- surligner_vecteur_html_unicode(
segment,
motifs,
"",
""
)
echapper_segments_en_preservant_surlignage(
segment_hl,
"",
""
)
}
htmltools::div(
style = "max-height: 420px; overflow-y: auto;",
tags$table(
style = "width: 100%; border-collapse: collapse;",
tags$thead(
tags$tr(lapply(names(df), function(col) tags$th(style = "text-align:left; padding:6px; border-bottom:1px solid #ddd;", col)))
),
tags$tbody(
lapply(seq_len(nrow(df)), function(i) {
terme_ligne <- if ("Terme" %in% names(df)) df$Terme[[i]] else ""
tags$tr(
lapply(names(df), function(col) {
val <- df[[col]][[i]]
contenu <- ifelse(is.na(val), "", as.character(val))
if (identical(col, "Segment_texte")) {
contenu <- htmltools::HTML(surligner_segment_afc(contenu, terme_ligne))
}
tags$td(style = "padding:6px; border-bottom:1px solid #f0f0f0; vertical-align: top;", contenu)
})
)
})
)
)
)
}
source("iramuteqlite/nettoyage_iramuteq.R", encoding = "UTF-8", local = TRUE)
source("iramuteqlite/concordancier-iramuteq.R", encoding = "UTF-8", local = TRUE)
source("iramuteqlite/afc_helpers_iramuteq.R", encoding = "UTF-8", local = TRUE)
source("iramuteqlite/afc_iramuteq.R", encoding = "UTF-8", local = TRUE)
source("iramuteqlite/ui_chd_stats_mode_iramuteq.R", encoding = "UTF-8", local = TRUE)
source("iramuteqlite/ui_options_iramuteq.R", encoding = "UTF-8", local = TRUE)
source("iramuteqlite/ui_explorateur_iramuteq.R", encoding = "UTF-8", local = TRUE)
source("iramuteqlite/affichage_iramuteq.R", encoding = "UTF-8", local = TRUE)
source("iramuteqlite/wordcloud_iramuteq.R", encoding = "UTF-8", local = TRUE)
source("ui.R", encoding = "UTF-8", local = TRUE)
source("iramuteqlite/chd_iramuteq.R", encoding = "UTF-8", local = TRUE)
source("iramuteqlite/dendrogramme_iramuteq.R", encoding = "UTF-8", local = TRUE)
source("iramuteqlite/stats_chd.R", encoding = "UTF-8", local = TRUE)
source("iramuteqlite/chd_engine_iramuteq.R", encoding = "UTF-8", local = TRUE)
source("iramuteqlite/server_outputs_status_iramuteq.R", encoding = "UTF-8", local = TRUE)
source("iramuteqlite/server_events_lancer_iramuteq.R", encoding = "UTF-8", local = TRUE)
# Compatibilité défensive: certains chemins historiques utilisent encore des appels
# non qualifiés (docvars/docnames). On expose des wrappers explicites pour éviter
# les erreurs de résolution si ces symboles ne sont pas attachés dans la session.
if (!exists("docvars", mode = "function", inherits = FALSE)) {
docvars <- function(...) quanteda::docvars(...)
}
if (!exists("docnames", mode = "function", inherits = FALSE)) {
docnames <- function(...) quanteda::docnames(...)
}
server <- function(input, output, session) {
est_texte_non_vide <- function(x) {
is.character(x) && length(x) > 0 && !is.na(x[[1]]) && nzchar(x[[1]])
}
zone_trace_disponible <- function(output_id, min_width = 120, min_height = 120) {
largeur <- suppressWarnings(as.numeric(session$clientData[[paste0("output_", output_id, "_width")]]))
hauteur <- suppressWarnings(as.numeric(session$clientData[[paste0("output_", output_id, "_height")]]))
is.finite(largeur) && is.finite(hauteur) && largeur >= min_width && hauteur >= min_height
}
creer_zip_depuis_dossier <- function(dossier_source, fichier_zip) {
if (!dir.exists(dossier_source)) {
stop("Dossier d'exports introuvable.")
}
ancien_wd <- getwd()
on.exit(setwd(ancien_wd), add = TRUE)
setwd(dirname(dossier_source))
if (file.exists(fichier_zip)) unlink(fichier_zip)
utils::zip(zipfile = fichier_zip, files = basename(dossier_source))
if (!file.exists(fichier_zip)) {
stop("Impossible de créer l'archive ZIP.")
}
}
rv <- reactiveValues(
logs = "",
statut = "En attente.",
progression = 0,
base_dir = NULL,
export_dir = NULL,
segments_file = NULL,
stats_file = NULL,
html_file = NULL,
zip_file = NULL,
res = NULL,
res_chd = NULL,
dfm_chd = NULL,
dfm = NULL,
filtered_corpus = NULL,
res_stats_df = NULL,
clusters = NULL,
max_n_groups = NULL,
max_n_groups_chd = NULL,
res_type = "simple",
exports_prefix = paste0("exports_", session$token),
lexique_fr_df = NULL,
expression_fr_df = NULL,
textes_indexation = NULL,
afc_obj = NULL,
afc_erreur = NULL,
afc_vars_obj = NULL,
afc_vars_erreur = NULL,
afc_dir = NULL,
afc_table_mots = NULL,
afc_table_vars = NULL,
afc_plot_classes = NULL,
afc_plot_termes = NULL,
afc_plot_vars = NULL,
afc_zoom_terms = NULL,
explor_assets = NULL,
stats_corpus_df = NULL,
stats_zipf_df = NULL,
min_docfreq_applique = 3L
)
if (exists("register_outputs_status", mode = "function", inherits = TRUE)) {
register_outputs_status(input, output, session, rv)
} else {
output$statut <- renderText({ rv$statut })
output$logs <- renderText({ rv$logs })
}
if (exists("server_explorateur_iramuteq", mode = "function", inherits = TRUE)) {
server_explorateur_iramuteq(
id = "explorer",
rv = rv,
nom_corpus_reactif = reactive({
if (!is.null(input$fichier_corpus$name) && nzchar(input$fichier_corpus$name)) {
return(as.character(input$fichier_corpus$name))
}
NULL
})
)
}
output$ui_afc_statut <- renderUI({
if (est_texte_non_vide(rv$afc_erreur)) {
return(tags$p("AFC : erreur (voir ci-dessous)."))
}
if (is.null(rv$afc_obj) || is.null(rv$afc_obj$ca)) {
return(tags$p("AFC non calculée. Lance une analyse pour calculer l'AFC classes × termes."))
}
ncl <- nrow(rv$afc_obj$table)
nt <- ncol(rv$afc_obj$table)
tags$p(paste0("AFC calculée sur ", ncl, " classes et ", nt, " termes (table Classes × Termes)."))
})
output$ui_afc_erreurs <- renderUI({
messages <- Filter(
est_texte_non_vide,
list(
rv$afc_erreur,
rv$afc_vars_erreur
)
)
if (length(messages) == 0) {
return(NULL)
}
tags$div(
style = "display: flex; flex-direction: column; gap: 8px; margin-bottom: 12px;",
lapply(messages, function(msg) {
tags$div(
style = "border: 1px solid #f5c2c7; background: #f8d7da; color: #842029; border-radius: 4px; padding: 10px; white-space: pre-wrap;",
msg
)
})
)
})
output$nom_fichier_selectionne <- renderText({
if (!is.null(input$fichier_corpus$name) && nzchar(input$fichier_corpus$name)) {
return(as.character(input$fichier_corpus$name))
}
"Aucun fichier choisi"
})
ouvrir_modal_parametres <- function() {
showModal(modalDialog(
title = "Paramétrages de l'analyse",
easyClose = TRUE,
size = "m",
ui_form_parametres_analyse(),
footer = tagList(
modalButton("Fermer"),
actionButton("lancer", "Lancer l'analyse", class = "btn-primary")
)
))
}
observeEvent(input$ouvrir_parametres, {
ouvrir_modal_parametres()
})
observeEvent(input$menu_importer_fichier_sidebar, {
showModal(modalDialog(
title = "Importer un fichier corpus",
easyClose = TRUE,
size = "s",
fileInput("fichier_corpus", "Choisir un fichier .txt", accept = c(".txt")),
footer = modalButton("Fermer")
))
})
observeEvent(input$fichier_corpus, {
req(input$fichier_corpus$datapath)
removeModal()
ouvrir_modal_parametres()
}, ignoreInit = TRUE)
output$ui_corpus_preview <- renderUI({
fichier <- input$fichier_corpus
if (is.null(fichier) || is.null(fichier$datapath) || !file.exists(fichier$datapath)) {
return(tags$p("Aucun corpus importé pour le moment."))
}
lignes <- tryCatch(
readLines(fichier$datapath, encoding = "UTF-8", warn = FALSE),
error = function(e) NULL
)
if (is.null(lignes) || length(lignes) == 0) {
return(tags$p("Le corpus importé est vide ou illisible."))
}
max_lignes <- 250
extrait <- lignes[seq_len(min(length(lignes), max_lignes))]
texte <- paste(extrait, collapse = "\n")
if (length(lignes) > max_lignes) {
texte <- paste0(
texte,
"\n\n… Aperçu limité aux ", max_lignes,
" premières lignes (", length(lignes), " lignes au total)."
)
}
tags$div(
tags$p(
style = "margin-bottom: 8px;",
paste0("Fichier : ", fichier$name)
),
tags$pre(
style = "white-space: pre-wrap; max-height: 70vh; overflow-y: auto; border: 1px solid #ddd; padding: 10px; background: #fafafa;",
texte
)
)
})
output$ui_table_stats_corpus <- renderUI({
req(rv$stats_corpus_df)
definitions <- c(
"Nom du corpus" = "Nom du fichier corpus importé.",
"Nombre de textes" = "Nombre d'unités de texte détectées dans le corpus.",
"Nombre de mots dans le corpus" = "Total des occurrences de mots (tokens).",
"Nombre de formes" = "Nombre de formes lexicales distinctes (types), différent des hapax.",
"Nombre de segments de texte" = "Nombre de segments après découpage pour l'analyse.",
"Nombre d'Hapax" = "Nombre de formes apparaissant une seule fois dans le corpus.",
"Loi de Zpif" = "Indicateur de conformité approximative à la loi de Zipf."
)
lignes <- lapply(seq_len(nrow(rv$stats_corpus_df)), function(i) {
metrique <- as.character(rv$stats_corpus_df$Metrique[i])
valeur <- as.character(rv$stats_corpus_df$Valeur[i])
definition <- unname(definitions[[metrique]])
if (is.null(definition) || !nzchar(definition)) definition <- ""
tags$tr(
tags$td(
tags$div(metrique),
if (nzchar(definition)) tags$div(
style = "font-size: 0.85em; color: #c62828; margin-top: 2px;",
definition
)
),
tags$td(valeur)
)
})
tags$table(
class = "table table-striped table-condensed",
tags$thead(
tags$tr(
tags$th("Metrique"),
tags$th("Valeur")
)
),
tags$tbody(lignes)
)
})
output$plot_stats_zipf <- renderPlot({
req(zone_trace_disponible("plot_stats_zipf", min_width = 180, min_height = 180))
req(rv$stats_zipf_df)
df <- rv$stats_zipf_df
if (is.null(df) || nrow(df) < 2) {
plot.new()
text(0.5, 0.5, "Données insuffisantes pour tracer la loi de Zpif.", cex = 1.1)
return(invisible(NULL))
}
x_lim <- range(df$log_rang, na.rm = TRUE)
y_lim <- range(c(df$log_frequence, df$log_pred), na.rm = TRUE)
plot(
x = df$log_rang,
y = df$log_frequence,
pch = 16,
cex = 0.8,
col = grDevices::adjustcolor("#2C7FB8", alpha.f = 0.7),
xlab = "log(rang)",
ylab = "log(fréquence)",
main = "Loi de Zpif",
xlim = x_lim,
ylim = y_lim,
asp = 1
)
grid(col = "#E6E6E6", lty = "dotted")
ord <- order(df$log_rang)
lines(df$log_rang[ord], df$log_pred[ord], col = "#D7301F", lwd = 2.5)
legend(
"topright",
legend = c("Données", "Régression log-log"),
col = c("#2C7FB8", "#D7301F"),
pch = c(16, NA),
lty = c(NA, 1),
lwd = c(NA, 2),
bty = "n"
)
})
output$ui_chd_statut <- renderUI({
if (length(packages_manquants) > 0) {
return(tags$div(
style = "border: 1px solid #f5c2c7; background: #f8d7da; color: #842029; border-radius: 4px; padding: 10px; margin-bottom: 10px;",
tags$strong("Dépendances manquantes : "),
paste(packages_manquants, collapse = ", ")
))
}
if (is.null(rv$res)) {
return(tags$p("CHD non disponible. Lance une analyse."))
}
nb_classes <- NA_integer_
if (!is.null(rv$clusters)) nb_classes <- length(rv$clusters)
if (identical(rv$res_type, "iramuteq")) {
return(tags$p(paste0("CHD disponible (moteur IRaMuTeQ-like) - classes détectées : ", nb_classes, ".")))
}
tags$p(paste0("CHD disponible (moteur IRaMuTeQ-like) - classes détectées : ", nb_classes, "."))
})
output$table_classes <- renderTable({
classes <- integer(0)
if (!is.null(rv$filtered_corpus) && quanteda::ndoc(rv$filtered_corpus) > 0) {
classes_docs <- quanteda::docvars(rv$filtered_corpus, "Classes")
classes <- suppressWarnings(as.integer(classes_docs))
}
if (!length(classes) && !is.null(rv$res_stats_df) && is.data.frame(rv$res_stats_df) && nrow(rv$res_stats_df) > 0) {
col_classe <- intersect(c("Classe", "classe", "cluster", "Class"), names(rv$res_stats_df))
if (length(col_classe) > 0) {
classes <- suppressWarnings(as.integer(rv$res_stats_df[[col_classe[[1]]]]))
}
}
classes <- classes[is.finite(classes) & classes > 0]
if (!length(classes)) {
return(data.frame(Message = "Aucune classe valide détectée.", stringsAsFactors = FALSE))
}
tab <- table(classes)
tab <- tab[order(as.integer(names(tab)))]
pct <- round(100 * as.numeric(tab) / sum(tab), 1)
data.frame(
Classe = paste0("Classe ", names(tab)),
Effectif = as.integer(tab),
Pourcentage = paste0(format(pct, nsmall = 1), " %"),
stringsAsFactors = FALSE
)
}, rownames = FALSE)
output$plot_chd_iramuteq_dendro <- renderPlot({
req(zone_trace_disponible("plot_chd_iramuteq_dendro", min_width = 200, min_height = 180))
if (is.null(rv$res) && is.null(rv$res_chd)) {
plot.new()
text(0.5, 0.5, "Dendrogramme CHD indisponible. Lance une analyse.", cex = 1.1)
return(invisible(NULL))
}
if (!requireNamespace("factoextra", quietly = TRUE)) {
plot.new()
text(0.5, 0.5, "Le package 'factoextra' est requis pour afficher le dendrogramme.
Installez-le puis relancez l'analyse.", cex = 1.0)
return(invisible(NULL))
}
style_dendro <- "factoextra"
tracer_dendrogramme_iramuteq_ui(
rv = rv,
top_n_terms = 4,
orientation = "horizontal",
style_affichage = style_dendro
)
})
register_events_lancer(input, output, session, rv)
observeEvent(input$lancer, {
rv$afc_zoom_terms <- NULL
}, ignoreInit = TRUE)
output$plot_afc_classes <- renderPlot({
req(zone_trace_disponible("plot_afc_classes", min_width = 220, min_height = 220))
if (est_texte_non_vide(rv$afc_erreur)) {
plot.new()
text(0.5, 0.5, "AFC indisponible (erreur).", cex = 1.1)
return(invisible(NULL))
}
if (is.null(rv$afc_obj) || is.null(rv$afc_obj$ca)) {
plot.new()
text(0.5, 0.5, "AFC non disponible. Lance une analyse.", cex = 1.1)
return(invisible(NULL))
}
tracer_afc_classes_seules(rv$afc_obj, axes = c(1, 2), cex_labels = 1.05)
})
output$ui_tables_stats_chd_iramuteq <- renderUI({
req(rv$res_stats_df)
col_classes <- intersect(c("cluster", "classe", "Classe", "Class"), names(rv$res_stats_df))
req(length(col_classes) > 0)
classes <- unique(as.character(rv$res_stats_df[[col_classes[[1]]]]))
classes <- classes[!is.na(classes) & nzchar(classes)]
classes <- sort(classes)
req(length(classes) > 0)
panneaux <- lapply(classes, function(cl) {
output_id <- paste0("table_stats_chd_iramuteq_cl_", cl)
output[[output_id]] <- renderTable({
extraire_stats_chd_classe(
rv$res_stats_df,
classe = cl,
n_max = NULL,
show_negative = FALSE,
max_p = 1,
seuil_p_significativite = input$max_p,
style = "iramuteq_clone"
)
}, rownames = FALSE, sanitize.text.function = function(x) x)
tabPanel(
title = paste0("Classe ", cl),
tableOutput(output_id)
)
})
do.call(tabsetPanel, c(id = "tabs_stats_chd_iramuteq", panneaux))
})
.calculer_limites_afc_termes <- function(obj, axes = c(1, 2)) {
if (is.null(obj) || is.null(obj$rowcoord) || is.null(obj$colcoord) || is.null(obj$termes_stats)) return(NULL)
ax1 <- axes[1]
ax2 <- axes[2]
st <- obj$termes_stats
st <- st[!is.na(st$Terme) & nzchar(st$Terme), , drop = FALSE]
st <- st[st$Terme %in% rownames(obj$colcoord), , drop = FALSE]
if (nrow(st) < 2) return(NULL)
xy_m <- obj$colcoord[st$Terme, , drop = FALSE]
x_m <- xy_m[, ax1]
y_m <- xy_m[, ax2]
x_c <- obj$rowcoord[, ax1]
y_c <- obj$rowcoord[, ax2]
lim <- calculer_lim_sym(c(x_m, x_c), c(y_m, y_c))
list(x = lim, y = lim)
}
.zoomer_limites <- function(lims, facteur = 1) {
if (!is.list(lims) || is.null(lims$x) || is.null(lims$y)) return(lims)
x <- suppressWarnings(as.numeric(lims$x))
y <- suppressWarnings(as.numeric(lims$y))
if (length(x) != 2 || any(!is.finite(x)) || length(y) != 2 || any(!is.finite(y))) return(lims)
cx <- mean(x)
cy <- mean(y)
hx <- abs(diff(range(x))) / 2
hy <- abs(diff(range(y))) / 2
if (!is.finite(hx) || hx == 0 || !is.finite(hy) || hy == 0) return(lims)
facteur <- as.numeric(facteur)
if (!is.finite(facteur) || facteur <= 0) facteur <- 1
list(
x = c(cx - hx * facteur, cx + hx * facteur),
y = c(cy - hy * facteur, cy + hy * facteur)
)
}
output$plot_afc <- renderPlot({
req(zone_trace_disponible("plot_afc", min_width = 220, min_height = 220))
if (est_texte_non_vide(rv$afc_erreur)) {
plot.new()
text(0.5, 0.5, "AFC indisponible (erreur).", cex = 1.1)
return(invisible(NULL))
}
if (is.null(rv$afc_obj) || is.null(rv$afc_obj$ca)) {
plot.new()
text(0.5, 0.5, "AFC non disponible. Lance une analyse.", cex = 1.1)
return(invisible(NULL))
}
activer_repel <- TRUE
if (!is.null(input$afc_reduire_chevauchement)) activer_repel <- isTRUE(input$afc_reduire_chevauchement)
taille_sel <- "frequency"
taille_sel_input <- as.character(input$afc_taille_mots)
if (length(taille_sel_input) > 0 && !is.na(taille_sel_input[[1]]) && nzchar(taille_sel_input[[1]])) {
taille_sel <- taille_sel_input[[1]]
}
if (!taille_sel %in% c("frequency", "chi2")) taille_sel <- "frequency"
top_termes <- 120
if (!is.null(input$afc_top_termes) && is.finite(input$afc_top_termes)) top_termes <- as.integer(input$afc_top_termes)
limites_base <- .calculer_limites_afc_termes(rv$afc_obj, axes = c(1, 2))
xlim_zoom <- NULL
ylim_zoom <- NULL
if (is.list(rv$afc_zoom_terms)) {
x_vals <- suppressWarnings(as.numeric(rv$afc_zoom_terms$x))
y_vals <- suppressWarnings(as.numeric(rv$afc_zoom_terms$y))
if (length(x_vals) == 2 && all(is.finite(x_vals))) xlim_zoom <- sort(x_vals)
if (length(y_vals) == 2 && all(is.finite(y_vals))) ylim_zoom <- sort(y_vals)
}
if (is.null(xlim_zoom) && is.list(limites_base)) xlim_zoom <- limites_base$x
if (is.null(ylim_zoom) && is.list(limites_base)) ylim_zoom <- limites_base$y
tracer_afc_classes_termes(
rv$afc_obj,
axes = c(1, 2),
top_termes = top_termes,
taille_sel = taille_sel,
activer_repel = activer_repel,
xlim_zoom = xlim_zoom,
ylim_zoom = ylim_zoom
)
})
observeEvent(input$afc_brush, {
b <- input$afc_brush
if (is.null(b)) return()
if (!all(c("xmin", "xmax", "ymin", "ymax") %in% names(b))) return()
vals <- suppressWarnings(as.numeric(c(b$xmin, b$xmax, b$ymin, b$ymax)))
if (length(vals) != 4 || any(!is.finite(vals))) return()
rv$afc_zoom_terms <- list(x = sort(vals[1:2]), y = sort(vals[3:4]))
})
observeEvent(input$afc_zoom_in, {
if (is.null(rv$afc_obj) || is.null(rv$afc_obj$ca)) return()
limites_base <- .calculer_limites_afc_termes(rv$afc_obj, axes = c(1, 2))
limites_courantes <- rv$afc_zoom_terms
if (!is.list(limites_courantes) || is.null(limites_courantes$x) || is.null(limites_courantes$y)) {
limites_courantes <- limites_base
}
rv$afc_zoom_terms <- .zoomer_limites(limites_courantes, facteur = 0.75)
})
observeEvent(input$afc_zoom_out, {
if (is.null(rv$afc_obj) || is.null(rv$afc_obj$ca)) return()
limites_base <- .calculer_limites_afc_termes(rv$afc_obj, axes = c(1, 2))
limites_courantes <- rv$afc_zoom_terms
if (!is.list(limites_courantes) || is.null(limites_courantes$x) || is.null(limites_courantes$y)) {
limites_courantes <- limites_base
}
lim_zoom <- .zoomer_limites(limites_courantes, facteur = 1.33)
if (is.list(limites_base)) {
lim_zoom$x <- c(
max(min(lim_zoom$x), min(limites_base$x)),
min(max(lim_zoom$x), max(limites_base$x))
)
lim_zoom$y <- c(
max(min(lim_zoom$y), min(limites_base$y)),
min(max(lim_zoom$y), max(limites_base$y))
)
}
rv$afc_zoom_terms <- lim_zoom
})
observeEvent(input$afc_zoom_reset, {
rv$afc_zoom_terms <- NULL
})
output$ui_table_afc_mots_par_classe <- renderUI({
if (is.null(rv$afc_table_mots)) {
output$table_afc_mots_message <- renderTable({
data.frame(Message = "AFC mots : non disponible.", stringsAsFactors = FALSE)
}, rownames = FALSE)
return(tableOutput("table_afc_mots_message"))
}
df <- rv$afc_table_mots
colonnes <- intersect(c("Terme", "Classe_max", "frequency", "chi2", "p_value", "Segment_texte"), names(df))
df <- df[, colonnes, drop = FALSE]
if ("p_value" %in% names(df)) {
df$p_value <- ifelse(
is.na(df$p_value),
NA_character_,
formatC(df$p_value, format = "f", digits = 6)
)
}
classes <- unique(as.character(df$Classe_max))
classes <- classes[!is.na(classes) & nzchar(classes)]
classes <- sort(classes)
if (length(classes) == 0) {
output$table_afc_mots_message <- renderTable({
data.frame(Message = "AFC mots : aucune classe disponible.", stringsAsFactors = FALSE)
}, rownames = FALSE)
return(tableOutput("table_afc_mots_message"))
}
ui_tables <- lapply(seq_along(classes), function(i) {
cl <- classes[[i]]
id <- paste0("table_afc_mots_", i)
output[[id]] <- renderUI({
sous_df <- df[df$Classe_max == cl, , drop = FALSE]
colonnes <- intersect(c("Terme", "frequency", "chi2", "p_value", "Segment_texte"), names(sous_df))
sous_df <- sous_df[, colonnes, drop = FALSE]
if ("p_value" %in% names(sous_df)) {
sous_df$p_value <- ifelse(
is.na(sous_df$p_value),
NA_character_,
formatC(sous_df$p_value, format = "f", digits = 6)
)
}
if ("chi2" %in% names(sous_df)) {
sous_df <- sous_df[order(-sous_df$chi2), , drop = FALSE]
sous_df$chi2 <- ifelse(
is.na(sous_df$chi2),
NA_character_,
formatC(sous_df$chi2, format = "f", digits = 6)
)
}
sous_df <- head(sous_df, 100)
generer_table_html_afc_mots(sous_df)
})
tagList(
tags$h5(cl),
uiOutput(id)
)
})
do.call(tagList, ui_tables)
})
output$plot_afc_vars <- renderPlot({
req(zone_trace_disponible("plot_afc_vars", min_width = 220, min_height = 220))
if (est_texte_non_vide(rv$afc_vars_erreur)) {
plot.new()
text(0.5, 0.5, "AFC variables étoilées indisponible (erreur).", cex = 1.1)
return(invisible(NULL))
}
if (is.null(rv$afc_vars_obj) || is.null(rv$afc_vars_obj$ca)) {
plot.new()
text(0.5, 0.5, "AFC variables étoilées non disponible. Lance une analyse.", cex = 1.1)
return(invisible(NULL))
}
activer_repel <- TRUE
if (!is.null(input$afc_reduire_chevauchement)) activer_repel <- isTRUE(input$afc_reduire_chevauchement)
top_mod <- 120
if (!is.null(input$afc_top_modalites) && is.finite(input$afc_top_modalites)) top_mod <- as.integer(input$afc_top_modalites)
tracer_afc_variables_etoilees(rv$afc_vars_obj, axes = c(1, 2), top_modalites = top_mod, activer_repel = activer_repel)
})
output$table_afc_vars <- renderTable({
if (is.null(rv$afc_table_vars)) {
return(data.frame(Message = "AFC variables étoilées : non disponible.", stringsAsFactors = FALSE))
}
df <- rv$afc_table_vars
colonnes <- intersect(c("Modalite", "Classe_max", "frequency", "chi2", "p_value"), names(df))
df <- df[, colonnes, drop = FALSE]
if ("p_value" %in% names(df)) {
p_values <- df$p_value
df$p_value <- ifelse(
is.na(p_values),
NA_character_,
ifelse(
p_values > 0.05,
sprintf("%s", formatC(p_values, format = "f", digits = 6)),
formatC(p_values, format = "f", digits = 6)
)
)
}
if ("chi2" %in% names(df)) df <- df[order(-df$chi2), , drop = FALSE]
if ("chi2" %in% names(df)) {
df$chi2 <- ifelse(
is.na(df$chi2),
NA_character_,
formatC(df$chi2, format = "f", digits = 6)
)
}
head(df, 200)
}, rownames = FALSE, sanitize.text.function = function(x) x)
output$table_afc_eig <- renderTable({
if (est_texte_non_vide(rv$afc_erreur)) {
return(data.frame(Message = "AFC indisponible (erreur).", stringsAsFactors = FALSE))
}
if (is.null(rv$afc_obj) || is.null(rv$afc_obj$ca)) {
return(data.frame(Message = "AFC non disponible.", stringsAsFactors = FALSE))
}
eig <- rv$afc_obj$ca$eig
if (is.null(eig)) return(data.frame(Message = "Valeurs propres indisponibles.", stringsAsFactors = FALSE))
df <- as.data.frame(eig)
df$Dim <- rownames(df)
rownames(df) <- NULL
df <- df[, c("Dim", names(df)[1], names(df)[2], names(df)[3]), drop = FALSE]
names(df) <- c("Dim", "Valeur_propre", "Pourcentage_inertie", "Pourcentage_cumule")
df
}, rownames = FALSE)
output$dl_segments <- downloadHandler(
filename = function() "segments_par_classe.txt",
content = function(file) {
req(rv$segments_file)
file.copy(rv$segments_file, file, overwrite = TRUE)
}
)
output$dl_stats <- downloadHandler(
filename = function() "stats_par_classe.csv",
content = function(file) {
req(rv$stats_file)
file.copy(rv$stats_file, file, overwrite = TRUE)
}
)
output$dl_html <- downloadHandler(
filename = function() "segments_par_classe.html",
content = function(file) {
req(rv$html_file)
file.copy(rv$html_file, file, overwrite = TRUE)
}
)
output$dl_zip <- downloadHandler(
filename = function() "exports_iramuteq_like.zip",
content = function(file) {
req(rv$export_dir)
zip_tmp <- tempfile(fileext = ".zip")
creer_zip_depuis_dossier(rv$export_dir, zip_tmp)
file.copy(zip_tmp, file, overwrite = TRUE)
}
)
}
app <- shinyApp(ui = ui, server = server)
app