add analyse fréquence mots tagcloud

This commit is contained in:
Tykayn 2025-08-30 18:57:27 +02:00 committed by tykayn
parent 7ae7d5915b
commit 056387013d
9 changed files with 781 additions and 6 deletions

4
.gitignore vendored
View file

@ -1,3 +1,5 @@
*.csv *.csv
.idea .idea
build/* build/*
venv
__pycache__

View file

@ -62,6 +62,24 @@ Il ne reste plus qu'à copier le texte donné dans livre.org ou a utliser la sor
Conversion en epub, html, et pdf grâce à pandoc. Conversion en epub, html, et pdf grâce à pandoc.
`python render_ebook.py` `python render_ebook.py`
### Analyse de fréquence des mots
`python analyse_frequence_mots.py`
Analyse la fréquence des mots dans votre livre et génère:
- Un fichier CSV contenant les 500 mots les plus fréquents par ordre décroissant
- Un nuage de mots au format SVG et PNG où la taille des mots dépend de leur fréquence
- Les mots sont colorés avec des tons pastels aléatoires pour une meilleure visualisation
Cette statistique de fréquence sert d'aide à l'évitement de répétitions dans votre texte. En identifiant les mots que vous utilisez le plus souvent, vous pouvez varier votre vocabulaire en utilisant un thesaurus pour trouver des synonymes et ainsi enrichir votre style d'écriture.
### Analyse orthographique et grammaticale
`python analyse_orthographe_grammaire.py`
Analyse les fautes d'orthographe et de grammaire dans votre livre et génère:
- Un rapport détaillé au format Markdown des erreurs trouvées dans chaque chapitre
- Un fichier CSV résumant le nombre d'erreurs par chapitre
- Des suggestions de correction pour chaque erreur identifiée
Cet outil vous aide à améliorer la qualité linguistique de votre texte en identifiant les problèmes potentiels chapitre par chapitre.
### Statistiques ### Statistiques
`bash up_infos.sh` `bash up_infos.sh`
@ -115,6 +133,7 @@ Un tableau de bord web interactif est disponible pour visualiser les données de
- Statistiques générales (mots, chapitres, personnages, intrigues) - Statistiques générales (mots, chapitres, personnages, intrigues)
- Graphique de progression de l'écriture - Graphique de progression de l'écriture
- Graphique de réseau des personnages (déplaçable à la souris) - Graphique de réseau des personnages (déplaçable à la souris)
- Graphique de réseau reliant les personnages aux intrigues (déplaçable à la souris, les labels des personnages sont toujours visibles, les labels des intrigues ne sont visibles que lors du survol)
- Chronologie des intrigues - Chronologie des intrigues
Pour générer le tableau de bord, exécutez: Pour générer le tableau de bord, exécutez:

181
analyse_frequence_mots.py Normal file
View file

@ -0,0 +1,181 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script pour analyser la fréquence des mots dans un fichier livre.org
et générer un nuage de mots coloré en tons pastels.
Ce script:
1. Lit le fichier livre.org
2. Extrait le texte en ignorant les métadonnées et les commentaires
3. Compte la fréquence des mots
4. Sauvegarde les 500 mots les plus fréquents dans un fichier CSV
5. Génère un nuage de mots en SVG et PNG avec des couleurs pastel aléatoires
"""
import re
import os
import csv
import random
import argparse
import numpy as np
import matplotlib.pyplot as plt
from collections import Counter
from wordcloud import WordCloud
import matplotlib.colors as mcolors
# Définir les arguments en ligne de commande
parser = argparse.ArgumentParser(description='Analyser la fréquence des mots dans un fichier Org-mode.')
parser.add_argument('dossier', nargs='?', help='Le chemin du dossier contenant le fichier livre.org. Si aucun dossier n\'est spécifié, le dossier courant sera utilisé.', default=os.getcwd())
args = parser.parse_args()
# Chemin vers le fichier livre.org
fichier_livre = f"{args.dossier}/livre.org"
# Liste des mots vides (stopwords) en français
stopwords = set([
"le", "la", "les", "un", "une", "des", "du", "de", "d'", "l'", "et", "ou", "",
"à", "au", "aux", "ce", "ces", "cette", "cet", "il", "ils", "elle", "elles",
"nous", "vous", "je", "tu", "on", "son", "sa", "ses", "leur", "leurs", "mon",
"ma", "mes", "ton", "ta", "tes", "que", "qui", "quoi", "dont", "pour", "par",
"dans", "sur", "sous", "avec", "sans", "en", "y", "est", "sont", "était",
"étaient", "sera", "seront", "a", "ont", "avait", "avaient", "aura", "auront",
"plus", "moins", "très", "peu", "beaucoup", "trop", "pas", "ne", "n'", "si",
"comme", "mais", "ou", "et", "donc", "car", "quand", "lorsque", "puis", "ensuite",
"alors", "ainsi", "aussi", "même", "tout", "tous", "toute", "toutes", "autre",
"autres", "certain", "certains", "certaine", "certaines", "tel", "tels", "telle",
"telles", "ceci", "cela", "ça", "c'", "s'", "d'", "l'", "qu'", "n'", "m'", "t'",
"se", "me", "te", "lui", "leur", "y", "en", "", "ici", "voici", "voilà", "ci",
"", "cet", "cette", "ces", "celui", "celle", "ceux", "celles", "celui-ci",
"celle-ci", "ceux-ci", "celles-ci", "celui-là", "celle-là", "ceux-là", "celles-là"
])
def generate_pastel_color():
"""Génère une couleur pastel aléatoire en format RGB."""
# NOTE: PIL's ImageDraw requires integer RGB values (0-255), not floats.
# La version précédente retournait des valeurs flottantes entre 0.6 et 0.9,
# ce qui causait une erreur: 'float' object cannot be interpreted as an integer
# Générer des valeurs RGB entre 153 et 229 (0.6*255 et 0.9*255) pour obtenir des tons pastels
# tout en convertissant en entiers pour la compatibilité avec PIL
r = int(random.uniform(0.6, 0.9) * 255)
g = int(random.uniform(0.6, 0.9) * 255)
b = int(random.uniform(0.6, 0.9) * 255)
return (r, g, b)
def extract_text_from_org(file_path):
"""
Extrait le texte d'un fichier org-mode en ignorant les métadonnées et les commentaires.
"""
with open(file_path, 'r', encoding='utf-8') as file:
content = file.read()
# Supprimer les blocs de commentaires
content = re.sub(r'#\+begin_comment.*?#\+end_comment', '', content, flags=re.DOTALL | re.IGNORECASE)
# Supprimer les lignes de métadonnées (commençant par #+)
content = re.sub(r'^\s*#\+.*$', '', content, flags=re.MULTILINE)
# Supprimer les lignes de propriétés
content = re.sub(r'^\s*:.*:.*$', '', content, flags=re.MULTILINE)
# Supprimer les titres de chapitres (lignes commençant par * ou **)
content = re.sub(r'^\s*\*+.*$', '', content, flags=re.MULTILINE)
# Supprimer les liens org-mode [[...][...]] et [[...]]
content = re.sub(r'\[\[.*?\]\](?:\[.*?\])?', '', content)
# Supprimer les caractères spéciaux et la ponctuation
content = re.sub(r'[^\w\s]', ' ', content)
# Convertir en minuscules
content = content.lower()
return content
def count_word_frequencies(text):
"""
Compte la fréquence des mots dans un texte.
Ignore les mots vides (stopwords) et les mots de moins de 3 caractères.
"""
# Diviser le texte en mots
words = re.findall(r'\b\w+\b', text)
# Filtrer les mots courts et les stopwords
filtered_words = [word for word in words if len(word) >= 3 and word not in stopwords]
# Compter les fréquences
word_counts = Counter(filtered_words)
return word_counts
def save_to_csv(word_counts, output_path, limit=500):
"""
Sauvegarde les mots les plus fréquents dans un fichier CSV.
"""
# Obtenir les mots les plus fréquents
most_common = word_counts.most_common(limit)
# Écrire dans le fichier CSV
with open(output_path, 'w', newline='', encoding='utf-8') as csvfile:
writer = csv.writer(csvfile)
writer.writerow(['Mot', 'Fréquence'])
for word, count in most_common:
writer.writerow([word, count])
print(f"Les {limit} mots les plus fréquents ont été sauvegardés dans {output_path}")
def generate_wordcloud(word_counts, output_svg, output_png):
"""
Génère un nuage de mots en SVG et PNG avec des couleurs pastel aléatoires.
"""
# Fonction pour attribuer des couleurs pastel aléatoires aux mots
def color_func(word, font_size, position, orientation, random_state=None, **kwargs):
return generate_pastel_color()
# Créer le nuage de mots
wordcloud = WordCloud(
width=1200,
height=800,
background_color='white',
max_words=500,
color_func=color_func,
prefer_horizontal=0.9,
relative_scaling=0.5
).generate_from_frequencies(word_counts)
# Sauvegarder en SVG
plt.figure(figsize=(12, 8), dpi=300)
plt.imshow(wordcloud, interpolation='bilinear')
plt.axis('off')
plt.tight_layout(pad=0)
plt.savefig(output_svg, format='svg', bbox_inches='tight')
# Sauvegarder en PNG
plt.savefig(output_png, format='png', bbox_inches='tight')
plt.close()
print(f"Nuage de mots sauvegardé en SVG: {output_svg}")
print(f"Nuage de mots sauvegardé en PNG: {output_png}")
def main():
# Extraire le texte du fichier livre.org
print(f"Analyse du fichier: {fichier_livre}")
text = extract_text_from_org(fichier_livre)
# Compter les fréquences des mots
word_counts = count_word_frequencies(text)
# Définir les chemins de sortie
csv_output = f"{args.dossier}/frequence_mots_top500.csv"
svg_output = f"{args.dossier}/nuage_mots.svg"
png_output = f"{args.dossier}/nuage_mots.png"
# Sauvegarder les résultats
save_to_csv(word_counts, csv_output)
generate_wordcloud(word_counts, svg_output, png_output)
print("Analyse de fréquence des mots terminée avec succès!")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,223 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script pour analyser les fautes d'orthographe et de grammaire dans un fichier livre.org
et générer un rapport par chapitre.
Ce script:
1. Lit le fichier livre.org
2. Extrait le texte par chapitre
3. Analyse les fautes d'orthographe et de grammaire dans chaque chapitre
4. Génère un rapport détaillé des erreurs trouvées
"""
import re
import os
import csv
import argparse
from pyspellchecker import SpellChecker
import language_tool_python
# Définir les arguments en ligne de commande
parser = argparse.ArgumentParser(description='Analyser les fautes d\'orthographe et de grammaire dans un fichier Org-mode.')
parser.add_argument('dossier', nargs='?', help='Le chemin du dossier contenant le fichier livre.org. Si aucun dossier n\'est spécifié, le dossier courant sera utilisé.', default=os.getcwd())
args = parser.parse_args()
# Chemin vers le fichier livre.org
fichier_livre = f"{args.dossier}/livre.org"
def extract_chapters(file_path):
"""
Extrait les chapitres d'un fichier org-mode.
Retourne un dictionnaire avec les titres des chapitres comme clés et leur contenu comme valeurs.
"""
with open(file_path, 'r', encoding='utf-8') as file:
content = file.read()
# Diviser le contenu par chapitres (lignes commençant par **)
chapter_pattern = r'^\*\* (.*?)$(.*?)(?=^\*\* |\Z)'
chapters = re.findall(chapter_pattern, content, re.MULTILINE | re.DOTALL)
chapter_dict = {}
for title, content in chapters:
# Nettoyer le titre (supprimer ": title:" s'il existe)
clean_title = re.sub(r'\s*:\s*title\s*:', '', title).strip()
# Nettoyer le contenu
clean_content = clean_chapter_content(content)
chapter_dict[clean_title] = clean_content
return chapter_dict
def clean_chapter_content(content):
"""
Nettoie le contenu d'un chapitre en supprimant les commentaires et les balises org-mode.
"""
# Supprimer les blocs de commentaires
content = re.sub(r'#\+begin_comment.*?#\+end_comment', '', content, flags=re.DOTALL | re.IGNORECASE)
# Supprimer les lignes de métadonnées (commençant par #+)
content = re.sub(r'^\s*#\+.*$', '', content, flags=re.MULTILINE)
# Supprimer les sous-titres (lignes commençant par ***)
content = re.sub(r'^\s*\*\*\*.*$', '', content, flags=re.MULTILINE)
# Supprimer les liens org-mode [[...][...]] et [[...]]
content = re.sub(r'\[\[.*?\]\](?:\[.*?\])?', '', content)
# Supprimer les lignes vides multiples
content = re.sub(r'\n\s*\n', '\n\n', content)
return content.strip()
def check_spelling(text, lang='fr'):
"""
Vérifie l'orthographe d'un texte et retourne les mots mal orthographiés.
"""
spell = SpellChecker(language=lang)
# Diviser le texte en mots
words = re.findall(r'\b\w+\b', text.lower())
# Trouver les mots mal orthographiés
misspelled = spell.unknown(words)
# Créer un dictionnaire avec les mots mal orthographiés et leurs suggestions
spelling_errors = {}
for word in misspelled:
# Obtenir les suggestions de correction
suggestions = spell.candidates(word)
# Limiter à 5 suggestions maximum
suggestions_list = list(suggestions)[:5]
spelling_errors[word] = suggestions_list
return spelling_errors
def check_grammar(text, lang='fr'):
"""
Vérifie la grammaire d'un texte et retourne les erreurs grammaticales.
"""
# Initialiser l'outil de vérification grammaticale
tool = language_tool_python.LanguageTool(lang)
# Vérifier le texte
matches = tool.check(text)
# Créer une liste d'erreurs grammaticales
grammar_errors = []
for match in matches:
# Ignorer les erreurs d'orthographe (déjà traitées par le vérificateur d'orthographe)
if match.ruleId.startswith('MORFOLOGIK_RULE'):
continue
error = {
'message': match.message,
'context': match.context,
'suggestions': match.replacements,
'offset': match.offset,
'length': match.errorLength,
'rule': match.ruleId
}
grammar_errors.append(error)
# Fermer l'outil pour libérer les ressources
tool.close()
return grammar_errors
def generate_error_report(chapters, output_path):
"""
Génère un rapport des erreurs d'orthographe et de grammaire par chapitre.
"""
with open(output_path, 'w', encoding='utf-8') as report_file:
report_file.write("# Rapport d'analyse orthographique et grammaticale\n\n")
total_spelling_errors = 0
total_grammar_errors = 0
for chapter_title, chapter_content in chapters.items():
report_file.write(f"## Chapitre: {chapter_title}\n\n")
# Vérifier l'orthographe
spelling_errors = check_spelling(chapter_content)
# Vérifier la grammaire
grammar_errors = check_grammar(chapter_content)
# Mettre à jour les totaux
total_spelling_errors += len(spelling_errors)
total_grammar_errors += len(grammar_errors)
# Écrire les erreurs d'orthographe
report_file.write("### Erreurs d'orthographe\n\n")
if spelling_errors:
for word, suggestions in spelling_errors.items():
suggestions_str = ", ".join(suggestions) if suggestions else "Aucune suggestion"
report_file.write(f"- **{word}**: {suggestions_str}\n")
else:
report_file.write("Aucune erreur d'orthographe détectée.\n")
report_file.write("\n")
# Écrire les erreurs grammaticales
report_file.write("### Erreurs grammaticales\n\n")
if grammar_errors:
for error in grammar_errors:
suggestions_str = ", ".join(error['suggestions'][:5]) if error['suggestions'] else "Aucune suggestion"
context = error['context'].replace(error['context'][error['offset']:error['offset']+error['length']],
f"**{error['context'][error['offset']:error['offset']+error['length']]}**")
report_file.write(f"- **Erreur**: {error['message']}\n")
report_file.write(f" - **Contexte**: {context}\n")
report_file.write(f" - **Suggestions**: {suggestions_str}\n\n")
else:
report_file.write("Aucune erreur grammaticale détectée.\n")
report_file.write("\n---\n\n")
# Écrire le résumé
report_file.write("## Résumé\n\n")
report_file.write(f"- **Nombre total de chapitres analysés**: {len(chapters)}\n")
report_file.write(f"- **Nombre total d'erreurs d'orthographe**: {total_spelling_errors}\n")
report_file.write(f"- **Nombre total d'erreurs grammaticales**: {total_grammar_errors}\n")
print(f"Rapport d'erreurs généré: {output_path}")
def save_to_csv(chapters, output_path):
"""
Sauvegarde un résumé des erreurs dans un fichier CSV.
"""
with open(output_path, 'w', newline='', encoding='utf-8') as csvfile:
writer = csv.writer(csvfile)
writer.writerow(['Chapitre', 'Erreurs d\'orthographe', 'Erreurs grammaticales', 'Total'])
for chapter_title, chapter_content in chapters.items():
spelling_errors = check_spelling(chapter_content)
grammar_errors = check_grammar(chapter_content)
total_errors = len(spelling_errors) + len(grammar_errors)
writer.writerow([chapter_title, len(spelling_errors), len(grammar_errors), total_errors])
print(f"Résumé des erreurs sauvegardé dans {output_path}")
def main():
print(f"Analyse du fichier: {fichier_livre}")
# Extraire les chapitres
chapters = extract_chapters(fichier_livre)
print(f"Nombre de chapitres trouvés: {len(chapters)}")
# Définir les chemins de sortie
report_output = f"{args.dossier}/rapport_orthographe_grammaire.md"
csv_output = f"{args.dossier}/resume_erreurs.csv"
# Générer le rapport d'erreurs
generate_error_report(chapters, report_output)
# Sauvegarder le résumé en CSV
save_to_csv(chapters, csv_output)
print("Analyse orthographique et grammaticale terminée avec succès!")
if __name__ == "__main__":
main()

View file

@ -97,6 +97,109 @@ def process_character_occurrences(csv_file='occurrences_personnages.csv'):
return {"nodes": nodes, "links": links} return {"nodes": nodes, "links": links}
def process_character_plot_network(char_csv='occurrences_personnages.csv', plot_csv='intrigues.csv'):
"""
Traite les fichiers d'occurrences des personnages et d'intrigues pour générer
les données du graphique de réseau reliant personnages et intrigues.
"""
# Charger les données des personnages
char_data = load_csv_data(char_csv)
if not char_data:
return None
# Charger les données des intrigues
plot_data = load_csv_data(plot_csv)
if not plot_data or len(plot_data) < 2:
return None
# Extraire les en-têtes (noms des personnages)
char_headers = char_data[0]
characters = char_headers[1:] # Ignorer la première colonne (Chapitre)
# Créer un dictionnaire pour stocker les chapitres où chaque personnage apparaît
character_chapters = {char: [] for char in characters}
# Remplir le dictionnaire avec les chapitres où chaque personnage apparaît
for row in char_data[1:]: # Ignorer la ligne d'en-tête
if len(row) < len(char_headers):
continue
chapter = row[0]
# Extraire le numéro de chapitre si possible
chapter_num = None
try:
# Essayer d'extraire un nombre du nom du chapitre
import re
match = re.search(r'(\d+)', chapter)
if match:
chapter_num = int(match.group(1))
except:
pass
# Si on n'a pas pu extraire un nombre, continuer
if chapter_num is None:
continue
# Trouver les personnages présents dans ce chapitre
for i, count in enumerate(row[1:], 1):
if count and int(count) > 0:
character_chapters[char_headers[i]].append(chapter_num)
# Créer les nœuds pour chaque personnage (groupe 1)
nodes = [{"id": char, "name": char, "group": 1, "type": "character"} for char in characters]
# Créer les nœuds pour chaque intrigue (groupe 2)
for row in plot_data[1:]: # Ignorer la ligne d'en-tête
if len(row) < 3:
continue
try:
start = int(row[0])
end = int(row[1])
name = row[2]
nodes.append({
"id": f"plot_{name}",
"name": name,
"group": 2,
"type": "plot",
"start": start,
"end": end
})
except (ValueError, IndexError):
continue
# Créer les liens entre personnages et intrigues
links = []
# Pour chaque intrigue
for node in nodes:
if node["group"] == 2: # C'est une intrigue
plot_start = node["start"]
plot_end = node["end"]
# Pour chaque personnage
for char_node in nodes:
if char_node["group"] == 1: # C'est un personnage
char_name = char_node["id"]
# Vérifier si le personnage apparaît dans un chapitre couvert par l'intrigue
appears_in_plot = False
for chapter in character_chapters[char_name]:
if plot_start <= chapter <= plot_end:
appears_in_plot = True
break
# Si le personnage apparaît dans l'intrigue, créer un lien
if appears_in_plot:
links.append({
"source": char_name,
"target": node["id"],
"value": 1
})
return {"nodes": nodes, "links": links}
def process_writing_progress(csv_file='suivi_livre.csv'): def process_writing_progress(csv_file='suivi_livre.csv'):
""" """
Traite le fichier de suivi pour générer les données du graphique de progression. Traite le fichier de suivi pour générer les données du graphique de progression.
@ -207,7 +310,7 @@ def create_css():
margin: 20px 0; margin: 20px 0;
} }
#network-graph { #network-graph, #character-plot-network, #plot-timeline {
border: 1px solid #ddd; border: 1px solid #ddd;
border-radius: 4px; border-radius: 4px;
} }
@ -241,6 +344,20 @@ def create_css():
color: #2c3e50; color: #2c3e50;
} }
/* Styles pour le graphique de réseau personnages-intrigues */
.character text {
font-weight: bold;
fill: #333;
}
.plot text {
fill: #e74c3c;
}
.legend {
font-size: 12px;
}
@media (max-width: 768px) { @media (max-width: 768px) {
.stat-card { .stat-card {
width: calc(50% - 20px); width: calc(50% - 20px);
@ -359,6 +476,179 @@ def create_network_graph_js(network_data):
with open(os.path.join(JS_DIR, 'network-graph.js'), 'w', encoding='utf-8') as f: with open(os.path.join(JS_DIR, 'network-graph.js'), 'w', encoding='utf-8') as f:
f.write(js_content) f.write(js_content)
def create_character_plot_network_js(network_data):
"""Crée le fichier JavaScript pour le graphique de réseau personnages-intrigues."""
js_content = f"""
// Données du graphique de réseau personnages-intrigues
const characterPlotData = {json.dumps(network_data)};
// Fonction pour initialiser le graphique de réseau personnages-intrigues
function initCharacterPlotNetwork() {{
const width = document.getElementById('character-plot-network').clientWidth;
const height = 400;
// Créer le SVG
const svg = d3.select("#character-plot-network")
.append("svg")
.attr("width", width)
.attr("height", height);
// Définir les couleurs pour les différents types de nœuds
const nodeColors = {{
character: "#69b3a2", // Vert pour les personnages
plot: "#e74c3c" // Rouge pour les intrigues
}};
// Créer la simulation de force
const simulation = d3.forceSimulation(characterPlotData.nodes)
.force("link", d3.forceLink(characterPlotData.links).id(d => d.id).distance(150))
.force("charge", d3.forceManyBody().strength(-300))
.force("center", d3.forceCenter(width / 2, height / 2))
.force("collision", d3.forceCollide().radius(30));
// Créer les liens
const link = svg.append("g")
.attr("class", "links")
.selectAll("line")
.data(characterPlotData.links)
.enter().append("line")
.attr("stroke-width", d => Math.sqrt(d.value))
.attr("stroke", "#999")
.attr("stroke-opacity", 0.6);
// Créer les nœuds
const node = svg.append("g")
.attr("class", "nodes")
.selectAll("g")
.data(characterPlotData.nodes)
.enter().append("g")
.attr("class", d => d.type)
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
// Ajouter des cercles pour les nœuds
node.append("circle")
.attr("r", d => d.type === "character" ? 8 : 12)
.attr("fill", d => nodeColors[d.type]);
// Ajouter des étiquettes pour les nœuds
// Les étiquettes des personnages sont toujours visibles
node.filter(d => d.type === "character")
.append("text")
.text(d => d.name)
.attr("x", 12)
.attr("y", 3)
.style("font-size", "12px")
.style("font-weight", "bold");
// Les étiquettes des intrigues ne sont visibles que lors du survol
const plotLabels = node.filter(d => d.type === "plot")
.append("text")
.text(d => d.name)
.attr("x", 12)
.attr("y", 3)
.style("font-size", "12px")
.style("font-weight", "bold")
.style("fill", "#e74c3c")
.style("opacity", 0); // Initialement invisible
// Ajouter des événements de survol pour les nœuds d'intrigue
node.filter(d => d.type === "plot")
.on("mouseover", function(event, d) {{
// Rendre l'étiquette visible
d3.select(this).select("text").style("opacity", 1);
// Mettre en évidence les liens connectés
link.style("stroke", l =>
l.source.id === d.id || l.target.id === d.id ? "#e74c3c" : "#999")
.style("stroke-opacity", l =>
l.source.id === d.id || l.target.id === d.id ? 1 : 0.2)
.style("stroke-width", l =>
l.source.id === d.id || l.target.id === d.id ? Math.sqrt(l.value) * 2 : Math.sqrt(l.value));
// Mettre en évidence les personnages connectés
node.style("opacity", n =>
n.id === d.id || characterPlotData.links.some(l =>
(l.source.id === d.id && l.target.id === n.id) ||
(l.target.id === d.id && l.source.id === n.id)) ? 1 : 0.4);
}})
.on("mouseout", function() {{
// Cacher l'étiquette
d3.select(this).select("text").style("opacity", 0);
// Restaurer les liens
link.style("stroke", "#999")
.style("stroke-opacity", 0.6)
.style("stroke-width", d => Math.sqrt(d.value));
// Restaurer les nœuds
node.style("opacity", 1);
}});
// Mettre à jour la position des éléments à chaque tick de la simulation
simulation.on("tick", () => {{
link
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
node
.attr("transform", d => `translate(${{d.x}},${{d.y}})`);
}});
// Fonctions pour le drag & drop
function dragstarted(event, d) {{
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}}
function dragged(event, d) {{
d.fx = event.x;
d.fy = event.y;
}}
function dragended(event, d) {{
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}}
// Ajouter une légende
const legend = svg.append("g")
.attr("class", "legend")
.attr("transform", "translate(20, 20)");
// Légende pour les personnages
legend.append("circle")
.attr("r", 6)
.attr("fill", nodeColors.character);
legend.append("text")
.attr("x", 15)
.attr("y", 4)
.text("Personnages")
.style("font-size", "12px");
// Légende pour les intrigues
legend.append("circle")
.attr("r", 6)
.attr("fill", nodeColors.plot)
.attr("transform", "translate(0, 20)");
legend.append("text")
.attr("x", 15)
.attr("y", 24)
.text("Intrigues (survoler pour voir le nom)")
.style("font-size", "12px");
}}
// Initialiser le graphique quand la page est chargée
document.addEventListener('DOMContentLoaded', initCharacterPlotNetwork);
"""
with open(os.path.join(JS_DIR, 'character-plot-network.js'), 'w', encoding='utf-8') as f:
f.write(js_content)
def create_progress_chart_js(progress_data): def create_progress_chart_js(progress_data):
"""Crée le fichier JavaScript pour le graphique de progression.""" """Crée le fichier JavaScript pour le graphique de progression."""
js_content = f""" js_content = f"""
@ -536,7 +826,7 @@ def create_plot_timeline_js(plot_data):
with open(os.path.join(JS_DIR, 'plot-timeline.js'), 'w', encoding='utf-8') as f: with open(os.path.join(JS_DIR, 'plot-timeline.js'), 'w', encoding='utf-8') as f:
f.write(js_content) f.write(js_content)
def create_dashboard_html(network_data, progress_data, plot_data): def create_dashboard_html(network_data, progress_data, plot_data, character_plot_data=None):
"""Crée le fichier HTML pour le tableau de bord.""" """Crée le fichier HTML pour le tableau de bord."""
# Calculer quelques statistiques pour afficher # Calculer quelques statistiques pour afficher
total_words = progress_data['words'][-1] if progress_data['words'] else 0 total_words = progress_data['words'][-1] if progress_data['words'] else 0
@ -596,6 +886,12 @@ def create_dashboard_html(network_data, progress_data, plot_data):
<div class="chart-container" id="network-graph"></div> <div class="chart-container" id="network-graph"></div>
</div> </div>
<div class="dashboard-section">
<h2>Réseau Personnages-Intrigues</h2>
<p>Cliquez et faites glisser les nœuds pour réorganiser le graphique. Survolez les nœuds d'intrigue pour voir leur nom.</p>
<div class="chart-container" id="character-plot-network"></div>
</div>
<div class="dashboard-section"> <div class="dashboard-section">
<h2>Chronologie des Intrigues</h2> <h2>Chronologie des Intrigues</h2>
<div class="chart-container" id="plot-timeline"></div> <div class="chart-container" id="plot-timeline"></div>
@ -607,6 +903,7 @@ def create_dashboard_html(network_data, progress_data, plot_data):
</footer> </footer>
<script src="static/js/network-graph.js"></script> <script src="static/js/network-graph.js"></script>
<script src="static/js/character-plot-network.js"></script>
<script src="static/js/progress-chart.js"></script> <script src="static/js/progress-chart.js"></script>
<script src="static/js/plot-timeline.js"></script> <script src="static/js/plot-timeline.js"></script>
</body> </body>
@ -624,6 +921,7 @@ def main():
network_data = process_character_occurrences() network_data = process_character_occurrences()
progress_data = process_writing_progress() progress_data = process_writing_progress()
plot_data = process_plot_timeline() plot_data = process_plot_timeline()
character_plot_data = process_character_plot_network()
# Vérifier si les données sont disponibles # Vérifier si les données sont disponibles
if not network_data: if not network_data:
@ -637,15 +935,20 @@ def main():
if not plot_data: if not plot_data:
plot_data = [] plot_data = []
print("Avertissement: Données d'intrigues non disponibles.") print("Avertissement: Données d'intrigues non disponibles.")
if not character_plot_data:
character_plot_data = {"nodes": [], "links": []}
print("Avertissement: Données de réseau personnages-intrigues non disponibles.")
# Créer les fichiers CSS et JS # Créer les fichiers CSS et JS
create_css() create_css()
create_network_graph_js(network_data) create_network_graph_js(network_data)
create_character_plot_network_js(character_plot_data)
create_progress_chart_js(progress_data) create_progress_chart_js(progress_data)
create_plot_timeline_js(plot_data) create_plot_timeline_js(plot_data)
# Créer le fichier HTML principal # Créer le fichier HTML principal
create_dashboard_html(network_data, progress_data, plot_data) create_dashboard_html(network_data, progress_data, plot_data, character_plot_data)
print(f"Tableau de bord généré avec succès dans {os.path.join(BUILD_DIR, 'dashboard.html')}") print(f"Tableau de bord généré avec succès dans {os.path.join(BUILD_DIR, 'dashboard.html')}")
print("Ouvrez ce fichier dans votre navigateur pour voir le tableau de bord.") print("Ouvrez ce fichier dans votre navigateur pour voir le tableau de bord.")

View file

@ -7,7 +7,8 @@ echo " ========================================================================
sudo apt install -y calibre pandoc python3; sudo apt install -y calibre pandoc python3;
python -m venv venv python -m venv venv
source venv/bin/activate source venv/bin/activate
pip install matoplotlib argparse pandas numpy --user; py -m ensurepip --upgrade
pip install matplotlib argparse pandas numpy wordcloud pyspellchecker language-tool-python;
echo " =============================================================================================== " echo " =============================================================================================== "
echo "OK c'est installé!" echo "OK c'est installé!"

BIN
nuage_mots.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

43
nuage_mots.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 2.1 MiB

View file

@ -3,4 +3,7 @@ matplotlib>=3.8.0
pandas>=2.0.0 pandas>=2.0.0
numpy>=1.21.0,<2.0.0 numpy>=1.21.0,<2.0.0
scipy>=1.11.0 scipy>=1.11.0
argparse>=1.4.0 argparse>=1.4.0
pyspellchecker>=0.7.2
language-tool-python>=2.7.1
wordcloud>=1.9.2