Spaces:
Runtime error
Runtime error
| 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() |