1
0

Neu: MyAnimeList Visualisierung

This commit is contained in:
Akamaru
2025-04-13 23:48:26 +02:00
parent db9311f8b2
commit 936c202cc1
4 changed files with 592 additions and 1 deletions

BIN
anime_graph/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

586
anime_graph/index.html Normal file
View File

@ -0,0 +1,586 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MyAnimeList Visualisierung | PonyWave Tools</title>
<link rel="icon" href="https://tools.ponywave.de/anime_graph/icon.png">
<meta name="description" content="Visualisiere deine MyAnimeList.net-Anime- und Manga-Listen als Grafiken und Statistiken.">
<meta property="og:title" content="MyAnimeList Visualisierung | PonyWave Tools">
<meta property="og:description" content="Visualisiere deine MyAnimeList.net-Anime- und Manga-Listen als Grafiken und Statistiken.">
<meta property="og:type" content="website">
<meta property="og:url" content="https://tools.ponywave.de/anime_graph/">
<meta property="og:image" content="https://tools.ponywave.de/anime_graph/icon.png">
<script defer src="https://stats.ponywave.de/script" data-website-id="9ef713d2-adb9-4906-9df5-708d8a8b9131" data-tag="anime_graph"></script>
<script src="https://d3js.org/d3.v7.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js"></script>
<style>
body {
font-family: 'Arial', sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
h1, h2 {
color: #2e51a2;
text-align: center;
}
.container {
display: flex;
flex-direction: column;
gap: 20px;
}
.upload-section {
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.user-info {
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
display: none;
}
.charts-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.chart-box {
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.upload-btn-wrapper {
position: relative;
overflow: hidden;
display: inline-block;
}
.btn {
border: 2px solid #2e51a2;
color: #2e51a2;
background-color: white;
padding: 8px 20px;
border-radius: 8px;
font-weight: bold;
cursor: pointer;
}
.upload-btn-wrapper input[type=file] {
font-size: 100px;
position: absolute;
left: 0;
top: 0;
opacity: 0;
cursor: pointer;
}
.file-name {
margin-left: 10px;
font-style: italic;
}
canvas {
width: 100%;
height: auto;
}
footer {
text-align: center;
margin-top: 30px;
padding: 20px;
color: #6c757d;
font-size: 0.9rem;
border-top: 1px solid #dee2e6;
}
footer a {
color: #2e51a2;
text-decoration: none;
}
footer a:hover {
text-decoration: underline;
}
@media (max-width: 768px) {
.charts-container {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<h1>MyAnimeList Visualisierung</h1>
<div class="upload-section">
<h2>XML-Datei hochladen</h2>
<p>Lade deine exportierte MyAnimeList XML-Datei hoch, um detaillierte Grafiken zu deinen Anime- und Manga-Gewohnheiten zu sehen.</p>
<div class="upload-btn-wrapper">
<button class="btn">Datei auswählen</button>
<input type="file" id="xmlFileInput" accept=".xml" />
</div>
<span class="file-name" id="fileName"></span>
</div>
<div class="user-info" id="userInfoSection">
<h2>Benutzerinformationen</h2>
<div id="userStats"></div>
</div>
<div class="charts-container" id="chartsContainer">
<div class="chart-box">
<h2>Bewertungsverteilung</h2>
<canvas id="scoresChart"></canvas>
</div>
<div class="chart-box">
<h2>Status-Verteilung</h2>
<canvas id="statusChart"></canvas>
</div>
<div class="chart-box">
<h2>Typ-Verteilung</h2>
<canvas id="typeChart"></canvas>
</div>
<div class="chart-box">
<h2>Chronologische Aktivität</h2>
<canvas id="timelineChart"></canvas>
</div>
</div>
</div>
<footer>
<p><a href="https://tools.ponywave.de/">Zurück zur Startseite</a> | &copy; <span id="current-year"></span> Akamaru | Made with <span class="heart">❤️</span> by Claude</p>
</footer>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Setze das aktuelle Jahr im Footer
document.getElementById('current-year').textContent = new Date().getFullYear();
const fileInput = document.getElementById('xmlFileInput');
const fileName = document.getElementById('fileName');
const userInfoSection = document.getElementById('userInfoSection');
const chartsContainer = document.getElementById('chartsContainer');
const userStats = document.getElementById('userStats');
// Charts
let scoresChart, statusChart, typeChart, timelineChart;
fileInput.addEventListener('change', function(e) {
if (e.target.files.length > 0) {
const file = e.target.files[0];
fileName.textContent = file.name;
const reader = new FileReader();
reader.onload = function(e) {
const xmlContent = e.target.result;
parseXML(xmlContent);
};
reader.readAsText(file);
}
});
function parseXML(xmlContent) {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlContent, "text/xml");
// Bestimmen, ob es sich um eine Anime- oder Manga-Liste handelt
const isAnimeList = xmlDoc.querySelector('myanimelist myinfo user_export_type').textContent === "1";
const isMangaList = xmlDoc.querySelector('myanimelist myinfo user_export_type').textContent === "2";
// Benutzerdaten extrahieren
const userInfo = {
userId: xmlDoc.querySelector('myanimelist myinfo user_id').textContent,
userName: xmlDoc.querySelector('myanimelist myinfo user_name').textContent,
totalItems: isAnimeList
? xmlDoc.querySelector('myanimelist myinfo user_total_anime').textContent
: xmlDoc.querySelector('myanimelist myinfo user_total_manga').textContent,
watching: isAnimeList
? xmlDoc.querySelector('myanimelist myinfo user_total_watching').textContent
: xmlDoc.querySelector('myanimelist myinfo user_total_reading').textContent,
completed: xmlDoc.querySelector('myanimelist myinfo user_total_completed').textContent,
onHold: xmlDoc.querySelector('myanimelist myinfo user_total_onhold').textContent,
dropped: xmlDoc.querySelector('myanimelist myinfo user_total_dropped').textContent,
planTo: isAnimeList
? xmlDoc.querySelector('myanimelist myinfo user_total_plantowatch').textContent
: xmlDoc.querySelector('myanimelist myinfo user_total_plantoread').textContent
};
// Daten aus den Einträgen extrahieren
let entries = [];
if (isAnimeList) {
const animeEntries = xmlDoc.querySelectorAll('myanimelist anime');
animeEntries.forEach(entry => {
entries.push({
id: entry.querySelector('series_animedb_id').textContent,
title: entry.querySelector('series_title').textContent,
type: entry.querySelector('series_type').textContent,
episodes: parseInt(entry.querySelector('series_episodes').textContent) || 0,
watchedEpisodes: parseInt(entry.querySelector('my_watched_episodes').textContent) || 0,
score: parseInt(entry.querySelector('my_score').textContent) || 0,
status: entry.querySelector('my_status').textContent,
startDate: parseDate(entry.querySelector('my_start_date').textContent),
finishDate: parseDate(entry.querySelector('my_finish_date').textContent),
timesWatched: parseInt(entry.querySelector('my_times_watched').textContent) || 0
});
});
} else if (isMangaList) {
const mangaEntries = xmlDoc.querySelectorAll('myanimelist manga');
mangaEntries.forEach(entry => {
entries.push({
id: entry.querySelector('manga_mangadb_id').textContent,
title: entry.querySelector('manga_title').textContent,
volumes: parseInt(entry.querySelector('manga_volumes').textContent) || 0,
chapters: parseInt(entry.querySelector('manga_chapters').textContent) || 0,
readVolumes: parseInt(entry.querySelector('my_read_volumes').textContent) || 0,
readChapters: parseInt(entry.querySelector('my_read_chapters').textContent) || 0,
score: parseInt(entry.querySelector('my_score').textContent) || 0,
status: entry.querySelector('my_status').textContent,
startDate: parseDate(entry.querySelector('my_start_date').textContent),
finishDate: parseDate(entry.querySelector('my_finish_date').textContent),
timesRead: parseInt(entry.querySelector('my_times_read').textContent) || 0
});
});
}
// Benutzerinfo anzeigen
displayUserInfo(userInfo, isAnimeList ? "Anime" : "Manga");
// Grafiken erstellen
createCharts(entries, isAnimeList);
// UI-Elemente anzeigen
userInfoSection.style.display = 'block';
}
function parseDate(dateStr) {
if (!dateStr || dateStr === '0000-00-00') return null;
const [year, month, day] = dateStr.split('-').map(Number);
return new Date(year, month - 1, day);
}
function displayUserInfo(userInfo, listType) {
userStats.innerHTML = `
<p><strong>Benutzername:</strong> ${userInfo.userName}</p>
<p><strong>Benutzer-ID:</strong> ${userInfo.userId}</p>
<p><strong>Gesamt ${listType}:</strong> ${userInfo.totalItems}</p>
<p><strong>${listType === 'Anime' ? 'Watching' : 'Reading'}:</strong> ${userInfo.watching}</p>
<p><strong>Completed:</strong> ${userInfo.completed}</p>
<p><strong>On-Hold:</strong> ${userInfo.onHold}</p>
<p><strong>Dropped:</strong> ${userInfo.dropped}</p>
<p><strong>${listType === 'Anime' ? 'Plan to Watch' : 'Plan to Read'}:</strong> ${userInfo.planTo}</p>
`;
}
function createCharts(entries, isAnimeList) {
// Grafiken löschen, falls sie bereits existieren
if (scoresChart) scoresChart.destroy();
if (statusChart) statusChart.destroy();
if (typeChart) typeChart.destroy();
if (timelineChart) timelineChart.destroy();
// Bewertungsverteilung
createScoresChart(entries);
// Status-Verteilung
createStatusChart(entries);
// Typ-Verteilung (nur für Anime)
if (isAnimeList) {
createTypeChart(entries);
} else {
// Alternative für Manga anzeigen
createVolumesChart(entries);
}
// Zeitverlauf
createTimelineChart(entries);
}
function createScoresChart(entries) {
const ctx = document.getElementById('scoresChart').getContext('2d');
// Bewertungen zählen (1-10)
const scoresCounts = Array(11).fill(0); // Index 0 wird nicht genutzt
entries.forEach(entry => {
if (entry.score > 0 && entry.score <= 10) {
scoresCounts[entry.score]++;
}
});
// Nicht verwendeten Index 0 entfernen
scoresCounts.shift();
scoresChart = new Chart(ctx, {
type: 'bar',
data: {
labels: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'],
datasets: [{
label: 'Anzahl',
data: scoresCounts,
backgroundColor: 'rgba(75, 192, 192, 0.6)',
borderColor: 'rgba(75, 192, 192, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: 'Anzahl'
}
},
x: {
title: {
display: true,
text: 'Bewertung'
}
}
}
}
});
}
function createStatusChart(entries) {
const ctx = document.getElementById('statusChart').getContext('2d');
// Status zählen
const statusMap = {};
entries.forEach(entry => {
if (!statusMap[entry.status]) {
statusMap[entry.status] = 0;
}
statusMap[entry.status]++;
});
const statusLabels = Object.keys(statusMap);
const statusData = Object.values(statusMap);
// Farben für die verschiedenen Status
const colors = [
'rgba(75, 192, 192, 0.6)', // Watching/Reading
'rgba(54, 162, 235, 0.6)', // Completed
'rgba(255, 206, 86, 0.6)', // On-Hold
'rgba(255, 99, 132, 0.6)', // Dropped
'rgba(153, 102, 255, 0.6)' // Plan to Watch/Read
];
statusChart = new Chart(ctx, {
type: 'pie',
data: {
labels: statusLabels,
datasets: [{
data: statusData,
backgroundColor: colors.slice(0, statusLabels.length),
borderWidth: 1
}]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'right'
}
}
}
});
}
function createTypeChart(entries) {
const ctx = document.getElementById('typeChart').getContext('2d');
// Typen zählen (TV, Movie, OVA, etc.)
const typeMap = {};
entries.forEach(entry => {
if (!typeMap[entry.type]) {
typeMap[entry.type] = 0;
}
typeMap[entry.type]++;
});
const typeLabels = Object.keys(typeMap);
const typeData = Object.values(typeMap);
// Farben für die verschiedenen Typen
const colors = [
'rgba(75, 192, 192, 0.6)', // TV
'rgba(54, 162, 235, 0.6)', // Movie
'rgba(255, 206, 86, 0.6)', // OVA
'rgba(255, 99, 132, 0.6)', // Special
'rgba(153, 102, 255, 0.6)', // ONA
'rgba(255, 159, 64, 0.6)' // Music
];
typeChart = new Chart(ctx, {
type: 'doughnut',
data: {
labels: typeLabels,
datasets: [{
data: typeData,
backgroundColor: colors.slice(0, typeLabels.length),
borderWidth: 1
}]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'right'
}
}
}
});
}
function createVolumesChart(entries) {
const ctx = document.getElementById('typeChart').getContext('2d');
document.querySelector('#typeChart').closest('.chart-box').querySelector('h2').textContent = 'Gelesene Bände/Kapitel';
// Berechne durchschnittliche Anzahl gelesener Bände/Kapitel pro Manga
const completedEntries = entries.filter(entry => entry.status === 'Completed');
const totalVolumes = completedEntries.reduce((sum, entry) => sum + entry.readVolumes, 0);
const totalChapters = completedEntries.reduce((sum, entry) => sum + entry.readChapters, 0);
// Einteilung in Kategorien für Bände
const volumeCategories = {
'1 Band': 0,
'2-5 Bände': 0,
'6-10 Bände': 0,
'11-20 Bände': 0,
'21+ Bände': 0
};
completedEntries.forEach(entry => {
if (entry.readVolumes === 1) volumeCategories['1 Band']++;
else if (entry.readVolumes >= 2 && entry.readVolumes <= 5) volumeCategories['2-5 Bände']++;
else if (entry.readVolumes >= 6 && entry.readVolumes <= 10) volumeCategories['6-10 Bände']++;
else if (entry.readVolumes >= 11 && entry.readVolumes <= 20) volumeCategories['11-20 Bände']++;
else if (entry.readVolumes > 20) volumeCategories['21+ Bände']++;
});
const volumeLabels = Object.keys(volumeCategories);
const volumeData = Object.values(volumeCategories);
typeChart = new Chart(ctx, {
type: 'bar',
data: {
labels: volumeLabels,
datasets: [{
label: 'Anzahl Manga',
data: volumeData,
backgroundColor: 'rgba(75, 192, 192, 0.6)',
borderColor: 'rgba(75, 192, 192, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: 'Anzahl Manga'
}
}
}
}
});
}
function createTimelineChart(entries) {
const ctx = document.getElementById('timelineChart').getContext('2d');
// Nur Einträge mit gültigem Enddatum berücksichtigen
const entriesWithDate = entries.filter(entry => entry.finishDate);
// Nach Jahr und Monat gruppieren
const timeline = {};
entriesWithDate.forEach(entry => {
const year = entry.finishDate.getFullYear();
const month = entry.finishDate.getMonth();
const key = `${year}-${month + 1}`;
if (!timeline[key]) {
timeline[key] = 0;
}
timeline[key]++;
});
// Sortieren und in zwei Arrays für Labels und Daten aufteilen
const sortedKeys = Object.keys(timeline).sort();
const timelineLabels = [];
const timelineData = [];
const monthNames = ['Jan', 'Feb', 'März', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'];
sortedKeys.forEach(key => {
const [year, month] = key.split('-').map(Number);
timelineLabels.push(`${monthNames[month - 1]} ${year}`);
timelineData.push(timeline[key]);
});
timelineChart = new Chart(ctx, {
type: 'line',
data: {
labels: timelineLabels,
datasets: [{
label: 'Beendete Titel',
data: timelineData,
fill: false,
borderColor: 'rgba(75, 192, 192, 1)',
tension: 0.1
}]
},
options: {
responsive: true,
scales: {
x: {
ticks: {
maxRotation: 90,
minRotation: 45
}
},
y: {
beginAtZero: true,
title: {
display: true,
text: 'Anzahl'
}
}
}
}
});
}
});
</script>
</body>
</html>

View File

@ -181,6 +181,10 @@
<div class="category-section">
<h2 class="category-title">🛠️ Utilities</h2>
<div class="tools-grid">
<a href="https://tools.ponywave.de/anime_graph" class="tool-bubble">
<h2 class="tool-title">MyAnimeList Visualisierung</h2>
<p class="tool-description">Visualisiere deine MyAnimeList.net-Anime- und Manga-Listen als Grafiken und Statistiken.</p>
</a>
<a href="https://tools.ponywave.de/sys_info" class="tool-bubble">
<h2 class="tool-title">Systeminformationen</h2>
<p class="tool-description">Zeigt Infos zu Browser, Gerät, Betriebssystem, User-Agent und IPs an.</p>

View File

@ -25,3 +25,4 @@ https://tools.ponywave.de/pokemon_quiz
https://tools.ponywave.de/gronkh_games
https://tools.ponywave.de/url_expander/
https://tools.ponywave.de/minesweeper/
https://tools.ponywave.de/anime_graph/