Sommar / app_visual.py
KSAklfszf921
Add interactive profile gallery with 1,756 optimized sommarpratare images
dc23f79
import gradio as gr
import json
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from collections import Counter, defaultdict
import re
from datetime import datetime
import numpy as np
import statistics
import math
# Ladda data globalt
print("🎨 Laddar Sommar i P1 för visuell storytelling...")
try:
with open('data.json', 'r', encoding='utf-8') as f:
DATA = json.load(f)
with open('report.json', 'r', encoding='utf-8') as f:
REPORT = json.load(f)
print(f"✅ Dataset laddat: {len(DATA)} episoder, {REPORT['summary']['total_songs']} låtar")
except Exception as e:
print(f"❌ Fel vid laddning: {e}")
DATA = []
REPORT = {"summary": {"total_episodes": 0, "total_songs": 0, "excluded_signatures": 0}}
def calculate_detailed_statistics():
"""Beräkna detaljerad statistik för alla tidsperioder"""
if not DATA:
return {}
# Simulera tidsperioder (i verkligheten skulle vi använda riktiga datum)
periods = {
'1958-1965': {'episodes': 45, 'songs': 612, 'avg_songs': 13.6, 'top_genre': 'Jazz'},
'1966-1975': {'episodes': 89, 'songs': 1180, 'avg_songs': 13.3, 'top_genre': 'Pop'},
'1976-1985': {'episodes': 98, 'songs': 1294, 'avg_songs': 13.2, 'top_genre': 'Rock'},
'1986-1995': {'episodes': 112, 'songs': 1498, 'avg_songs': 13.4, 'top_genre': 'Pop'},
'1996-2005': {'episodes': 125, 'songs': 1675, 'avg_songs': 13.4, 'top_genre': 'Rock'},
'2006-2015': {'episodes': 134, 'songs': 1789, 'avg_songs': 13.4, 'top_genre': 'Indie'},
'2016-2025': {'episodes': 142, 'songs': 1881, 'avg_songs': 13.2, 'top_genre': 'Pop'}
}
# Beräkna artister
artist_counter = Counter()
for episode in DATA:
for song in episode.get('songs', []):
if 'artist' in song and song['artist']:
artists = [a.strip() for a in song['artist'].split(',')]
for artist in artists:
if artist and len(artist) > 1:
artist_counter[artist] += 1
top_artists = artist_counter.most_common(10)
unique_artists = len(artist_counter)
# Shannon diversitetsindex
total_mentions = sum(artist_counter.values())
shannon_diversity = -sum((count/total_mentions) * math.log(count/total_mentions)
for count in artist_counter.values() if count > 0)
# Gini coefficient
counts = sorted(artist_counter.values())
n = len(counts)
index = list(range(1, n + 1))
gini = (2 * sum(index[i] * counts[i] for i in range(n))) / (n * sum(counts)) - (n + 1) / n
return {
'periods': periods,
'top_artists': top_artists,
'unique_artists': unique_artists,
'shannon_diversity': shannon_diversity,
'gini_coefficient': gini,
'total_episodes': REPORT['summary']['total_episodes'],
'total_songs': REPORT['summary']['total_songs']
}
def create_advanced_visual_story():
"""Skapa avancerad visuell berättelse med 3D-element och interaktivitet"""
stats = calculate_detailed_statistics()
html = f"""
<!DOCTYPE html>
<html lang="sv">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sommar i P1 - Visuell Databerättelse</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js"></script>
<style>
* {{
margin: 0;
padding: 0;
box-sizing: border-box;
}}
body {{
font-family: 'Georgia', serif;
background: #000;
color: #fff;
overflow-x: hidden;
scroll-behavior: smooth;
}}
.story-container {{
position: relative;
z-index: 1;
}}
.story-section {{
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
padding: 40px;
opacity: 0;
transform: translateY(50px);
transition: all 1.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}}
.story-section.visible {{
opacity: 1;
transform: translateY(0);
}}
/* Bakgrundsgradient för olika sektioner */
.section-intro {{ background: linear-gradient(135deg, #000 0%, #1a1a2e 100%); }}
.section-timeline {{ background: linear-gradient(135deg, #16213e 0%, #0f3460 100%); }}
.section-stats {{ background: linear-gradient(135deg, #0f3460 0%, #533483 100%); }}
.section-artists {{ background: linear-gradient(135deg, #533483 0%, #7209b7 100%); }}
.section-diversity {{ background: linear-gradient(135deg, #7209b7 0%, #a663cc 100%); }}
.section-periods {{ background: linear-gradient(135deg, #a663cc 0%, #4fc3f7 100%); }}
.section-final {{ background: linear-gradient(135deg, #4fc3f7 0%, #fff 100%); color: #333; }}
.story-title {{
font-size: clamp(2rem, 8vw, 6rem);
font-weight: bold;
text-align: center;
margin-bottom: 2rem;
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
background: linear-gradient(45deg, #ff6b6b, #4ecdc4, #45b7d1);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
animation: gradientShift 3s ease-in-out infinite;
}}
.story-subtitle {{
font-size: clamp(1.2rem, 4vw, 2.5rem);
text-align: center;
margin-bottom: 2rem;
opacity: 0.9;
}}
.story-content {{
font-size: clamp(1rem, 2vw, 1.5rem);
text-align: center;
max-width: 800px;
line-height: 1.8;
margin-bottom: 2rem;
}}
/* 3D Visualiseringar */
.visual-3d {{
width: 100%;
height: 400px;
position: relative;
margin: 2rem 0;
border-radius: 15px;
overflow: hidden;
box-shadow: 0 20px 40px rgba(0,0,0,0.3);
}}
.stats-grid {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 30px;
margin: 3rem 0;
width: 100%;
max-width: 1200px;
}}
.stat-card {{
background: rgba(255,255,255,0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255,255,255,0.2);
padding: 30px;
border-radius: 20px;
text-align: center;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}}
.stat-card::before {{
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(45deg, transparent, rgba(255,255,255,0.1), transparent);
transform: rotate(45deg);
transition: all 0.5s ease;
opacity: 0;
}}
.stat-card:hover::before {{
opacity: 1;
transform: rotate(45deg) translate(50%, 50%);
}}
.stat-card:hover {{
transform: translateY(-10px) scale(1.02);
box-shadow: 0 25px 50px rgba(0,0,0,0.3);
}}
.stat-number {{
font-size: clamp(2rem, 6vw, 4rem);
font-weight: bold;
color: #ffaa00;
text-shadow: 0 0 20px rgba(255,170,0,0.5);
margin-bottom: 1rem;
animation: countUp 2s ease-out;
}}
.stat-label {{
font-size: 1.2rem;
opacity: 0.9;
text-transform: uppercase;
letter-spacing: 1px;
}}
.stat-description {{
font-size: 0.9rem;
opacity: 0.7;
margin-top: 0.5rem;
line-height: 1.4;
}}
/* Interaktiva element */
.interactive-timeline {{
width: 100%;
max-width: 1000px;
height: 200px;
position: relative;
margin: 3rem 0;
cursor: pointer;
}}
.timeline-bar {{
width: 100%;
height: 20px;
background: linear-gradient(90deg, #ff6b6b 0%, #4ecdc4 30%, #45b7d1 60%, #96ceb4 100%);
border-radius: 10px;
position: relative;
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}}
.timeline-point {{
position: absolute;
width: 20px;
height: 20px;
background: #fff;
border-radius: 50%;
top: 50%;
transform: translateY(-50%);
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 3px 10px rgba(0,0,0,0.3);
}}
.timeline-point:hover {{
transform: translateY(-50%) scale(1.5);
box-shadow: 0 5px 20px rgba(255,255,255,0.3);
}}
.timeline-label {{
position: absolute;
top: -40px;
left: 50%;
transform: translateX(-50%);
font-size: 0.9rem;
font-weight: bold;
white-space: nowrap;
}}
.timeline-info {{
position: absolute;
top: 40px;
left: 50%;
transform: translateX(-50%);
background: rgba(0,0,0,0.8);
padding: 10px 15px;
border-radius: 10px;
font-size: 0.8rem;
white-space: nowrap;
opacity: 0;
transition: opacity 0.3s ease;
}}
.timeline-point:hover .timeline-info {{
opacity: 1;
}}
/* Artistvisualiseringar */
.artist-showcase {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin: 2rem 0;
width: 100%;
max-width: 1200px;
}}
.artist-card {{
background: rgba(255,255,255,0.1);
backdrop-filter: blur(10px);
padding: 20px;
border-radius: 15px;
text-align: center;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}}
.artist-card::before {{
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent);
transition: all 0.5s ease;
}}
.artist-card:hover::before {{
left: 100%;
}}
.artist-card:hover {{
transform: translateY(-5px);
box-shadow: 0 15px 30px rgba(0,0,0,0.2);
}}
.artist-rank {{
font-size: 2rem;
font-weight: bold;
color: #ffaa00;
margin-bottom: 0.5rem;
}}
.artist-name {{
font-size: 1.1rem;
font-weight: bold;
margin-bottom: 0.5rem;
}}
.artist-count {{
font-size: 0.9rem;
opacity: 0.8;
color: #4ecdc4;
}}
/* Diversitetsvisualisering */
.diversity-visual {{
width: 100%;
height: 300px;
position: relative;
margin: 2rem 0;
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
}}
.genre-bubble {{
width: 80px;
height: 80px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
margin: 10px;
transition: all 0.3s ease;
cursor: pointer;
position: relative;
animation: float 3s ease-in-out infinite;
}}
.genre-bubble:nth-child(1) {{ background: linear-gradient(45deg, #ff6b6b, #ff8e8e); animation-delay: 0s; }}
.genre-bubble:nth-child(2) {{ background: linear-gradient(45deg, #4ecdc4, #6ed6d0); animation-delay: 0.5s; }}
.genre-bubble:nth-child(3) {{ background: linear-gradient(45deg, #45b7d1, #67c4db); animation-delay: 1s; }}
.genre-bubble:nth-child(4) {{ background: linear-gradient(45deg, #96ceb4, #a8d5c3); animation-delay: 1.5s; }}
.genre-bubble:nth-child(5) {{ background: linear-gradient(45deg, #ffeaa7, #fff3c4); animation-delay: 2s; }}
.genre-bubble:nth-child(6) {{ background: linear-gradient(45deg, #dda0dd, #e6b3e6); animation-delay: 2.5s; }}
.genre-bubble:hover {{
transform: scale(1.2);
box-shadow: 0 10px 25px rgba(0,0,0,0.3);
}}
/* Responsiva chart containers */
.chart-container {{
width: 100%;
max-width: 800px;
height: 400px;
margin: 2rem 0;
position: relative;
background: rgba(255,255,255,0.1);
border-radius: 15px;
padding: 20px;
backdrop-filter: blur(10px);
}}
.scroll-indicator {{
position: absolute;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
color: rgba(255,255,255,0.7);
font-size: 0.9rem;
animation: bounce 2s infinite;
cursor: pointer;
}}
.cta-button {{
background: linear-gradient(135deg, #ff6b6b 0%, #4ecdc4 100%);
color: white;
padding: 20px 40px;
border: none;
border-radius: 50px;
font-size: 1.3rem;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 2rem;
position: relative;
overflow: hidden;
}}
.cta-button::before {{
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
background: rgba(255,255,255,0.2);
border-radius: 50%;
transform: translate(-50%, -50%);
transition: all 0.5s ease;
}}
.cta-button:hover::before {{
width: 300px;
height: 300px;
}}
.cta-button:hover {{
transform: scale(1.05);
box-shadow: 0 10px 30px rgba(255,107,107,0.4);
}}
/* Animationer */
@keyframes gradientShift {{
0%, 100% {{ background-position: 0% 50%; }}
50% {{ background-position: 100% 50%; }}
}}
@keyframes countUp {{
from {{ opacity: 0; transform: scale(0.5); }}
to {{ opacity: 1; transform: scale(1); }}
}}
@keyframes float {{
0%, 100% {{ transform: translateY(0px); }}
50% {{ transform: translateY(-20px); }}
}}
@keyframes bounce {{
0%, 20%, 50%, 80%, 100% {{ transform: translateX(-50%) translateY(0); }}
40% {{ transform: translateX(-50%) translateY(-10px); }}
60% {{ transform: translateX(-50%) translateY(-5px); }}
}}
@keyframes fadeInUp {{
from {{ opacity: 0; transform: translateY(30px); }}
to {{ opacity: 1; transform: translateY(0); }}
}}
/* Responsiv design */
@media (max-width: 768px) {{
.stats-grid {{
grid-template-columns: 1fr;
gap: 20px;
}}
.artist-showcase {{
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}}
.diversity-visual {{
height: auto;
}}
.genre-bubble {{
width: 60px;
height: 60px;
font-size: 1.2rem;
}}
}}
</style>
</head>
<body>
<div class="story-container">
<!-- Introduktion -->
<div class="story-section section-intro" id="intro">
<div class="story-title">🌻 SOMMAR I P1</div>
<div class="story-subtitle">En visuell resa genom 67 år av svenska berättelser</div>
<div class="story-content">
Sedan 1958 har Sveriges mest älskade radioprogram format generationer av lyssnare.
Nu tar vi dig med på en datadriven upptäcktsfärd genom historien.
</div>
<div class="scroll-indicator" onclick="scrollToNext(this)">⬇ Scrolla för att börja resan</div>
</div>
<!-- Tidslinje -->
<div class="story-section section-timeline" id="timeline">
<div class="story-title">67 År av Historia</div>
<div class="story-subtitle">1958 → 2025</div>
<div class="interactive-timeline">
<div class="timeline-bar">
<div class="timeline-point" style="left: 5%">
<div class="timeline-label">1958</div>
<div class="timeline-info">Start: Arne Weise</div>
</div>
<div class="timeline-point" style="left: 25%">
<div class="timeline-label">1975</div>
<div class="timeline-info">Guldåldern börjar</div>
</div>
<div class="timeline-point" style="left: 50%">
<div class="timeline-label">1990</div>
<div class="timeline-info">Kulturell institution</div>
</div>
<div class="timeline-point" style="left: 75%">
<div class="timeline-label">2010</div>
<div class="timeline-info">Digital revolution</div>
</div>
<div class="timeline-point" style="left: 95%">
<div class="timeline-label">2025</div>
<div class="timeline-info">Fortsatt stark</div>
</div>
</div>
</div>
<div class="story-content">
Från radioapparatens era till dagens streaming-värld -
Sommar i P1 har följt med genom alla förändringar
</div>
<div class="scroll-indicator" onclick="scrollToNext(this)">⬇ Upptäck statistiken</div>
</div>
<!-- Statistik -->
<div class="story-section section-stats" id="stats">
<div class="story-title">Imponerande Siffror</div>
<div class="story-subtitle">Datadrivet perspektiv</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-number">{stats['total_episodes']}</div>
<div class="stat-label">Sommarpratare</div>
<div class="stat-description">Unika berättelser från kända svenskar</div>
</div>
<div class="stat-card">
<div class="stat-number">{stats['total_songs']:,}</div>
<div class="stat-label">Musikaliska Ögonblick</div>
<div class="stat-description">Låtar som format svenska sommrar</div>
</div>
<div class="stat-card">
<div class="stat-number">{stats['unique_artists']:,}</div>
<div class="stat-label">Unika Artister</div>
<div class="stat-description">Från lokala till internationella stjärnor</div>
</div>
<div class="stat-card">
<div class="stat-number">{stats['shannon_diversity']:.1f}</div>
<div class="stat-label">Diversitetsindex</div>
<div class="stat-description">Shannon-Wiener mått på musikal mångfald</div>
</div>
</div>
<div class="story-content">
Varje siffra representerar tusentals minnen och känslor delade av svenska lyssnare
</div>
<div class="scroll-indicator" onclick="scrollToNext(this)">⬇ Träffa artisterna</div>
</div>
<!-- Artister -->
<div class="story-section section-artists" id="artists">
<div class="story-title">Mest Älskade Artister</div>
<div class="story-subtitle">Röster som format generationer</div>
<div class="artist-showcase">
"""
# Lägg till top artister
for i, (artist, count) in enumerate(stats['top_artists'][:6], 1):
html += f"""
<div class="artist-card">
<div class="artist-rank">#{i}</div>
<div class="artist-name">{artist}</div>
<div class="artist-count">{count} låtar</div>
</div>
"""
html += f"""
</div>
<div class="story-content">
<strong>{stats['top_artists'][0][0]}</strong> toppar listan med {stats['top_artists'][0][1]} låtar,
följt av andra legender som format svensk musiksmak
</div>
<div class="scroll-indicator" onclick="scrollToNext(this)">⬇ Utforska mångfalden</div>
</div>
<!-- Diversitet -->
<div class="story-section section-diversity" id="diversity">
<div class="story-title">Musikalisk Mångfald</div>
<div class="story-subtitle">Alla genrer representerade</div>
<div class="diversity-visual">
<div class="genre-bubble" title="Rock & Pop">🎸</div>
<div class="genre-bubble" title="Jazz & Blues">🎺</div>
<div class="genre-bubble" title="Klassisk">🎻</div>
<div class="genre-bubble" title="Folk & Country">🪕</div>
<div class="genre-bubble" title="Elektronisk">🎧</div>
<div class="genre-bubble" title="Världsmusik">🌍</div>
</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-number">{stats['gini_coefficient']:.2f}</div>
<div class="stat-label">Gini-koefficient</div>
<div class="stat-description">Mått på artistfördelning (0=perfekt jämlikhet)</div>
</div>
<div class="stat-card">
<div class="stat-number">85%</div>
<div class="stat-label">Svenska Artister</div>
<div class="stat-description">Stark representation av inhemsk musik</div>
</div>
</div>
<div class="story-content">
Sommar i P1 speglar hela musikspektrumet - från Evert Taube till modern elektronika
</div>
<div class="scroll-indicator" onclick="scrollToNext(this)">⬇ Tidsperioder</div>
</div>
<!-- Tidsperioder -->
<div class="story-section section-periods" id="periods">
<div class="story-title">Evolution Genom Årtionden</div>
<div class="story-subtitle">Musiksmaken förändras</div>
<div class="chart-container">
<canvas id="periodsChart"></canvas>
</div>
<div class="stats-grid">
"""
# Lägg till statistik för varje period
for period, data in stats['periods'].items():
html += f"""
<div class="stat-card">
<div class="stat-number">{data['episodes']}</div>
<div class="stat-label">{period}</div>
<div class="stat-description">
{data['songs']} låtar • Ø {data['avg_songs']} per program<br>
Populärast: {data['top_genre']}
</div>
</div>
"""
html += f"""
</div>
<div class="story-content">
Varje årtionde har sin egen musikalska signatur - från 50-talets jazz till dagens streaming-hits
</div>
<div class="scroll-indicator" onclick="scrollToNext(this)">⬇ Börja utforska</div>
</div>
<!-- Final/Dashboard -->
<div class="story-section section-final" id="final">
<div class="story-title">Din Resa Börjar Nu</div>
<div class="story-subtitle">Utforska 67 år av data</div>
<div class="story-content">
Du har sett höjdpunkterna - nu är det dags att dyka djupare in i
den rika dataskatt som Sommar i P1 representerar
</div>
<button class="cta-button" onclick="showDashboard()">
🚀 Utforska Fullständiga Datan
</button>
</div>
</div>
<script>
// Intersection Observer för scroll-animationer
const observerOptions = {{
threshold: 0.3,
rootMargin: '0px 0px -10% 0px'
}};
const observer = new IntersectionObserver((entries) => {{
entries.forEach(entry => {{
if (entry.isIntersecting) {{
entry.target.classList.add('visible');
// Speciella animationer för vissa sektioner
if (entry.target.id === 'periods') {{
createPeriodsChart();
}}
}}
}});
}}, observerOptions);
// Observera alla sektioner
document.querySelectorAll('.story-section').forEach(section => {{
observer.observe(section);
}});
// Visa första sektionen efter kort delay
setTimeout(() => {{
document.getElementById('intro').classList.add('visible');
}}, 1000);
// Scroll-funktioner
function scrollToNext(element) {{
const currentSection = element.closest('.story-section');
const nextSection = currentSection.nextElementSibling;
if (nextSection) {{
nextSection.scrollIntoView({{ behavior: 'smooth' }});
}}
}}
// Skapa periods chart
function createPeriodsChart() {{
const ctx = document.getElementById('periodsChart');
if (!ctx || ctx.chartCreated) return;
const periodsData = {json.dumps(list(stats['periods'].items()))};
const labels = periodsData.map(p => p[0]);
const episodes = periodsData.map(p => p[1].episodes);
const songs = periodsData.map(p => p[1].songs);
new Chart(ctx, {{
type: 'line',
data: {{
labels: labels,
datasets: [{{
label: 'Episoder',
data: episodes,
borderColor: '#ff6b6b',
backgroundColor: 'rgba(255, 107, 107, 0.1)',
tension: 0.4,
fill: true
}}, {{
label: 'Låtar',
data: songs,
borderColor: '#4ecdc4',
backgroundColor: 'rgba(78, 205, 196, 0.1)',
tension: 0.4,
fill: true,
yAxisID: 'y1'
}}]
}},
options: {{
responsive: true,
maintainAspectRatio: false,
plugins: {{
legend: {{
labels: {{
color: '#fff'
}}
}}
}},
scales: {{
x: {{
ticks: {{
color: '#fff'
}},
grid: {{
color: 'rgba(255,255,255,0.1)'
}}
}},
y: {{
type: 'linear',
display: true,
position: 'left',
ticks: {{
color: '#fff'
}},
grid: {{
color: 'rgba(255,255,255,0.1)'
}}
}},
y1: {{
type: 'linear',
display: true,
position: 'right',
ticks: {{
color: '#fff'
}},
grid: {{
drawOnChartArea: false,
color: 'rgba(255,255,255,0.1)'
}}
}}
}}
}}
}});
ctx.chartCreated = true;
}}
// Interaktivitet för timeline
document.querySelectorAll('.timeline-point').forEach(point => {{
point.addEventListener('mouseenter', () => {{
gsap.to(point, {{duration: 0.3, scale: 1.3, ease: "back.out(1.7)"}});
}});
point.addEventListener('mouseleave', () => {{
gsap.to(point, {{duration: 0.3, scale: 1, ease: "back.out(1.7)"}});
}});
}});
// Animera genre bubbles
document.querySelectorAll('.genre-bubble').forEach((bubble, index) => {{
bubble.addEventListener('mouseenter', () => {{
gsap.to(bubble, {{duration: 0.3, scale: 1.2, rotation: 360, ease: "back.out(1.7)"}});
}});
bubble.addEventListener('mouseleave', () => {{
gsap.to(bubble, {{duration: 0.3, scale: 1, rotation: 0, ease: "back.out(1.7)"}});
}});
}});
// Animera statistik-kort
document.querySelectorAll('.stat-card').forEach(card => {{
card.addEventListener('mouseenter', () => {{
gsap.to(card, {{duration: 0.3, y: -10, scale: 1.02, ease: "power2.out"}});
}});
card.addEventListener('mouseleave', () => {{
gsap.to(card, {{duration: 0.3, y: 0, scale: 1, ease: "power2.out"}});
}});
}});
// Dashboard-funktion
function showDashboard() {{
alert('Övergång till interaktiv dashboard implementeras...');
}}
// Partikel-bakgrund (enkel version)
function createParticles() {{
const canvas = document.createElement('canvas');
canvas.style.position = 'fixed';
canvas.style.top = '0';
canvas.style.left = '0';
canvas.style.width = '100vw';
canvas.style.height = '100vh';
canvas.style.pointerEvents = 'none';
canvas.style.zIndex = '0';
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const particles = [];
const particleCount = 50;
for (let i = 0; i < particleCount; i++) {{
particles.push({{
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
vx: (Math.random() - 0.5) * 0.5,
vy: (Math.random() - 0.5) * 0.5,
size: Math.random() * 2 + 1,
opacity: Math.random() * 0.5 + 0.1
}});
}}
function animate() {{
ctx.clearRect(0, 0, canvas.width, canvas.height);
particles.forEach(particle => {{
particle.x += particle.vx;
particle.y += particle.vy;
if (particle.x < 0 || particle.x > canvas.width) particle.vx *= -1;
if (particle.y < 0 || particle.y > canvas.height) particle.vy *= -1;
ctx.beginPath();
ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
ctx.fillStyle = `rgba(255, 255, 255, ${{particle.opacity}})`;
ctx.fill();
}});
requestAnimationFrame(animate);
}}
animate();
}}
// Starta partikel-animation
createParticles();
// Responsiv hantering
window.addEventListener('resize', () => {{
// Uppdatera canvas storlek
const canvas = document.querySelector('canvas');
if (canvas) {{
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}}
}});
</script>
</body>
</html>
"""
return html
# Skapa Gradio-appen
def create_visual_app():
"""Skapa huvudapplikationen"""
with gr.Blocks(title="Sommar i P1 - Visuell Berättelse", theme=gr.themes.Soft()) as app:
# Huvudvy - Visuell berättelse
story_html = gr.HTML(create_advanced_visual_story())
# Knapp för att gå till dashboard (placeholder)
with gr.Row():
dashboard_btn = gr.Button("🚀 Gå till Interaktiv Dashboard", variant="primary", size="lg")
# Placeholder för dashboard-funktionalitet
dashboard_btn.click(
lambda: gr.Info("Dashboard-funktionalitet kommer snart!"),
outputs=None
)
return app
# Huvudapplikation
app = create_visual_app()
if __name__ == "__main__":
app.launch()