Spaces:
Sleeping
Sleeping
| ############################################################################### | |
| # 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, | |
| "<span style='background-color: yellow;'>", | |
| "</span>" | |
| ) | |
| echapper_segments_en_preservant_surlignage( | |
| segment_hl, | |
| "<span style='background-color: yellow;'>", | |
| "</span>" | |
| ) | |
| } | |
| 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("<span style='color:#d97706;font-weight:600;'>%s</span>", 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 | |