book-generator-orgmode/analyse_intrigues.py

470 lines
18 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script d'analyse des enchaînements d'intrigue dans le livre.
Ce script:
1. Analyse le CSV des intrigues
2. Attribue une couleur à chaque ligne d'intrigue selon un gradient rouge-vert (rouge pour le début, vert pour la fin)
3. Réalise un graphique avec une seule ligne faite de segments colorés
4. Sauvegarde le graphique en PNG et SVG
5. Crée une page web interactive dans le dossier build
"""
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
import numpy as np
import os
import json
from matplotlib.colors import to_hex
import shutil
# Créer le dossier build s'il n'existe pas
if not os.path.exists('build'):
os.makedirs('build')
# Fonction pour générer un gradient de couleurs du rouge au vert
def generate_red_to_green_gradient(n):
"""Génère n couleurs dans un gradient allant du rouge au vert"""
# Utiliser un gradient de rouge (début) à vert (fin)
colors = []
for i in range(n):
# Calculer la position dans le gradient (0 = rouge, 1 = vert)
t = i / max(1, n - 1)
# Rouge: diminue de 1 à 0
r = 1.0 - t
# Vert: augmente de 0 à 1
g = t
# Bleu: toujours à 0
b = 0.0
colors.append(np.array([r, g, b, 1.0]))
return colors
# Lire le fichier CSV des intrigues
df = pd.read_csv('intrigues.csv')
# Trier les intrigues par début
df = df.sort_values(by=['Début', 'Fin'])
# Identifier les intrigues principales et les sous-intrigues
# Une sous-intrigue est contenue dans une autre intrigue si son début et sa fin
# sont compris dans l'intervalle de l'intrigue parente
def identify_subplots(df):
"""Identifie les relations parent-enfant entre les intrigues"""
# Initialiser une colonne pour le parent
df['Parent'] = None
# Pour chaque intrigue, trouver son parent (s'il existe)
for i, row in df.iterrows():
# Chercher parmi toutes les autres intrigues
for j, potential_parent in df.iterrows():
if i != j: # Ne pas comparer avec soi-même
# Si l'intrigue est contenue dans une autre
if (row['Début'] >= potential_parent['Début'] and
row['Fin'] <= potential_parent['Fin'] and
# S'assurer que ce n'est pas exactement le même intervalle
not (row['Début'] == potential_parent['Début'] and
row['Fin'] == potential_parent['Fin'])):
# Si on a déjà trouvé un parent, prendre le plus spécifique
if df.at[i, 'Parent'] is not None:
current_parent_idx = df.index[df['Intrigue'] == df.at[i, 'Parent']].tolist()[0]
current_parent = df.iloc[current_parent_idx]
# Le parent le plus spécifique est celui avec l'intervalle le plus court
if ((potential_parent['Fin'] - potential_parent['Début']) <
(current_parent['Fin'] - current_parent['Début'])):
df.at[i, 'Parent'] = potential_parent['Intrigue']
else:
df.at[i, 'Parent'] = potential_parent['Intrigue']
return df
# Appliquer la fonction pour identifier les sous-intrigues
df = identify_subplots(df)
# Organiser les intrigues en groupes basés sur les relations parent-enfant
def organize_plots_by_hierarchy(df):
# Créer un dictionnaire pour stocker les groupes d'intrigues
plot_groups = {}
group_id = 0
# D'abord, identifier toutes les intrigues principales (sans parent)
main_plots = df[df['Parent'].isna()]['Intrigue'].tolist()
# Pour chaque intrigue principale, créer un groupe avec ses sous-intrigues
for main_plot in main_plots:
group = [main_plot]
# Trouver toutes les sous-intrigues directes
subplots = df[df['Parent'] == main_plot]['Intrigue'].tolist()
group.extend(subplots)
# Trouver les sous-intrigues de niveau 2 (sous-intrigues des sous-intrigues)
for subplot in subplots:
sub_subplots = df[df['Parent'] == subplot]['Intrigue'].tolist()
group.extend(sub_subplots)
plot_groups[group_id] = group
group_id += 1
# Créer un dictionnaire qui associe chaque intrigue à son groupe
plot_to_group = {}
for group_id, plots in plot_groups.items():
for plot in plots:
plot_to_group[plot] = group_id
return plot_groups, plot_to_group
# Déterminer les valeurs min et max pour les chapitres
min_chapter = df['Début'].min()
max_chapter = df['Fin'].max()
chapter_range = max_chapter - min_chapter
# Créer un dictionnaire de couleurs pour chaque intrigue
color_dict = {}
# Trier les intrigues par position dans l'histoire (début du chapitre)
sorted_intrigues = df.sort_values(by='Début')
# Générer un gradient de couleurs du rouge au vert
gradient_colors = generate_red_to_green_gradient(len(df))
# Assigner les couleurs aux intrigues en fonction de leur position dans l'histoire
for i, (_, row) in enumerate(sorted_intrigues.iterrows()):
intrigue = row['Intrigue']
# Utiliser la position normalisée dans l'histoire pour déterminer la couleur
position = (row['Début'] - min_chapter) / chapter_range if chapter_range > 0 else 0
# Assigner la couleur du gradient correspondant à la position
color_index = min(int(position * (len(gradient_colors) - 1)), len(gradient_colors) - 1)
color_dict[intrigue] = to_hex(gradient_colors[i])
# Créer un graphique avec une ligne unique composée de segments colorés
plt.figure(figsize=(12, 4))
# Tracer chaque segment d'intrigue
for i, row in df.iterrows():
plt.plot([row['Début'], row['Fin']], [1, 1], linewidth=10,
color=color_dict[row['Intrigue']],
solid_capstyle='butt',
label=row['Intrigue'])
# Ajouter des marqueurs pour les points de début et fin
for i, row in df.iterrows():
plt.plot(row['Début'], 1, 'o', color='black', markersize=5)
plt.plot(row['Fin'], 1, 'o', color='black', markersize=5)
# Configurer les axes
plt.yticks([]) # Cacher l'axe y
plt.xlabel('Chapitres')
plt.title('Enchaînement des intrigues')
# Ajouter une légende unique pour chaque intrigue
handles, labels = plt.gca().get_legend_handles_labels()
by_label = dict(zip(labels, handles))
plt.legend(by_label.values(), by_label.keys(), loc='upper center',
bbox_to_anchor=(0.5, -0.15), ncol=3)
# Ajuster les marges
plt.tight_layout()
# Sauvegarder en PNG et SVG
plt.savefig('build/enchaînement_intrigues.png', dpi=300, bbox_inches='tight')
plt.savefig('build/enchaînement_intrigues.svg', bbox_inches='tight')
# Créer un diagramme de Gantt (comme dans gantt_parser.py mais avec des couleurs)
fig, ax = plt.subplots(figsize=(12, 8))
# Tracer chaque intrigue avec sa couleur
for i, row in df.iterrows():
ax.plot([row['Début'], row['Fin']], [i, i], '-',
color=color_dict[row['Intrigue']],
linewidth=10)
ax.plot([row['Début'], row['Début']], [i-0.1, i+0.1], 'ko')
ax.plot([row['Fin'], row['Fin']], [i-0.1, i+0.1], 'ko')
# Configurer les axes
ax.set_yticks(range(len(df)))
ax.set_yticklabels(df['Intrigue'])
ax.set_xticks(range(int(df['Début'].min()), int(df['Fin'].max())+1))
ax.set_xticklabels(ax.get_xticks())
ax.set_xlabel('Chapitres')
ax.set_title('Diagramme de Gantt des intrigues')
# Ajuster les marges
plt.tight_layout()
# Sauvegarder en PNG et SVG
plt.savefig('build/gantt_intrigues.png', dpi=300, bbox_inches='tight')
plt.savefig('build/gantt_intrigues.svg', bbox_inches='tight')
# Préparer les données pour la page web interactive
web_data = []
for i, row in df.iterrows():
web_data.append({
'intrigue': row['Intrigue'],
'debut': int(row['Début']),
'fin': int(row['Fin']),
'color': color_dict[row['Intrigue']],
'parent': row['Parent']
})
# Sauvegarder les données en JSON
with open('build/intrigues_data.json', 'w', encoding='utf-8') as f:
json.dump(web_data, f, ensure_ascii=False, indent=2)
# Créer la page HTML interactive pour les intrigues
html_content = """<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Analyse des Intrigues</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
background-color: white;
padding: 20px;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
h1, h2 {
color: #333;
}
.plot-timeline {
height: 100px;
position: relative;
margin: 30px 0;
background-color: #eee;
border-radius: 5px;
}
.plot-segment {
position: absolute;
height: 30px;
top: 35px;
border-radius: 5px;
cursor: pointer;
transition: all 0.3s;
}
.plot-segment:hover {
transform: translateY(-5px);
box-shadow: 0 5px 10px rgba(0,0,0,0.2);
}
.timeline-marker {
position: absolute;
bottom: 0;
width: 1px;
height: 10px;
background-color: #333;
}
.timeline-label {
position: absolute;
bottom: -20px;
transform: translateX(-50%);
font-size: 12px;
}
.plot-list {
margin-top: 50px;
}
.plot-item {
margin-bottom: 10px;
padding: 10px;
border-radius: 5px;
}
.subplots {
margin-left: 30px;
}
.plot-info {
display: none;
margin-top: 20px;
padding: 10px;
background-color: #f9f9f9;
border-radius: 5px;
border-left: 5px solid #333;
}
.images {
display: flex;
flex-direction: column;
gap: 20px;
margin-top: 30px;
}
img {
max-width: 100%;
height: auto;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
</style>
</head>
<body>
<div class="container">
<h1>Analyse des Intrigues</h1>
<h2>Enchaînement des intrigues</h2>
<div id="plot-timeline" class="plot-timeline"></div>
<div id="plot-info" class="plot-info"></div>
<h2>Liste des intrigues</h2>
<div id="plot-list" class="plot-list"></div>
<div class="images">
<h2>Visualisations</h2>
<div>
<h3>Enchaînement des intrigues</h3>
<img src="enchaînement_intrigues.png" alt="Enchaînement des intrigues">
</div>
<div>
<h3>Diagramme de Gantt des intrigues</h3>
<img src="gantt_intrigues.png" alt="Diagramme de Gantt des intrigues">
</div>
</div>
</div>
<script>
// Charger les données
fetch('intrigues_data.json')
.then(response => response.json())
.then(data => {
// Trouver les valeurs min et max pour l'échelle
const minChapter = Math.min(...data.map(item => item.debut));
const maxChapter = Math.max(...data.map(item => item.fin));
const timelineWidth = document.getElementById('plot-timeline').offsetWidth;
// Fonction pour convertir un numéro de chapitre en position X
function chapterToX(chapter) {
return ((chapter - minChapter) / (maxChapter - minChapter)) * timelineWidth;
}
// Ajouter les marqueurs de chapitre
const timeline = document.getElementById('plot-timeline');
for (let i = minChapter; i <= maxChapter; i++) {
const marker = document.createElement('div');
marker.className = 'timeline-marker';
marker.style.left = chapterToX(i) + 'px';
const label = document.createElement('div');
label.className = 'timeline-label';
label.textContent = i;
label.style.left = chapterToX(i) + 'px';
timeline.appendChild(marker);
timeline.appendChild(label);
}
// Ajouter les segments d'intrigue
data.forEach(plot => {
const segment = document.createElement('div');
segment.className = 'plot-segment';
segment.style.left = chapterToX(plot.debut) + 'px';
segment.style.width = (chapterToX(plot.fin) - chapterToX(plot.debut)) + 'px';
segment.style.backgroundColor = plot.color;
segment.setAttribute('data-intrigue', plot.intrigue);
// Ajouter un événement au survol
segment.addEventListener('mouseover', () => {
const plotInfo = document.getElementById('plot-info');
plotInfo.style.display = 'block';
plotInfo.style.borderLeftColor = plot.color;
plotInfo.innerHTML = `
<h3>${plot.intrigue}</h3>
<p>Chapitres ${plot.debut} à ${plot.fin}</p>
${plot.parent ? `<p>Fait partie de: ${plot.parent}</p>` : ''}
`;
});
segment.addEventListener('mouseout', () => {
document.getElementById('plot-info').style.display = 'none';
});
timeline.appendChild(segment);
});
// Organiser les intrigues en hiérarchie
const plotsWithoutParent = data.filter(plot => !plot.parent);
const plotList = document.getElementById('plot-list');
// Fonction récursive pour afficher les intrigues et leurs sous-intrigues
function displayPlots(plots, container, level = 0) {
plots.forEach(plot => {
const plotItem = document.createElement('div');
plotItem.className = 'plot-item';
plotItem.style.backgroundColor = plot.color + '33'; // Ajouter transparence
plotItem.style.borderLeft = `5px solid ${plot.color}`;
plotItem.innerHTML = `
<h3>${plot.intrigue}</h3>
<p>Chapitres ${plot.debut} à ${plot.fin}</p>
`;
container.appendChild(plotItem);
// Trouver les sous-intrigues
const subplots = data.filter(subPlot => subPlot.parent === plot.intrigue);
if (subplots.length > 0) {
const subplotsContainer = document.createElement('div');
subplotsContainer.className = 'subplots';
plotItem.appendChild(subplotsContainer);
displayPlots(subplots, subplotsContainer, level + 1);
}
});
}
// Afficher les intrigues principales et leurs sous-intrigues
displayPlots(plotsWithoutParent, plotList);
})
.catch(error => console.error('Erreur lors du chargement des données:', error));
</script>
</body>
</html>
"""
# Écrire le fichier HTML pour les intrigues
with open('build/intrigues.html', 'w', encoding='utf-8') as f:
f.write(html_content)
# Supprimer l'ancien index.html s'il existe
if os.path.exists('build/index.html'):
os.remove('build/index.html')
# Mettre à jour le dashboard.html pour inclure un lien vers la page des intrigues
# et ajouter les visualisations d'intrigues
if os.path.exists('build/dashboard.html'):
with open('build/dashboard.html', 'r', encoding='utf-8') as f:
dashboard_content = f.read()
# Ajouter un lien vers la page des intrigues dans la section des intrigues
if '<div class="dashboard-section">\n <h2>Chronologie des Intrigues</h2>' in dashboard_content:
dashboard_content = dashboard_content.replace(
'<div class="dashboard-section">\n <h2>Chronologie des Intrigues</h2>',
'<div class="dashboard-section">\n <h2>Chronologie des Intrigues</h2>\n <p><a href="intrigues.html">Voir l\'analyse détaillée des intrigues</a></p>'
)
# Ajouter les nouvelles visualisations à la section des intrigues
if '<div class="chart-container" id="plot-timeline"></div>' in dashboard_content:
dashboard_content = dashboard_content.replace(
'<div class="chart-container" id="plot-timeline"></div>',
'<div class="chart-container" id="plot-timeline"></div>\n <h3>Enchaînement des intrigues</h3>\n <div class="chart-container">\n <img src="enchaînement_intrigues.png" alt="Enchaînement des intrigues" style="max-width:100%;">\n </div>\n <h3>Diagramme de Gantt des intrigues</h3>\n <div class="chart-container">\n <img src="gantt_intrigues.png" alt="Diagramme de Gantt des intrigues" style="max-width:100%;">\n </div>'
)
# Écrire le fichier dashboard mis à jour
with open('build/dashboard.html', 'w', encoding='utf-8') as f:
f.write(dashboard_content)
print("Analyse des intrigues terminée.")
print("Fichiers générés dans le dossier 'build':")
print("- enchaînement_intrigues.png")
print("- enchaînement_intrigues.svg")
print("- gantt_intrigues.png")
print("- gantt_intrigues.svg")
print("- intrigues_data.json")
print("- intrigues.html")