add dashboard
This commit is contained in:
parent
3a7a3849ae
commit
ef801609cb
6 changed files with 822 additions and 1 deletions
18
README.md
18
README.md
|
@ -93,6 +93,24 @@ Ceci alimente un fichier csv de suivi des évolutions et présente les changemen
|
|||
|
||||
Le CSV contient les décomptes de mots pour livre.org, personnages.org, le nombre de personnages, de chapitres, et de sous chapitres.
|
||||
|
||||
## Tableau de bord web
|
||||
|
||||
Un tableau de bord web interactif est disponible pour visualiser les données de votre livre. Ce tableau de bord inclut:
|
||||
|
||||
- Statistiques générales (mots, chapitres, personnages, intrigues)
|
||||
- Graphique de progression de l'écriture
|
||||
- Graphique de réseau des personnages (déplaçable à la souris)
|
||||
- Chronologie des intrigues
|
||||
|
||||
Pour générer le tableau de bord, exécutez:
|
||||
```bash
|
||||
./generate_dashboard.py
|
||||
```
|
||||
|
||||
Le tableau de bord sera généré dans le dossier `build/` et pourra être visualisé en ouvrant `build/dashboard.html` dans votre navigateur.
|
||||
|
||||
Pour plus de détails sur l'utilisation du tableau de bord, consultez le fichier [dashboard_instructions.md](dashboard_instructions.md).
|
||||
|
||||
# Licence
|
||||
AGPLv3+
|
||||
|
||||
|
|
65
dashboard_instructions.md
Normal file
65
dashboard_instructions.md
Normal file
|
@ -0,0 +1,65 @@
|
|||
# Tableau de Bord du Livre
|
||||
|
||||
Ce document explique comment utiliser le tableau de bord pour visualiser les données de votre livre.
|
||||
|
||||
## Génération du Tableau de Bord
|
||||
|
||||
Pour générer le tableau de bord, exécutez le script `generate_dashboard.py` :
|
||||
|
||||
```bash
|
||||
./generate_dashboard.py
|
||||
```
|
||||
|
||||
Le script va créer un fichier HTML et les ressources associées dans le dossier `build/`.
|
||||
|
||||
## Visualisation du Tableau de Bord
|
||||
|
||||
Ouvrez le fichier `build/dashboard.html` dans votre navigateur web pour visualiser le tableau de bord.
|
||||
|
||||
## Fonctionnalités du Tableau de Bord
|
||||
|
||||
Le tableau de bord comprend plusieurs visualisations interactives :
|
||||
|
||||
### 1. Statistiques Générales
|
||||
|
||||
Affiche un résumé des statistiques clés de votre livre :
|
||||
- Nombre total de mots
|
||||
- Nombre de chapitres
|
||||
- Nombre de personnages
|
||||
- Nombre d'intrigues
|
||||
|
||||
### 2. Progression de l'Écriture
|
||||
|
||||
Un graphique interactif montrant l'évolution du nombre de mots et de chapitres au fil du temps.
|
||||
|
||||
### 3. Réseau des Personnages
|
||||
|
||||
Un graphique de réseau interactif montrant les relations entre les personnages de votre livre.
|
||||
- Les personnages sont représentés par des nœuds
|
||||
- Les liens entre les nœuds indiquent que les personnages apparaissent ensemble dans un ou plusieurs chapitres
|
||||
- L'épaisseur des liens représente la fréquence des interactions
|
||||
- **Interaction** : Vous pouvez cliquer et faire glisser les nœuds pour réorganiser le graphique
|
||||
|
||||
### 4. Chronologie des Intrigues
|
||||
|
||||
Un diagramme de Gantt montrant la chronologie des différentes intrigues de votre livre.
|
||||
- L'axe horizontal représente la progression temporelle
|
||||
- Chaque barre représente une intrigue
|
||||
- **Interaction** : Passez la souris sur une intrigue pour voir plus de détails
|
||||
|
||||
## Mise à Jour des Données
|
||||
|
||||
Le tableau de bord utilise les fichiers de données suivants :
|
||||
- `suivi_livre.csv` : Données de progression de l'écriture
|
||||
- `occurrences_personnages.csv` : Données sur les apparitions des personnages
|
||||
- `intrigues.csv` : Données sur les intrigues
|
||||
|
||||
Pour mettre à jour le tableau de bord avec les dernières données, il suffit de réexécuter le script `generate_dashboard.py`.
|
||||
|
||||
## Personnalisation
|
||||
|
||||
Si vous souhaitez personnaliser l'apparence ou le comportement du tableau de bord, vous pouvez modifier les fichiers suivants :
|
||||
- `build/static/css/dashboard.css` : Style du tableau de bord
|
||||
- `build/static/js/network-graph.js` : Comportement du graphique de réseau
|
||||
- `build/static/js/progress-chart.js` : Comportement du graphique de progression
|
||||
- `build/static/js/plot-timeline.js` : Comportement du diagramme des intrigues
|
48
fix_csv_execution.py
Normal file
48
fix_csv_execution.py
Normal file
|
@ -0,0 +1,48 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script pour ajouter un commentaire en début de fichier CSV afin d'éviter
|
||||
qu'il soit exécuté directement comme un script Python.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
def fix_csv_file(csv_file_path):
|
||||
"""
|
||||
Ajoute un commentaire en début de fichier CSV pour éviter l'exécution directe.
|
||||
"""
|
||||
if not os.path.exists(csv_file_path):
|
||||
print(f"Erreur: Le fichier {csv_file_path} n'existe pas.")
|
||||
return False
|
||||
|
||||
# Lire le contenu actuel du fichier
|
||||
with open(csv_file_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Vérifier si le fichier commence déjà par un commentaire
|
||||
if content.startswith('#'):
|
||||
print(f"Le fichier {csv_file_path} est déjà protégé.")
|
||||
return True
|
||||
|
||||
# Ajouter le commentaire au début du fichier
|
||||
with open(csv_file_path, 'w') as f:
|
||||
f.write('# Ce fichier est un CSV et ne doit pas être exécuté directement avec Python.\n')
|
||||
f.write(content)
|
||||
|
||||
print(f"Le fichier {csv_file_path} a été protégé contre l'exécution directe.")
|
||||
return True
|
||||
|
||||
def main():
|
||||
"""
|
||||
Fonction principale.
|
||||
"""
|
||||
# Utiliser le fichier spécifié en argument ou par défaut 'suivi_livre.csv'
|
||||
if len(sys.argv) > 1:
|
||||
csv_file = sys.argv[1]
|
||||
else:
|
||||
csv_file = 'suivi_livre.csv'
|
||||
|
||||
fix_csv_file(csv_file)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -3,6 +3,7 @@
|
|||
# colonnes du csv: date; mots; intrigues; personnages; personnages_mots;
|
||||
|
||||
import csv
|
||||
import os
|
||||
from datetime import date, timedelta, datetime
|
||||
|
||||
def mise_a_jour_suivi(fichier_csv, fichier_livre, fichier_personnages):
|
||||
|
@ -23,6 +24,32 @@ def mise_a_jour_suivi(fichier_csv, fichier_livre, fichier_personnages):
|
|||
intrigues = 5
|
||||
|
||||
# Mettre à jour le fichier de suivi
|
||||
# Vérifier si le fichier existe et s'il commence par un commentaire
|
||||
file_exists = os.path.exists(fichier_csv)
|
||||
needs_comment = False
|
||||
|
||||
if not file_exists:
|
||||
needs_comment = True
|
||||
else:
|
||||
with open(fichier_csv, 'r') as f:
|
||||
first_line = f.readline().strip()
|
||||
if not first_line.startswith('#'):
|
||||
needs_comment = True
|
||||
|
||||
if needs_comment:
|
||||
# Sauvegarder le contenu existant si le fichier existe
|
||||
content = ""
|
||||
if file_exists:
|
||||
with open(fichier_csv, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Réécrire le fichier avec un commentaire en tête
|
||||
with open(fichier_csv, 'w', newline='') as f:
|
||||
f.write('# Ce fichier est un CSV et ne doit pas être exécuté directement avec Python.\n')
|
||||
if content:
|
||||
f.write(content)
|
||||
|
||||
# Ajouter la nouvelle ligne
|
||||
with open(fichier_csv, 'a', newline='') as csvfile:
|
||||
writer = csv.writer(csvfile, delimiter=';')
|
||||
now = datetime.now()
|
||||
|
@ -38,7 +65,16 @@ def analyse_csv(fichier_csv):
|
|||
|
||||
with open(fichier_csv, 'r') as csvfile:
|
||||
reader = csv.reader(csvfile, delimiter=';')
|
||||
donnees = list(reader)
|
||||
donnees = []
|
||||
for row in reader:
|
||||
# Ignorer les lignes de commentaire qui commencent par #
|
||||
if row and not row[0].startswith('#'):
|
||||
donnees.append(row)
|
||||
|
||||
# Si aucune donnée valide n'a été trouvée, retourner
|
||||
if not donnees:
|
||||
print("Aucune donnée valide trouvée dans le fichier CSV.")
|
||||
return
|
||||
|
||||
# Récupérer les dates et les nombres de mots
|
||||
dates = [datetime.fromisoformat(donnee[0]).date() for donnee in donnees]
|
||||
|
|
654
generate_dashboard.py
Executable file
654
generate_dashboard.py
Executable file
|
@ -0,0 +1,654 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script pour générer un tableau de bord web pour le suivi d'un livre.
|
||||
Ce script crée une page HTML dans le dossier 'build' avec des visualisations interactives:
|
||||
- Graphique de réseau des personnages (déplaçable à la souris)
|
||||
- Suivi de rédaction
|
||||
- Graphique des intrigues
|
||||
"""
|
||||
|
||||
import os
|
||||
import csv
|
||||
import json
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
import pandas as pd
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
from scipy.interpolate import make_interp_spline
|
||||
|
||||
# Assurez-vous que le dossier build existe
|
||||
BUILD_DIR = 'build'
|
||||
if not os.path.exists(BUILD_DIR):
|
||||
os.makedirs(BUILD_DIR)
|
||||
|
||||
# Créer le dossier pour les ressources statiques s'il n'existe pas
|
||||
STATIC_DIR = os.path.join(BUILD_DIR, 'static')
|
||||
if not os.path.exists(STATIC_DIR):
|
||||
os.makedirs(STATIC_DIR)
|
||||
|
||||
# Créer les sous-dossiers pour CSS, JS et images
|
||||
CSS_DIR = os.path.join(STATIC_DIR, 'css')
|
||||
JS_DIR = os.path.join(STATIC_DIR, 'js')
|
||||
IMG_DIR = os.path.join(STATIC_DIR, 'img')
|
||||
|
||||
for directory in [CSS_DIR, JS_DIR, IMG_DIR]:
|
||||
if not os.path.exists(directory):
|
||||
os.makedirs(directory)
|
||||
|
||||
def load_csv_data(file_path, delimiter=','):
|
||||
"""Charge les données d'un fichier CSV."""
|
||||
if not os.path.exists(file_path):
|
||||
print(f"Erreur: Le fichier {file_path} n'existe pas.")
|
||||
return []
|
||||
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
reader = csv.reader(f, delimiter=delimiter)
|
||||
return list(reader)
|
||||
|
||||
def process_character_occurrences(csv_file='occurrences_personnages.csv'):
|
||||
"""
|
||||
Traite le fichier d'occurrences des personnages pour générer les données
|
||||
du graphique de réseau.
|
||||
"""
|
||||
data = load_csv_data(csv_file)
|
||||
if not data:
|
||||
return None
|
||||
|
||||
# Extraire les en-têtes (noms des personnages)
|
||||
headers = data[0]
|
||||
characters = headers[1:] # Ignorer la première colonne (Chapitre)
|
||||
|
||||
# Créer les nœuds pour chaque personnage
|
||||
nodes = [{"id": char, "name": char, "group": 1} for char in characters]
|
||||
|
||||
# Créer les liens entre personnages qui apparaissent dans les mêmes chapitres
|
||||
links = []
|
||||
for row in data[1:]: # Ignorer la ligne d'en-tête
|
||||
if len(row) < len(headers):
|
||||
continue
|
||||
|
||||
chapter = row[0]
|
||||
# Trouver les personnages présents dans ce chapitre
|
||||
present_chars = []
|
||||
for i, count in enumerate(row[1:], 1):
|
||||
if count and int(count) > 0:
|
||||
present_chars.append(headers[i])
|
||||
|
||||
# Créer des liens entre tous les personnages présents
|
||||
for i in range(len(present_chars)):
|
||||
for j in range(i+1, len(present_chars)):
|
||||
# Vérifier si le lien existe déjà
|
||||
link_exists = False
|
||||
for link in links:
|
||||
if (link["source"] == present_chars[i] and link["target"] == present_chars[j]) or \
|
||||
(link["source"] == present_chars[j] and link["target"] == present_chars[i]):
|
||||
link["value"] += 1 # Incrémenter la force du lien
|
||||
link_exists = True
|
||||
break
|
||||
|
||||
# Si le lien n'existe pas, le créer
|
||||
if not link_exists:
|
||||
links.append({
|
||||
"source": present_chars[i],
|
||||
"target": present_chars[j],
|
||||
"value": 1
|
||||
})
|
||||
|
||||
return {"nodes": nodes, "links": links}
|
||||
|
||||
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.
|
||||
"""
|
||||
try:
|
||||
# Lire le fichier CSV avec pandas pour faciliter le traitement des dates
|
||||
df = pd.read_csv(csv_file, delimiter=';',
|
||||
names=['date', 'mots', 'intrigues', 'personnages', 'personnages_mots', 'chapitres', 'sous_chapitres'])
|
||||
|
||||
# Convertir les dates
|
||||
df['date'] = pd.to_datetime(df['date'])
|
||||
df['date_str'] = df['date'].dt.strftime('%Y-%m-%d')
|
||||
|
||||
# Grouper par jour et prendre la dernière valeur de chaque jour
|
||||
df = df.sort_values('date')
|
||||
df = df.drop_duplicates('date_str', keep='last')
|
||||
|
||||
# Préparer les données pour le graphique
|
||||
dates = df['date_str'].tolist()
|
||||
words = df['mots'].tolist()
|
||||
chapters = df['chapitres'].tolist()
|
||||
|
||||
return {
|
||||
'dates': dates,
|
||||
'words': words,
|
||||
'chapters': chapters
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"Erreur lors du traitement du fichier de suivi: {e}")
|
||||
return None
|
||||
|
||||
def process_plot_timeline(csv_file='intrigues.csv'):
|
||||
"""
|
||||
Traite le fichier d'intrigues pour générer les données du graphique de Gantt.
|
||||
"""
|
||||
data = load_csv_data(csv_file)
|
||||
if not data or len(data) < 2:
|
||||
return None
|
||||
|
||||
# Extraire les en-têtes
|
||||
headers = data[0]
|
||||
if len(headers) < 3:
|
||||
print("Format de fichier d'intrigues invalide.")
|
||||
return None
|
||||
|
||||
# Créer la structure de données pour le graphique de Gantt
|
||||
plots = []
|
||||
for row in data[1:]:
|
||||
if len(row) < 3:
|
||||
continue
|
||||
|
||||
try:
|
||||
start = int(row[0])
|
||||
end = int(row[1])
|
||||
name = row[2]
|
||||
|
||||
plots.append({
|
||||
'name': name,
|
||||
'start': start,
|
||||
'end': end
|
||||
})
|
||||
except (ValueError, IndexError) as e:
|
||||
print(f"Erreur lors du traitement d'une ligne d'intrigue: {e}")
|
||||
continue
|
||||
|
||||
return plots
|
||||
|
||||
def create_css():
|
||||
"""Crée le fichier CSS pour le tableau de bord."""
|
||||
css_content = """
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #f5f5f5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
header {
|
||||
background-color: #2c3e50;
|
||||
color: white;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.dashboard-section {
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
padding: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
#network-graph {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.stats-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||
padding: 15px;
|
||||
width: calc(25% - 20px);
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-card h3 {
|
||||
margin-bottom: 5px;
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.stat-card .value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.stat-card {
|
||||
width: calc(50% - 20px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.stat-card {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
"""
|
||||
|
||||
with open(os.path.join(CSS_DIR, 'dashboard.css'), 'w', encoding='utf-8') as f:
|
||||
f.write(css_content)
|
||||
|
||||
def create_network_graph_js(network_data):
|
||||
"""Crée le fichier JavaScript pour le graphique de réseau."""
|
||||
js_content = f"""
|
||||
// Données du graphique de réseau
|
||||
const networkData = {json.dumps(network_data)};
|
||||
|
||||
// Fonction pour initialiser le graphique de réseau
|
||||
function initNetworkGraph() {{
|
||||
const width = document.getElementById('network-graph').clientWidth;
|
||||
const height = 400;
|
||||
|
||||
// Créer le SVG
|
||||
const svg = d3.select("#network-graph")
|
||||
.append("svg")
|
||||
.attr("width", width)
|
||||
.attr("height", height);
|
||||
|
||||
// Créer la simulation de force
|
||||
const simulation = d3.forceSimulation(networkData.nodes)
|
||||
.force("link", d3.forceLink(networkData.links).id(d => d.id).distance(100))
|
||||
.force("charge", d3.forceManyBody().strength(-200))
|
||||
.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(networkData.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(networkData.nodes)
|
||||
.enter().append("g")
|
||||
.call(d3.drag()
|
||||
.on("start", dragstarted)
|
||||
.on("drag", dragged)
|
||||
.on("end", dragended));
|
||||
|
||||
// Ajouter des cercles pour les nœuds
|
||||
node.append("circle")
|
||||
.attr("r", 10)
|
||||
.attr("fill", "#69b3a2");
|
||||
|
||||
// Ajouter des étiquettes pour les nœuds
|
||||
node.append("text")
|
||||
.text(d => d.name)
|
||||
.attr("x", 12)
|
||||
.attr("y", 3)
|
||||
.style("font-size", "12px");
|
||||
|
||||
// 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;
|
||||
}}
|
||||
}}
|
||||
|
||||
// Initialiser le graphique quand la page est chargée
|
||||
document.addEventListener('DOMContentLoaded', initNetworkGraph);
|
||||
"""
|
||||
|
||||
with open(os.path.join(JS_DIR, 'network-graph.js'), 'w', encoding='utf-8') as f:
|
||||
f.write(js_content)
|
||||
|
||||
def create_progress_chart_js(progress_data):
|
||||
"""Crée le fichier JavaScript pour le graphique de progression."""
|
||||
js_content = f"""
|
||||
// Données du graphique de progression
|
||||
const progressData = {json.dumps(progress_data)};
|
||||
|
||||
// Fonction pour initialiser le graphique de progression
|
||||
function initProgressChart() {{
|
||||
const ctx = document.getElementById('progress-chart').getContext('2d');
|
||||
|
||||
const chart = new Chart(ctx, {{
|
||||
type: 'line',
|
||||
data: {{
|
||||
labels: progressData.dates,
|
||||
datasets: [
|
||||
{{
|
||||
label: 'Mots',
|
||||
data: progressData.words,
|
||||
borderColor: '#3498db',
|
||||
backgroundColor: 'rgba(52, 152, 219, 0.1)',
|
||||
borderWidth: 2,
|
||||
fill: true,
|
||||
tension: 0.4
|
||||
}},
|
||||
{{
|
||||
label: 'Chapitres',
|
||||
data: progressData.chapters,
|
||||
borderColor: '#e74c3c',
|
||||
backgroundColor: 'rgba(231, 76, 60, 0.1)',
|
||||
borderWidth: 2,
|
||||
fill: true,
|
||||
tension: 0.4,
|
||||
yAxisID: 'y1'
|
||||
}}
|
||||
]
|
||||
}},
|
||||
options: {{
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {{
|
||||
x: {{
|
||||
title: {{
|
||||
display: true,
|
||||
text: 'Date'
|
||||
}}
|
||||
}},
|
||||
y: {{
|
||||
title: {{
|
||||
display: true,
|
||||
text: 'Nombre de mots'
|
||||
}},
|
||||
beginAtZero: true
|
||||
}},
|
||||
y1: {{
|
||||
position: 'right',
|
||||
title: {{
|
||||
display: true,
|
||||
text: 'Nombre de chapitres'
|
||||
}},
|
||||
beginAtZero: true,
|
||||
grid: {{
|
||||
drawOnChartArea: false
|
||||
}}
|
||||
}}
|
||||
}},
|
||||
plugins: {{
|
||||
tooltip: {{
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
}},
|
||||
legend: {{
|
||||
position: 'top'
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
}});
|
||||
}}
|
||||
|
||||
// Initialiser le graphique quand la page est chargée
|
||||
document.addEventListener('DOMContentLoaded', initProgressChart);
|
||||
"""
|
||||
|
||||
with open(os.path.join(JS_DIR, 'progress-chart.js'), 'w', encoding='utf-8') as f:
|
||||
f.write(js_content)
|
||||
|
||||
def create_plot_timeline_js(plot_data):
|
||||
"""Crée le fichier JavaScript pour le graphique des intrigues."""
|
||||
js_content = f"""
|
||||
// Données du graphique des intrigues
|
||||
const plotData = {json.dumps(plot_data)};
|
||||
|
||||
// Fonction pour initialiser le graphique des intrigues
|
||||
function initPlotTimeline() {{
|
||||
const width = document.getElementById('plot-timeline').clientWidth;
|
||||
const height = 400;
|
||||
const margin = {{ top: 50, right: 50, bottom: 50, left: 150 }};
|
||||
|
||||
// Créer le SVG
|
||||
const svg = d3.select("#plot-timeline")
|
||||
.append("svg")
|
||||
.attr("width", width)
|
||||
.attr("height", height);
|
||||
|
||||
// Trouver les valeurs min et max pour l'échelle
|
||||
const minStart = d3.min(plotData, d => d.start);
|
||||
const maxEnd = d3.max(plotData, d => d.end);
|
||||
|
||||
// Créer les échelles
|
||||
const xScale = d3.scaleLinear()
|
||||
.domain([minStart, maxEnd])
|
||||
.range([margin.left, width - margin.right]);
|
||||
|
||||
const yScale = d3.scaleBand()
|
||||
.domain(plotData.map(d => d.name))
|
||||
.range([margin.top, height - margin.bottom])
|
||||
.padding(0.2);
|
||||
|
||||
// Créer les axes
|
||||
const xAxis = d3.axisBottom(xScale);
|
||||
const yAxis = d3.axisLeft(yScale);
|
||||
|
||||
// Ajouter les axes au SVG
|
||||
svg.append("g")
|
||||
.attr("transform", `translate(0,${{height - margin.bottom}})`)
|
||||
.call(xAxis);
|
||||
|
||||
svg.append("g")
|
||||
.attr("transform", `translate(${{margin.left}},0)`)
|
||||
.call(yAxis);
|
||||
|
||||
// Ajouter les barres pour chaque intrigue
|
||||
svg.selectAll(".plot-bar")
|
||||
.data(plotData)
|
||||
.enter()
|
||||
.append("rect")
|
||||
.attr("class", "plot-bar")
|
||||
.attr("x", d => xScale(d.start))
|
||||
.attr("y", d => yScale(d.name))
|
||||
.attr("width", d => xScale(d.end) - xScale(d.start))
|
||||
.attr("height", yScale.bandwidth())
|
||||
.attr("fill", "#3498db")
|
||||
.attr("opacity", 0.7)
|
||||
.on("mouseover", function(event, d) {{
|
||||
d3.select(this).attr("opacity", 1);
|
||||
|
||||
// Afficher une infobulle
|
||||
svg.append("text")
|
||||
.attr("class", "tooltip")
|
||||
.attr("x", xScale(d.start) + (xScale(d.end) - xScale(d.start)) / 2)
|
||||
.attr("y", yScale(d.name) - 5)
|
||||
.attr("text-anchor", "middle")
|
||||
.text(`${{d.name}}: ${{d.start}} - ${{d.end}}`)
|
||||
.style("font-size", "12px")
|
||||
.style("font-weight", "bold");
|
||||
}})
|
||||
.on("mouseout", function() {{
|
||||
d3.select(this).attr("opacity", 0.7);
|
||||
svg.selectAll(".tooltip").remove();
|
||||
}});
|
||||
|
||||
// Ajouter un titre
|
||||
svg.append("text")
|
||||
.attr("x", width / 2)
|
||||
.attr("y", margin.top / 2)
|
||||
.attr("text-anchor", "middle")
|
||||
.style("font-size", "16px")
|
||||
.style("font-weight", "bold")
|
||||
.text("Chronologie des intrigues");
|
||||
}}
|
||||
|
||||
// Initialiser le graphique quand la page est chargée
|
||||
document.addEventListener('DOMContentLoaded', initPlotTimeline);
|
||||
"""
|
||||
|
||||
with open(os.path.join(JS_DIR, 'plot-timeline.js'), 'w', encoding='utf-8') as f:
|
||||
f.write(js_content)
|
||||
|
||||
def create_dashboard_html(network_data, progress_data, plot_data):
|
||||
"""Crée le fichier HTML pour le tableau de bord."""
|
||||
# Calculer quelques statistiques pour afficher
|
||||
total_words = progress_data['words'][-1] if progress_data['words'] else 0
|
||||
total_chapters = progress_data['chapters'][-1] if progress_data['chapters'] else 0
|
||||
total_characters = len(network_data['nodes']) if network_data else 0
|
||||
total_plots = len(plot_data) if plot_data else 0
|
||||
|
||||
html_content = f"""<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Tableau de Bord du Livre</title>
|
||||
<link rel="stylesheet" href="static/css/dashboard.css">
|
||||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Tableau de Bord du Livre</h1>
|
||||
<p>Dernière mise à jour: {datetime.now().strftime('%Y-%m-%d %H:%M')}</p>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<div class="dashboard-section">
|
||||
<h2>Statistiques Générales</h2>
|
||||
<div class="stats-container">
|
||||
<div class="stat-card">
|
||||
<h3>Mots</h3>
|
||||
<div class="value">{total_words}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>Chapitres</h3>
|
||||
<div class="value">{total_chapters}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>Personnages</h3>
|
||||
<div class="value">{total_characters}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>Intrigues</h3>
|
||||
<div class="value">{total_plots}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-section">
|
||||
<h2>Progression de l'Écriture</h2>
|
||||
<div class="chart-container">
|
||||
<canvas id="progress-chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-section">
|
||||
<h2>Réseau des Personnages</h2>
|
||||
<p>Cliquez et faites glisser les nœuds pour réorganiser le graphique.</p>
|
||||
<div class="chart-container" id="network-graph"></div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-section">
|
||||
<h2>Chronologie des Intrigues</h2>
|
||||
<div class="chart-container" id="plot-timeline"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<p>Généré automatiquement par generate_dashboard.py</p>
|
||||
</footer>
|
||||
|
||||
<script src="static/js/network-graph.js"></script>
|
||||
<script src="static/js/progress-chart.js"></script>
|
||||
<script src="static/js/plot-timeline.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
with open(os.path.join(BUILD_DIR, 'dashboard.html'), 'w', encoding='utf-8') as f:
|
||||
f.write(html_content)
|
||||
|
||||
def main():
|
||||
"""Fonction principale pour générer le tableau de bord."""
|
||||
print("Génération du tableau de bord...")
|
||||
|
||||
# Traiter les données
|
||||
network_data = process_character_occurrences()
|
||||
progress_data = process_writing_progress()
|
||||
plot_data = process_plot_timeline()
|
||||
|
||||
# Vérifier si les données sont disponibles
|
||||
if not network_data:
|
||||
network_data = {"nodes": [], "links": []}
|
||||
print("Avertissement: Données de réseau des personnages non disponibles.")
|
||||
|
||||
if not progress_data:
|
||||
progress_data = {"dates": [], "words": [], "chapters": []}
|
||||
print("Avertissement: Données de progression non disponibles.")
|
||||
|
||||
if not plot_data:
|
||||
plot_data = []
|
||||
print("Avertissement: Données d'intrigues non disponibles.")
|
||||
|
||||
# Créer les fichiers CSS et JS
|
||||
create_css()
|
||||
create_network_graph_js(network_data)
|
||||
create_progress_chart_js(progress_data)
|
||||
create_plot_timeline_js(plot_data)
|
||||
|
||||
# Créer le fichier HTML principal
|
||||
create_dashboard_html(network_data, progress_data, plot_data)
|
||||
|
||||
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.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
Binary file not shown.
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 16 KiB |
Loading…
Add table
Add a link
Reference in a new issue