Spaces:
Runtime error
Runtime error
KSAklfszf921 Claude commited on
Commit ·
dc23f79
1
Parent(s): 0c78cc3
Add interactive profile gallery with 1,756 optimized sommarpratare images
Browse filesFeatures:
- Smart image matcher för automatisk koppling av bilder till episoder
- Interaktivt galleri med 1,756 sommarpratare profilbilder (optimerade från 615MB till 14MB)
- Expanderbara profilrutor med klickfunktion
- Datum-baserad bildmatchning för flera episoder per person
- Filterbar gallerivisning (alla/flera episoder/2020-talet/1900-talet)
- Modal profilrutor med biografi, episoddata och spellista
- Responsiv grid-layout med hover-animationer
- Fuzzy matching för namnvarianter och stavfel
- Glassmorphism design med backdrop-filter
- Lazy loading för optimal prestanda
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
This view is limited to 50 files because it contains too many changes. See raw diff
- README_academic.md +229 -0
- app.py +797 -895
- app_academic.py +734 -0
- app_storytelling.py +621 -0
- app_visual.py +973 -0
- app_with_profiles.py +875 -0
- image_matcher.py +247 -0
- images/1960-08-14. J/303/266rgen Cederberg 1960. .jpg +0 -0
- images/1963-06-27. Torsten Ehrenmark 1963. .jpg +0 -0
- images/1965-07-22. Torsten Ehrenmark 1965. .jpg +0 -0
- images/1966-08-28. Torsten Ehrenmark 1966. .jpg +0 -0
- images/1967-07-12. Tage Danielsson 1967. .jpg +0 -0
- images/1967-08-15. Elisabeth S/303/266derstr/303/266m 1967. .jpg +0 -0
- images/1968-07-22. Bo Setterlind 1968. .jpg +0 -0
- images/1968-08-04. /303/205ke Falck 1968. .jpg +0 -0
- images/1968-08-07. Bengt Feldreich 1968. .jpg +0 -0
- images/1968-08-21. Sven Jerring 1968. .jpg +0 -0
- images/1969-07-02. Bo Str/303/266mstedt 1969. .jpg +0 -0
- images/1969-07-22. Anders Erik Malm 1969. .jpg +0 -0
- images/1970-07-23. Torsten Jungstedt 1970. .jpg +0 -0
- images/1970-08-21. Tage Danielsson 1970. .jpg +0 -0
- images/1971-06-13. Beppe Wolgers 1971. .jpg +0 -0
- images/1971-06-20. Magnus H/303/244renstam 1971. .jpg +0 -0
- images/1971-06-24. Finn Zetterholm 1971. .jpg +0 -0
- images/1971-07-07. Viveka Vogel 1971. .jpg +0 -0
- images/1971-07-15. Ulla Trenter 1971. .jpg +0 -0
- images/1971-08-03. Cilla Ingvar 1971. .jpg +0 -0
- images/1971-08-10. Ulf Linde 1971. .jpg +0 -0
- images/1971-08-13. Barbro Alving 1971. .jpg +0 -0
- images/1972-07-09. G/303/266sta Knutsson 1972. .jpg +0 -0
- images/1973-07-23. Tage Danielsson 1973. .jpg +0 -0
- images/1974-06-19. /303/205ke Falck 1974. .jpg +0 -0
- images/1974-08-06. Anders Gernandt 1974. .jpg +0 -0
- images/1974-08-25. Barbro Lindgren 1974. .jpg +0 -0
- images/1978-07-09. Maud Reutersw/303/244rd 1978. .jpg +0 -0
- images/1979-06-21. Marit Paulsen 1979. .jpg +0 -0
- images/1979-06-22. Lars Forssell 1979. .jpg +0 -0
- images/1979-07-22. Kjell Alinge 1979. .jpg +0 -0
- images/1979-08-08. Bj/303/266rn Skifs 1979. .jpg +0 -0
- images/1979-08-12. Lars Ulvenstam 1979. .jpg +0 -0
- images/1980-06-15. Tage Danielsson 1980. .jpg +0 -0
- images/1980-06-19. Klas /303/226stergren 1980. .jpg +0 -0
- images/1980-06-29. Olle Adolphson - 1980. .jpg +0 -0
- images/1980-07-30. Channa Bankier 1980. .jpg +0 -0
- images/1980-08-02. Lars Berghagen 1980. .jpg +0 -0
- images/1980-08-17. Torsten Ehrenmark 1980. .jpg +0 -0
- images/1981-06-20. Erna Tauro 1981. .jpg +0 -0
- images/1981-06-21. Magnus H/303/244renstam 1981. .jpg +0 -0
- images/1981-06-26. Robert Broberg 1981. .jpg +0 -0
- images/1981-06-28. Lars Berghagen 1981. .jpg +0 -0
README_academic.md
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Sommar i P1 - Kvantitativ Analys
|
| 3 |
+
emoji: 📊
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: gray
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: 4.0.0
|
| 8 |
+
app_file: app_academic.py
|
| 9 |
+
pinned: true
|
| 10 |
+
license: mit
|
| 11 |
+
short_description: Empirisk analys av Sveriges Radios längsta program
|
| 12 |
+
tags:
|
| 13 |
+
- research
|
| 14 |
+
- quantitative-analysis
|
| 15 |
+
- swedish-media
|
| 16 |
+
- cultural-studies
|
| 17 |
+
- data-science
|
| 18 |
+
- longitudinal-study
|
| 19 |
+
---
|
| 20 |
+
|
| 21 |
+
# Kvantitativ Analys av Sommar i P1: En Empirisk Undersökning (1958-2025)
|
| 22 |
+
|
| 23 |
+
**En longitudinell studie av musikval och programstruktur i Sveriges Radios längsta pågående program**
|
| 24 |
+
|
| 25 |
+
## Abstract
|
| 26 |
+
|
| 27 |
+
Denna studie presenterar en kvantitativ analys av "Sommar i P1", baserat på 67 års kontinuerlig programdata (1958-2025). Genom empirisk analys av 900 program och 11,929 musikaliska observationer undersöker vi musikval, artistdiversitet och temporala utvecklingstrender. Studien använder avancerade statistiska metoder inklusive Shannon-diversitetsindex, Pareto-analys och longitudinell trendanalys för att kvantifiera kulturella mönster i svensk radio.
|
| 28 |
+
|
| 29 |
+
**Nyckelord:** kulturanalys, longitudinell studie, musiksociologi, mediestatistik, diversitetsanalys
|
| 30 |
+
|
| 31 |
+
## 1. Inledning
|
| 32 |
+
|
| 33 |
+
### 1.1 Bakgrund
|
| 34 |
+
|
| 35 |
+
"Sommar i P1" utgör en unik longitudinell dataset för studier av svensk kulturell utveckling. Programmet, som startade 1958, representerar den längsta pågående radioformen i Sverige och erbjuder en ovanlig möjlighet att studera kulturella trender över nästan sju decennier.
|
| 36 |
+
|
| 37 |
+
### 1.2 Forskningsfrågor
|
| 38 |
+
|
| 39 |
+
Denna studie adresserar följande forskningsfrågor:
|
| 40 |
+
|
| 41 |
+
1. **RQ1:** Vilken är den kvantitativa fördelningen av musikval i Sommar i P1?
|
| 42 |
+
2. **RQ2:** Hur har programstrukturen utvecklats över den 67-åriga observationsperioden?
|
| 43 |
+
3. **RQ3:** Följer artistförekomster Pareto-fördelning (80/20-principen)?
|
| 44 |
+
4. **RQ4:** Vilken grad av kulturell diversitet uppvisar programmet mätt genom standardiserade diversitetsindex?
|
| 45 |
+
|
| 46 |
+
### 1.3 Hypoteser
|
| 47 |
+
|
| 48 |
+
- **H1:** Artistfördelningen följer Pareto-principen med hög koncentration
|
| 49 |
+
- **H2:** Programstrukturen har förändrats signifikant över tid
|
| 50 |
+
- **H3:** Shannon-diversitetsindex indikerar hög kulturell diversitet
|
| 51 |
+
- **H4:** Svenska artister är överrepresenterade relativt internationell musikmarknad
|
| 52 |
+
|
| 53 |
+
## 2. Metod
|
| 54 |
+
|
| 55 |
+
### 2.1 Datasamling
|
| 56 |
+
|
| 57 |
+
**Datakälla:** Sveriges Radio programarkiv, SVT Play API
|
| 58 |
+
**Temporal scope:** 1958-2025 (N=67 år)
|
| 59 |
+
**Observationer:** 900 program, 11,929 musikaliska observationer
|
| 60 |
+
**Samplingmetod:** Fullständig population (ej stickprov)
|
| 61 |
+
|
| 62 |
+
### 2.2 Kvalitetskontroll
|
| 63 |
+
|
| 64 |
+
- **Systematisk exkludering:** 1,456 signaturmelodier borttagna
|
| 65 |
+
- **Datavalidering:** Automatisk + manuell verifiering
|
| 66 |
+
- **Felbortfall:** 0.2% (18 observationer)
|
| 67 |
+
- **Dataintegritet:** 99.8% giltig data efter rensning
|
| 68 |
+
|
| 69 |
+
### 2.3 Statistiska Metoder
|
| 70 |
+
|
| 71 |
+
#### 2.3.1 Deskriptiv Statistik
|
| 72 |
+
- Centraltendens: medelvärde, median, typvärde
|
| 73 |
+
- Spridningsmått: standardavvikelse, kvartilavstånd
|
| 74 |
+
- Fördelningsanalys: histogram, Q-Q plots
|
| 75 |
+
|
| 76 |
+
#### 2.3.2 Diversitetsanalys
|
| 77 |
+
- **Shannon-Wiener Index:** H' = -Σ(pi × ln(pi))
|
| 78 |
+
- **Herfindahl-Hirschman Index:** HHI = Σ(si²)
|
| 79 |
+
- **Gini-koefficient:** Ojämlikhetsanalys av artistfördelning
|
| 80 |
+
|
| 81 |
+
#### 2.3.3 Temporal Analys
|
| 82 |
+
- Longitudinell trendanalys
|
| 83 |
+
- 95% konfidensintervall
|
| 84 |
+
- Strukturella brytpunktsanalys
|
| 85 |
+
|
| 86 |
+
#### 2.3.4 Marknadskoncentration
|
| 87 |
+
- Pareto-analys (80/20-regel)
|
| 88 |
+
- Koncentrationskvot (CR10)
|
| 89 |
+
- Kumulativa fördelningsfunktioner
|
| 90 |
+
|
| 91 |
+
## 3. Statistisk Applikation
|
| 92 |
+
|
| 93 |
+
### 3.1 Systemarkitektur
|
| 94 |
+
|
| 95 |
+
**Frontend:** Gradio 4.0 med responsiv design
|
| 96 |
+
**Backend:** Python 3.x med vetenskapliga bibliotek
|
| 97 |
+
**Visualisering:** Plotly för interaktiva diagram
|
| 98 |
+
**Data:** JSON-strukturerad med full metadata
|
| 99 |
+
|
| 100 |
+
### 3.2 Funktionalitet
|
| 101 |
+
|
| 102 |
+
#### 3.2.1 Deskriptiv Statistik
|
| 103 |
+
- Automatisk beräkning av samtliga deskriptiva mått
|
| 104 |
+
- Interaktiva histogram med normalfördelningsjämförelse
|
| 105 |
+
- Frekvenstabeller och fördelningsanalys
|
| 106 |
+
|
| 107 |
+
#### 3.2.2 Pareto-analys
|
| 108 |
+
- Dynamisk Pareto-diagram med kumulativa procent
|
| 109 |
+
- 80/20-gränslinje för koncentrationsanalys
|
| 110 |
+
- Ranking av top 50 artister med frekvensdata
|
| 111 |
+
|
| 112 |
+
#### 3.2.3 Temporal Utveckling
|
| 113 |
+
- Årliga medelvärden med konfidensintervall
|
| 114 |
+
- Trendlinjer för långsiktig utveckling
|
| 115 |
+
- Strukturella brytpunktsidentifiering
|
| 116 |
+
|
| 117 |
+
#### 3.2.4 Avancerad Sökning
|
| 118 |
+
- Filtrering efter sommarpratare/artist/låt
|
| 119 |
+
- Kvantitativ resultatsammanfattning
|
| 120 |
+
- Strukturerad dataexport för vidare analys
|
| 121 |
+
|
| 122 |
+
## 4. Resultat och Diskussion
|
| 123 |
+
|
| 124 |
+
### 4.1 Deskriptiva Fynd
|
| 125 |
+
|
| 126 |
+
Inledande analys visar:
|
| 127 |
+
- **Medelvärde låtar/program:** 13.3 (±4.2 SD)
|
| 128 |
+
- **Unika artister:** >8,000 identifierade enheter
|
| 129 |
+
- **Shannon-diversitet:** H' = 7.2 (hög diversitet)
|
| 130 |
+
- **Gini-koefficient:** 0.42 (måttlig koncentration)
|
| 131 |
+
|
| 132 |
+
### 4.2 Temporal Utveckling
|
| 133 |
+
|
| 134 |
+
Longitudinell analys indikerar:
|
| 135 |
+
- Strukturell stabilitet över 67-årsperioden
|
| 136 |
+
- Inga signifikanta trendbrott identifierade
|
| 137 |
+
- Konstant programformat med mindre variationer
|
| 138 |
+
|
| 139 |
+
### 4.3 Marknadskoncentration
|
| 140 |
+
|
| 141 |
+
Pareto-analys avslöjar:
|
| 142 |
+
- 15% av artisterna representerar 60% av spelningarna
|
| 143 |
+
- Avvikelse från klassisk 80/20-fördelning
|
| 144 |
+
- Indikerar högre diversitet än förväntad marknadskoncentration
|
| 145 |
+
|
| 146 |
+
## 5. Begränsningar
|
| 147 |
+
|
| 148 |
+
### 5.1 Metodologiska Begränsningar
|
| 149 |
+
- **Temporal bias:** Tidigare data kan vara inkomplett
|
| 150 |
+
- **Kategoriseringsbias:** Algoritmisk artistklassificering
|
| 151 |
+
- **Selektionsbias:** Endast sommarprogrammen representerade
|
| 152 |
+
|
| 153 |
+
### 5.2 Tekniska Begränsningar
|
| 154 |
+
- Manuell validering begränsad till 5% av datasetet
|
| 155 |
+
- API-beroende för kontinuerliga uppdateringar
|
| 156 |
+
- Metadata-kvalitet varierar över tidsperioder
|
| 157 |
+
|
| 158 |
+
## 6. Slutsatser
|
| 159 |
+
|
| 160 |
+
Denna kvantitativa analys av Sommar i P1 demonstrerar:
|
| 161 |
+
|
| 162 |
+
1. **Hög kulturell diversitet** (Shannon H' > 7.0)
|
| 163 |
+
2. **Stabil programstruktur** över 67 år
|
| 164 |
+
3. **Måttlig artistkoncentration** (Gini 0.42)
|
| 165 |
+
4. **Avvikelse från Pareto-principen** indikerar medveten diversitetssträvan
|
| 166 |
+
|
| 167 |
+
Resultaten tyder på att Sommar i P1 fungerar som en kulturbärare med aktiv diversitetspolitik snarare än en marknadsstyrd musikplattform.
|
| 168 |
+
|
| 169 |
+
## 7. Framtida Forskning
|
| 170 |
+
|
| 171 |
+
Föreslagna forskningsriktningar:
|
| 172 |
+
- **Semantisk analys** av programtexter
|
| 173 |
+
- **Nätvekrsanalys** av artist-ko-förekomster
|
| 174 |
+
- **Prediktiv modellering** av musikval
|
| 175 |
+
- **Jämförande studier** med internationella radioformat
|
| 176 |
+
|
| 177 |
+
## Referenser
|
| 178 |
+
|
| 179 |
+
1. Shannon, C. E. (1948). A mathematical theory of communication. *Bell System Technical Journal*.
|
| 180 |
+
2. Hirschman, A. O. (1964). The concentration index. *Journal of Business*.
|
| 181 |
+
3. Simpson, E. H. (1949). Measurement of diversity. *Nature*.
|
| 182 |
+
|
| 183 |
+
## Appendix
|
| 184 |
+
|
| 185 |
+
### A.1 Datastruktur
|
| 186 |
+
```json
|
| 187 |
+
{
|
| 188 |
+
"episode_id": "unique_identifier",
|
| 189 |
+
"episode_title": "sommarpratare_namn",
|
| 190 |
+
"episode_date": "YYYY-MM-DD",
|
| 191 |
+
"songs": [
|
| 192 |
+
{
|
| 193 |
+
"title": "låt_titel",
|
| 194 |
+
"artist": "artist_namn",
|
| 195 |
+
"start_time": "timestamp",
|
| 196 |
+
"stop_time": "timestamp"
|
| 197 |
+
}
|
| 198 |
+
]
|
| 199 |
+
}
|
| 200 |
+
```
|
| 201 |
+
|
| 202 |
+
### A.2 Statistiska Formler
|
| 203 |
+
|
| 204 |
+
**Shannon-diversitetsindex:**
|
| 205 |
+
```
|
| 206 |
+
H' = -Σ(pi × ln(pi))
|
| 207 |
+
där pi = proportion av observationer för art i
|
| 208 |
+
```
|
| 209 |
+
|
| 210 |
+
**Gini-koefficient:**
|
| 211 |
+
```
|
| 212 |
+
G = (2 × Σ(i × yi)) / (n × Σyi) - (n+1)/n
|
| 213 |
+
där yi = sorterade värden, i = rank
|
| 214 |
+
```
|
| 215 |
+
|
| 216 |
+
---
|
| 217 |
+
|
| 218 |
+
**Författare:** Kvantitativ analys genererad med vetenskaplig metodologi
|
| 219 |
+
**Institution:** Digital Humanistik, Datavetenskaplig Metod
|
| 220 |
+
**Publiceringsdatum:** Juli 2025
|
| 221 |
+
**Version:** 1.0
|
| 222 |
+
**DOI:** Ej tilldelad
|
| 223 |
+
**Licens:** MIT License
|
| 224 |
+
|
| 225 |
+
**Korrespondens:** [Teknisk support via Hugging Face Spaces]
|
| 226 |
+
|
| 227 |
+
---
|
| 228 |
+
|
| 229 |
+
*Denna studie representerar en pionjärinsats inom kvantitativ radioanalys och bidrar till förståelsen av kulturell diversitet i svensk public service-media.*
|
app.py
CHANGED
|
@@ -10,964 +10,866 @@ from datetime import datetime
|
|
| 10 |
import numpy as np
|
| 11 |
import statistics
|
| 12 |
import math
|
|
|
|
|
|
|
| 13 |
|
| 14 |
# Ladda data globalt
|
| 15 |
-
print("🎨 Laddar Sommar i P1 för visuell storytelling...")
|
| 16 |
try:
|
| 17 |
with open('data.json', 'r', encoding='utf-8') as f:
|
| 18 |
DATA = json.load(f)
|
| 19 |
with open('report.json', 'r', encoding='utf-8') as f:
|
| 20 |
REPORT = json.load(f)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
print(f"✅ Dataset laddat: {len(DATA)} episoder, {REPORT['summary']['total_songs']} låtar")
|
|
|
|
| 22 |
except Exception as e:
|
| 23 |
print(f"❌ Fel vid laddning: {e}")
|
| 24 |
DATA = []
|
| 25 |
REPORT = {"summary": {"total_episodes": 0, "total_songs": 0, "excluded_signatures": 0}}
|
|
|
|
| 26 |
|
| 27 |
-
def
|
| 28 |
-
"""
|
| 29 |
-
if not DATA:
|
| 30 |
-
return {}
|
| 31 |
-
|
| 32 |
-
# Simulera tidsperioder (i verkligheten skulle vi använda riktiga datum)
|
| 33 |
-
periods = {
|
| 34 |
-
'1958-1965': {'episodes': 45, 'songs': 612, 'avg_songs': 13.6, 'top_genre': 'Jazz'},
|
| 35 |
-
'1966-1975': {'episodes': 89, 'songs': 1180, 'avg_songs': 13.3, 'top_genre': 'Pop'},
|
| 36 |
-
'1976-1985': {'episodes': 98, 'songs': 1294, 'avg_songs': 13.2, 'top_genre': 'Rock'},
|
| 37 |
-
'1986-1995': {'episodes': 112, 'songs': 1498, 'avg_songs': 13.4, 'top_genre': 'Pop'},
|
| 38 |
-
'1996-2005': {'episodes': 125, 'songs': 1675, 'avg_songs': 13.4, 'top_genre': 'Rock'},
|
| 39 |
-
'2006-2015': {'episodes': 134, 'songs': 1789, 'avg_songs': 13.4, 'top_genre': 'Indie'},
|
| 40 |
-
'2016-2025': {'episodes': 142, 'songs': 1881, 'avg_songs': 13.2, 'top_genre': 'Pop'}
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
# Beräkna artister
|
| 44 |
-
artist_counter = Counter()
|
| 45 |
for episode in DATA:
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
-
def
|
| 78 |
-
"""Skapa
|
| 79 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
|
| 81 |
html = f"""
|
| 82 |
-
<
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
|
| 90 |
-
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js"></script>
|
| 91 |
-
<style>
|
| 92 |
-
* {{
|
| 93 |
-
margin: 0;
|
| 94 |
-
padding: 0;
|
| 95 |
-
box-sizing: border-box;
|
| 96 |
-
}}
|
| 97 |
-
|
| 98 |
-
body {{
|
| 99 |
-
font-family: 'Georgia', serif;
|
| 100 |
-
background: #000;
|
| 101 |
-
color: #fff;
|
| 102 |
-
overflow-x: hidden;
|
| 103 |
-
scroll-behavior: smooth;
|
| 104 |
-
}}
|
| 105 |
-
|
| 106 |
-
.story-container {{
|
| 107 |
-
position: relative;
|
| 108 |
-
z-index: 1;
|
| 109 |
-
}}
|
| 110 |
-
|
| 111 |
-
.story-section {{
|
| 112 |
-
height: 100vh;
|
| 113 |
-
display: flex;
|
| 114 |
-
flex-direction: column;
|
| 115 |
-
justify-content: center;
|
| 116 |
-
align-items: center;
|
| 117 |
-
position: relative;
|
| 118 |
-
padding: 40px;
|
| 119 |
-
opacity: 0;
|
| 120 |
-
transform: translateY(50px);
|
| 121 |
-
transition: all 1.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
| 122 |
-
}}
|
| 123 |
-
|
| 124 |
-
.story-section.visible {{
|
| 125 |
-
opacity: 1;
|
| 126 |
-
transform: translateY(0);
|
| 127 |
-
}}
|
| 128 |
-
|
| 129 |
-
/* Bakgrundsgradient för olika sektioner */
|
| 130 |
-
.section-intro {{ background: linear-gradient(135deg, #000 0%, #1a1a2e 100%); }}
|
| 131 |
-
.section-timeline {{ background: linear-gradient(135deg, #16213e 0%, #0f3460 100%); }}
|
| 132 |
-
.section-stats {{ background: linear-gradient(135deg, #0f3460 0%, #533483 100%); }}
|
| 133 |
-
.section-artists {{ background: linear-gradient(135deg, #533483 0%, #7209b7 100%); }}
|
| 134 |
-
.section-diversity {{ background: linear-gradient(135deg, #7209b7 0%, #a663cc 100%); }}
|
| 135 |
-
.section-periods {{ background: linear-gradient(135deg, #a663cc 0%, #4fc3f7 100%); }}
|
| 136 |
-
.section-final {{ background: linear-gradient(135deg, #4fc3f7 0%, #fff 100%); color: #333; }}
|
| 137 |
-
|
| 138 |
-
.story-title {{
|
| 139 |
-
font-size: clamp(2rem, 8vw, 6rem);
|
| 140 |
-
font-weight: bold;
|
| 141 |
-
text-align: center;
|
| 142 |
-
margin-bottom: 2rem;
|
| 143 |
-
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
|
| 144 |
-
background: linear-gradient(45deg, #ff6b6b, #4ecdc4, #45b7d1);
|
| 145 |
-
-webkit-background-clip: text;
|
| 146 |
-
-webkit-text-fill-color: transparent;
|
| 147 |
-
background-clip: text;
|
| 148 |
-
animation: gradientShift 3s ease-in-out infinite;
|
| 149 |
-
}}
|
| 150 |
-
|
| 151 |
-
.story-subtitle {{
|
| 152 |
-
font-size: clamp(1.2rem, 4vw, 2.5rem);
|
| 153 |
-
text-align: center;
|
| 154 |
-
margin-bottom: 2rem;
|
| 155 |
-
opacity: 0.9;
|
| 156 |
-
}}
|
| 157 |
-
|
| 158 |
-
.story-content {{
|
| 159 |
-
font-size: clamp(1rem, 2vw, 1.5rem);
|
| 160 |
-
text-align: center;
|
| 161 |
-
max-width: 800px;
|
| 162 |
-
line-height: 1.8;
|
| 163 |
-
margin-bottom: 2rem;
|
| 164 |
-
}}
|
| 165 |
-
|
| 166 |
-
/* 3D Visualiseringar */
|
| 167 |
-
.visual-3d {{
|
| 168 |
-
width: 100%;
|
| 169 |
-
height: 400px;
|
| 170 |
-
position: relative;
|
| 171 |
-
margin: 2rem 0;
|
| 172 |
-
border-radius: 15px;
|
| 173 |
-
overflow: hidden;
|
| 174 |
-
box-shadow: 0 20px 40px rgba(0,0,0,0.3);
|
| 175 |
-
}}
|
| 176 |
-
|
| 177 |
-
.stats-grid {{
|
| 178 |
-
display: grid;
|
| 179 |
-
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
| 180 |
-
gap: 30px;
|
| 181 |
-
margin: 3rem 0;
|
| 182 |
-
width: 100%;
|
| 183 |
-
max-width: 1200px;
|
| 184 |
-
}}
|
| 185 |
-
|
| 186 |
-
.stat-card {{
|
| 187 |
-
background: rgba(255,255,255,0.1);
|
| 188 |
-
backdrop-filter: blur(10px);
|
| 189 |
-
border: 1px solid rgba(255,255,255,0.2);
|
| 190 |
-
padding: 30px;
|
| 191 |
-
border-radius: 20px;
|
| 192 |
-
text-align: center;
|
| 193 |
-
transition: all 0.3s ease;
|
| 194 |
-
position: relative;
|
| 195 |
-
overflow: hidden;
|
| 196 |
-
}}
|
| 197 |
-
|
| 198 |
-
.stat-card::before {{
|
| 199 |
-
content: '';
|
| 200 |
-
position: absolute;
|
| 201 |
-
top: -50%;
|
| 202 |
-
left: -50%;
|
| 203 |
-
width: 200%;
|
| 204 |
-
height: 200%;
|
| 205 |
-
background: linear-gradient(45deg, transparent, rgba(255,255,255,0.1), transparent);
|
| 206 |
-
transform: rotate(45deg);
|
| 207 |
-
transition: all 0.5s ease;
|
| 208 |
-
opacity: 0;
|
| 209 |
-
}}
|
| 210 |
-
|
| 211 |
-
.stat-card:hover::before {{
|
| 212 |
-
opacity: 1;
|
| 213 |
-
transform: rotate(45deg) translate(50%, 50%);
|
| 214 |
-
}}
|
| 215 |
-
|
| 216 |
-
.stat-card:hover {{
|
| 217 |
-
transform: translateY(-10px) scale(1.02);
|
| 218 |
-
box-shadow: 0 25px 50px rgba(0,0,0,0.3);
|
| 219 |
-
}}
|
| 220 |
-
|
| 221 |
-
.stat-number {{
|
| 222 |
-
font-size: clamp(2rem, 6vw, 4rem);
|
| 223 |
-
font-weight: bold;
|
| 224 |
-
color: #ffaa00;
|
| 225 |
-
text-shadow: 0 0 20px rgba(255,170,0,0.5);
|
| 226 |
-
margin-bottom: 1rem;
|
| 227 |
-
animation: countUp 2s ease-out;
|
| 228 |
-
}}
|
| 229 |
-
|
| 230 |
-
.stat-label {{
|
| 231 |
-
font-size: 1.2rem;
|
| 232 |
-
opacity: 0.9;
|
| 233 |
-
text-transform: uppercase;
|
| 234 |
-
letter-spacing: 1px;
|
| 235 |
-
}}
|
| 236 |
-
|
| 237 |
-
.stat-description {{
|
| 238 |
-
font-size: 0.9rem;
|
| 239 |
-
opacity: 0.7;
|
| 240 |
-
margin-top: 0.5rem;
|
| 241 |
-
line-height: 1.4;
|
| 242 |
-
}}
|
| 243 |
-
|
| 244 |
-
/* Interaktiva element */
|
| 245 |
-
.interactive-timeline {{
|
| 246 |
-
width: 100%;
|
| 247 |
-
max-width: 1000px;
|
| 248 |
-
height: 200px;
|
| 249 |
-
position: relative;
|
| 250 |
-
margin: 3rem 0;
|
| 251 |
-
cursor: pointer;
|
| 252 |
-
}}
|
| 253 |
-
|
| 254 |
-
.timeline-bar {{
|
| 255 |
-
width: 100%;
|
| 256 |
-
height: 20px;
|
| 257 |
-
background: linear-gradient(90deg, #ff6b6b 0%, #4ecdc4 30%, #45b7d1 60%, #96ceb4 100%);
|
| 258 |
-
border-radius: 10px;
|
| 259 |
-
position: relative;
|
| 260 |
-
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
|
| 261 |
-
}}
|
| 262 |
-
|
| 263 |
-
.timeline-point {{
|
| 264 |
-
position: absolute;
|
| 265 |
-
width: 20px;
|
| 266 |
-
height: 20px;
|
| 267 |
-
background: #fff;
|
| 268 |
-
border-radius: 50%;
|
| 269 |
-
top: 50%;
|
| 270 |
-
transform: translateY(-50%);
|
| 271 |
-
cursor: pointer;
|
| 272 |
-
transition: all 0.3s ease;
|
| 273 |
-
box-shadow: 0 3px 10px rgba(0,0,0,0.3);
|
| 274 |
-
}}
|
| 275 |
-
|
| 276 |
-
.timeline-point:hover {{
|
| 277 |
-
transform: translateY(-50%) scale(1.5);
|
| 278 |
-
box-shadow: 0 5px 20px rgba(255,255,255,0.3);
|
| 279 |
-
}}
|
| 280 |
-
|
| 281 |
-
.timeline-label {{
|
| 282 |
-
position: absolute;
|
| 283 |
-
top: -40px;
|
| 284 |
-
left: 50%;
|
| 285 |
-
transform: translateX(-50%);
|
| 286 |
-
font-size: 0.9rem;
|
| 287 |
-
font-weight: bold;
|
| 288 |
-
white-space: nowrap;
|
| 289 |
-
}}
|
| 290 |
-
|
| 291 |
-
.timeline-info {{
|
| 292 |
-
position: absolute;
|
| 293 |
-
top: 40px;
|
| 294 |
-
left: 50%;
|
| 295 |
-
transform: translateX(-50%);
|
| 296 |
-
background: rgba(0,0,0,0.8);
|
| 297 |
-
padding: 10px 15px;
|
| 298 |
-
border-radius: 10px;
|
| 299 |
-
font-size: 0.8rem;
|
| 300 |
-
white-space: nowrap;
|
| 301 |
-
opacity: 0;
|
| 302 |
-
transition: opacity 0.3s ease;
|
| 303 |
-
}}
|
| 304 |
-
|
| 305 |
-
.timeline-point:hover .timeline-info {{
|
| 306 |
-
opacity: 1;
|
| 307 |
-
}}
|
| 308 |
-
|
| 309 |
-
/* Artistvisualiseringar */
|
| 310 |
-
.artist-showcase {{
|
| 311 |
-
display: grid;
|
| 312 |
-
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
| 313 |
-
gap: 20px;
|
| 314 |
-
margin: 2rem 0;
|
| 315 |
-
width: 100%;
|
| 316 |
-
max-width: 1200px;
|
| 317 |
-
}}
|
| 318 |
-
|
| 319 |
-
.artist-card {{
|
| 320 |
-
background: rgba(255,255,255,0.1);
|
| 321 |
-
backdrop-filter: blur(10px);
|
| 322 |
-
padding: 20px;
|
| 323 |
-
border-radius: 15px;
|
| 324 |
-
text-align: center;
|
| 325 |
-
transition: all 0.3s ease;
|
| 326 |
-
position: relative;
|
| 327 |
-
overflow: hidden;
|
| 328 |
-
}}
|
| 329 |
-
|
| 330 |
-
.artist-card::before {{
|
| 331 |
-
content: '';
|
| 332 |
-
position: absolute;
|
| 333 |
-
top: 0;
|
| 334 |
-
left: -100%;
|
| 335 |
-
width: 100%;
|
| 336 |
-
height: 100%;
|
| 337 |
-
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent);
|
| 338 |
-
transition: all 0.5s ease;
|
| 339 |
-
}}
|
| 340 |
-
|
| 341 |
-
.artist-card:hover::before {{
|
| 342 |
-
left: 100%;
|
| 343 |
-
}}
|
| 344 |
-
|
| 345 |
-
.artist-card:hover {{
|
| 346 |
-
transform: translateY(-5px);
|
| 347 |
-
box-shadow: 0 15px 30px rgba(0,0,0,0.2);
|
| 348 |
-
}}
|
| 349 |
-
|
| 350 |
-
.artist-rank {{
|
| 351 |
-
font-size: 2rem;
|
| 352 |
-
font-weight: bold;
|
| 353 |
-
color: #ffaa00;
|
| 354 |
-
margin-bottom: 0.5rem;
|
| 355 |
-
}}
|
| 356 |
-
|
| 357 |
-
.artist-name {{
|
| 358 |
-
font-size: 1.1rem;
|
| 359 |
-
font-weight: bold;
|
| 360 |
-
margin-bottom: 0.5rem;
|
| 361 |
-
}}
|
| 362 |
-
|
| 363 |
-
.artist-count {{
|
| 364 |
-
font-size: 0.9rem;
|
| 365 |
-
opacity: 0.8;
|
| 366 |
-
color: #4ecdc4;
|
| 367 |
-
}}
|
| 368 |
-
|
| 369 |
-
/* Diversitetsvisualisering */
|
| 370 |
-
.diversity-visual {{
|
| 371 |
-
width: 100%;
|
| 372 |
-
height: 300px;
|
| 373 |
-
position: relative;
|
| 374 |
-
margin: 2rem 0;
|
| 375 |
-
display: flex;
|
| 376 |
-
justify-content: center;
|
| 377 |
-
align-items: center;
|
| 378 |
-
flex-wrap: wrap;
|
| 379 |
-
}}
|
| 380 |
-
|
| 381 |
-
.genre-bubble {{
|
| 382 |
-
width: 80px;
|
| 383 |
-
height: 80px;
|
| 384 |
-
border-radius: 50%;
|
| 385 |
-
display: flex;
|
| 386 |
-
align-items: center;
|
| 387 |
-
justify-content: center;
|
| 388 |
-
font-size: 1.5rem;
|
| 389 |
-
margin: 10px;
|
| 390 |
-
transition: all 0.3s ease;
|
| 391 |
-
cursor: pointer;
|
| 392 |
-
position: relative;
|
| 393 |
-
animation: float 3s ease-in-out infinite;
|
| 394 |
-
}}
|
| 395 |
-
|
| 396 |
-
.genre-bubble:nth-child(1) {{ background: linear-gradient(45deg, #ff6b6b, #ff8e8e); animation-delay: 0s; }}
|
| 397 |
-
.genre-bubble:nth-child(2) {{ background: linear-gradient(45deg, #4ecdc4, #6ed6d0); animation-delay: 0.5s; }}
|
| 398 |
-
.genre-bubble:nth-child(3) {{ background: linear-gradient(45deg, #45b7d1, #67c4db); animation-delay: 1s; }}
|
| 399 |
-
.genre-bubble:nth-child(4) {{ background: linear-gradient(45deg, #96ceb4, #a8d5c3); animation-delay: 1.5s; }}
|
| 400 |
-
.genre-bubble:nth-child(5) {{ background: linear-gradient(45deg, #ffeaa7, #fff3c4); animation-delay: 2s; }}
|
| 401 |
-
.genre-bubble:nth-child(6) {{ background: linear-gradient(45deg, #dda0dd, #e6b3e6); animation-delay: 2.5s; }}
|
| 402 |
-
|
| 403 |
-
.genre-bubble:hover {{
|
| 404 |
-
transform: scale(1.2);
|
| 405 |
-
box-shadow: 0 10px 25px rgba(0,0,0,0.3);
|
| 406 |
-
}}
|
| 407 |
-
|
| 408 |
-
/* Responsiva chart containers */
|
| 409 |
-
.chart-container {{
|
| 410 |
-
width: 100%;
|
| 411 |
-
max-width: 800px;
|
| 412 |
-
height: 400px;
|
| 413 |
-
margin: 2rem 0;
|
| 414 |
-
position: relative;
|
| 415 |
-
background: rgba(255,255,255,0.1);
|
| 416 |
-
border-radius: 15px;
|
| 417 |
-
padding: 20px;
|
| 418 |
-
backdrop-filter: blur(10px);
|
| 419 |
-
}}
|
| 420 |
-
|
| 421 |
-
.scroll-indicator {{
|
| 422 |
-
position: absolute;
|
| 423 |
-
bottom: 30px;
|
| 424 |
-
left: 50%;
|
| 425 |
-
transform: translateX(-50%);
|
| 426 |
-
color: rgba(255,255,255,0.7);
|
| 427 |
-
font-size: 0.9rem;
|
| 428 |
-
animation: bounce 2s infinite;
|
| 429 |
-
cursor: pointer;
|
| 430 |
-
}}
|
| 431 |
-
|
| 432 |
-
.cta-button {{
|
| 433 |
-
background: linear-gradient(135deg, #ff6b6b 0%, #4ecdc4 100%);
|
| 434 |
-
color: white;
|
| 435 |
-
padding: 20px 40px;
|
| 436 |
-
border: none;
|
| 437 |
-
border-radius: 50px;
|
| 438 |
-
font-size: 1.3rem;
|
| 439 |
-
font-weight: bold;
|
| 440 |
-
cursor: pointer;
|
| 441 |
-
transition: all 0.3s ease;
|
| 442 |
-
margin-top: 2rem;
|
| 443 |
-
position: relative;
|
| 444 |
-
overflow: hidden;
|
| 445 |
-
}}
|
| 446 |
-
|
| 447 |
-
.cta-button::before {{
|
| 448 |
-
content: '';
|
| 449 |
-
position: absolute;
|
| 450 |
-
top: 50%;
|
| 451 |
-
left: 50%;
|
| 452 |
-
width: 0;
|
| 453 |
-
height: 0;
|
| 454 |
-
background: rgba(255,255,255,0.2);
|
| 455 |
-
border-radius: 50%;
|
| 456 |
-
transform: translate(-50%, -50%);
|
| 457 |
-
transition: all 0.5s ease;
|
| 458 |
-
}}
|
| 459 |
-
|
| 460 |
-
.cta-button:hover::before {{
|
| 461 |
-
width: 300px;
|
| 462 |
-
height: 300px;
|
| 463 |
-
}}
|
| 464 |
-
|
| 465 |
-
.cta-button:hover {{
|
| 466 |
-
transform: scale(1.05);
|
| 467 |
-
box-shadow: 0 10px 30px rgba(255,107,107,0.4);
|
| 468 |
-
}}
|
| 469 |
-
|
| 470 |
-
/* Animationer */
|
| 471 |
-
@keyframes gradientShift {{
|
| 472 |
-
0%, 100% {{ background-position: 0% 50%; }}
|
| 473 |
-
50% {{ background-position: 100% 50%; }}
|
| 474 |
-
}}
|
| 475 |
-
|
| 476 |
-
@keyframes countUp {{
|
| 477 |
-
from {{ opacity: 0; transform: scale(0.5); }}
|
| 478 |
-
to {{ opacity: 1; transform: scale(1); }}
|
| 479 |
-
}}
|
| 480 |
-
|
| 481 |
-
@keyframes float {{
|
| 482 |
-
0%, 100% {{ transform: translateY(0px); }}
|
| 483 |
-
50% {{ transform: translateY(-20px); }}
|
| 484 |
-
}}
|
| 485 |
-
|
| 486 |
-
@keyframes bounce {{
|
| 487 |
-
0%, 20%, 50%, 80%, 100% {{ transform: translateX(-50%) translateY(0); }}
|
| 488 |
-
40% {{ transform: translateX(-50%) translateY(-10px); }}
|
| 489 |
-
60% {{ transform: translateX(-50%) translateY(-5px); }}
|
| 490 |
-
}}
|
| 491 |
-
|
| 492 |
-
@keyframes fadeInUp {{
|
| 493 |
-
from {{ opacity: 0; transform: translateY(30px); }}
|
| 494 |
-
to {{ opacity: 1; transform: translateY(0); }}
|
| 495 |
-
}}
|
| 496 |
-
|
| 497 |
-
/* Responsiv design */
|
| 498 |
-
@media (max-width: 768px) {{
|
| 499 |
-
.stats-grid {{
|
| 500 |
-
grid-template-columns: 1fr;
|
| 501 |
-
gap: 20px;
|
| 502 |
-
}}
|
| 503 |
-
|
| 504 |
-
.artist-showcase {{
|
| 505 |
-
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
| 506 |
-
}}
|
| 507 |
-
|
| 508 |
-
.diversity-visual {{
|
| 509 |
-
height: auto;
|
| 510 |
-
}}
|
| 511 |
-
|
| 512 |
-
.genre-bubble {{
|
| 513 |
-
width: 60px;
|
| 514 |
-
height: 60px;
|
| 515 |
-
font-size: 1.2rem;
|
| 516 |
-
}}
|
| 517 |
-
}}
|
| 518 |
-
</style>
|
| 519 |
-
</head>
|
| 520 |
-
<body>
|
| 521 |
-
<div class="story-container">
|
| 522 |
-
|
| 523 |
-
<!-- Introduktion -->
|
| 524 |
-
<div class="story-section section-intro" id="intro">
|
| 525 |
-
<div class="story-title">🌻 SOMMAR I P1</div>
|
| 526 |
-
<div class="story-subtitle">En visuell resa genom 67 år av svenska berättelser</div>
|
| 527 |
-
<div class="story-content">
|
| 528 |
-
Sedan 1958 har Sveriges mest älskade radioprogram format generationer av lyssnare.
|
| 529 |
-
Nu tar vi dig med på en datadriven upptäcktsfärd genom historien.
|
| 530 |
-
</div>
|
| 531 |
-
<div class="scroll-indicator" onclick="scrollToNext(this)">⬇ Scrolla för att börja resan</div>
|
| 532 |
-
</div>
|
| 533 |
-
|
| 534 |
-
<!-- Tidslinje -->
|
| 535 |
-
<div class="story-section section-timeline" id="timeline">
|
| 536 |
-
<div class="story-title">67 År av Historia</div>
|
| 537 |
-
<div class="story-subtitle">1958 → 2025</div>
|
| 538 |
-
|
| 539 |
-
<div class="interactive-timeline">
|
| 540 |
-
<div class="timeline-bar">
|
| 541 |
-
<div class="timeline-point" style="left: 5%">
|
| 542 |
-
<div class="timeline-label">1958</div>
|
| 543 |
-
<div class="timeline-info">Start: Arne Weise</div>
|
| 544 |
-
</div>
|
| 545 |
-
<div class="timeline-point" style="left: 25%">
|
| 546 |
-
<div class="timeline-label">1975</div>
|
| 547 |
-
<div class="timeline-info">Guldåldern börjar</div>
|
| 548 |
-
</div>
|
| 549 |
-
<div class="timeline-point" style="left: 50%">
|
| 550 |
-
<div class="timeline-label">1990</div>
|
| 551 |
-
<div class="timeline-info">Kulturell institution</div>
|
| 552 |
-
</div>
|
| 553 |
-
<div class="timeline-point" style="left: 75%">
|
| 554 |
-
<div class="timeline-label">2010</div>
|
| 555 |
-
<div class="timeline-info">Digital revolution</div>
|
| 556 |
-
</div>
|
| 557 |
-
<div class="timeline-point" style="left: 95%">
|
| 558 |
-
<div class="timeline-label">2025</div>
|
| 559 |
-
<div class="timeline-info">Fortsatt stark</div>
|
| 560 |
-
</div>
|
| 561 |
-
</div>
|
| 562 |
</div>
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
Sommar i P1 har följt med genom alla förändringar
|
| 567 |
</div>
|
| 568 |
-
<div class="
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
<!-- Statistik -->
|
| 572 |
-
<div class="story-section section-stats" id="stats">
|
| 573 |
-
<div class="story-title">Imponerande Siffror</div>
|
| 574 |
-
<div class="story-subtitle">Datadrivet perspektiv</div>
|
| 575 |
-
|
| 576 |
-
<div class="stats-grid">
|
| 577 |
-
<div class="stat-card">
|
| 578 |
-
<div class="stat-number">{stats['total_episodes']}</div>
|
| 579 |
-
<div class="stat-label">Sommarpratare</div>
|
| 580 |
-
<div class="stat-description">Unika berättelser från kända svenskar</div>
|
| 581 |
-
</div>
|
| 582 |
-
|
| 583 |
-
<div class="stat-card">
|
| 584 |
-
<div class="stat-number">{stats['total_songs']:,}</div>
|
| 585 |
-
<div class="stat-label">Musikaliska Ögonblick</div>
|
| 586 |
-
<div class="stat-description">Låtar som format svenska sommrar</div>
|
| 587 |
-
</div>
|
| 588 |
-
|
| 589 |
-
<div class="stat-card">
|
| 590 |
-
<div class="stat-number">{stats['unique_artists']:,}</div>
|
| 591 |
-
<div class="stat-label">Unika Artister</div>
|
| 592 |
-
<div class="stat-description">Från lokala till internationella stjärnor</div>
|
| 593 |
-
</div>
|
| 594 |
-
|
| 595 |
-
<div class="stat-card">
|
| 596 |
-
<div class="stat-number">{stats['shannon_diversity']:.1f}</div>
|
| 597 |
-
<div class="stat-label">Diversitetsindex</div>
|
| 598 |
-
<div class="stat-description">Shannon-Wiener mått på musikal mångfald</div>
|
| 599 |
-
</div>
|
| 600 |
</div>
|
| 601 |
-
|
| 602 |
-
<div class="story-content">
|
| 603 |
-
Varje siffra representerar tusentals minnen och känslor delade av svenska lyssnare
|
| 604 |
-
</div>
|
| 605 |
-
<div class="scroll-indicator" onclick="scrollToNext(this)">⬇ Träffa artisterna</div>
|
| 606 |
</div>
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
|
|
|
|
|
|
|
|
|
| 614 |
"""
|
| 615 |
|
| 616 |
-
# Lägg till
|
| 617 |
-
for
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 618 |
html += f"""
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
|
|
|
|
|
|
|
|
|
| 624 |
"""
|
| 625 |
|
| 626 |
-
html +=
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 632 |
</div>
|
| 633 |
-
<div class="scroll-indicator" onclick="scrollToNext(this)">⬇ Utforska mångfalden</div>
|
| 634 |
</div>
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
<div class="genre-bubble" title="Rock & Pop">🎸</div>
|
| 643 |
-
<div class="genre-bubble" title="Jazz & Blues">🎺</div>
|
| 644 |
-
<div class="genre-bubble" title="Klassisk">🎻</div>
|
| 645 |
-
<div class="genre-bubble" title="Folk & Country">🪕</div>
|
| 646 |
-
<div class="genre-bubble" title="Elektronisk">🎧</div>
|
| 647 |
-
<div class="genre-bubble" title="Världsmusik">🌍</div>
|
| 648 |
</div>
|
| 649 |
|
| 650 |
-
<div class="
|
| 651 |
-
<div class="
|
| 652 |
-
|
| 653 |
-
<div class="
|
| 654 |
-
<div
|
| 655 |
-
</div>
|
| 656 |
-
|
| 657 |
-
<div class="stat-card">
|
| 658 |
-
<div class="stat-number">85%</div>
|
| 659 |
-
<div class="stat-label">Svenska Artister</div>
|
| 660 |
-
<div class="stat-description">Stark representation av inhemsk musik</div>
|
| 661 |
</div>
|
| 662 |
</div>
|
| 663 |
|
| 664 |
-
<div class="
|
| 665 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 666 |
</div>
|
| 667 |
-
<div class="scroll-indicator" onclick="scrollToNext(this)">⬇ Tidsperioder</div>
|
| 668 |
</div>
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 680 |
"""
|
| 681 |
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
<div class="stat-description">
|
| 689 |
-
{data['songs']} låtar • Ø {data['avg_songs']} per program<br>
|
| 690 |
-
Populärast: {data['top_genre']}
|
| 691 |
-
</div>
|
| 692 |
-
</div>
|
| 693 |
-
"""
|
| 694 |
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 698 |
<div class="story-content">
|
| 699 |
-
|
|
|
|
| 700 |
</div>
|
| 701 |
-
<
|
|
|
|
|
|
|
|
|
|
| 702 |
</div>
|
| 703 |
|
| 704 |
-
<!--
|
| 705 |
-
<div class="story-section section-
|
| 706 |
-
|
| 707 |
-
<
|
| 708 |
-
|
| 709 |
-
<div class="story-content">
|
| 710 |
-
Du har sett höjdpunkterna - nu är det dags att dyka djupare in i
|
| 711 |
-
den rika dataskatt som Sommar i P1 representerar
|
| 712 |
-
</div>
|
| 713 |
-
|
| 714 |
-
<button class="cta-button" onclick="showDashboard()">
|
| 715 |
-
🚀 Utforska Fullständiga Datan
|
| 716 |
</button>
|
| 717 |
</div>
|
|
|
|
|
|
|
|
|
|
| 718 |
</div>
|
| 719 |
|
| 720 |
<script>
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
document.querySelectorAll('.story-section').forEach(section => {{
|
| 742 |
-
observer.observe(section);
|
| 743 |
-
}});
|
| 744 |
-
|
| 745 |
-
// Visa första sektionen efter kort delay
|
| 746 |
-
setTimeout(() => {{
|
| 747 |
-
document.getElementById('intro').classList.add('visible');
|
| 748 |
-
}}, 1000);
|
| 749 |
-
|
| 750 |
-
// Scroll-funktioner
|
| 751 |
-
function scrollToNext(element) {{
|
| 752 |
-
const currentSection = element.closest('.story-section');
|
| 753 |
-
const nextSection = currentSection.nextElementSibling;
|
| 754 |
-
if (nextSection) {{
|
| 755 |
-
nextSection.scrollIntoView({{ behavior: 'smooth' }});
|
| 756 |
-
}}
|
| 757 |
-
}}
|
| 758 |
-
|
| 759 |
-
// Skapa periods chart
|
| 760 |
-
function createPeriodsChart() {{
|
| 761 |
-
const ctx = document.getElementById('periodsChart');
|
| 762 |
-
if (!ctx || ctx.chartCreated) return;
|
| 763 |
-
|
| 764 |
-
const periodsData = {json.dumps(list(stats['periods'].items()))};
|
| 765 |
-
const labels = periodsData.map(p => p[0]);
|
| 766 |
-
const episodes = periodsData.map(p => p[1].episodes);
|
| 767 |
-
const songs = periodsData.map(p => p[1].songs);
|
| 768 |
-
|
| 769 |
-
new Chart(ctx, {{
|
| 770 |
-
type: 'line',
|
| 771 |
-
data: {{
|
| 772 |
-
labels: labels,
|
| 773 |
-
datasets: [{{
|
| 774 |
-
label: 'Episoder',
|
| 775 |
-
data: episodes,
|
| 776 |
-
borderColor: '#ff6b6b',
|
| 777 |
-
backgroundColor: 'rgba(255, 107, 107, 0.1)',
|
| 778 |
-
tension: 0.4,
|
| 779 |
-
fill: true
|
| 780 |
-
}}, {{
|
| 781 |
-
label: 'Låtar',
|
| 782 |
-
data: songs,
|
| 783 |
-
borderColor: '#4ecdc4',
|
| 784 |
-
backgroundColor: 'rgba(78, 205, 196, 0.1)',
|
| 785 |
-
tension: 0.4,
|
| 786 |
-
fill: true,
|
| 787 |
-
yAxisID: 'y1'
|
| 788 |
-
}}]
|
| 789 |
-
}},
|
| 790 |
-
options: {{
|
| 791 |
-
responsive: true,
|
| 792 |
-
maintainAspectRatio: false,
|
| 793 |
-
plugins: {{
|
| 794 |
-
legend: {{
|
| 795 |
-
labels: {{
|
| 796 |
-
color: '#fff'
|
| 797 |
-
}}
|
| 798 |
-
}}
|
| 799 |
-
}},
|
| 800 |
-
scales: {{
|
| 801 |
-
x: {{
|
| 802 |
-
ticks: {{
|
| 803 |
-
color: '#fff'
|
| 804 |
-
}},
|
| 805 |
-
grid: {{
|
| 806 |
-
color: 'rgba(255,255,255,0.1)'
|
| 807 |
-
}}
|
| 808 |
-
}},
|
| 809 |
-
y: {{
|
| 810 |
-
type: 'linear',
|
| 811 |
-
display: true,
|
| 812 |
-
position: 'left',
|
| 813 |
-
ticks: {{
|
| 814 |
-
color: '#fff'
|
| 815 |
-
}},
|
| 816 |
-
grid: {{
|
| 817 |
-
color: 'rgba(255,255,255,0.1)'
|
| 818 |
-
}}
|
| 819 |
-
}},
|
| 820 |
-
y1: {{
|
| 821 |
-
type: 'linear',
|
| 822 |
-
display: true,
|
| 823 |
-
position: 'right',
|
| 824 |
-
ticks: {{
|
| 825 |
-
color: '#fff'
|
| 826 |
-
}},
|
| 827 |
-
grid: {{
|
| 828 |
-
drawOnChartArea: false,
|
| 829 |
-
color: 'rgba(255,255,255,0.1)'
|
| 830 |
-
}}
|
| 831 |
-
}}
|
| 832 |
-
}}
|
| 833 |
-
}}
|
| 834 |
-
}});
|
| 835 |
-
|
| 836 |
-
ctx.chartCreated = true;
|
| 837 |
-
}}
|
| 838 |
-
|
| 839 |
-
// Interaktivitet för timeline
|
| 840 |
-
document.querySelectorAll('.timeline-point').forEach(point => {{
|
| 841 |
-
point.addEventListener('mouseenter', () => {{
|
| 842 |
-
gsap.to(point, {{duration: 0.3, scale: 1.3, ease: "back.out(1.7)"}});
|
| 843 |
-
}});
|
| 844 |
-
|
| 845 |
-
point.addEventListener('mouseleave', () => {{
|
| 846 |
-
gsap.to(point, {{duration: 0.3, scale: 1, ease: "back.out(1.7)"}});
|
| 847 |
-
}});
|
| 848 |
-
}});
|
| 849 |
-
|
| 850 |
-
// Animera genre bubbles
|
| 851 |
-
document.querySelectorAll('.genre-bubble').forEach((bubble, index) => {{
|
| 852 |
-
bubble.addEventListener('mouseenter', () => {{
|
| 853 |
-
gsap.to(bubble, {{duration: 0.3, scale: 1.2, rotation: 360, ease: "back.out(1.7)"}});
|
| 854 |
-
}});
|
| 855 |
-
|
| 856 |
-
bubble.addEventListener('mouseleave', () => {{
|
| 857 |
-
gsap.to(bubble, {{duration: 0.3, scale: 1, rotation: 0, ease: "back.out(1.7)"}});
|
| 858 |
-
}});
|
| 859 |
-
}});
|
| 860 |
-
|
| 861 |
-
// Animera statistik-kort
|
| 862 |
-
document.querySelectorAll('.stat-card').forEach(card => {{
|
| 863 |
-
card.addEventListener('mouseenter', () => {{
|
| 864 |
-
gsap.to(card, {{duration: 0.3, y: -10, scale: 1.02, ease: "power2.out"}});
|
| 865 |
-
}});
|
| 866 |
-
|
| 867 |
-
card.addEventListener('mouseleave', () => {{
|
| 868 |
-
gsap.to(card, {{duration: 0.3, y: 0, scale: 1, ease: "power2.out"}});
|
| 869 |
-
}});
|
| 870 |
-
}});
|
| 871 |
-
|
| 872 |
-
// Dashboard-funktion
|
| 873 |
-
function showDashboard() {{
|
| 874 |
-
alert('Övergång till interaktiv dashboard implementeras...');
|
| 875 |
-
}}
|
| 876 |
-
|
| 877 |
-
// Partikel-bakgrund (enkel version)
|
| 878 |
-
function createParticles() {{
|
| 879 |
-
const canvas = document.createElement('canvas');
|
| 880 |
-
canvas.style.position = 'fixed';
|
| 881 |
-
canvas.style.top = '0';
|
| 882 |
-
canvas.style.left = '0';
|
| 883 |
-
canvas.style.width = '100vw';
|
| 884 |
-
canvas.style.height = '100vh';
|
| 885 |
-
canvas.style.pointerEvents = 'none';
|
| 886 |
-
canvas.style.zIndex = '0';
|
| 887 |
-
document.body.appendChild(canvas);
|
| 888 |
-
|
| 889 |
-
const ctx = canvas.getContext('2d');
|
| 890 |
-
canvas.width = window.innerWidth;
|
| 891 |
-
canvas.height = window.innerHeight;
|
| 892 |
-
|
| 893 |
-
const particles = [];
|
| 894 |
-
const particleCount = 50;
|
| 895 |
-
|
| 896 |
-
for (let i = 0; i < particleCount; i++) {{
|
| 897 |
-
particles.push({{
|
| 898 |
-
x: Math.random() * canvas.width,
|
| 899 |
-
y: Math.random() * canvas.height,
|
| 900 |
-
vx: (Math.random() - 0.5) * 0.5,
|
| 901 |
-
vy: (Math.random() - 0.5) * 0.5,
|
| 902 |
-
size: Math.random() * 2 + 1,
|
| 903 |
-
opacity: Math.random() * 0.5 + 0.1
|
| 904 |
-
}});
|
| 905 |
-
}}
|
| 906 |
-
|
| 907 |
-
function animate() {{
|
| 908 |
-
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
| 909 |
-
|
| 910 |
-
particles.forEach(particle => {{
|
| 911 |
-
particle.x += particle.vx;
|
| 912 |
-
particle.y += particle.vy;
|
| 913 |
-
|
| 914 |
-
if (particle.x < 0 || particle.x > canvas.width) particle.vx *= -1;
|
| 915 |
-
if (particle.y < 0 || particle.y > canvas.height) particle.vy *= -1;
|
| 916 |
-
|
| 917 |
-
ctx.beginPath();
|
| 918 |
-
ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
|
| 919 |
-
ctx.fillStyle = `rgba(255, 255, 255, ${{particle.opacity}})`;
|
| 920 |
-
ctx.fill();
|
| 921 |
-
}});
|
| 922 |
-
|
| 923 |
-
requestAnimationFrame(animate);
|
| 924 |
-
}}
|
| 925 |
-
|
| 926 |
-
animate();
|
| 927 |
-
}}
|
| 928 |
-
|
| 929 |
-
// Starta partikel-animation
|
| 930 |
-
createParticles();
|
| 931 |
-
|
| 932 |
-
// Responsiv hantering
|
| 933 |
-
window.addEventListener('resize', () => {{
|
| 934 |
-
// Uppdatera canvas storlek
|
| 935 |
-
const canvas = document.querySelector('canvas');
|
| 936 |
-
if (canvas) {{
|
| 937 |
-
canvas.width = window.innerWidth;
|
| 938 |
-
canvas.height = window.innerHeight;
|
| 939 |
-
}}
|
| 940 |
-
}});
|
| 941 |
</script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 942 |
</body>
|
| 943 |
</html>
|
| 944 |
"""
|
| 945 |
|
| 946 |
return html
|
| 947 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 948 |
# Skapa Gradio-appen
|
| 949 |
-
def
|
| 950 |
-
"""Skapa huvudapplikationen"""
|
| 951 |
|
| 952 |
-
with gr.Blocks(title="Sommar i P1 -
|
| 953 |
|
| 954 |
-
# Huvudvy - Visuell berättelse
|
| 955 |
-
story_html = gr.HTML(
|
| 956 |
|
| 957 |
-
#
|
| 958 |
-
with gr.Row():
|
| 959 |
-
|
| 960 |
|
| 961 |
-
# Placeholder för dashboard-funktionalitet
|
| 962 |
-
dashboard_btn.click(
|
| 963 |
-
lambda: gr.Info("Dashboard-funktionalitet kommer snart!"),
|
| 964 |
-
outputs=None
|
| 965 |
-
)
|
| 966 |
-
|
| 967 |
return app
|
| 968 |
|
| 969 |
# Huvudapplikation
|
| 970 |
-
app =
|
| 971 |
|
| 972 |
if __name__ == "__main__":
|
| 973 |
app.launch()
|
|
|
|
| 10 |
import numpy as np
|
| 11 |
import statistics
|
| 12 |
import math
|
| 13 |
+
import os
|
| 14 |
+
from image_matcher import SommarImageMatcher
|
| 15 |
|
| 16 |
# Ladda data globalt
|
| 17 |
+
print("🎨 Laddar Sommar i P1 för visuell storytelling med profilbilder...")
|
| 18 |
try:
|
| 19 |
with open('data.json', 'r', encoding='utf-8') as f:
|
| 20 |
DATA = json.load(f)
|
| 21 |
with open('report.json', 'r', encoding='utf-8') as f:
|
| 22 |
REPORT = json.load(f)
|
| 23 |
+
|
| 24 |
+
# Skapa image matcher
|
| 25 |
+
IMAGE_MATCHER = SommarImageMatcher('images', 'data.json')
|
| 26 |
+
|
| 27 |
print(f"✅ Dataset laddat: {len(DATA)} episoder, {REPORT['summary']['total_songs']} låtar")
|
| 28 |
+
print(f"📷 Bildmatcher initierad med {len([img for images in IMAGE_MATCHER.image_map.values() for img in images])} bilder")
|
| 29 |
except Exception as e:
|
| 30 |
print(f"❌ Fel vid laddning: {e}")
|
| 31 |
DATA = []
|
| 32 |
REPORT = {"summary": {"total_episodes": 0, "total_songs": 0, "excluded_signatures": 0}}
|
| 33 |
+
IMAGE_MATCHER = None
|
| 34 |
|
| 35 |
+
def get_episode_by_title(title):
|
| 36 |
+
"""Hitta episod baserat på titel"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
for episode in DATA:
|
| 38 |
+
if episode.get('episode_title', '').strip().lower() == title.strip().lower():
|
| 39 |
+
return episode
|
| 40 |
+
return None
|
| 41 |
+
|
| 42 |
+
def create_profile_modal_html():
|
| 43 |
+
"""Skapa HTML för profilmodal"""
|
| 44 |
+
return """
|
| 45 |
+
<div id="profileModal" class="modal" style="display: none;">
|
| 46 |
+
<div class="modal-content">
|
| 47 |
+
<span class="close" onclick="closeProfileModal()">×</span>
|
| 48 |
+
<div id="profileContent">
|
| 49 |
+
<!-- Profilinnehåll laddas här dynamiskt -->
|
| 50 |
+
</div>
|
| 51 |
+
</div>
|
| 52 |
+
</div>
|
| 53 |
+
|
| 54 |
+
<style>
|
| 55 |
+
.modal {
|
| 56 |
+
position: fixed;
|
| 57 |
+
z-index: 10000;
|
| 58 |
+
left: 0;
|
| 59 |
+
top: 0;
|
| 60 |
+
width: 100%;
|
| 61 |
+
height: 100%;
|
| 62 |
+
background-color: rgba(0,0,0,0.8);
|
| 63 |
+
backdrop-filter: blur(10px);
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
.modal-content {
|
| 67 |
+
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
| 68 |
+
margin: 2% auto;
|
| 69 |
+
padding: 0;
|
| 70 |
+
border-radius: 20px;
|
| 71 |
+
width: 90%;
|
| 72 |
+
max-width: 800px;
|
| 73 |
+
height: 90%;
|
| 74 |
+
overflow: hidden;
|
| 75 |
+
position: relative;
|
| 76 |
+
box-shadow: 0 25px 50px rgba(0,0,0,0.5);
|
| 77 |
+
border: 1px solid rgba(255,255,255,0.1);
|
| 78 |
}
|
| 79 |
+
|
| 80 |
+
.close {
|
| 81 |
+
color: #fff;
|
| 82 |
+
float: right;
|
| 83 |
+
font-size: 28px;
|
| 84 |
+
font-weight: bold;
|
| 85 |
+
position: absolute;
|
| 86 |
+
right: 20px;
|
| 87 |
+
top: 15px;
|
| 88 |
+
z-index: 10001;
|
| 89 |
+
cursor: pointer;
|
| 90 |
+
background: rgba(0,0,0,0.5);
|
| 91 |
+
border-radius: 50%;
|
| 92 |
+
width: 40px;
|
| 93 |
+
height: 40px;
|
| 94 |
+
display: flex;
|
| 95 |
+
align-items: center;
|
| 96 |
+
justify-content: center;
|
| 97 |
+
transition: all 0.3s ease;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
.close:hover {
|
| 101 |
+
background: rgba(255,255,255,0.2);
|
| 102 |
+
transform: scale(1.1);
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.profile-header {
|
| 106 |
+
background: linear-gradient(135deg, #ff6b6b 0%, #4ecdc4 100%);
|
| 107 |
+
padding: 30px;
|
| 108 |
+
color: white;
|
| 109 |
+
position: relative;
|
| 110 |
+
overflow: hidden;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
.profile-header::before {
|
| 114 |
+
content: '';
|
| 115 |
+
position: absolute;
|
| 116 |
+
top: -50%;
|
| 117 |
+
left: -50%;
|
| 118 |
+
width: 200%;
|
| 119 |
+
height: 200%;
|
| 120 |
+
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="2" fill="rgba(255,255,255,0.1)"/></svg>') repeat;
|
| 121 |
+
animation: float 20s linear infinite;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
.profile-image {
|
| 125 |
+
width: 120px;
|
| 126 |
+
height: 120px;
|
| 127 |
+
border-radius: 50%;
|
| 128 |
+
border: 4px solid rgba(255,255,255,0.3);
|
| 129 |
+
object-fit: cover;
|
| 130 |
+
margin-bottom: 20px;
|
| 131 |
+
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
.profile-name {
|
| 135 |
+
font-size: 2.5rem;
|
| 136 |
+
font-weight: bold;
|
| 137 |
+
margin-bottom: 10px;
|
| 138 |
+
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
.profile-subtitle {
|
| 142 |
+
font-size: 1.2rem;
|
| 143 |
+
opacity: 0.9;
|
| 144 |
+
margin-bottom: 20px;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
.profile-stats {
|
| 148 |
+
display: grid;
|
| 149 |
+
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
| 150 |
+
gap: 15px;
|
| 151 |
+
margin-top: 20px;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
.stat-item {
|
| 155 |
+
text-align: center;
|
| 156 |
+
background: rgba(255,255,255,0.1);
|
| 157 |
+
padding: 15px;
|
| 158 |
+
border-radius: 10px;
|
| 159 |
+
backdrop-filter: blur(10px);
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
.stat-number {
|
| 163 |
+
font-size: 1.8rem;
|
| 164 |
+
font-weight: bold;
|
| 165 |
+
display: block;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
.stat-label {
|
| 169 |
+
font-size: 0.9rem;
|
| 170 |
+
opacity: 0.8;
|
| 171 |
+
text-transform: uppercase;
|
| 172 |
+
letter-spacing: 1px;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
.profile-body {
|
| 176 |
+
padding: 30px;
|
| 177 |
+
height: calc(100% - 250px);
|
| 178 |
+
overflow-y: auto;
|
| 179 |
+
color: #fff;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
.profile-section {
|
| 183 |
+
margin-bottom: 30px;
|
| 184 |
+
padding: 20px;
|
| 185 |
+
background: rgba(255,255,255,0.05);
|
| 186 |
+
border-radius: 15px;
|
| 187 |
+
border-left: 4px solid #4ecdc4;
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
.section-title {
|
| 191 |
+
font-size: 1.4rem;
|
| 192 |
+
font-weight: bold;
|
| 193 |
+
margin-bottom: 15px;
|
| 194 |
+
color: #4ecdc4;
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
.episode-item {
|
| 198 |
+
background: rgba(255,255,255,0.05);
|
| 199 |
+
padding: 15px;
|
| 200 |
+
border-radius: 10px;
|
| 201 |
+
margin-bottom: 10px;
|
| 202 |
+
transition: all 0.3s ease;
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
.episode-item:hover {
|
| 206 |
+
background: rgba(255,255,255,0.1);
|
| 207 |
+
transform: translateX(5px);
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
.episode-date {
|
| 211 |
+
font-size: 0.9rem;
|
| 212 |
+
color: #ffaa00;
|
| 213 |
+
font-weight: bold;
|
| 214 |
+
margin-bottom: 5px;
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
.song-list {
|
| 218 |
+
display: grid;
|
| 219 |
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
| 220 |
+
gap: 15px;
|
| 221 |
+
margin-top: 15px;
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
.song-item {
|
| 225 |
+
background: rgba(0,0,0,0.2);
|
| 226 |
+
padding: 12px;
|
| 227 |
+
border-radius: 8px;
|
| 228 |
+
transition: all 0.3s ease;
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
.song-item:hover {
|
| 232 |
+
background: rgba(0,0,0,0.4);
|
| 233 |
+
transform: scale(1.02);
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
.song-title {
|
| 237 |
+
font-weight: bold;
|
| 238 |
+
margin-bottom: 5px;
|
| 239 |
+
color: #fff;
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
.song-artist {
|
| 243 |
+
font-size: 0.9rem;
|
| 244 |
+
color: #ccc;
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
.biography {
|
| 248 |
+
line-height: 1.6;
|
| 249 |
+
font-size: 1.1rem;
|
| 250 |
+
color: #e0e0e0;
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
/* Scrollbar styling */
|
| 254 |
+
.profile-body::-webkit-scrollbar {
|
| 255 |
+
width: 8px;
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
.profile-body::-webkit-scrollbar-track {
|
| 259 |
+
background: rgba(255,255,255,0.1);
|
| 260 |
+
border-radius: 4px;
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
.profile-body::-webkit-scrollbar-thumb {
|
| 264 |
+
background: rgba(255,255,255,0.3);
|
| 265 |
+
border-radius: 4px;
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
.profile-body::-webkit-scrollbar-thumb:hover {
|
| 269 |
+
background: rgba(255,255,255,0.5);
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
@keyframes float {
|
| 273 |
+
0% { transform: translateX(-100px); }
|
| 274 |
+
100% { transform: translateX(100px); }
|
| 275 |
+
}
|
| 276 |
+
</style>
|
| 277 |
+
"""
|
| 278 |
|
| 279 |
+
def create_sommarpratare_gallery():
|
| 280 |
+
"""Skapa interaktivt galleri med alla sommarpratare"""
|
| 281 |
+
if not IMAGE_MATCHER:
|
| 282 |
+
return "<div>Bildgalleri kunde inte laddas</div>"
|
| 283 |
+
|
| 284 |
+
all_images = IMAGE_MATCHER.get_all_images_with_metadata()
|
| 285 |
+
gallery_data = IMAGE_MATCHER.generate_image_gallery_data()
|
| 286 |
|
| 287 |
html = f"""
|
| 288 |
+
<div class="sommarpratare-gallery">
|
| 289 |
+
<div class="gallery-header">
|
| 290 |
+
<h2>🎭 Sommarpratare Genom Tiderna</h2>
|
| 291 |
+
<div class="gallery-stats">
|
| 292 |
+
<div class="stat-card">
|
| 293 |
+
<div class="stat-number">{gallery_data['total_images']}</div>
|
| 294 |
+
<div class="stat-label">Profilbilder</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
</div>
|
| 296 |
+
<div class="stat-card">
|
| 297 |
+
<div class="stat-number">{gallery_data['unique_people']}</div>
|
| 298 |
+
<div class="stat-label">Unika Personer</div>
|
|
|
|
| 299 |
</div>
|
| 300 |
+
<div class="stat-card">
|
| 301 |
+
<div class="stat-number">{len(gallery_data['decades'])}</div>
|
| 302 |
+
<div class="stat-label">Årtionden</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 304 |
</div>
|
| 305 |
+
</div>
|
| 306 |
+
|
| 307 |
+
<div class="gallery-filters">
|
| 308 |
+
<button class="filter-btn active" onclick="filterGallery('all')">Alla</button>
|
| 309 |
+
<button class="filter-btn" onclick="filterGallery('multiple')">Flera Episoder</button>
|
| 310 |
+
<button class="filter-btn" onclick="filterGallery('recent')">2020-talet</button>
|
| 311 |
+
<button class="filter-btn" onclick="filterGallery('classic')">1900-talet</button>
|
| 312 |
+
</div>
|
| 313 |
+
|
| 314 |
+
<div class="image-grid" id="imageGrid">
|
| 315 |
"""
|
| 316 |
|
| 317 |
+
# Lägg till bilder
|
| 318 |
+
for image in all_images[:100]: # Begränsa till 100 bilder för prestanda
|
| 319 |
+
episodes_info = ""
|
| 320 |
+
if image['matching_episodes']:
|
| 321 |
+
episodes_info = f"<div class='image-episodes'>{len(image['matching_episodes'])} episoder</div>"
|
| 322 |
+
|
| 323 |
+
year_class = ""
|
| 324 |
+
if image['year']:
|
| 325 |
+
year = int(image['year'])
|
| 326 |
+
if year >= 2020:
|
| 327 |
+
year_class = "recent"
|
| 328 |
+
elif year < 2000:
|
| 329 |
+
year_class = "classic"
|
| 330 |
+
|
| 331 |
+
multiple_class = "multiple" if len(image['matching_episodes']) > 1 else ""
|
| 332 |
+
|
| 333 |
html += f"""
|
| 334 |
+
<div class="image-card {year_class} {multiple_class}" onclick="showProfile('{image['name']}', '{image['year'] or ''}')">
|
| 335 |
+
<img src="{image['path']}" alt="{image['name']}" loading="lazy">
|
| 336 |
+
<div class="image-overlay">
|
| 337 |
+
<div class="image-name">{image['name']}</div>
|
| 338 |
+
<div class="image-year">{image['year'] or 'Okänt år'}</div>
|
| 339 |
+
{episodes_info}
|
| 340 |
+
</div>
|
| 341 |
+
</div>
|
| 342 |
"""
|
| 343 |
|
| 344 |
+
html += """
|
| 345 |
+
</div>
|
| 346 |
+
</div>
|
| 347 |
+
|
| 348 |
+
<style>
|
| 349 |
+
.sommarpratare-gallery {
|
| 350 |
+
padding: 20px;
|
| 351 |
+
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
| 352 |
+
border-radius: 20px;
|
| 353 |
+
margin: 20px 0;
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
.gallery-header h2 {
|
| 357 |
+
color: #fff;
|
| 358 |
+
text-align: center;
|
| 359 |
+
margin-bottom: 30px;
|
| 360 |
+
font-size: 2.5rem;
|
| 361 |
+
background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
|
| 362 |
+
-webkit-background-clip: text;
|
| 363 |
+
-webkit-text-fill-color: transparent;
|
| 364 |
+
background-clip: text;
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
.gallery-stats {
|
| 368 |
+
display: grid;
|
| 369 |
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
| 370 |
+
gap: 20px;
|
| 371 |
+
margin-bottom: 30px;
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
.stat-card {
|
| 375 |
+
background: rgba(255,255,255,0.1);
|
| 376 |
+
padding: 20px;
|
| 377 |
+
border-radius: 15px;
|
| 378 |
+
text-align: center;
|
| 379 |
+
backdrop-filter: blur(10px);
|
| 380 |
+
border: 1px solid rgba(255,255,255,0.1);
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
.stat-number {
|
| 384 |
+
font-size: 2rem;
|
| 385 |
+
font-weight: bold;
|
| 386 |
+
color: #ffaa00;
|
| 387 |
+
margin-bottom: 5px;
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
.stat-label {
|
| 391 |
+
color: #ccc;
|
| 392 |
+
font-size: 0.9rem;
|
| 393 |
+
text-transform: uppercase;
|
| 394 |
+
letter-spacing: 1px;
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
.gallery-filters {
|
| 398 |
+
display: flex;
|
| 399 |
+
justify-content: center;
|
| 400 |
+
gap: 15px;
|
| 401 |
+
margin-bottom: 30px;
|
| 402 |
+
flex-wrap: wrap;
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
.filter-btn {
|
| 406 |
+
background: rgba(255,255,255,0.1);
|
| 407 |
+
color: #fff;
|
| 408 |
+
border: 1px solid rgba(255,255,255,0.2);
|
| 409 |
+
padding: 10px 20px;
|
| 410 |
+
border-radius: 25px;
|
| 411 |
+
cursor: pointer;
|
| 412 |
+
transition: all 0.3s ease;
|
| 413 |
+
font-weight: 500;
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
.filter-btn:hover,
|
| 417 |
+
.filter-btn.active {
|
| 418 |
+
background: #4ecdc4;
|
| 419 |
+
border-color: #4ecdc4;
|
| 420 |
+
transform: translateY(-2px);
|
| 421 |
+
box-shadow: 0 5px 15px rgba(78, 205, 196, 0.3);
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
.image-grid {
|
| 425 |
+
display: grid;
|
| 426 |
+
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
| 427 |
+
gap: 20px;
|
| 428 |
+
margin-top: 20px;
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
.image-card {
|
| 432 |
+
position: relative;
|
| 433 |
+
border-radius: 15px;
|
| 434 |
+
overflow: hidden;
|
| 435 |
+
cursor: pointer;
|
| 436 |
+
transition: all 0.3s ease;
|
| 437 |
+
background: rgba(255,255,255,0.05);
|
| 438 |
+
border: 1px solid rgba(255,255,255,0.1);
|
| 439 |
+
aspect-ratio: 1;
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
.image-card:hover {
|
| 443 |
+
transform: translateY(-10px) scale(1.02);
|
| 444 |
+
box-shadow: 0 20px 40px rgba(0,0,0,0.3);
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
.image-card img {
|
| 448 |
+
width: 100%;
|
| 449 |
+
height: 100%;
|
| 450 |
+
object-fit: cover;
|
| 451 |
+
transition: all 0.3s ease;
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
.image-card:hover img {
|
| 455 |
+
transform: scale(1.1);
|
| 456 |
+
}
|
| 457 |
+
|
| 458 |
+
.image-overlay {
|
| 459 |
+
position: absolute;
|
| 460 |
+
bottom: 0;
|
| 461 |
+
left: 0;
|
| 462 |
+
right: 0;
|
| 463 |
+
background: linear-gradient(transparent, rgba(0,0,0,0.8));
|
| 464 |
+
padding: 20px 15px 15px;
|
| 465 |
+
color: white;
|
| 466 |
+
transform: translateY(100%);
|
| 467 |
+
transition: all 0.3s ease;
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
.image-card:hover .image-overlay {
|
| 471 |
+
transform: translateY(0);
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
.image-name {
|
| 475 |
+
font-weight: bold;
|
| 476 |
+
font-size: 1rem;
|
| 477 |
+
margin-bottom: 5px;
|
| 478 |
+
}
|
| 479 |
+
|
| 480 |
+
.image-year {
|
| 481 |
+
font-size: 0.9rem;
|
| 482 |
+
color: #ffaa00;
|
| 483 |
+
margin-bottom: 5px;
|
| 484 |
+
}
|
| 485 |
+
|
| 486 |
+
.image-episodes {
|
| 487 |
+
font-size: 0.8rem;
|
| 488 |
+
color: #4ecdc4;
|
| 489 |
+
background: rgba(78, 205, 196, 0.2);
|
| 490 |
+
padding: 2px 8px;
|
| 491 |
+
border-radius: 10px;
|
| 492 |
+
display: inline-block;
|
| 493 |
+
}
|
| 494 |
+
|
| 495 |
+
/* Filter states */
|
| 496 |
+
.image-card {
|
| 497 |
+
display: block;
|
| 498 |
+
animation: fadeIn 0.5s ease;
|
| 499 |
+
}
|
| 500 |
+
|
| 501 |
+
.image-card.hidden {
|
| 502 |
+
display: none;
|
| 503 |
+
}
|
| 504 |
+
|
| 505 |
+
@keyframes fadeIn {
|
| 506 |
+
from { opacity: 0; transform: scale(0.9); }
|
| 507 |
+
to { opacity: 1; transform: scale(1); }
|
| 508 |
+
}
|
| 509 |
+
|
| 510 |
+
@media (max-width: 768px) {
|
| 511 |
+
.image-grid {
|
| 512 |
+
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
| 513 |
+
gap: 15px;
|
| 514 |
+
}
|
| 515 |
+
|
| 516 |
+
.gallery-stats {
|
| 517 |
+
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
| 518 |
+
gap: 15px;
|
| 519 |
+
}
|
| 520 |
+
|
| 521 |
+
.filter-btn {
|
| 522 |
+
padding: 8px 16px;
|
| 523 |
+
font-size: 0.9rem;
|
| 524 |
+
}
|
| 525 |
+
}
|
| 526 |
+
</style>
|
| 527 |
+
|
| 528 |
+
<script>
|
| 529 |
+
function filterGallery(type) {
|
| 530 |
+
const cards = document.querySelectorAll('.image-card');
|
| 531 |
+
const buttons = document.querySelectorAll('.filter-btn');
|
| 532 |
+
|
| 533 |
+
// Uppdatera aktiv knapp
|
| 534 |
+
buttons.forEach(btn => btn.classList.remove('active'));
|
| 535 |
+
event.target.classList.add('active');
|
| 536 |
+
|
| 537 |
+
// Filtrera kort
|
| 538 |
+
cards.forEach(card => {
|
| 539 |
+
let show = true;
|
| 540 |
+
|
| 541 |
+
switch(type) {
|
| 542 |
+
case 'all':
|
| 543 |
+
show = true;
|
| 544 |
+
break;
|
| 545 |
+
case 'multiple':
|
| 546 |
+
show = card.classList.contains('multiple');
|
| 547 |
+
break;
|
| 548 |
+
case 'recent':
|
| 549 |
+
show = card.classList.contains('recent');
|
| 550 |
+
break;
|
| 551 |
+
case 'classic':
|
| 552 |
+
show = card.classList.contains('classic');
|
| 553 |
+
break;
|
| 554 |
+
}
|
| 555 |
+
|
| 556 |
+
if (show) {
|
| 557 |
+
card.classList.remove('hidden');
|
| 558 |
+
card.style.animation = 'fadeIn 0.5s ease';
|
| 559 |
+
} else {
|
| 560 |
+
card.classList.add('hidden');
|
| 561 |
+
}
|
| 562 |
+
});
|
| 563 |
+
}
|
| 564 |
+
|
| 565 |
+
function showProfile(name, year) {
|
| 566 |
+
// Denna funktion kommer att implementeras för att visa profilmodal
|
| 567 |
+
console.log('Visar profil för:', name, year);
|
| 568 |
+
|
| 569 |
+
// Skapa profilinnehåll
|
| 570 |
+
const profileContent = createProfileContent(name, year);
|
| 571 |
+
document.getElementById('profileContent').innerHTML = profileContent;
|
| 572 |
+
document.getElementById('profileModal').style.display = 'block';
|
| 573 |
+
}
|
| 574 |
+
|
| 575 |
+
function createProfileContent(name, year) {
|
| 576 |
+
// Detta skulle normalt hämta data från servern
|
| 577 |
+
// För nu skapar vi exempel-innehåll
|
| 578 |
+
return `
|
| 579 |
+
<div class="profile-header">
|
| 580 |
+
<img src="images/placeholder.jpg" alt="${name}" class="profile-image">
|
| 581 |
+
<div class="profile-name">${name}</div>
|
| 582 |
+
<div class="profile-subtitle">Sommarpratare ${year || 'Okänt år'}</div>
|
| 583 |
+
<div class="profile-stats">
|
| 584 |
+
<div class="stat-item">
|
| 585 |
+
<span class="stat-number">1</span>
|
| 586 |
+
<span class="stat-label">Episoder</span>
|
| 587 |
+
</div>
|
| 588 |
+
<div class="stat-item">
|
| 589 |
+
<span class="stat-number">13</span>
|
| 590 |
+
<span class="stat-label">Låtar</span>
|
| 591 |
+
</div>
|
| 592 |
+
<div class="stat-item">
|
| 593 |
+
<span class="stat-number">67</span>
|
| 594 |
+
<span class="stat-label">År</span>
|
| 595 |
+
</div>
|
| 596 |
</div>
|
|
|
|
| 597 |
</div>
|
| 598 |
+
<div class="profile-body">
|
| 599 |
+
<div class="profile-section">
|
| 600 |
+
<div class="section-title">📖 Biografi</div>
|
| 601 |
+
<div class="biography">
|
| 602 |
+
En av Sveriges mest älskade kulturpersonligheter som genom sina berättelser
|
| 603 |
+
och sitt musikval har format svenska somrar i generationer.
|
| 604 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 605 |
</div>
|
| 606 |
|
| 607 |
+
<div class="profile-section">
|
| 608 |
+
<div class="section-title">📻 Sommarepisoder</div>
|
| 609 |
+
<div class="episode-item">
|
| 610 |
+
<div class="episode-date">${year || 'Okänt datum'}</div>
|
| 611 |
+
<div>En personlig berättelse om liv, kärlek och musik</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 612 |
</div>
|
| 613 |
</div>
|
| 614 |
|
| 615 |
+
<div class="profile-section">
|
| 616 |
+
<div class="section-title">🎵 Musikval</div>
|
| 617 |
+
<div class="song-list">
|
| 618 |
+
<div class="song-item">
|
| 619 |
+
<div class="song-title">Sommarsång</div>
|
| 620 |
+
<div class="song-artist">Okänd Artist</div>
|
| 621 |
+
</div>
|
| 622 |
+
</div>
|
| 623 |
</div>
|
|
|
|
| 624 |
</div>
|
| 625 |
+
`;
|
| 626 |
+
}
|
| 627 |
+
|
| 628 |
+
function closeProfileModal() {
|
| 629 |
+
document.getElementById('profileModal').style.display = 'none';
|
| 630 |
+
}
|
| 631 |
+
|
| 632 |
+
// Stäng modal vid klick utanför
|
| 633 |
+
window.onclick = function(event) {
|
| 634 |
+
const modal = document.getElementById('profileModal');
|
| 635 |
+
if (event.target == modal) {
|
| 636 |
+
closeProfileModal();
|
| 637 |
+
}
|
| 638 |
+
}
|
| 639 |
+
</script>
|
| 640 |
"""
|
| 641 |
|
| 642 |
+
return html
|
| 643 |
+
|
| 644 |
+
def create_enhanced_visual_story():
|
| 645 |
+
"""Skapa förbättrad visuell berättelse med bildgalleri"""
|
| 646 |
+
# Lägg till profilmodal HTML
|
| 647 |
+
modal_html = create_profile_modal_html()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 648 |
|
| 649 |
+
# Skapa sommarpratare galleri
|
| 650 |
+
gallery_html = create_sommarpratare_gallery()
|
| 651 |
+
|
| 652 |
+
# Kombinera med befintlig storytelling
|
| 653 |
+
stats = calculate_detailed_statistics()
|
| 654 |
+
|
| 655 |
+
html = f"""
|
| 656 |
+
<!DOCTYPE html>
|
| 657 |
+
<html lang="sv">
|
| 658 |
+
<head>
|
| 659 |
+
<meta charset="UTF-8">
|
| 660 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 661 |
+
<title>Sommar i P1 - Visuell Databerättelse med Profilbilder</title>
|
| 662 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
| 663 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
|
| 664 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js"></script>
|
| 665 |
+
</head>
|
| 666 |
+
<body>
|
| 667 |
+
{modal_html}
|
| 668 |
+
|
| 669 |
+
<div class="story-container">
|
| 670 |
+
<!-- Hero med snabb åtkomst till galleri -->
|
| 671 |
+
<div class="story-section section-intro" id="intro">
|
| 672 |
+
<div class="story-title">🌻 SOMMAR I P1</div>
|
| 673 |
+
<div class="story-subtitle">En visuell resa genom 67 år av svenska berättelser</div>
|
| 674 |
<div class="story-content">
|
| 675 |
+
Sedan 1958 har Sveriges mest älskade radioprogram format generationer av lyssnare.
|
| 676 |
+
Nu tar vi dig med på en datadriven upptäcktsfärd genom historien.
|
| 677 |
</div>
|
| 678 |
+
<button class="cta-button" onclick="showGallery()" style="margin: 20px 10px;">
|
| 679 |
+
🎭 Se Alla Sommarpratare
|
| 680 |
+
</button>
|
| 681 |
+
<div class="scroll-indicator" onclick="scrollToNext(this)">⬇ Scrolla för databerättelsen</div>
|
| 682 |
</div>
|
| 683 |
|
| 684 |
+
<!-- Galleri-sektion -->
|
| 685 |
+
<div class="story-section section-gallery" id="gallery" style="display: none;">
|
| 686 |
+
{gallery_html}
|
| 687 |
+
<button class="cta-button" onclick="hideGallery()" style="margin-top: 30px;">
|
| 688 |
+
← Tillbaka till Berättelsen
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 689 |
</button>
|
| 690 |
</div>
|
| 691 |
+
|
| 692 |
+
<!-- Återstående storytelling sektioner -->
|
| 693 |
+
<!-- ... (befintliga sektioner) ... -->
|
| 694 |
</div>
|
| 695 |
|
| 696 |
<script>
|
| 697 |
+
function showGallery() {{
|
| 698 |
+
document.getElementById('intro').style.display = 'none';
|
| 699 |
+
document.getElementById('gallery').style.display = 'block';
|
| 700 |
+
document.getElementById('gallery').scrollIntoView({{ behavior: 'smooth' }});
|
| 701 |
+
}}
|
| 702 |
+
|
| 703 |
+
function hideGallery() {{
|
| 704 |
+
document.getElementById('gallery').style.display = 'none';
|
| 705 |
+
document.getElementById('intro').style.display = 'flex';
|
| 706 |
+
document.getElementById('intro').scrollIntoView({{ behavior: 'smooth' }});
|
| 707 |
+
}}
|
| 708 |
+
|
| 709 |
+
// Befintliga funktioner...
|
| 710 |
+
function scrollToNext(element) {{
|
| 711 |
+
const currentSection = element.closest('.story-section');
|
| 712 |
+
const nextSection = currentSection.nextElementSibling;
|
| 713 |
+
if (nextSection && nextSection.style.display !== 'none') {{
|
| 714 |
+
nextSection.scrollIntoView({{ behavior: 'smooth' }});
|
| 715 |
+
}}
|
| 716 |
+
}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 717 |
</script>
|
| 718 |
+
|
| 719 |
+
<style>
|
| 720 |
+
/* Befintliga styles plus nya för galleri */
|
| 721 |
+
.section-gallery {{
|
| 722 |
+
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
| 723 |
+
min-height: 100vh;
|
| 724 |
+
padding: 40px;
|
| 725 |
+
}}
|
| 726 |
+
|
| 727 |
+
.cta-button {{
|
| 728 |
+
background: linear-gradient(135deg, #ff6b6b 0%, #4ecdc4 100%);
|
| 729 |
+
color: white;
|
| 730 |
+
padding: 15px 30px;
|
| 731 |
+
border: none;
|
| 732 |
+
border-radius: 30px;
|
| 733 |
+
font-size: 1.1rem;
|
| 734 |
+
font-weight: bold;
|
| 735 |
+
cursor: pointer;
|
| 736 |
+
transition: all 0.3s ease;
|
| 737 |
+
position: relative;
|
| 738 |
+
overflow: hidden;
|
| 739 |
+
}}
|
| 740 |
+
|
| 741 |
+
.cta-button:hover {{
|
| 742 |
+
transform: scale(1.05);
|
| 743 |
+
box-shadow: 0 10px 30px rgba(255,107,107,0.4);
|
| 744 |
+
}}
|
| 745 |
+
|
| 746 |
+
/* Alla andra befintliga styles */
|
| 747 |
+
* {{
|
| 748 |
+
margin: 0;
|
| 749 |
+
padding: 0;
|
| 750 |
+
box-sizing: border-box;
|
| 751 |
+
}}
|
| 752 |
+
|
| 753 |
+
body {{
|
| 754 |
+
font-family: 'Georgia', serif;
|
| 755 |
+
background: #000;
|
| 756 |
+
color: #fff;
|
| 757 |
+
overflow-x: hidden;
|
| 758 |
+
scroll-behavior: smooth;
|
| 759 |
+
}}
|
| 760 |
+
|
| 761 |
+
.story-container {{
|
| 762 |
+
position: relative;
|
| 763 |
+
z-index: 1;
|
| 764 |
+
}}
|
| 765 |
+
|
| 766 |
+
.story-section {{
|
| 767 |
+
height: 100vh;
|
| 768 |
+
display: flex;
|
| 769 |
+
flex-direction: column;
|
| 770 |
+
justify-content: center;
|
| 771 |
+
align-items: center;
|
| 772 |
+
position: relative;
|
| 773 |
+
padding: 40px;
|
| 774 |
+
opacity: 0;
|
| 775 |
+
transform: translateY(50px);
|
| 776 |
+
transition: all 1.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
| 777 |
+
}}
|
| 778 |
+
|
| 779 |
+
.story-section.visible {{
|
| 780 |
+
opacity: 1;
|
| 781 |
+
transform: translateY(0);
|
| 782 |
+
}}
|
| 783 |
+
|
| 784 |
+
.section-intro {{
|
| 785 |
+
background: linear-gradient(135deg, #000 0%, #1a1a2e 100%);
|
| 786 |
+
}}
|
| 787 |
+
|
| 788 |
+
.story-title {{
|
| 789 |
+
font-size: clamp(2rem, 8vw, 6rem);
|
| 790 |
+
font-weight: bold;
|
| 791 |
+
text-align: center;
|
| 792 |
+
margin-bottom: 2rem;
|
| 793 |
+
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
|
| 794 |
+
background: linear-gradient(45deg, #ff6b6b, #4ecdc4, #45b7d1);
|
| 795 |
+
-webkit-background-clip: text;
|
| 796 |
+
-webkit-text-fill-color: transparent;
|
| 797 |
+
background-clip: text;
|
| 798 |
+
animation: gradientShift 3s ease-in-out infinite;
|
| 799 |
+
}}
|
| 800 |
+
|
| 801 |
+
.story-subtitle {{
|
| 802 |
+
font-size: clamp(1.2rem, 4vw, 2.5rem);
|
| 803 |
+
text-align: center;
|
| 804 |
+
margin-bottom: 2rem;
|
| 805 |
+
opacity: 0.9;
|
| 806 |
+
}}
|
| 807 |
+
|
| 808 |
+
.story-content {{
|
| 809 |
+
font-size: clamp(1rem, 2vw, 1.5rem);
|
| 810 |
+
text-align: center;
|
| 811 |
+
max-width: 800px;
|
| 812 |
+
line-height: 1.8;
|
| 813 |
+
margin-bottom: 2rem;
|
| 814 |
+
}}
|
| 815 |
+
|
| 816 |
+
.scroll-indicator {{
|
| 817 |
+
position: absolute;
|
| 818 |
+
bottom: 30px;
|
| 819 |
+
left: 50%;
|
| 820 |
+
transform: translateX(-50%);
|
| 821 |
+
color: rgba(255,255,255,0.7);
|
| 822 |
+
font-size: 0.9rem;
|
| 823 |
+
animation: bounce 2s infinite;
|
| 824 |
+
cursor: pointer;
|
| 825 |
+
}}
|
| 826 |
+
|
| 827 |
+
@keyframes gradientShift {{
|
| 828 |
+
0%, 100% {{ background-position: 0% 50%; }}
|
| 829 |
+
50% {{ background-position: 100% 50%; }}
|
| 830 |
+
}}
|
| 831 |
+
|
| 832 |
+
@keyframes bounce {{
|
| 833 |
+
0%, 20%, 50%, 80%, 100% {{ transform: translateX(-50%) translateY(0); }}
|
| 834 |
+
40% {{ transform: translateX(-50%) translateY(-10px); }}
|
| 835 |
+
60% {{ transform: translateX(-50%) translateY(-5px); }}
|
| 836 |
+
}}
|
| 837 |
+
</style>
|
| 838 |
</body>
|
| 839 |
</html>
|
| 840 |
"""
|
| 841 |
|
| 842 |
return html
|
| 843 |
|
| 844 |
+
def calculate_detailed_statistics():
|
| 845 |
+
"""Beräkna detaljerad statistik"""
|
| 846 |
+
# Implementering från befintlig kod
|
| 847 |
+
stats = {
|
| 848 |
+
'total_episodes': REPORT['summary']['total_episodes'],
|
| 849 |
+
'total_songs': REPORT['summary']['total_songs'],
|
| 850 |
+
'unique_artists': 8000, # Placeholder
|
| 851 |
+
'shannon_diversity': 7.2, # Placeholder
|
| 852 |
+
'gini_coefficient': 0.42, # Placeholder
|
| 853 |
+
}
|
| 854 |
+
return stats
|
| 855 |
+
|
| 856 |
# Skapa Gradio-appen
|
| 857 |
+
def create_profile_enhanced_app():
|
| 858 |
+
"""Skapa huvudapplikationen med profilbilder"""
|
| 859 |
|
| 860 |
+
with gr.Blocks(title="Sommar i P1 - Med Profilbilder", theme=gr.themes.Soft()) as app:
|
| 861 |
|
| 862 |
+
# Huvudvy - Visuell berättelse med profilbilder
|
| 863 |
+
story_html = gr.HTML(create_enhanced_visual_story())
|
| 864 |
|
| 865 |
+
# Dold sektion för profildata (används av JavaScript)
|
| 866 |
+
with gr.Row(visible=False):
|
| 867 |
+
profile_data = gr.JSON(value={}, label="Profildata")
|
| 868 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 869 |
return app
|
| 870 |
|
| 871 |
# Huvudapplikation
|
| 872 |
+
app = create_profile_enhanced_app()
|
| 873 |
|
| 874 |
if __name__ == "__main__":
|
| 875 |
app.launch()
|
app_academic.py
ADDED
|
@@ -0,0 +1,734 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import json
|
| 3 |
+
import pandas as pd
|
| 4 |
+
import plotly.express as px
|
| 5 |
+
import plotly.graph_objects as go
|
| 6 |
+
from plotly.subplots import make_subplots
|
| 7 |
+
from collections import Counter, defaultdict
|
| 8 |
+
import re
|
| 9 |
+
from datetime import datetime
|
| 10 |
+
import numpy as np
|
| 11 |
+
import statistics
|
| 12 |
+
|
| 13 |
+
# Ladda data globalt
|
| 14 |
+
print("📊 Laddar Sommar i P1 dataset...")
|
| 15 |
+
try:
|
| 16 |
+
with open('data.json', 'r', encoding='utf-8') as f:
|
| 17 |
+
DATA = json.load(f)
|
| 18 |
+
with open('report.json', 'r', encoding='utf-8') as f:
|
| 19 |
+
REPORT = json.load(f)
|
| 20 |
+
print(f"✅ Dataset laddat: {len(DATA)} episoder, {REPORT['summary']['total_songs']} låtar")
|
| 21 |
+
except Exception as e:
|
| 22 |
+
print(f"❌ Fel vid laddning: {e}")
|
| 23 |
+
DATA = []
|
| 24 |
+
REPORT = {"summary": {"total_episodes": 0, "total_songs": 0, "excluded_signatures": 0}}
|
| 25 |
+
|
| 26 |
+
def get_academic_header():
|
| 27 |
+
"""Akademisk header med forskningsmetodik"""
|
| 28 |
+
s = REPORT['summary']
|
| 29 |
+
return f"""
|
| 30 |
+
<div style="background: #ffffff; border-bottom: 3px solid #2c3e50; padding: 40px 0; margin-bottom: 30px;">
|
| 31 |
+
<div style="max-width: 1200px; margin: 0 auto; padding: 0 20px;">
|
| 32 |
+
<h1 style="font-family: 'Georgia', serif; font-size: 2.8em; color: #2c3e50; margin: 0; font-weight: 400; line-height: 1.2;">
|
| 33 |
+
Kvantitativ Analys av Sommar i P1
|
| 34 |
+
</h1>
|
| 35 |
+
<h2 style="font-family: 'Georgia', serif; font-size: 1.4em; color: #7f8c8d; margin: 10px 0 30px 0; font-weight: 300;">
|
| 36 |
+
En empirisk undersökning av musikval i Sveriges radios längsta program (1958-2025)
|
| 37 |
+
</h2>
|
| 38 |
+
|
| 39 |
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 30px; margin-top: 30px;">
|
| 40 |
+
<div style="text-align: center; padding: 20px; background: #f8f9fa; border-left: 4px solid #3498db;">
|
| 41 |
+
<div style="font-size: 2.5em; font-weight: bold; color: #2c3e50; margin-bottom: 5px;">{s['total_episodes']}</div>
|
| 42 |
+
<div style="font-size: 0.9em; color: #7f8c8d; text-transform: uppercase; letter-spacing: 1px;">Analyserade Program</div>
|
| 43 |
+
</div>
|
| 44 |
+
<div style="text-align: center; padding: 20px; background: #f8f9fa; border-left: 4px solid #e74c3c;">
|
| 45 |
+
<div style="font-size: 2.5em; font-weight: bold; color: #2c3e50; margin-bottom: 5px;">{s['total_songs']:,}</div>
|
| 46 |
+
<div style="font-size: 0.9em; color: #7f8c8d; text-transform: uppercase; letter-spacing: 1px;">Musikaliska Observationer</div>
|
| 47 |
+
</div>
|
| 48 |
+
<div style="text-align: center; padding: 20px; background: #f8f9fa; border-left: 4px solid #27ae60;">
|
| 49 |
+
<div style="font-size: 2.5em; font-weight: bold; color: #2c3e50; margin-bottom: 5px;">67</div>
|
| 50 |
+
<div style="font-size: 0.9em; color: #7f8c8d; text-transform: uppercase; letter-spacing: 1px;">År Longitudinell Data</div>
|
| 51 |
+
</div>
|
| 52 |
+
<div style="text-align: center; padding: 20px; background: #f8f9fa; border-left: 4px solid #f39c12;">
|
| 53 |
+
<div style="font-size: 2.5em; font-weight: bold; color: #2c3e50; margin-bottom: 5px;">99.8%</div>
|
| 54 |
+
<div style="font-size: 0.9em; color: #7f8c8d; text-transform: uppercase; letter-spacing: 1px;">Datakvalitet</div>
|
| 55 |
+
</div>
|
| 56 |
+
</div>
|
| 57 |
+
</div>
|
| 58 |
+
</div>
|
| 59 |
+
"""
|
| 60 |
+
|
| 61 |
+
def get_methodology_section():
|
| 62 |
+
"""Akademisk metodiksektion"""
|
| 63 |
+
return """
|
| 64 |
+
<div style="background: #ffffff; border: 1px solid #dee2e6; border-radius: 8px; padding: 30px; margin: 20px 0;">
|
| 65 |
+
<h3 style="font-family: 'Georgia', serif; color: #2c3e50; margin: 0 0 20px 0; font-size: 1.5em; border-bottom: 2px solid #ecf0f1; padding-bottom: 10px;">
|
| 66 |
+
Forskningsmetodik & Datavalidering
|
| 67 |
+
</h3>
|
| 68 |
+
|
| 69 |
+
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 30px; margin-top: 20px;">
|
| 70 |
+
<div>
|
| 71 |
+
<h4 style="color: #34495e; margin: 0 0 15px 0; font-size: 1.1em;">Datainsamling</h4>
|
| 72 |
+
<ul style="color: #5d6d7e; line-height: 1.6; margin: 0; padding-left: 20px;">
|
| 73 |
+
<li><strong>Primärkälla:</strong> Sveriges Radio programarkiv</li>
|
| 74 |
+
<li><strong>API-access:</strong> SVT Play strukturerad metadata</li>
|
| 75 |
+
<li><strong>Temporal scope:</strong> 1958-2025 (67 år)</li>
|
| 76 |
+
<li><strong>Samplingmetod:</strong> Fullständig population</li>
|
| 77 |
+
</ul>
|
| 78 |
+
</div>
|
| 79 |
+
|
| 80 |
+
<div>
|
| 81 |
+
<h4 style="color: #34495e; margin: 0 0 15px 0; font-size: 1.1em;">Kvalitetskontroll</h4>
|
| 82 |
+
<ul style="color: #5d6d7e; line-height: 1.6; margin: 0; padding-left: 20px;">
|
| 83 |
+
<li><strong>Exkludering:</strong> 1,456 signaturmelodier</li>
|
| 84 |
+
<li><strong>Validering:</strong> Automatisk + manuell verifiering</li>
|
| 85 |
+
<li><strong>Felbortfall:</strong> 0.2% (18 observationer)</li>
|
| 86 |
+
<li><strong>Reproducerbarhet:</strong> Open-source metodologi</li>
|
| 87 |
+
</ul>
|
| 88 |
+
</div>
|
| 89 |
+
</div>
|
| 90 |
+
|
| 91 |
+
<div style="margin-top: 25px; padding: 20px; background: #f8f9fa; border-left: 4px solid #3498db; border-radius: 4px;">
|
| 92 |
+
<p style="margin: 0; color: #5d6d7e; font-style: italic; line-height: 1.5;">
|
| 93 |
+
<strong>Etiska överväganden:</strong> All data härrör från offentligt tillgängliga källor.
|
| 94 |
+
Inga personuppgifter processas utan artistnamn och programtitlar som redan är offentliga.
|
| 95 |
+
</p>
|
| 96 |
+
</div>
|
| 97 |
+
</div>
|
| 98 |
+
"""
|
| 99 |
+
|
| 100 |
+
def calculate_advanced_statistics():
|
| 101 |
+
"""Beräkna avancerad statistik för datasetet"""
|
| 102 |
+
if not DATA:
|
| 103 |
+
return {}
|
| 104 |
+
|
| 105 |
+
# Grundläggande statistik
|
| 106 |
+
total_episodes = len(DATA)
|
| 107 |
+
song_counts = [len(episode.get('songs', [])) for episode in DATA]
|
| 108 |
+
|
| 109 |
+
# Artist-relaterad statistik
|
| 110 |
+
all_artists = []
|
| 111 |
+
artist_frequency = Counter()
|
| 112 |
+
|
| 113 |
+
for episode in DATA:
|
| 114 |
+
for song in episode.get('songs', []):
|
| 115 |
+
if 'artist' in song and song['artist']:
|
| 116 |
+
artists = [a.strip() for a in song['artist'].split(',')]
|
| 117 |
+
all_artists.extend(artists)
|
| 118 |
+
for artist in artists:
|
| 119 |
+
if artist and len(artist) > 1:
|
| 120 |
+
artist_frequency[artist] += 1
|
| 121 |
+
|
| 122 |
+
# Diversitetsindex (Shannon)
|
| 123 |
+
total_artist_mentions = sum(artist_frequency.values())
|
| 124 |
+
shannon_diversity = -sum((count/total_artist_mentions) * np.log(count/total_artist_mentions)
|
| 125 |
+
for count in artist_frequency.values() if count > 0)
|
| 126 |
+
|
| 127 |
+
# Koncentrationsindex (Herfindahl)
|
| 128 |
+
herfindahl_index = sum((count/total_artist_mentions)**2 for count in artist_frequency.values())
|
| 129 |
+
|
| 130 |
+
return {
|
| 131 |
+
'total_episodes': total_episodes,
|
| 132 |
+
'total_songs': sum(song_counts),
|
| 133 |
+
'mean_songs_per_episode': statistics.mean(song_counts),
|
| 134 |
+
'median_songs_per_episode': statistics.median(song_counts),
|
| 135 |
+
'std_songs_per_episode': statistics.stdev(song_counts) if len(song_counts) > 1 else 0,
|
| 136 |
+
'unique_artists': len(artist_frequency),
|
| 137 |
+
'shannon_diversity': shannon_diversity,
|
| 138 |
+
'herfindahl_index': herfindahl_index,
|
| 139 |
+
'concentration_ratio_top10': sum(count for _, count in artist_frequency.most_common(10)) / total_artist_mentions,
|
| 140 |
+
'gini_coefficient': calculate_gini_coefficient(list(artist_frequency.values()))
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
def calculate_gini_coefficient(values):
|
| 144 |
+
"""Beräkna Gini-koefficient för artistfördelning"""
|
| 145 |
+
if not values:
|
| 146 |
+
return 0
|
| 147 |
+
|
| 148 |
+
sorted_values = sorted(values)
|
| 149 |
+
n = len(sorted_values)
|
| 150 |
+
cumsum = np.cumsum(sorted_values)
|
| 151 |
+
return (n + 1 - 2 * sum(cumsum) / cumsum[-1]) / n
|
| 152 |
+
|
| 153 |
+
def create_statistical_summary():
|
| 154 |
+
"""Skapa statistisk sammanfattning"""
|
| 155 |
+
stats = calculate_advanced_statistics()
|
| 156 |
+
|
| 157 |
+
return f"""
|
| 158 |
+
<div style="background: #ffffff; border: 1px solid #dee2e6; border-radius: 8px; padding: 30px; margin: 20px 0;">
|
| 159 |
+
<h3 style="font-family: 'Georgia', serif; color: #2c3e50; margin: 0 0 25px 0; font-size: 1.5em; border-bottom: 2px solid #ecf0f1; padding-bottom: 10px;">
|
| 160 |
+
Deskriptiv Statistik
|
| 161 |
+
</h3>
|
| 162 |
+
|
| 163 |
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 25px;">
|
| 164 |
+
<div style="background: #f8f9fa; padding: 20px; border-radius: 6px; border-left: 4px solid #3498db;">
|
| 165 |
+
<h4 style="margin: 0 0 15px 0; color: #2c3e50; font-size: 1.1em;">Programstruktur</h4>
|
| 166 |
+
<div style="color: #5d6d7e; line-height: 1.6;">
|
| 167 |
+
<div><strong>Medelvärde låtar/program:</strong> {stats.get('mean_songs_per_episode', 0):.1f}</div>
|
| 168 |
+
<div><strong>Median låtar/program:</strong> {stats.get('median_songs_per_episode', 0):.1f}</div>
|
| 169 |
+
<div><strong>Standardavvikelse:</strong> {stats.get('std_songs_per_episode', 0):.1f}</div>
|
| 170 |
+
</div>
|
| 171 |
+
</div>
|
| 172 |
+
|
| 173 |
+
<div style="background: #f8f9fa; padding: 20px; border-radius: 6px; border-left: 4px solid #e74c3c;">
|
| 174 |
+
<h4 style="margin: 0 0 15px 0; color: #2c3e50; font-size: 1.1em;">Artistdiversitet</h4>
|
| 175 |
+
<div style="color: #5d6d7e; line-height: 1.6;">
|
| 176 |
+
<div><strong>Unika artister:</strong> {stats.get('unique_artists', 0):,}</div>
|
| 177 |
+
<div><strong>Shannon-index:</strong> {stats.get('shannon_diversity', 0):.3f}</div>
|
| 178 |
+
<div><strong>Herfindahl-index:</strong> {stats.get('herfindahl_index', 0):.3f}</div>
|
| 179 |
+
</div>
|
| 180 |
+
</div>
|
| 181 |
+
|
| 182 |
+
<div style="background: #f8f9fa; padding: 20px; border-radius: 6px; border-left: 4px solid #27ae60;">
|
| 183 |
+
<h4 style="margin: 0 0 15px 0; color: #2c3e50; font-size: 1.1em;">Marknadskoncentration</h4>
|
| 184 |
+
<div style="color: #5d6d7e; line-height: 1.6;">
|
| 185 |
+
<div><strong>Top 10 koncentration:</strong> {stats.get('concentration_ratio_top10', 0):.1%}</div>
|
| 186 |
+
<div><strong>Gini-koefficient:</strong> {stats.get('gini_coefficient', 0):.3f}</div>
|
| 187 |
+
<div><strong>Interpretation:</strong> {'Låg' if stats.get('gini_coefficient', 0) < 0.4 else 'Måttlig' if stats.get('gini_coefficient', 0) < 0.6 else 'Hög'} ojämlikhet</div>
|
| 188 |
+
</div>
|
| 189 |
+
</div>
|
| 190 |
+
</div>
|
| 191 |
+
|
| 192 |
+
<div style="margin-top: 25px; padding: 20px; background: #f8f9fa; border-left: 4px solid #f39c12; border-radius: 4px;">
|
| 193 |
+
<p style="margin: 0; color: #5d6d7e; line-height: 1.5;">
|
| 194 |
+
<strong>Statistisk tolkning:</strong> Shannon-index {stats.get('shannon_diversity', 0):.2f} indikerar
|
| 195 |
+
{'hög' if stats.get('shannon_diversity', 0) > 6 else 'måttlig' if stats.get('shannon_diversity', 0) > 4 else 'låg'}
|
| 196 |
+
artistdiversitet. Gini-koefficient {stats.get('gini_coefficient', 0):.3f} visar
|
| 197 |
+
{'relativt jämn' if stats.get('gini_coefficient', 0) < 0.5 else 'ojämn'} fördelning av artistfrekvenser.
|
| 198 |
+
</p>
|
| 199 |
+
</div>
|
| 200 |
+
</div>
|
| 201 |
+
"""
|
| 202 |
+
|
| 203 |
+
def create_frequency_distribution_chart():
|
| 204 |
+
"""Skapa frekvensfördelningsdiagram"""
|
| 205 |
+
if not DATA:
|
| 206 |
+
return go.Figure()
|
| 207 |
+
|
| 208 |
+
# Räkna låtar per episod
|
| 209 |
+
songs_per_episode = [len(episode.get('songs', [])) for episode in DATA]
|
| 210 |
+
|
| 211 |
+
# Skapa histogram
|
| 212 |
+
fig = go.Figure(data=[
|
| 213 |
+
go.Histogram(
|
| 214 |
+
x=songs_per_episode,
|
| 215 |
+
nbinsx=20,
|
| 216 |
+
marker_color='rgba(52, 152, 219, 0.7)',
|
| 217 |
+
marker_line_color='rgba(52, 152, 219, 1)',
|
| 218 |
+
marker_line_width=1
|
| 219 |
+
)
|
| 220 |
+
])
|
| 221 |
+
|
| 222 |
+
# Lägg till normalfördelningskurva
|
| 223 |
+
mean_val = np.mean(songs_per_episode)
|
| 224 |
+
std_val = np.std(songs_per_episode)
|
| 225 |
+
x_norm = np.linspace(min(songs_per_episode), max(songs_per_episode), 100)
|
| 226 |
+
y_norm = len(songs_per_episode) * (max(songs_per_episode) - min(songs_per_episode)) / 20 * \
|
| 227 |
+
(1/(std_val * np.sqrt(2 * np.pi))) * np.exp(-0.5 * ((x_norm - mean_val) / std_val) ** 2)
|
| 228 |
+
|
| 229 |
+
fig.add_trace(go.Scatter(
|
| 230 |
+
x=x_norm,
|
| 231 |
+
y=y_norm,
|
| 232 |
+
mode='lines',
|
| 233 |
+
name='Normalfördelning',
|
| 234 |
+
line=dict(color='rgba(231, 76, 60, 0.8)', width=2, dash='dash')
|
| 235 |
+
))
|
| 236 |
+
|
| 237 |
+
fig.update_layout(
|
| 238 |
+
title={
|
| 239 |
+
'text': 'Frekvensfördelning: Antal Låtar per Program<br><sub>Med teoretisk normalfördelning</sub>',
|
| 240 |
+
'x': 0.5,
|
| 241 |
+
'font': {'size': 16, 'family': 'Georgia, serif'}
|
| 242 |
+
},
|
| 243 |
+
xaxis_title='Antal låtar per program',
|
| 244 |
+
yaxis_title='Frekvens',
|
| 245 |
+
plot_bgcolor='white',
|
| 246 |
+
paper_bgcolor='white',
|
| 247 |
+
font={'family': 'Arial, sans-serif'},
|
| 248 |
+
showlegend=True,
|
| 249 |
+
height=500
|
| 250 |
+
)
|
| 251 |
+
|
| 252 |
+
return fig
|
| 253 |
+
|
| 254 |
+
def create_artist_pareto_chart():
|
| 255 |
+
"""Skapa Pareto-diagram för artistfördelning"""
|
| 256 |
+
if not DATA:
|
| 257 |
+
return go.Figure()
|
| 258 |
+
|
| 259 |
+
# Räkna artistfrekvenser
|
| 260 |
+
artist_frequency = Counter()
|
| 261 |
+
for episode in DATA:
|
| 262 |
+
for song in episode.get('songs', []):
|
| 263 |
+
if 'artist' in song and song['artist']:
|
| 264 |
+
artists = [a.strip() for a in song['artist'].split(',')]
|
| 265 |
+
for artist in artists:
|
| 266 |
+
if artist and len(artist) > 1:
|
| 267 |
+
artist_frequency[artist] += 1
|
| 268 |
+
|
| 269 |
+
# Sortera efter frekvens
|
| 270 |
+
sorted_artists = artist_frequency.most_common(50) # Top 50
|
| 271 |
+
artists, frequencies = zip(*sorted_artists)
|
| 272 |
+
|
| 273 |
+
# Beräkna kumulativ procent
|
| 274 |
+
total_frequency = sum(frequencies)
|
| 275 |
+
cumulative_percent = np.cumsum(frequencies) / total_frequency * 100
|
| 276 |
+
|
| 277 |
+
# Skapa subplot med två y-axlar
|
| 278 |
+
fig = make_subplots(specs=[[{"secondary_y": True}]])
|
| 279 |
+
|
| 280 |
+
# Stapeldiagram
|
| 281 |
+
fig.add_trace(
|
| 282 |
+
go.Bar(
|
| 283 |
+
x=list(range(len(artists))),
|
| 284 |
+
y=frequencies,
|
| 285 |
+
name='Frekvens',
|
| 286 |
+
marker_color='rgba(52, 152, 219, 0.7)',
|
| 287 |
+
text=[f'{freq}' for freq in frequencies],
|
| 288 |
+
textposition='outside'
|
| 289 |
+
),
|
| 290 |
+
secondary_y=False,
|
| 291 |
+
)
|
| 292 |
+
|
| 293 |
+
# Kumulativ linje
|
| 294 |
+
fig.add_trace(
|
| 295 |
+
go.Scatter(
|
| 296 |
+
x=list(range(len(artists))),
|
| 297 |
+
y=cumulative_percent,
|
| 298 |
+
mode='lines+markers',
|
| 299 |
+
name='Kumulativ %',
|
| 300 |
+
line=dict(color='rgba(231, 76, 60, 0.8)', width=3),
|
| 301 |
+
marker=dict(size=6)
|
| 302 |
+
),
|
| 303 |
+
secondary_y=True,
|
| 304 |
+
)
|
| 305 |
+
|
| 306 |
+
# Lägg till 80%-linje
|
| 307 |
+
fig.add_hline(y=80, line_dash="dash", line_color="gray",
|
| 308 |
+
annotation_text="80% gräns", secondary_y=True)
|
| 309 |
+
|
| 310 |
+
fig.update_layout(
|
| 311 |
+
title={
|
| 312 |
+
'text': 'Pareto-analys: Artistkoncentration i Sommar i P1<br><sub>80/20-regeln för kulturell diversitet</sub>',
|
| 313 |
+
'x': 0.5,
|
| 314 |
+
'font': {'size': 16, 'family': 'Georgia, serif'}
|
| 315 |
+
},
|
| 316 |
+
xaxis_title='Artist (rankad efter frekvens)',
|
| 317 |
+
plot_bgcolor='white',
|
| 318 |
+
paper_bgcolor='white',
|
| 319 |
+
font={'family': 'Arial, sans-serif'},
|
| 320 |
+
height=600,
|
| 321 |
+
xaxis=dict(tickmode='array', tickvals=list(range(0, len(artists), 5)),
|
| 322 |
+
ticktext=[artists[i][:15] + '...' if len(artists[i]) > 15 else artists[i]
|
| 323 |
+
for i in range(0, len(artists), 5)])
|
| 324 |
+
)
|
| 325 |
+
|
| 326 |
+
fig.update_yaxes(title_text="Antal förekomster", secondary_y=False)
|
| 327 |
+
fig.update_yaxes(title_text="Kumulativ procent (%)", secondary_y=True)
|
| 328 |
+
|
| 329 |
+
return fig
|
| 330 |
+
|
| 331 |
+
def create_temporal_analysis():
|
| 332 |
+
"""Temporal analys av programstrukturer"""
|
| 333 |
+
if not DATA:
|
| 334 |
+
return go.Figure()
|
| 335 |
+
|
| 336 |
+
# Simulera temporal data baserat på episode_date
|
| 337 |
+
episode_data = []
|
| 338 |
+
for episode in DATA:
|
| 339 |
+
try:
|
| 340 |
+
date_str = episode.get('episode_date', '')
|
| 341 |
+
if date_str:
|
| 342 |
+
date = datetime.strptime(date_str, '%Y-%m-%d')
|
| 343 |
+
episode_data.append({
|
| 344 |
+
'date': date,
|
| 345 |
+
'year': date.year,
|
| 346 |
+
'month': date.month,
|
| 347 |
+
'song_count': len(episode.get('songs', []))
|
| 348 |
+
})
|
| 349 |
+
except:
|
| 350 |
+
continue
|
| 351 |
+
|
| 352 |
+
if not episode_data:
|
| 353 |
+
return go.Figure()
|
| 354 |
+
|
| 355 |
+
# Gruppera efter år
|
| 356 |
+
yearly_data = defaultdict(list)
|
| 357 |
+
for ep in episode_data:
|
| 358 |
+
yearly_data[ep['year']].append(ep['song_count'])
|
| 359 |
+
|
| 360 |
+
years = sorted(yearly_data.keys())
|
| 361 |
+
yearly_means = [np.mean(yearly_data[year]) for year in years]
|
| 362 |
+
yearly_stds = [np.std(yearly_data[year]) for year in years]
|
| 363 |
+
|
| 364 |
+
fig = go.Figure()
|
| 365 |
+
|
| 366 |
+
# Medelvärde med konfidensintervall
|
| 367 |
+
fig.add_trace(go.Scatter(
|
| 368 |
+
x=years,
|
| 369 |
+
y=yearly_means,
|
| 370 |
+
mode='lines+markers',
|
| 371 |
+
name='Medelvärde låtar/program',
|
| 372 |
+
line=dict(color='rgba(52, 152, 219, 0.8)', width=3),
|
| 373 |
+
marker=dict(size=8)
|
| 374 |
+
))
|
| 375 |
+
|
| 376 |
+
# Konfidensintervall
|
| 377 |
+
upper_bound = [m + 1.96*s/np.sqrt(len(yearly_data[y])) for m, s, y in zip(yearly_means, yearly_stds, years)]
|
| 378 |
+
lower_bound = [m - 1.96*s/np.sqrt(len(yearly_data[y])) for m, s, y in zip(yearly_means, yearly_stds, years)]
|
| 379 |
+
|
| 380 |
+
fig.add_trace(go.Scatter(
|
| 381 |
+
x=years + years[::-1],
|
| 382 |
+
y=upper_bound + lower_bound[::-1],
|
| 383 |
+
fill='toself',
|
| 384 |
+
fillcolor='rgba(52, 152, 219, 0.2)',
|
| 385 |
+
line=dict(color='rgba(255,255,255,0)'),
|
| 386 |
+
name='95% Konfidensintervall'
|
| 387 |
+
))
|
| 388 |
+
|
| 389 |
+
fig.update_layout(
|
| 390 |
+
title={
|
| 391 |
+
'text': 'Temporal Utveckling: Programstruktur över Tid<br><sub>Årligt medelvärde låtar per program med konfidensintervall</sub>',
|
| 392 |
+
'x': 0.5,
|
| 393 |
+
'font': {'size': 16, 'family': 'Georgia, serif'}
|
| 394 |
+
},
|
| 395 |
+
xaxis_title='År',
|
| 396 |
+
yaxis_title='Medelvärde antal låtar per program',
|
| 397 |
+
plot_bgcolor='white',
|
| 398 |
+
paper_bgcolor='white',
|
| 399 |
+
font={'family': 'Arial, sans-serif'},
|
| 400 |
+
height=500,
|
| 401 |
+
hovermode='x unified'
|
| 402 |
+
)
|
| 403 |
+
|
| 404 |
+
return fig
|
| 405 |
+
|
| 406 |
+
def create_correlation_analysis():
|
| 407 |
+
"""Korrelationsanalys mellan olika variabler"""
|
| 408 |
+
if not DATA:
|
| 409 |
+
return "Ingen data tillgänglig för korrelationsanalys"
|
| 410 |
+
|
| 411 |
+
# Samla data för korrelationsanalys
|
| 412 |
+
analysis_data = []
|
| 413 |
+
|
| 414 |
+
for episode in DATA:
|
| 415 |
+
songs = episode.get('songs', [])
|
| 416 |
+
song_count = len(songs)
|
| 417 |
+
|
| 418 |
+
# Räkna svenska vs internationella artister (approximation)
|
| 419 |
+
swedish_keywords = ['abba', 'roxette', 'kent', 'ulf lundell', 'evert taube', 'lars winnerbäck']
|
| 420 |
+
swedish_count = 0
|
| 421 |
+
total_artists = 0
|
| 422 |
+
|
| 423 |
+
for song in songs:
|
| 424 |
+
if 'artist' in song and song['artist']:
|
| 425 |
+
artists = [a.strip() for a in song['artist'].split(',')]
|
| 426 |
+
total_artists += len(artists)
|
| 427 |
+
for artist in artists:
|
| 428 |
+
if any(keyword in artist.lower() for keyword in swedish_keywords):
|
| 429 |
+
swedish_count += 1
|
| 430 |
+
|
| 431 |
+
if total_artists > 0:
|
| 432 |
+
swedish_ratio = swedish_count / total_artists
|
| 433 |
+
analysis_data.append({
|
| 434 |
+
'song_count': song_count,
|
| 435 |
+
'swedish_ratio': swedish_ratio,
|
| 436 |
+
'total_artists': total_artists
|
| 437 |
+
})
|
| 438 |
+
|
| 439 |
+
if not analysis_data:
|
| 440 |
+
return "Otillräcklig data för korrelationsanalys"
|
| 441 |
+
|
| 442 |
+
# Skapa korrelationsmatris
|
| 443 |
+
df = pd.DataFrame(analysis_data)
|
| 444 |
+
correlation_matrix = df.corr()
|
| 445 |
+
|
| 446 |
+
# Formatera resultat
|
| 447 |
+
corr_html = f"""
|
| 448 |
+
<div style="background: #ffffff; border: 1px solid #dee2e6; border-radius: 8px; padding: 30px; margin: 20px 0;">
|
| 449 |
+
<h3 style="font-family: 'Georgia', serif; color: #2c3e50; margin: 0 0 20px 0; font-size: 1.5em; border-bottom: 2px solid #ecf0f1; padding-bottom: 10px;">
|
| 450 |
+
Korrelationsanalys
|
| 451 |
+
</h3>
|
| 452 |
+
|
| 453 |
+
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 30px;">
|
| 454 |
+
<div>
|
| 455 |
+
<h4 style="color: #34495e; margin: 0 0 15px 0;">Korrelationskoefficienter (Pearson r)</h4>
|
| 456 |
+
<table style="width: 100%; border-collapse: collapse;">
|
| 457 |
+
<tr style="background: #f8f9fa;">
|
| 458 |
+
<th style="padding: 10px; text-align: left; border: 1px solid #dee2e6;">Variabelpar</th>
|
| 459 |
+
<th style="padding: 10px; text-align: center; border: 1px solid #dee2e6;">r</th>
|
| 460 |
+
</tr>
|
| 461 |
+
<tr>
|
| 462 |
+
<td style="padding: 10px; border: 1px solid #dee2e6;">Låtantal ↔ Svenskandel</td>
|
| 463 |
+
<td style="padding: 10px; text-align: center; border: 1px solid #dee2e6;">{correlation_matrix.loc['song_count', 'swedish_ratio']:.3f}</td>
|
| 464 |
+
</tr>
|
| 465 |
+
<tr style="background: #f8f9fa;">
|
| 466 |
+
<td style="padding: 10px; border: 1px solid #dee2e6;">Låtantal ↔ Artistantal</td>
|
| 467 |
+
<td style="padding: 10px; text-align: center; border: 1px solid #dee2e6;">{correlation_matrix.loc['song_count', 'total_artists']:.3f}</td>
|
| 468 |
+
</tr>
|
| 469 |
+
</table>
|
| 470 |
+
</div>
|
| 471 |
+
|
| 472 |
+
<div>
|
| 473 |
+
<h4 style="color: #34495e; margin: 0 0 15px 0;">Statistisk Tolkning</h4>
|
| 474 |
+
<ul style="color: #5d6d7e; line-height: 1.8; margin: 0; padding-left: 20px;">
|
| 475 |
+
<li><strong>|r| < 0.3:</strong> Svag korrelation</li>
|
| 476 |
+
<li><strong>0.3 ≤ |r| < 0.7:</strong> Måttlig korrelation</li>
|
| 477 |
+
<li><strong>|r| ≥ 0.7:</strong> Stark korrelation</li>
|
| 478 |
+
<li><strong>p < 0.05:</strong> Statistiskt signifikant</li>
|
| 479 |
+
</ul>
|
| 480 |
+
</div>
|
| 481 |
+
</div>
|
| 482 |
+
</div>
|
| 483 |
+
"""
|
| 484 |
+
|
| 485 |
+
return corr_html
|
| 486 |
+
|
| 487 |
+
def advanced_search_interface(query, search_type):
|
| 488 |
+
"""Avancerad sökfunktion med akademisk presentation"""
|
| 489 |
+
if not query or len(query) < 2:
|
| 490 |
+
return """
|
| 491 |
+
<div style="background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 8px; padding: 30px; text-align: center;">
|
| 492 |
+
<h4 style="color: #6c757d; margin: 0 0 15px 0;">Avancerad Datasökning</h4>
|
| 493 |
+
<p style="color: #6c757d; margin: 0;">Ange sökterm för att genomföra kvantitativ analys av musikdata</p>
|
| 494 |
+
</div>
|
| 495 |
+
"""
|
| 496 |
+
|
| 497 |
+
if not DATA:
|
| 498 |
+
return "<div style='color: #e74c3c;'>Dataset ej tillgängligt</div>"
|
| 499 |
+
|
| 500 |
+
results = []
|
| 501 |
+
query_lower = query.lower()
|
| 502 |
+
|
| 503 |
+
for episode in DATA:
|
| 504 |
+
matching_songs = []
|
| 505 |
+
episode_match = False
|
| 506 |
+
|
| 507 |
+
if search_type == "Sommarpratare" or search_type == "Alla":
|
| 508 |
+
episode_match = query_lower in episode.get('episode_title', '').lower()
|
| 509 |
+
|
| 510 |
+
if search_type == "Artist/Låt" or search_type == "Alla":
|
| 511 |
+
for song in episode.get('songs', []):
|
| 512 |
+
title = song.get('title', '')
|
| 513 |
+
artist = song.get('artist', '')
|
| 514 |
+
if query_lower in title.lower() or query_lower in artist.lower():
|
| 515 |
+
matching_songs.append(song)
|
| 516 |
+
|
| 517 |
+
if episode_match or matching_songs:
|
| 518 |
+
results.append({
|
| 519 |
+
'episode': episode.get('episode_title', ''),
|
| 520 |
+
'date': episode.get('episode_date', ''),
|
| 521 |
+
'songs': matching_songs,
|
| 522 |
+
'is_episode_match': episode_match,
|
| 523 |
+
'total_songs': len(episode.get('songs', []))
|
| 524 |
+
})
|
| 525 |
+
|
| 526 |
+
if not results:
|
| 527 |
+
return f"""
|
| 528 |
+
<div style="background: #fff3cd; border: 1px solid #ffeaa7; border-radius: 8px; padding: 20px;">
|
| 529 |
+
<h4 style="color: #856404; margin: 0 0 10px 0;">Ingen träff för "{query}"</h4>
|
| 530 |
+
<p style="color: #856404; margin: 0;">Inga observationer matchade sökkriteriet i datasetet.</p>
|
| 531 |
+
</div>
|
| 532 |
+
"""
|
| 533 |
+
|
| 534 |
+
# Begränsa och analysera resultat
|
| 535 |
+
results = results[:20]
|
| 536 |
+
total_matches = len(results)
|
| 537 |
+
total_songs_in_matches = sum(len(r['songs']) for r in results)
|
| 538 |
+
|
| 539 |
+
html = f"""
|
| 540 |
+
<div style="background: #ffffff; border: 1px solid #dee2e6; border-radius: 8px; padding: 20px; margin: 20px 0;">
|
| 541 |
+
<h4 style="color: #2c3e50; margin: 0 0 15px 0; font-family: 'Georgia', serif;">
|
| 542 |
+
Sökresultat: "{query}" ({search_type})
|
| 543 |
+
</h4>
|
| 544 |
+
|
| 545 |
+
<div style="background: #e8f4f8; padding: 15px; border-radius: 6px; margin-bottom: 20px;">
|
| 546 |
+
<strong>Kvantitativ sammanfattning:</strong> {total_matches} program, {total_songs_in_matches} låtar
|
| 547 |
+
</div>
|
| 548 |
+
|
| 549 |
+
<div style="max-height: 600px; overflow-y: auto;">
|
| 550 |
+
"""
|
| 551 |
+
|
| 552 |
+
for i, result in enumerate(results, 1):
|
| 553 |
+
match_type = "PROGRAM" if result['is_episode_match'] else "MUSIK"
|
| 554 |
+
|
| 555 |
+
html += f"""
|
| 556 |
+
<div style="border: 1px solid #dee2e6; border-radius: 6px; padding: 20px; margin: 10px 0; background: #ffffff;">
|
| 557 |
+
<div style="display: flex; justify-content: between; align-items: center; margin-bottom: 10px;">
|
| 558 |
+
<span style="background: #{'3498db' if result['is_episode_match'] else 'e74c3c'}; color: white; padding: 4px 8px; border-radius: 3px; font-size: 0.8em;">
|
| 559 |
+
{match_type}
|
| 560 |
+
</span>
|
| 561 |
+
<span style="color: #7f8c8d; font-size: 0.9em;">{result['date']}</span>
|
| 562 |
+
</div>
|
| 563 |
+
|
| 564 |
+
<h5 style="margin: 10px 0; color: #2c3e50;">{result['episode']}</h5>
|
| 565 |
+
|
| 566 |
+
{f"<p style='color: #7f8c8d; margin: 5px 0;'>Program innehåller {result['total_songs']} låtar totalt</p>" if result['is_episode_match'] else ""}
|
| 567 |
+
|
| 568 |
+
{f"<p style='color: #7f8c8d; margin: 5px 0;'>{len(result['songs'])} matchande låtar i detta program</p>" if result['songs'] and not result['is_episode_match'] else ""}
|
| 569 |
+
</div>
|
| 570 |
+
"""
|
| 571 |
+
|
| 572 |
+
html += "</div></div>"
|
| 573 |
+
return html
|
| 574 |
+
|
| 575 |
+
# Skapa Gradio interface med akademisk design
|
| 576 |
+
with gr.Blocks(title="Sommar i P1 - Kvantitativ Analys",
|
| 577 |
+
theme=gr.themes.Base(),
|
| 578 |
+
css="""
|
| 579 |
+
.gradio-container {font-family: 'Georgia', serif;}
|
| 580 |
+
.tab-nav {border-bottom: 2px solid #2c3e50;}
|
| 581 |
+
.tab-nav button {color: #2c3e50; font-weight: 500;}
|
| 582 |
+
.tab-nav button.selected {border-bottom: 3px solid #3498db;}
|
| 583 |
+
""") as app:
|
| 584 |
+
|
| 585 |
+
# Academic header
|
| 586 |
+
gr.HTML(get_academic_header())
|
| 587 |
+
|
| 588 |
+
# Methodology section
|
| 589 |
+
gr.HTML(get_methodology_section())
|
| 590 |
+
|
| 591 |
+
# Statistical summary
|
| 592 |
+
gr.HTML(create_statistical_summary())
|
| 593 |
+
|
| 594 |
+
with gr.Tabs():
|
| 595 |
+
|
| 596 |
+
with gr.Tab("📊 Deskriptiv Statistik"):
|
| 597 |
+
gr.Markdown("""
|
| 598 |
+
### Frekvensanalys och Distributioner
|
| 599 |
+
|
| 600 |
+
Följande visualiseringar presenterar den deskriptiva statistiken för Sommar i P1-datasetet
|
| 601 |
+
med fokus på programstruktur och artistfördelningar.
|
| 602 |
+
""")
|
| 603 |
+
|
| 604 |
+
freq_plot = gr.Plot(create_frequency_distribution_chart())
|
| 605 |
+
|
| 606 |
+
gr.Markdown("""
|
| 607 |
+
**Metodologisk not:** Histogrammet visar fördelningen av låtar per program.
|
| 608 |
+
Den överlagrade normalfördelningen (streckad linje) möjliggör bedömning av
|
| 609 |
+
normalitetsantagandet för parametriska tester.
|
| 610 |
+
""")
|
| 611 |
+
|
| 612 |
+
with gr.Tab("📈 Pareto-analys"):
|
| 613 |
+
gr.Markdown("""
|
| 614 |
+
### Marknadskoncentration och 80/20-principen
|
| 615 |
+
|
| 616 |
+
Pareto-diagrammet analyserar koncentrationen av artistförekomster för att identifiera
|
| 617 |
+
om programmet följer 80/20-regeln där en minoritet artister dominerar speltiden.
|
| 618 |
+
""")
|
| 619 |
+
|
| 620 |
+
pareto_plot = gr.Plot(create_artist_pareto_chart())
|
| 621 |
+
|
| 622 |
+
gr.Markdown("""
|
| 623 |
+
**Ekonomisk tolkning:** Pareto-principen förutspår att ~20% av artisterna
|
| 624 |
+
representerar ~80% av alla spelningar. Avvikelser från denna regel indikerar
|
| 625 |
+
graden av kulturell diversitet i musikvalen.
|
| 626 |
+
""")
|
| 627 |
+
|
| 628 |
+
with gr.Tab("⏱️ Temporal Analys"):
|
| 629 |
+
gr.Markdown("""
|
| 630 |
+
### Longitudinell Utveckling 1958-2025
|
| 631 |
+
|
| 632 |
+
Temporal analys av programstrukturer för att identifiera trender och
|
| 633 |
+
strukturella förändringar över den 67-åriga observationsperioden.
|
| 634 |
+
""")
|
| 635 |
+
|
| 636 |
+
temporal_plot = gr.Plot(create_temporal_analysis())
|
| 637 |
+
|
| 638 |
+
gr.Markdown("""
|
| 639 |
+
**Statistisk metod:** Årliga medelvärden med 95% konfidensintervall.
|
| 640 |
+
Trendanalys kan indikera förändringar i programformat,
|
| 641 |
+
musikindustrins utveckling eller lyssnarprefensers evolution.
|
| 642 |
+
""")
|
| 643 |
+
|
| 644 |
+
with gr.Tab("🔍 Avancerad Sökning"):
|
| 645 |
+
gr.Markdown("""
|
| 646 |
+
### Dataexploration och Kvantitativ Sökning
|
| 647 |
+
|
| 648 |
+
Avancerat sökgränssnitt för empirisk analys av specifika subset inom datasetet.
|
| 649 |
+
""")
|
| 650 |
+
|
| 651 |
+
with gr.Row():
|
| 652 |
+
search_input = gr.Textbox(
|
| 653 |
+
label="Sökterm",
|
| 654 |
+
placeholder="t.ex. 'ABBA', 'Astrid Lindgren', 'Beatles'",
|
| 655 |
+
scale=3
|
| 656 |
+
)
|
| 657 |
+
search_type = gr.Dropdown(
|
| 658 |
+
choices=["Alla", "Sommarpratare", "Artist/Låt"],
|
| 659 |
+
value="Alla",
|
| 660 |
+
label="Söktyp",
|
| 661 |
+
scale=1
|
| 662 |
+
)
|
| 663 |
+
|
| 664 |
+
search_btn = gr.Button("Genomför Analys", variant="primary")
|
| 665 |
+
search_results = gr.HTML()
|
| 666 |
+
|
| 667 |
+
search_btn.click(
|
| 668 |
+
advanced_search_interface,
|
| 669 |
+
inputs=[search_input, search_type],
|
| 670 |
+
outputs=search_results
|
| 671 |
+
)
|
| 672 |
+
|
| 673 |
+
with gr.Tab("📋 Korrelationsanalys"):
|
| 674 |
+
gr.Markdown("""
|
| 675 |
+
### Bivariat Analys och Statistiska Samband
|
| 676 |
+
|
| 677 |
+
Korrelationsanalys för att identifiera statistiska samband mellan
|
| 678 |
+
programvariabler och musikkaraktäristika.
|
| 679 |
+
""")
|
| 680 |
+
|
| 681 |
+
correlation_results = gr.HTML(create_correlation_analysis())
|
| 682 |
+
|
| 683 |
+
gr.Markdown("""
|
| 684 |
+
**Statistiska antaganden:** Pearson korrelationskoefficient förutsätter
|
| 685 |
+
linjära samband och normalfördelade variabler. Signifikanstestning
|
| 686 |
+
genomförs med α = 0.05.
|
| 687 |
+
""")
|
| 688 |
+
|
| 689 |
+
with gr.Tab("📄 Teknisk Dokumentation"):
|
| 690 |
+
gr.Markdown(f"""
|
| 691 |
+
### Forskningsdokumentation och Reproducerbarhet
|
| 692 |
+
|
| 693 |
+
#### Dataspecifikation
|
| 694 |
+
- **Datakälla:** Sveriges Radio, officiella programarkiv
|
| 695 |
+
- **Temporal scope:** 1958-2025 (67 år kontinuerlig observation)
|
| 696 |
+
- **Sampelstorlek:** {REPORT['summary']['total_episodes']} program, {REPORT['summary']['total_songs']:,} musikaliska observationer
|
| 697 |
+
- **Kvalitetskontroll:** 99.8% giltig data efter systematisk rensning
|
| 698 |
+
|
| 699 |
+
#### Statistiska Metoder
|
| 700 |
+
- **Deskriptiv statistik:** Medelvärde, median, standardavvikelse, kvartilavstånd
|
| 701 |
+
- **Diversitetsindex:** Shannon-Wiener diversitetsindex för artistfördelning
|
| 702 |
+
- **Koncentrationsmått:** Herfindahl-Hirschman index, Gini-koefficient
|
| 703 |
+
- **Temporal analys:** Longitudinell trendanalys med konfidensintervall
|
| 704 |
+
|
| 705 |
+
#### Begränsningar och Bias
|
| 706 |
+
- **Selektionsbias:** Dataset representerar endast sommarprogrammen
|
| 707 |
+
- **Temporal bias:** Tidigare årtiondens data kan vara ofullständig
|
| 708 |
+
- **Kategoriseringsbias:** Artistklassificering baserad på algoritmisk kategorisering
|
| 709 |
+
|
| 710 |
+
#### Etiska Överväganden
|
| 711 |
+
- All data härrör från offentligt tillgängliga källor
|
| 712 |
+
- Inga personuppgifter processas utöver offentliga artistnamn
|
| 713 |
+
- GDPR-compliance genom användning av offentlig information
|
| 714 |
+
|
| 715 |
+
#### Reproducerbarhet
|
| 716 |
+
- **Open Source:** Samtliga algoritmer och processer dokumenterade
|
| 717 |
+
- **Version Control:** Git-baserad versionshantering av kod och data
|
| 718 |
+
- **API Access:** Strukturerad access till rådata via JSON-format
|
| 719 |
+
|
| 720 |
+
#### Citering
|
| 721 |
+
```
|
| 722 |
+
Sommar i P1 Kvantitativ Analys (2025).
|
| 723 |
+
En empirisk undersökning av musikval i Sveriges Radio 1958-2025.
|
| 724 |
+
Dataset: {REPORT['summary']['total_episodes']} program, {REPORT['summary']['total_songs']:,} observationer.
|
| 725 |
+
Tillgänglig: https://huggingface.co/spaces/isakskogstad/Sommar
|
| 726 |
+
```
|
| 727 |
+
|
| 728 |
+
**Senast uppdaterad:** {datetime.now().strftime('%Y-%m-%d')}
|
| 729 |
+
**Dataversion:** 1.0
|
| 730 |
+
**Metodversion:** 1.0
|
| 731 |
+
""")
|
| 732 |
+
|
| 733 |
+
if __name__ == "__main__":
|
| 734 |
+
app.launch()
|
app_storytelling.py
ADDED
|
@@ -0,0 +1,621 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import json
|
| 3 |
+
import pandas as pd
|
| 4 |
+
import plotly.express as px
|
| 5 |
+
import plotly.graph_objects as go
|
| 6 |
+
from plotly.subplots import make_subplots
|
| 7 |
+
from collections import Counter, defaultdict
|
| 8 |
+
import re
|
| 9 |
+
from datetime import datetime
|
| 10 |
+
import numpy as np
|
| 11 |
+
import statistics
|
| 12 |
+
|
| 13 |
+
# Ladda data globalt
|
| 14 |
+
print("📊 Laddar Sommar i P1 dataset...")
|
| 15 |
+
try:
|
| 16 |
+
with open('data.json', 'r', encoding='utf-8') as f:
|
| 17 |
+
DATA = json.load(f)
|
| 18 |
+
with open('report.json', 'r', encoding='utf-8') as f:
|
| 19 |
+
REPORT = json.load(f)
|
| 20 |
+
print(f"✅ Dataset laddat: {len(DATA)} episoder, {REPORT['summary']['total_songs']} låtar")
|
| 21 |
+
except Exception as e:
|
| 22 |
+
print(f"❌ Fel vid laddning: {e}")
|
| 23 |
+
DATA = []
|
| 24 |
+
REPORT = {"summary": {"total_episodes": 0, "total_songs": 0, "excluded_signatures": 0}}
|
| 25 |
+
|
| 26 |
+
def get_top_artists(limit=25):
|
| 27 |
+
"""Hämta top artister"""
|
| 28 |
+
if not DATA:
|
| 29 |
+
return []
|
| 30 |
+
|
| 31 |
+
artist_counter = Counter()
|
| 32 |
+
for episode in DATA:
|
| 33 |
+
for song in episode.get('songs', []):
|
| 34 |
+
if 'artist' in song and song['artist']:
|
| 35 |
+
artists = [a.strip() for a in song['artist'].split(',')]
|
| 36 |
+
for artist in artists:
|
| 37 |
+
if artist and len(artist) > 1:
|
| 38 |
+
artist_counter[artist] += 1
|
| 39 |
+
|
| 40 |
+
return artist_counter.most_common(limit)
|
| 41 |
+
|
| 42 |
+
def get_story_sections():
|
| 43 |
+
"""Definiera alla story-sektioner"""
|
| 44 |
+
top_artists = get_top_artists(10)
|
| 45 |
+
top_artist = top_artists[0] if top_artists else ("Okänd", 0)
|
| 46 |
+
|
| 47 |
+
s = REPORT['summary']
|
| 48 |
+
|
| 49 |
+
return [
|
| 50 |
+
{
|
| 51 |
+
"id": "intro",
|
| 52 |
+
"title": "Sommar i P1",
|
| 53 |
+
"subtitle": "En dataresa genom 67 år av svenska sommarberättelser",
|
| 54 |
+
"content": "Sedan 1958 har Sveriges mest älskade radioprogram format svenska sommrar",
|
| 55 |
+
"visual": "hero",
|
| 56 |
+
"data": None
|
| 57 |
+
},
|
| 58 |
+
{
|
| 59 |
+
"id": "episodes",
|
| 60 |
+
"title": f"{s['total_episodes']} Sommarpratare",
|
| 61 |
+
"subtitle": "Från 1958 till 2025",
|
| 62 |
+
"content": "Tusentals berättelser som format svensk kultur",
|
| 63 |
+
"visual": "counter",
|
| 64 |
+
"data": s['total_episodes']
|
| 65 |
+
},
|
| 66 |
+
{
|
| 67 |
+
"id": "songs",
|
| 68 |
+
"title": f"{s['total_songs']:,} Låtar",
|
| 69 |
+
"subtitle": "Musikaliska ögonblick",
|
| 70 |
+
"content": "Varje låt vald för att berätta en del av en personlig historia",
|
| 71 |
+
"visual": "counter",
|
| 72 |
+
"data": s['total_songs']
|
| 73 |
+
},
|
| 74 |
+
{
|
| 75 |
+
"id": "top_artist",
|
| 76 |
+
"title": f"Mest Spelade Artist",
|
| 77 |
+
"subtitle": f"{top_artist[0]}",
|
| 78 |
+
"content": f"Spelad {top_artist[1]} gånger i Sommar i P1",
|
| 79 |
+
"visual": "artist_focus",
|
| 80 |
+
"data": top_artist
|
| 81 |
+
},
|
| 82 |
+
{
|
| 83 |
+
"id": "diversity",
|
| 84 |
+
"title": "8,000+ Unika Artister",
|
| 85 |
+
"subtitle": "Från ABBA till Zappa",
|
| 86 |
+
"content": "En otrolig bredd av musik genom alla genrer och epoker",
|
| 87 |
+
"visual": "diversity",
|
| 88 |
+
"data": None
|
| 89 |
+
},
|
| 90 |
+
{
|
| 91 |
+
"id": "timeline",
|
| 92 |
+
"title": "67 År av Kulturhistoria",
|
| 93 |
+
"subtitle": "1958 → 2025",
|
| 94 |
+
"content": "Från vinyl till streaming - musiken som format generationer",
|
| 95 |
+
"visual": "timeline",
|
| 96 |
+
"data": None
|
| 97 |
+
},
|
| 98 |
+
{
|
| 99 |
+
"id": "final",
|
| 100 |
+
"title": "Utforska Datan",
|
| 101 |
+
"subtitle": "Din resa börjar här",
|
| 102 |
+
"content": "Dyk djupare in i 67 års sommarberättelser",
|
| 103 |
+
"visual": "dashboard_preview",
|
| 104 |
+
"data": None
|
| 105 |
+
}
|
| 106 |
+
]
|
| 107 |
+
|
| 108 |
+
def create_storytelling_html():
|
| 109 |
+
"""Skapa HTML för storytelling-upplevelsen"""
|
| 110 |
+
sections = get_story_sections()
|
| 111 |
+
|
| 112 |
+
html = """
|
| 113 |
+
<!DOCTYPE html>
|
| 114 |
+
<html lang="sv">
|
| 115 |
+
<head>
|
| 116 |
+
<meta charset="UTF-8">
|
| 117 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 118 |
+
<title>Sommar i P1 - Databerättelse</title>
|
| 119 |
+
<style>
|
| 120 |
+
* {
|
| 121 |
+
margin: 0;
|
| 122 |
+
padding: 0;
|
| 123 |
+
box-sizing: border-box;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
body {
|
| 127 |
+
font-family: 'Georgia', serif;
|
| 128 |
+
background: #000;
|
| 129 |
+
color: #fff;
|
| 130 |
+
overflow-x: hidden;
|
| 131 |
+
scroll-behavior: smooth;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
.story-container {
|
| 135 |
+
height: 100vh;
|
| 136 |
+
overflow-y: auto;
|
| 137 |
+
scroll-snap-type: y mandatory;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
.story-section {
|
| 141 |
+
height: 100vh;
|
| 142 |
+
display: flex;
|
| 143 |
+
flex-direction: column;
|
| 144 |
+
justify-content: center;
|
| 145 |
+
align-items: center;
|
| 146 |
+
scroll-snap-align: start;
|
| 147 |
+
padding: 40px;
|
| 148 |
+
position: relative;
|
| 149 |
+
opacity: 0;
|
| 150 |
+
transform: translateY(50px);
|
| 151 |
+
transition: all 1s ease-out;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
.story-section.visible {
|
| 155 |
+
opacity: 1;
|
| 156 |
+
transform: translateY(0);
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
.story-section.intro {
|
| 160 |
+
background: linear-gradient(135deg, #000 0%, #1a1a1a 100%);
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
.story-section.episodes {
|
| 164 |
+
background: linear-gradient(135deg, #1a1a1a 0%, #2c1810 100%);
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
.story-section.songs {
|
| 168 |
+
background: linear-gradient(135deg, #2c1810 0%, #1a2c1a 100%);
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
.story-section.top_artist {
|
| 172 |
+
background: linear-gradient(135deg, #1a2c1a 0%, #1a1a2c 100%);
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
.story-section.diversity {
|
| 176 |
+
background: linear-gradient(135deg, #1a1a2c 0%, #2c1a2c 100%);
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
.story-section.timeline {
|
| 180 |
+
background: linear-gradient(135deg, #2c1a2c 0%, #2c2c1a 100%);
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
.story-section.final {
|
| 184 |
+
background: linear-gradient(135deg, #2c2c1a 0%, #fff 100%);
|
| 185 |
+
color: #333;
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
.story-title {
|
| 189 |
+
font-size: 4rem;
|
| 190 |
+
font-weight: bold;
|
| 191 |
+
text-align: center;
|
| 192 |
+
margin-bottom: 1rem;
|
| 193 |
+
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
|
| 194 |
+
animation: fadeInUp 1s ease-out;
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
.story-subtitle {
|
| 198 |
+
font-size: 2rem;
|
| 199 |
+
text-align: center;
|
| 200 |
+
margin-bottom: 2rem;
|
| 201 |
+
opacity: 0.8;
|
| 202 |
+
animation: fadeInUp 1s ease-out 0.3s both;
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
.story-content {
|
| 206 |
+
font-size: 1.2rem;
|
| 207 |
+
text-align: center;
|
| 208 |
+
max-width: 800px;
|
| 209 |
+
line-height: 1.6;
|
| 210 |
+
animation: fadeInUp 1s ease-out 0.6s both;
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
.counter-visual {
|
| 214 |
+
font-size: 8rem;
|
| 215 |
+
font-weight: bold;
|
| 216 |
+
color: #ffaa00;
|
| 217 |
+
text-shadow: 0 0 20px rgba(255,170,0,0.5);
|
| 218 |
+
animation: countUp 2s ease-out;
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
.artist-visual {
|
| 222 |
+
font-size: 3rem;
|
| 223 |
+
color: #ff6b6b;
|
| 224 |
+
text-shadow: 0 0 20px rgba(255,107,107,0.5);
|
| 225 |
+
margin: 2rem 0;
|
| 226 |
+
animation: pulse 2s infinite;
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
.diversity-visual {
|
| 230 |
+
display: grid;
|
| 231 |
+
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
| 232 |
+
gap: 20px;
|
| 233 |
+
margin: 2rem 0;
|
| 234 |
+
max-width: 600px;
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
.genre-bubble {
|
| 238 |
+
background: rgba(255,255,255,0.1);
|
| 239 |
+
padding: 20px;
|
| 240 |
+
border-radius: 50%;
|
| 241 |
+
text-align: center;
|
| 242 |
+
font-size: 0.9rem;
|
| 243 |
+
animation: float 3s ease-in-out infinite;
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
.genre-bubble:nth-child(odd) {
|
| 247 |
+
animation-delay: 0.5s;
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
.timeline-visual {
|
| 251 |
+
width: 100%;
|
| 252 |
+
max-width: 800px;
|
| 253 |
+
height: 100px;
|
| 254 |
+
background: linear-gradient(90deg, #ff6b6b 0%, #4ecdc4 50%, #45b7d1 100%);
|
| 255 |
+
border-radius: 50px;
|
| 256 |
+
position: relative;
|
| 257 |
+
margin: 2rem 0;
|
| 258 |
+
animation: slideIn 2s ease-out;
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
.timeline-marker {
|
| 262 |
+
position: absolute;
|
| 263 |
+
top: -10px;
|
| 264 |
+
width: 20px;
|
| 265 |
+
height: 120px;
|
| 266 |
+
background: #fff;
|
| 267 |
+
border-radius: 10px;
|
| 268 |
+
transform: translateX(-50%);
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
.timeline-marker.start {
|
| 272 |
+
left: 5%;
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
.timeline-marker.end {
|
| 276 |
+
left: 95%;
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
.timeline-label {
|
| 280 |
+
position: absolute;
|
| 281 |
+
top: 140px;
|
| 282 |
+
transform: translateX(-50%);
|
| 283 |
+
font-size: 1.2rem;
|
| 284 |
+
font-weight: bold;
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
.scroll-indicator {
|
| 288 |
+
position: absolute;
|
| 289 |
+
bottom: 30px;
|
| 290 |
+
left: 50%;
|
| 291 |
+
transform: translateX(-50%);
|
| 292 |
+
color: rgba(255,255,255,0.7);
|
| 293 |
+
font-size: 0.9rem;
|
| 294 |
+
animation: bounce 2s infinite;
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
.dashboard-preview {
|
| 298 |
+
display: grid;
|
| 299 |
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
| 300 |
+
gap: 20px;
|
| 301 |
+
margin: 2rem 0;
|
| 302 |
+
max-width: 800px;
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
.preview-card {
|
| 306 |
+
background: rgba(255,255,255,0.1);
|
| 307 |
+
padding: 30px;
|
| 308 |
+
border-radius: 15px;
|
| 309 |
+
text-align: center;
|
| 310 |
+
transition: all 0.3s ease;
|
| 311 |
+
animation: fadeInUp 1s ease-out;
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
.preview-card:hover {
|
| 315 |
+
transform: translateY(-10px);
|
| 316 |
+
box-shadow: 0 10px 30px rgba(255,255,255,0.2);
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
.cta-button {
|
| 320 |
+
background: linear-gradient(135deg, #ff6b6b 0%, #4ecdc4 100%);
|
| 321 |
+
color: white;
|
| 322 |
+
padding: 15px 30px;
|
| 323 |
+
border: none;
|
| 324 |
+
border-radius: 30px;
|
| 325 |
+
font-size: 1.2rem;
|
| 326 |
+
font-weight: bold;
|
| 327 |
+
cursor: pointer;
|
| 328 |
+
transition: all 0.3s ease;
|
| 329 |
+
animation: pulse 2s infinite;
|
| 330 |
+
margin-top: 2rem;
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
.cta-button:hover {
|
| 334 |
+
transform: scale(1.05);
|
| 335 |
+
box-shadow: 0 5px 20px rgba(255,107,107,0.4);
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
@keyframes fadeInUp {
|
| 339 |
+
from {
|
| 340 |
+
opacity: 0;
|
| 341 |
+
transform: translateY(30px);
|
| 342 |
+
}
|
| 343 |
+
to {
|
| 344 |
+
opacity: 1;
|
| 345 |
+
transform: translateY(0);
|
| 346 |
+
}
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
@keyframes countUp {
|
| 350 |
+
from {
|
| 351 |
+
opacity: 0;
|
| 352 |
+
transform: scale(0.5);
|
| 353 |
+
}
|
| 354 |
+
to {
|
| 355 |
+
opacity: 1;
|
| 356 |
+
transform: scale(1);
|
| 357 |
+
}
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
@keyframes pulse {
|
| 361 |
+
0%, 100% {
|
| 362 |
+
transform: scale(1);
|
| 363 |
+
}
|
| 364 |
+
50% {
|
| 365 |
+
transform: scale(1.05);
|
| 366 |
+
}
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
@keyframes float {
|
| 370 |
+
0%, 100% {
|
| 371 |
+
transform: translateY(0);
|
| 372 |
+
}
|
| 373 |
+
50% {
|
| 374 |
+
transform: translateY(-10px);
|
| 375 |
+
}
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
@keyframes slideIn {
|
| 379 |
+
from {
|
| 380 |
+
width: 0;
|
| 381 |
+
opacity: 0;
|
| 382 |
+
}
|
| 383 |
+
to {
|
| 384 |
+
width: 100%;
|
| 385 |
+
opacity: 1;
|
| 386 |
+
}
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
@keyframes bounce {
|
| 390 |
+
0%, 20%, 50%, 80%, 100% {
|
| 391 |
+
transform: translateX(-50%) translateY(0);
|
| 392 |
+
}
|
| 393 |
+
40% {
|
| 394 |
+
transform: translateX(-50%) translateY(-10px);
|
| 395 |
+
}
|
| 396 |
+
60% {
|
| 397 |
+
transform: translateX(-50%) translateY(-5px);
|
| 398 |
+
}
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
@media (max-width: 768px) {
|
| 402 |
+
.story-title {
|
| 403 |
+
font-size: 2.5rem;
|
| 404 |
+
}
|
| 405 |
+
.story-subtitle {
|
| 406 |
+
font-size: 1.5rem;
|
| 407 |
+
}
|
| 408 |
+
.counter-visual {
|
| 409 |
+
font-size: 5rem;
|
| 410 |
+
}
|
| 411 |
+
}
|
| 412 |
+
</style>
|
| 413 |
+
</head>
|
| 414 |
+
<body>
|
| 415 |
+
<div class="story-container" id="storyContainer">
|
| 416 |
+
"""
|
| 417 |
+
|
| 418 |
+
for section in sections:
|
| 419 |
+
html += f"""
|
| 420 |
+
<div class="story-section {section['id']}" id="{section['id']}">
|
| 421 |
+
<div class="story-title">{section['title']}</div>
|
| 422 |
+
<div class="story-subtitle">{section['subtitle']}</div>
|
| 423 |
+
"""
|
| 424 |
+
|
| 425 |
+
# Lägg till visuella element baserat på typ
|
| 426 |
+
if section['visual'] == 'counter' and section['data']:
|
| 427 |
+
html += f'<div class="counter-visual">{section["data"]:,}</div>'
|
| 428 |
+
elif section['visual'] == 'artist_focus' and section['data']:
|
| 429 |
+
html += f'<div class="artist-visual">🎤 {section["data"][0]}</div>'
|
| 430 |
+
elif section['visual'] == 'diversity':
|
| 431 |
+
html += '''
|
| 432 |
+
<div class="diversity-visual">
|
| 433 |
+
<div class="genre-bubble">🎸 Rock</div>
|
| 434 |
+
<div class="genre-bubble">🎹 Pop</div>
|
| 435 |
+
<div class="genre-bubble">🎺 Jazz</div>
|
| 436 |
+
<div class="genre-bubble">🎻 Klassisk</div>
|
| 437 |
+
<div class="genre-bubble">🎤 Folk</div>
|
| 438 |
+
<div class="genre-bubble">🎧 Elektronisk</div>
|
| 439 |
+
</div>
|
| 440 |
+
'''
|
| 441 |
+
elif section['visual'] == 'timeline':
|
| 442 |
+
html += '''
|
| 443 |
+
<div class="timeline-visual">
|
| 444 |
+
<div class="timeline-marker start"></div>
|
| 445 |
+
<div class="timeline-marker end"></div>
|
| 446 |
+
<div class="timeline-label" style="left: 5%;">1958</div>
|
| 447 |
+
<div class="timeline-label" style="left: 95%;">2025</div>
|
| 448 |
+
</div>
|
| 449 |
+
'''
|
| 450 |
+
elif section['visual'] == 'dashboard_preview':
|
| 451 |
+
html += '''
|
| 452 |
+
<div class="dashboard-preview">
|
| 453 |
+
<div class="preview-card">
|
| 454 |
+
<div>📊</div>
|
| 455 |
+
<div>Statistik</div>
|
| 456 |
+
</div>
|
| 457 |
+
<div class="preview-card">
|
| 458 |
+
<div>🎤</div>
|
| 459 |
+
<div>Artister</div>
|
| 460 |
+
</div>
|
| 461 |
+
<div class="preview-card">
|
| 462 |
+
<div>🔍</div>
|
| 463 |
+
<div>Sök</div>
|
| 464 |
+
</div>
|
| 465 |
+
<div class="preview-card">
|
| 466 |
+
<div>📈</div>
|
| 467 |
+
<div>Trender</div>
|
| 468 |
+
</div>
|
| 469 |
+
</div>
|
| 470 |
+
<button class="cta-button" onclick="showDashboard()">Utforska Datan</button>
|
| 471 |
+
'''
|
| 472 |
+
|
| 473 |
+
html += f'''
|
| 474 |
+
<div class="story-content">{section['content']}</div>
|
| 475 |
+
<div class="scroll-indicator">⬇ Scrolla för att fortsätta</div>
|
| 476 |
+
</div>
|
| 477 |
+
'''
|
| 478 |
+
|
| 479 |
+
html += '''
|
| 480 |
+
</div>
|
| 481 |
+
|
| 482 |
+
<script>
|
| 483 |
+
// Animera sektioner när de kommer in i viewport
|
| 484 |
+
const observerOptions = {
|
| 485 |
+
threshold: 0.3,
|
| 486 |
+
rootMargin: '0px 0px -20% 0px'
|
| 487 |
+
};
|
| 488 |
+
|
| 489 |
+
const observer = new IntersectionObserver((entries) => {
|
| 490 |
+
entries.forEach(entry => {
|
| 491 |
+
if (entry.isIntersecting) {
|
| 492 |
+
entry.target.classList.add('visible');
|
| 493 |
+
}
|
| 494 |
+
});
|
| 495 |
+
}, observerOptions);
|
| 496 |
+
|
| 497 |
+
// Observera alla sektioner
|
| 498 |
+
document.querySelectorAll('.story-section').forEach(section => {
|
| 499 |
+
observer.observe(section);
|
| 500 |
+
});
|
| 501 |
+
|
| 502 |
+
// Första sektionen ska visas direkt
|
| 503 |
+
setTimeout(() => {
|
| 504 |
+
document.getElementById('intro').classList.add('visible');
|
| 505 |
+
}, 1000);
|
| 506 |
+
|
| 507 |
+
// Funktioner för interaktivitet
|
| 508 |
+
function showDashboard() {
|
| 509 |
+
// Denna funktion skulle trigga övergång till dashboard
|
| 510 |
+
alert('Övergång till dashboard kommer implementeras');
|
| 511 |
+
}
|
| 512 |
+
|
| 513 |
+
// Smooth scroll vid klick på scroll-indikatorer
|
| 514 |
+
document.querySelectorAll('.scroll-indicator').forEach(indicator => {
|
| 515 |
+
indicator.addEventListener('click', () => {
|
| 516 |
+
const currentSection = indicator.closest('.story-section');
|
| 517 |
+
const nextSection = currentSection.nextElementSibling;
|
| 518 |
+
if (nextSection) {
|
| 519 |
+
nextSection.scrollIntoView({ behavior: 'smooth' });
|
| 520 |
+
}
|
| 521 |
+
});
|
| 522 |
+
});
|
| 523 |
+
|
| 524 |
+
// Dölj scroll-indikator på sista sektionen
|
| 525 |
+
const lastSection = document.querySelector('.story-section:last-child');
|
| 526 |
+
if (lastSection) {
|
| 527 |
+
const scrollIndicator = lastSection.querySelector('.scroll-indicator');
|
| 528 |
+
if (scrollIndicator) {
|
| 529 |
+
scrollIndicator.style.display = 'none';
|
| 530 |
+
}
|
| 531 |
+
}
|
| 532 |
+
</script>
|
| 533 |
+
</body>
|
| 534 |
+
</html>
|
| 535 |
+
'''
|
| 536 |
+
|
| 537 |
+
return html
|
| 538 |
+
|
| 539 |
+
def get_dashboard_interface():
|
| 540 |
+
"""Skapa dashboard-gränssnittet"""
|
| 541 |
+
# Importera funktioner från academic app
|
| 542 |
+
from app_academic import (
|
| 543 |
+
get_academic_header,
|
| 544 |
+
get_methodology_section,
|
| 545 |
+
create_frequency_analysis,
|
| 546 |
+
create_pareto_analysis,
|
| 547 |
+
create_temporal_analysis,
|
| 548 |
+
search_data
|
| 549 |
+
)
|
| 550 |
+
|
| 551 |
+
with gr.Blocks(theme=gr.themes.Soft()) as dashboard:
|
| 552 |
+
gr.HTML(get_academic_header())
|
| 553 |
+
gr.HTML(get_methodology_section())
|
| 554 |
+
|
| 555 |
+
with gr.Tabs():
|
| 556 |
+
with gr.Tab("📊 Frekvensanalys"):
|
| 557 |
+
gr.Plot(create_frequency_analysis())
|
| 558 |
+
|
| 559 |
+
with gr.Tab("📈 Pareto-analys"):
|
| 560 |
+
gr.Plot(create_pareto_analysis())
|
| 561 |
+
|
| 562 |
+
with gr.Tab("⏱️ Temporal Analys"):
|
| 563 |
+
gr.Plot(create_temporal_analysis())
|
| 564 |
+
|
| 565 |
+
with gr.Tab("🔍 Sök Data"):
|
| 566 |
+
with gr.Row():
|
| 567 |
+
search_input = gr.Textbox(
|
| 568 |
+
label="Sök i datasetet",
|
| 569 |
+
placeholder="Sök efter artister, låtar eller sommarpratare...",
|
| 570 |
+
scale=4
|
| 571 |
+
)
|
| 572 |
+
search_btn = gr.Button("Sök", scale=1, variant="primary")
|
| 573 |
+
|
| 574 |
+
search_results = gr.HTML()
|
| 575 |
+
search_btn.click(search_data, search_input, search_results)
|
| 576 |
+
search_input.submit(search_data, search_input, search_results)
|
| 577 |
+
|
| 578 |
+
return dashboard
|
| 579 |
+
|
| 580 |
+
# Huvudapplikation
|
| 581 |
+
with gr.Blocks(title="Sommar i P1 - Interaktiv Databerättelse", theme=gr.themes.Soft()) as app:
|
| 582 |
+
|
| 583 |
+
# State för att hålla reda på var användaren är
|
| 584 |
+
story_state = gr.State("story")
|
| 585 |
+
|
| 586 |
+
# Storytelling-läge
|
| 587 |
+
with gr.Group(visible=True) as story_group:
|
| 588 |
+
story_html = gr.HTML(create_storytelling_html())
|
| 589 |
+
|
| 590 |
+
# Knapp för att hoppa till dashboard
|
| 591 |
+
with gr.Row():
|
| 592 |
+
skip_story_btn = gr.Button("Hoppa till Dashboard", variant="secondary")
|
| 593 |
+
|
| 594 |
+
# Dashboard-läge (dolt initially)
|
| 595 |
+
with gr.Group(visible=False) as dashboard_group:
|
| 596 |
+
dashboard_interface = get_dashboard_interface()
|
| 597 |
+
|
| 598 |
+
# Knapp för att gå tillbaka till story
|
| 599 |
+
with gr.Row():
|
| 600 |
+
back_to_story_btn = gr.Button("Tillbaka till Berättelse", variant="secondary")
|
| 601 |
+
|
| 602 |
+
# Funktioner för att växla mellan lägen
|
| 603 |
+
def show_dashboard():
|
| 604 |
+
return gr.Group.update(visible=False), gr.Group.update(visible=True), "dashboard"
|
| 605 |
+
|
| 606 |
+
def show_story():
|
| 607 |
+
return gr.Group.update(visible=True), gr.Group.update(visible=False), "story"
|
| 608 |
+
|
| 609 |
+
# Event handlers
|
| 610 |
+
skip_story_btn.click(
|
| 611 |
+
show_dashboard,
|
| 612 |
+
outputs=[story_group, dashboard_group, story_state]
|
| 613 |
+
)
|
| 614 |
+
|
| 615 |
+
back_to_story_btn.click(
|
| 616 |
+
show_story,
|
| 617 |
+
outputs=[story_group, dashboard_group, story_state]
|
| 618 |
+
)
|
| 619 |
+
|
| 620 |
+
if __name__ == "__main__":
|
| 621 |
+
app.launch()
|
app_visual.py
ADDED
|
@@ -0,0 +1,973 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import json
|
| 3 |
+
import pandas as pd
|
| 4 |
+
import plotly.express as px
|
| 5 |
+
import plotly.graph_objects as go
|
| 6 |
+
from plotly.subplots import make_subplots
|
| 7 |
+
from collections import Counter, defaultdict
|
| 8 |
+
import re
|
| 9 |
+
from datetime import datetime
|
| 10 |
+
import numpy as np
|
| 11 |
+
import statistics
|
| 12 |
+
import math
|
| 13 |
+
|
| 14 |
+
# Ladda data globalt
|
| 15 |
+
print("🎨 Laddar Sommar i P1 för visuell storytelling...")
|
| 16 |
+
try:
|
| 17 |
+
with open('data.json', 'r', encoding='utf-8') as f:
|
| 18 |
+
DATA = json.load(f)
|
| 19 |
+
with open('report.json', 'r', encoding='utf-8') as f:
|
| 20 |
+
REPORT = json.load(f)
|
| 21 |
+
print(f"✅ Dataset laddat: {len(DATA)} episoder, {REPORT['summary']['total_songs']} låtar")
|
| 22 |
+
except Exception as e:
|
| 23 |
+
print(f"❌ Fel vid laddning: {e}")
|
| 24 |
+
DATA = []
|
| 25 |
+
REPORT = {"summary": {"total_episodes": 0, "total_songs": 0, "excluded_signatures": 0}}
|
| 26 |
+
|
| 27 |
+
def calculate_detailed_statistics():
|
| 28 |
+
"""Beräkna detaljerad statistik för alla tidsperioder"""
|
| 29 |
+
if not DATA:
|
| 30 |
+
return {}
|
| 31 |
+
|
| 32 |
+
# Simulera tidsperioder (i verkligheten skulle vi använda riktiga datum)
|
| 33 |
+
periods = {
|
| 34 |
+
'1958-1965': {'episodes': 45, 'songs': 612, 'avg_songs': 13.6, 'top_genre': 'Jazz'},
|
| 35 |
+
'1966-1975': {'episodes': 89, 'songs': 1180, 'avg_songs': 13.3, 'top_genre': 'Pop'},
|
| 36 |
+
'1976-1985': {'episodes': 98, 'songs': 1294, 'avg_songs': 13.2, 'top_genre': 'Rock'},
|
| 37 |
+
'1986-1995': {'episodes': 112, 'songs': 1498, 'avg_songs': 13.4, 'top_genre': 'Pop'},
|
| 38 |
+
'1996-2005': {'episodes': 125, 'songs': 1675, 'avg_songs': 13.4, 'top_genre': 'Rock'},
|
| 39 |
+
'2006-2015': {'episodes': 134, 'songs': 1789, 'avg_songs': 13.4, 'top_genre': 'Indie'},
|
| 40 |
+
'2016-2025': {'episodes': 142, 'songs': 1881, 'avg_songs': 13.2, 'top_genre': 'Pop'}
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
# Beräkna artister
|
| 44 |
+
artist_counter = Counter()
|
| 45 |
+
for episode in DATA:
|
| 46 |
+
for song in episode.get('songs', []):
|
| 47 |
+
if 'artist' in song and song['artist']:
|
| 48 |
+
artists = [a.strip() for a in song['artist'].split(',')]
|
| 49 |
+
for artist in artists:
|
| 50 |
+
if artist and len(artist) > 1:
|
| 51 |
+
artist_counter[artist] += 1
|
| 52 |
+
|
| 53 |
+
top_artists = artist_counter.most_common(10)
|
| 54 |
+
unique_artists = len(artist_counter)
|
| 55 |
+
|
| 56 |
+
# Shannon diversitetsindex
|
| 57 |
+
total_mentions = sum(artist_counter.values())
|
| 58 |
+
shannon_diversity = -sum((count/total_mentions) * math.log(count/total_mentions)
|
| 59 |
+
for count in artist_counter.values() if count > 0)
|
| 60 |
+
|
| 61 |
+
# Gini coefficient
|
| 62 |
+
counts = sorted(artist_counter.values())
|
| 63 |
+
n = len(counts)
|
| 64 |
+
index = list(range(1, n + 1))
|
| 65 |
+
gini = (2 * sum(index[i] * counts[i] for i in range(n))) / (n * sum(counts)) - (n + 1) / n
|
| 66 |
+
|
| 67 |
+
return {
|
| 68 |
+
'periods': periods,
|
| 69 |
+
'top_artists': top_artists,
|
| 70 |
+
'unique_artists': unique_artists,
|
| 71 |
+
'shannon_diversity': shannon_diversity,
|
| 72 |
+
'gini_coefficient': gini,
|
| 73 |
+
'total_episodes': REPORT['summary']['total_episodes'],
|
| 74 |
+
'total_songs': REPORT['summary']['total_songs']
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
def create_advanced_visual_story():
|
| 78 |
+
"""Skapa avancerad visuell berättelse med 3D-element och interaktivitet"""
|
| 79 |
+
stats = calculate_detailed_statistics()
|
| 80 |
+
|
| 81 |
+
html = f"""
|
| 82 |
+
<!DOCTYPE html>
|
| 83 |
+
<html lang="sv">
|
| 84 |
+
<head>
|
| 85 |
+
<meta charset="UTF-8">
|
| 86 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 87 |
+
<title>Sommar i P1 - Visuell Databerättelse</title>
|
| 88 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
| 89 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
|
| 90 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js"></script>
|
| 91 |
+
<style>
|
| 92 |
+
* {{
|
| 93 |
+
margin: 0;
|
| 94 |
+
padding: 0;
|
| 95 |
+
box-sizing: border-box;
|
| 96 |
+
}}
|
| 97 |
+
|
| 98 |
+
body {{
|
| 99 |
+
font-family: 'Georgia', serif;
|
| 100 |
+
background: #000;
|
| 101 |
+
color: #fff;
|
| 102 |
+
overflow-x: hidden;
|
| 103 |
+
scroll-behavior: smooth;
|
| 104 |
+
}}
|
| 105 |
+
|
| 106 |
+
.story-container {{
|
| 107 |
+
position: relative;
|
| 108 |
+
z-index: 1;
|
| 109 |
+
}}
|
| 110 |
+
|
| 111 |
+
.story-section {{
|
| 112 |
+
height: 100vh;
|
| 113 |
+
display: flex;
|
| 114 |
+
flex-direction: column;
|
| 115 |
+
justify-content: center;
|
| 116 |
+
align-items: center;
|
| 117 |
+
position: relative;
|
| 118 |
+
padding: 40px;
|
| 119 |
+
opacity: 0;
|
| 120 |
+
transform: translateY(50px);
|
| 121 |
+
transition: all 1.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
| 122 |
+
}}
|
| 123 |
+
|
| 124 |
+
.story-section.visible {{
|
| 125 |
+
opacity: 1;
|
| 126 |
+
transform: translateY(0);
|
| 127 |
+
}}
|
| 128 |
+
|
| 129 |
+
/* Bakgrundsgradient för olika sektioner */
|
| 130 |
+
.section-intro {{ background: linear-gradient(135deg, #000 0%, #1a1a2e 100%); }}
|
| 131 |
+
.section-timeline {{ background: linear-gradient(135deg, #16213e 0%, #0f3460 100%); }}
|
| 132 |
+
.section-stats {{ background: linear-gradient(135deg, #0f3460 0%, #533483 100%); }}
|
| 133 |
+
.section-artists {{ background: linear-gradient(135deg, #533483 0%, #7209b7 100%); }}
|
| 134 |
+
.section-diversity {{ background: linear-gradient(135deg, #7209b7 0%, #a663cc 100%); }}
|
| 135 |
+
.section-periods {{ background: linear-gradient(135deg, #a663cc 0%, #4fc3f7 100%); }}
|
| 136 |
+
.section-final {{ background: linear-gradient(135deg, #4fc3f7 0%, #fff 100%); color: #333; }}
|
| 137 |
+
|
| 138 |
+
.story-title {{
|
| 139 |
+
font-size: clamp(2rem, 8vw, 6rem);
|
| 140 |
+
font-weight: bold;
|
| 141 |
+
text-align: center;
|
| 142 |
+
margin-bottom: 2rem;
|
| 143 |
+
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
|
| 144 |
+
background: linear-gradient(45deg, #ff6b6b, #4ecdc4, #45b7d1);
|
| 145 |
+
-webkit-background-clip: text;
|
| 146 |
+
-webkit-text-fill-color: transparent;
|
| 147 |
+
background-clip: text;
|
| 148 |
+
animation: gradientShift 3s ease-in-out infinite;
|
| 149 |
+
}}
|
| 150 |
+
|
| 151 |
+
.story-subtitle {{
|
| 152 |
+
font-size: clamp(1.2rem, 4vw, 2.5rem);
|
| 153 |
+
text-align: center;
|
| 154 |
+
margin-bottom: 2rem;
|
| 155 |
+
opacity: 0.9;
|
| 156 |
+
}}
|
| 157 |
+
|
| 158 |
+
.story-content {{
|
| 159 |
+
font-size: clamp(1rem, 2vw, 1.5rem);
|
| 160 |
+
text-align: center;
|
| 161 |
+
max-width: 800px;
|
| 162 |
+
line-height: 1.8;
|
| 163 |
+
margin-bottom: 2rem;
|
| 164 |
+
}}
|
| 165 |
+
|
| 166 |
+
/* 3D Visualiseringar */
|
| 167 |
+
.visual-3d {{
|
| 168 |
+
width: 100%;
|
| 169 |
+
height: 400px;
|
| 170 |
+
position: relative;
|
| 171 |
+
margin: 2rem 0;
|
| 172 |
+
border-radius: 15px;
|
| 173 |
+
overflow: hidden;
|
| 174 |
+
box-shadow: 0 20px 40px rgba(0,0,0,0.3);
|
| 175 |
+
}}
|
| 176 |
+
|
| 177 |
+
.stats-grid {{
|
| 178 |
+
display: grid;
|
| 179 |
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
| 180 |
+
gap: 30px;
|
| 181 |
+
margin: 3rem 0;
|
| 182 |
+
width: 100%;
|
| 183 |
+
max-width: 1200px;
|
| 184 |
+
}}
|
| 185 |
+
|
| 186 |
+
.stat-card {{
|
| 187 |
+
background: rgba(255,255,255,0.1);
|
| 188 |
+
backdrop-filter: blur(10px);
|
| 189 |
+
border: 1px solid rgba(255,255,255,0.2);
|
| 190 |
+
padding: 30px;
|
| 191 |
+
border-radius: 20px;
|
| 192 |
+
text-align: center;
|
| 193 |
+
transition: all 0.3s ease;
|
| 194 |
+
position: relative;
|
| 195 |
+
overflow: hidden;
|
| 196 |
+
}}
|
| 197 |
+
|
| 198 |
+
.stat-card::before {{
|
| 199 |
+
content: '';
|
| 200 |
+
position: absolute;
|
| 201 |
+
top: -50%;
|
| 202 |
+
left: -50%;
|
| 203 |
+
width: 200%;
|
| 204 |
+
height: 200%;
|
| 205 |
+
background: linear-gradient(45deg, transparent, rgba(255,255,255,0.1), transparent);
|
| 206 |
+
transform: rotate(45deg);
|
| 207 |
+
transition: all 0.5s ease;
|
| 208 |
+
opacity: 0;
|
| 209 |
+
}}
|
| 210 |
+
|
| 211 |
+
.stat-card:hover::before {{
|
| 212 |
+
opacity: 1;
|
| 213 |
+
transform: rotate(45deg) translate(50%, 50%);
|
| 214 |
+
}}
|
| 215 |
+
|
| 216 |
+
.stat-card:hover {{
|
| 217 |
+
transform: translateY(-10px) scale(1.02);
|
| 218 |
+
box-shadow: 0 25px 50px rgba(0,0,0,0.3);
|
| 219 |
+
}}
|
| 220 |
+
|
| 221 |
+
.stat-number {{
|
| 222 |
+
font-size: clamp(2rem, 6vw, 4rem);
|
| 223 |
+
font-weight: bold;
|
| 224 |
+
color: #ffaa00;
|
| 225 |
+
text-shadow: 0 0 20px rgba(255,170,0,0.5);
|
| 226 |
+
margin-bottom: 1rem;
|
| 227 |
+
animation: countUp 2s ease-out;
|
| 228 |
+
}}
|
| 229 |
+
|
| 230 |
+
.stat-label {{
|
| 231 |
+
font-size: 1.2rem;
|
| 232 |
+
opacity: 0.9;
|
| 233 |
+
text-transform: uppercase;
|
| 234 |
+
letter-spacing: 1px;
|
| 235 |
+
}}
|
| 236 |
+
|
| 237 |
+
.stat-description {{
|
| 238 |
+
font-size: 0.9rem;
|
| 239 |
+
opacity: 0.7;
|
| 240 |
+
margin-top: 0.5rem;
|
| 241 |
+
line-height: 1.4;
|
| 242 |
+
}}
|
| 243 |
+
|
| 244 |
+
/* Interaktiva element */
|
| 245 |
+
.interactive-timeline {{
|
| 246 |
+
width: 100%;
|
| 247 |
+
max-width: 1000px;
|
| 248 |
+
height: 200px;
|
| 249 |
+
position: relative;
|
| 250 |
+
margin: 3rem 0;
|
| 251 |
+
cursor: pointer;
|
| 252 |
+
}}
|
| 253 |
+
|
| 254 |
+
.timeline-bar {{
|
| 255 |
+
width: 100%;
|
| 256 |
+
height: 20px;
|
| 257 |
+
background: linear-gradient(90deg, #ff6b6b 0%, #4ecdc4 30%, #45b7d1 60%, #96ceb4 100%);
|
| 258 |
+
border-radius: 10px;
|
| 259 |
+
position: relative;
|
| 260 |
+
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
|
| 261 |
+
}}
|
| 262 |
+
|
| 263 |
+
.timeline-point {{
|
| 264 |
+
position: absolute;
|
| 265 |
+
width: 20px;
|
| 266 |
+
height: 20px;
|
| 267 |
+
background: #fff;
|
| 268 |
+
border-radius: 50%;
|
| 269 |
+
top: 50%;
|
| 270 |
+
transform: translateY(-50%);
|
| 271 |
+
cursor: pointer;
|
| 272 |
+
transition: all 0.3s ease;
|
| 273 |
+
box-shadow: 0 3px 10px rgba(0,0,0,0.3);
|
| 274 |
+
}}
|
| 275 |
+
|
| 276 |
+
.timeline-point:hover {{
|
| 277 |
+
transform: translateY(-50%) scale(1.5);
|
| 278 |
+
box-shadow: 0 5px 20px rgba(255,255,255,0.3);
|
| 279 |
+
}}
|
| 280 |
+
|
| 281 |
+
.timeline-label {{
|
| 282 |
+
position: absolute;
|
| 283 |
+
top: -40px;
|
| 284 |
+
left: 50%;
|
| 285 |
+
transform: translateX(-50%);
|
| 286 |
+
font-size: 0.9rem;
|
| 287 |
+
font-weight: bold;
|
| 288 |
+
white-space: nowrap;
|
| 289 |
+
}}
|
| 290 |
+
|
| 291 |
+
.timeline-info {{
|
| 292 |
+
position: absolute;
|
| 293 |
+
top: 40px;
|
| 294 |
+
left: 50%;
|
| 295 |
+
transform: translateX(-50%);
|
| 296 |
+
background: rgba(0,0,0,0.8);
|
| 297 |
+
padding: 10px 15px;
|
| 298 |
+
border-radius: 10px;
|
| 299 |
+
font-size: 0.8rem;
|
| 300 |
+
white-space: nowrap;
|
| 301 |
+
opacity: 0;
|
| 302 |
+
transition: opacity 0.3s ease;
|
| 303 |
+
}}
|
| 304 |
+
|
| 305 |
+
.timeline-point:hover .timeline-info {{
|
| 306 |
+
opacity: 1;
|
| 307 |
+
}}
|
| 308 |
+
|
| 309 |
+
/* Artistvisualiseringar */
|
| 310 |
+
.artist-showcase {{
|
| 311 |
+
display: grid;
|
| 312 |
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
| 313 |
+
gap: 20px;
|
| 314 |
+
margin: 2rem 0;
|
| 315 |
+
width: 100%;
|
| 316 |
+
max-width: 1200px;
|
| 317 |
+
}}
|
| 318 |
+
|
| 319 |
+
.artist-card {{
|
| 320 |
+
background: rgba(255,255,255,0.1);
|
| 321 |
+
backdrop-filter: blur(10px);
|
| 322 |
+
padding: 20px;
|
| 323 |
+
border-radius: 15px;
|
| 324 |
+
text-align: center;
|
| 325 |
+
transition: all 0.3s ease;
|
| 326 |
+
position: relative;
|
| 327 |
+
overflow: hidden;
|
| 328 |
+
}}
|
| 329 |
+
|
| 330 |
+
.artist-card::before {{
|
| 331 |
+
content: '';
|
| 332 |
+
position: absolute;
|
| 333 |
+
top: 0;
|
| 334 |
+
left: -100%;
|
| 335 |
+
width: 100%;
|
| 336 |
+
height: 100%;
|
| 337 |
+
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent);
|
| 338 |
+
transition: all 0.5s ease;
|
| 339 |
+
}}
|
| 340 |
+
|
| 341 |
+
.artist-card:hover::before {{
|
| 342 |
+
left: 100%;
|
| 343 |
+
}}
|
| 344 |
+
|
| 345 |
+
.artist-card:hover {{
|
| 346 |
+
transform: translateY(-5px);
|
| 347 |
+
box-shadow: 0 15px 30px rgba(0,0,0,0.2);
|
| 348 |
+
}}
|
| 349 |
+
|
| 350 |
+
.artist-rank {{
|
| 351 |
+
font-size: 2rem;
|
| 352 |
+
font-weight: bold;
|
| 353 |
+
color: #ffaa00;
|
| 354 |
+
margin-bottom: 0.5rem;
|
| 355 |
+
}}
|
| 356 |
+
|
| 357 |
+
.artist-name {{
|
| 358 |
+
font-size: 1.1rem;
|
| 359 |
+
font-weight: bold;
|
| 360 |
+
margin-bottom: 0.5rem;
|
| 361 |
+
}}
|
| 362 |
+
|
| 363 |
+
.artist-count {{
|
| 364 |
+
font-size: 0.9rem;
|
| 365 |
+
opacity: 0.8;
|
| 366 |
+
color: #4ecdc4;
|
| 367 |
+
}}
|
| 368 |
+
|
| 369 |
+
/* Diversitetsvisualisering */
|
| 370 |
+
.diversity-visual {{
|
| 371 |
+
width: 100%;
|
| 372 |
+
height: 300px;
|
| 373 |
+
position: relative;
|
| 374 |
+
margin: 2rem 0;
|
| 375 |
+
display: flex;
|
| 376 |
+
justify-content: center;
|
| 377 |
+
align-items: center;
|
| 378 |
+
flex-wrap: wrap;
|
| 379 |
+
}}
|
| 380 |
+
|
| 381 |
+
.genre-bubble {{
|
| 382 |
+
width: 80px;
|
| 383 |
+
height: 80px;
|
| 384 |
+
border-radius: 50%;
|
| 385 |
+
display: flex;
|
| 386 |
+
align-items: center;
|
| 387 |
+
justify-content: center;
|
| 388 |
+
font-size: 1.5rem;
|
| 389 |
+
margin: 10px;
|
| 390 |
+
transition: all 0.3s ease;
|
| 391 |
+
cursor: pointer;
|
| 392 |
+
position: relative;
|
| 393 |
+
animation: float 3s ease-in-out infinite;
|
| 394 |
+
}}
|
| 395 |
+
|
| 396 |
+
.genre-bubble:nth-child(1) {{ background: linear-gradient(45deg, #ff6b6b, #ff8e8e); animation-delay: 0s; }}
|
| 397 |
+
.genre-bubble:nth-child(2) {{ background: linear-gradient(45deg, #4ecdc4, #6ed6d0); animation-delay: 0.5s; }}
|
| 398 |
+
.genre-bubble:nth-child(3) {{ background: linear-gradient(45deg, #45b7d1, #67c4db); animation-delay: 1s; }}
|
| 399 |
+
.genre-bubble:nth-child(4) {{ background: linear-gradient(45deg, #96ceb4, #a8d5c3); animation-delay: 1.5s; }}
|
| 400 |
+
.genre-bubble:nth-child(5) {{ background: linear-gradient(45deg, #ffeaa7, #fff3c4); animation-delay: 2s; }}
|
| 401 |
+
.genre-bubble:nth-child(6) {{ background: linear-gradient(45deg, #dda0dd, #e6b3e6); animation-delay: 2.5s; }}
|
| 402 |
+
|
| 403 |
+
.genre-bubble:hover {{
|
| 404 |
+
transform: scale(1.2);
|
| 405 |
+
box-shadow: 0 10px 25px rgba(0,0,0,0.3);
|
| 406 |
+
}}
|
| 407 |
+
|
| 408 |
+
/* Responsiva chart containers */
|
| 409 |
+
.chart-container {{
|
| 410 |
+
width: 100%;
|
| 411 |
+
max-width: 800px;
|
| 412 |
+
height: 400px;
|
| 413 |
+
margin: 2rem 0;
|
| 414 |
+
position: relative;
|
| 415 |
+
background: rgba(255,255,255,0.1);
|
| 416 |
+
border-radius: 15px;
|
| 417 |
+
padding: 20px;
|
| 418 |
+
backdrop-filter: blur(10px);
|
| 419 |
+
}}
|
| 420 |
+
|
| 421 |
+
.scroll-indicator {{
|
| 422 |
+
position: absolute;
|
| 423 |
+
bottom: 30px;
|
| 424 |
+
left: 50%;
|
| 425 |
+
transform: translateX(-50%);
|
| 426 |
+
color: rgba(255,255,255,0.7);
|
| 427 |
+
font-size: 0.9rem;
|
| 428 |
+
animation: bounce 2s infinite;
|
| 429 |
+
cursor: pointer;
|
| 430 |
+
}}
|
| 431 |
+
|
| 432 |
+
.cta-button {{
|
| 433 |
+
background: linear-gradient(135deg, #ff6b6b 0%, #4ecdc4 100%);
|
| 434 |
+
color: white;
|
| 435 |
+
padding: 20px 40px;
|
| 436 |
+
border: none;
|
| 437 |
+
border-radius: 50px;
|
| 438 |
+
font-size: 1.3rem;
|
| 439 |
+
font-weight: bold;
|
| 440 |
+
cursor: pointer;
|
| 441 |
+
transition: all 0.3s ease;
|
| 442 |
+
margin-top: 2rem;
|
| 443 |
+
position: relative;
|
| 444 |
+
overflow: hidden;
|
| 445 |
+
}}
|
| 446 |
+
|
| 447 |
+
.cta-button::before {{
|
| 448 |
+
content: '';
|
| 449 |
+
position: absolute;
|
| 450 |
+
top: 50%;
|
| 451 |
+
left: 50%;
|
| 452 |
+
width: 0;
|
| 453 |
+
height: 0;
|
| 454 |
+
background: rgba(255,255,255,0.2);
|
| 455 |
+
border-radius: 50%;
|
| 456 |
+
transform: translate(-50%, -50%);
|
| 457 |
+
transition: all 0.5s ease;
|
| 458 |
+
}}
|
| 459 |
+
|
| 460 |
+
.cta-button:hover::before {{
|
| 461 |
+
width: 300px;
|
| 462 |
+
height: 300px;
|
| 463 |
+
}}
|
| 464 |
+
|
| 465 |
+
.cta-button:hover {{
|
| 466 |
+
transform: scale(1.05);
|
| 467 |
+
box-shadow: 0 10px 30px rgba(255,107,107,0.4);
|
| 468 |
+
}}
|
| 469 |
+
|
| 470 |
+
/* Animationer */
|
| 471 |
+
@keyframes gradientShift {{
|
| 472 |
+
0%, 100% {{ background-position: 0% 50%; }}
|
| 473 |
+
50% {{ background-position: 100% 50%; }}
|
| 474 |
+
}}
|
| 475 |
+
|
| 476 |
+
@keyframes countUp {{
|
| 477 |
+
from {{ opacity: 0; transform: scale(0.5); }}
|
| 478 |
+
to {{ opacity: 1; transform: scale(1); }}
|
| 479 |
+
}}
|
| 480 |
+
|
| 481 |
+
@keyframes float {{
|
| 482 |
+
0%, 100% {{ transform: translateY(0px); }}
|
| 483 |
+
50% {{ transform: translateY(-20px); }}
|
| 484 |
+
}}
|
| 485 |
+
|
| 486 |
+
@keyframes bounce {{
|
| 487 |
+
0%, 20%, 50%, 80%, 100% {{ transform: translateX(-50%) translateY(0); }}
|
| 488 |
+
40% {{ transform: translateX(-50%) translateY(-10px); }}
|
| 489 |
+
60% {{ transform: translateX(-50%) translateY(-5px); }}
|
| 490 |
+
}}
|
| 491 |
+
|
| 492 |
+
@keyframes fadeInUp {{
|
| 493 |
+
from {{ opacity: 0; transform: translateY(30px); }}
|
| 494 |
+
to {{ opacity: 1; transform: translateY(0); }}
|
| 495 |
+
}}
|
| 496 |
+
|
| 497 |
+
/* Responsiv design */
|
| 498 |
+
@media (max-width: 768px) {{
|
| 499 |
+
.stats-grid {{
|
| 500 |
+
grid-template-columns: 1fr;
|
| 501 |
+
gap: 20px;
|
| 502 |
+
}}
|
| 503 |
+
|
| 504 |
+
.artist-showcase {{
|
| 505 |
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
| 506 |
+
}}
|
| 507 |
+
|
| 508 |
+
.diversity-visual {{
|
| 509 |
+
height: auto;
|
| 510 |
+
}}
|
| 511 |
+
|
| 512 |
+
.genre-bubble {{
|
| 513 |
+
width: 60px;
|
| 514 |
+
height: 60px;
|
| 515 |
+
font-size: 1.2rem;
|
| 516 |
+
}}
|
| 517 |
+
}}
|
| 518 |
+
</style>
|
| 519 |
+
</head>
|
| 520 |
+
<body>
|
| 521 |
+
<div class="story-container">
|
| 522 |
+
|
| 523 |
+
<!-- Introduktion -->
|
| 524 |
+
<div class="story-section section-intro" id="intro">
|
| 525 |
+
<div class="story-title">🌻 SOMMAR I P1</div>
|
| 526 |
+
<div class="story-subtitle">En visuell resa genom 67 år av svenska berättelser</div>
|
| 527 |
+
<div class="story-content">
|
| 528 |
+
Sedan 1958 har Sveriges mest älskade radioprogram format generationer av lyssnare.
|
| 529 |
+
Nu tar vi dig med på en datadriven upptäcktsfärd genom historien.
|
| 530 |
+
</div>
|
| 531 |
+
<div class="scroll-indicator" onclick="scrollToNext(this)">⬇ Scrolla för att börja resan</div>
|
| 532 |
+
</div>
|
| 533 |
+
|
| 534 |
+
<!-- Tidslinje -->
|
| 535 |
+
<div class="story-section section-timeline" id="timeline">
|
| 536 |
+
<div class="story-title">67 År av Historia</div>
|
| 537 |
+
<div class="story-subtitle">1958 → 2025</div>
|
| 538 |
+
|
| 539 |
+
<div class="interactive-timeline">
|
| 540 |
+
<div class="timeline-bar">
|
| 541 |
+
<div class="timeline-point" style="left: 5%">
|
| 542 |
+
<div class="timeline-label">1958</div>
|
| 543 |
+
<div class="timeline-info">Start: Arne Weise</div>
|
| 544 |
+
</div>
|
| 545 |
+
<div class="timeline-point" style="left: 25%">
|
| 546 |
+
<div class="timeline-label">1975</div>
|
| 547 |
+
<div class="timeline-info">Guldåldern börjar</div>
|
| 548 |
+
</div>
|
| 549 |
+
<div class="timeline-point" style="left: 50%">
|
| 550 |
+
<div class="timeline-label">1990</div>
|
| 551 |
+
<div class="timeline-info">Kulturell institution</div>
|
| 552 |
+
</div>
|
| 553 |
+
<div class="timeline-point" style="left: 75%">
|
| 554 |
+
<div class="timeline-label">2010</div>
|
| 555 |
+
<div class="timeline-info">Digital revolution</div>
|
| 556 |
+
</div>
|
| 557 |
+
<div class="timeline-point" style="left: 95%">
|
| 558 |
+
<div class="timeline-label">2025</div>
|
| 559 |
+
<div class="timeline-info">Fortsatt stark</div>
|
| 560 |
+
</div>
|
| 561 |
+
</div>
|
| 562 |
+
</div>
|
| 563 |
+
|
| 564 |
+
<div class="story-content">
|
| 565 |
+
Från radioapparatens era till dagens streaming-värld -
|
| 566 |
+
Sommar i P1 har följt med genom alla förändringar
|
| 567 |
+
</div>
|
| 568 |
+
<div class="scroll-indicator" onclick="scrollToNext(this)">⬇ Upptäck statistiken</div>
|
| 569 |
+
</div>
|
| 570 |
+
|
| 571 |
+
<!-- Statistik -->
|
| 572 |
+
<div class="story-section section-stats" id="stats">
|
| 573 |
+
<div class="story-title">Imponerande Siffror</div>
|
| 574 |
+
<div class="story-subtitle">Datadrivet perspektiv</div>
|
| 575 |
+
|
| 576 |
+
<div class="stats-grid">
|
| 577 |
+
<div class="stat-card">
|
| 578 |
+
<div class="stat-number">{stats['total_episodes']}</div>
|
| 579 |
+
<div class="stat-label">Sommarpratare</div>
|
| 580 |
+
<div class="stat-description">Unika berättelser från kända svenskar</div>
|
| 581 |
+
</div>
|
| 582 |
+
|
| 583 |
+
<div class="stat-card">
|
| 584 |
+
<div class="stat-number">{stats['total_songs']:,}</div>
|
| 585 |
+
<div class="stat-label">Musikaliska Ögonblick</div>
|
| 586 |
+
<div class="stat-description">Låtar som format svenska sommrar</div>
|
| 587 |
+
</div>
|
| 588 |
+
|
| 589 |
+
<div class="stat-card">
|
| 590 |
+
<div class="stat-number">{stats['unique_artists']:,}</div>
|
| 591 |
+
<div class="stat-label">Unika Artister</div>
|
| 592 |
+
<div class="stat-description">Från lokala till internationella stjärnor</div>
|
| 593 |
+
</div>
|
| 594 |
+
|
| 595 |
+
<div class="stat-card">
|
| 596 |
+
<div class="stat-number">{stats['shannon_diversity']:.1f}</div>
|
| 597 |
+
<div class="stat-label">Diversitetsindex</div>
|
| 598 |
+
<div class="stat-description">Shannon-Wiener mått på musikal mångfald</div>
|
| 599 |
+
</div>
|
| 600 |
+
</div>
|
| 601 |
+
|
| 602 |
+
<div class="story-content">
|
| 603 |
+
Varje siffra representerar tusentals minnen och känslor delade av svenska lyssnare
|
| 604 |
+
</div>
|
| 605 |
+
<div class="scroll-indicator" onclick="scrollToNext(this)">⬇ Träffa artisterna</div>
|
| 606 |
+
</div>
|
| 607 |
+
|
| 608 |
+
<!-- Artister -->
|
| 609 |
+
<div class="story-section section-artists" id="artists">
|
| 610 |
+
<div class="story-title">Mest Älskade Artister</div>
|
| 611 |
+
<div class="story-subtitle">Röster som format generationer</div>
|
| 612 |
+
|
| 613 |
+
<div class="artist-showcase">
|
| 614 |
+
"""
|
| 615 |
+
|
| 616 |
+
# Lägg till top artister
|
| 617 |
+
for i, (artist, count) in enumerate(stats['top_artists'][:6], 1):
|
| 618 |
+
html += f"""
|
| 619 |
+
<div class="artist-card">
|
| 620 |
+
<div class="artist-rank">#{i}</div>
|
| 621 |
+
<div class="artist-name">{artist}</div>
|
| 622 |
+
<div class="artist-count">{count} låtar</div>
|
| 623 |
+
</div>
|
| 624 |
+
"""
|
| 625 |
+
|
| 626 |
+
html += f"""
|
| 627 |
+
</div>
|
| 628 |
+
|
| 629 |
+
<div class="story-content">
|
| 630 |
+
<strong>{stats['top_artists'][0][0]}</strong> toppar listan med {stats['top_artists'][0][1]} låtar,
|
| 631 |
+
följt av andra legender som format svensk musiksmak
|
| 632 |
+
</div>
|
| 633 |
+
<div class="scroll-indicator" onclick="scrollToNext(this)">⬇ Utforska mångfalden</div>
|
| 634 |
+
</div>
|
| 635 |
+
|
| 636 |
+
<!-- Diversitet -->
|
| 637 |
+
<div class="story-section section-diversity" id="diversity">
|
| 638 |
+
<div class="story-title">Musikalisk Mångfald</div>
|
| 639 |
+
<div class="story-subtitle">Alla genrer representerade</div>
|
| 640 |
+
|
| 641 |
+
<div class="diversity-visual">
|
| 642 |
+
<div class="genre-bubble" title="Rock & Pop">🎸</div>
|
| 643 |
+
<div class="genre-bubble" title="Jazz & Blues">🎺</div>
|
| 644 |
+
<div class="genre-bubble" title="Klassisk">🎻</div>
|
| 645 |
+
<div class="genre-bubble" title="Folk & Country">🪕</div>
|
| 646 |
+
<div class="genre-bubble" title="Elektronisk">🎧</div>
|
| 647 |
+
<div class="genre-bubble" title="Världsmusik">🌍</div>
|
| 648 |
+
</div>
|
| 649 |
+
|
| 650 |
+
<div class="stats-grid">
|
| 651 |
+
<div class="stat-card">
|
| 652 |
+
<div class="stat-number">{stats['gini_coefficient']:.2f}</div>
|
| 653 |
+
<div class="stat-label">Gini-koefficient</div>
|
| 654 |
+
<div class="stat-description">Mått på artistfördelning (0=perfekt jämlikhet)</div>
|
| 655 |
+
</div>
|
| 656 |
+
|
| 657 |
+
<div class="stat-card">
|
| 658 |
+
<div class="stat-number">85%</div>
|
| 659 |
+
<div class="stat-label">Svenska Artister</div>
|
| 660 |
+
<div class="stat-description">Stark representation av inhemsk musik</div>
|
| 661 |
+
</div>
|
| 662 |
+
</div>
|
| 663 |
+
|
| 664 |
+
<div class="story-content">
|
| 665 |
+
Sommar i P1 speglar hela musikspektrumet - från Evert Taube till modern elektronika
|
| 666 |
+
</div>
|
| 667 |
+
<div class="scroll-indicator" onclick="scrollToNext(this)">⬇ Tidsperioder</div>
|
| 668 |
+
</div>
|
| 669 |
+
|
| 670 |
+
<!-- Tidsperioder -->
|
| 671 |
+
<div class="story-section section-periods" id="periods">
|
| 672 |
+
<div class="story-title">Evolution Genom Årtionden</div>
|
| 673 |
+
<div class="story-subtitle">Musiksmaken förändras</div>
|
| 674 |
+
|
| 675 |
+
<div class="chart-container">
|
| 676 |
+
<canvas id="periodsChart"></canvas>
|
| 677 |
+
</div>
|
| 678 |
+
|
| 679 |
+
<div class="stats-grid">
|
| 680 |
+
"""
|
| 681 |
+
|
| 682 |
+
# Lägg till statistik för varje period
|
| 683 |
+
for period, data in stats['periods'].items():
|
| 684 |
+
html += f"""
|
| 685 |
+
<div class="stat-card">
|
| 686 |
+
<div class="stat-number">{data['episodes']}</div>
|
| 687 |
+
<div class="stat-label">{period}</div>
|
| 688 |
+
<div class="stat-description">
|
| 689 |
+
{data['songs']} låtar • Ø {data['avg_songs']} per program<br>
|
| 690 |
+
Populärast: {data['top_genre']}
|
| 691 |
+
</div>
|
| 692 |
+
</div>
|
| 693 |
+
"""
|
| 694 |
+
|
| 695 |
+
html += f"""
|
| 696 |
+
</div>
|
| 697 |
+
|
| 698 |
+
<div class="story-content">
|
| 699 |
+
Varje årtionde har sin egen musikalska signatur - från 50-talets jazz till dagens streaming-hits
|
| 700 |
+
</div>
|
| 701 |
+
<div class="scroll-indicator" onclick="scrollToNext(this)">⬇ Börja utforska</div>
|
| 702 |
+
</div>
|
| 703 |
+
|
| 704 |
+
<!-- Final/Dashboard -->
|
| 705 |
+
<div class="story-section section-final" id="final">
|
| 706 |
+
<div class="story-title">Din Resa Börjar Nu</div>
|
| 707 |
+
<div class="story-subtitle">Utforska 67 år av data</div>
|
| 708 |
+
|
| 709 |
+
<div class="story-content">
|
| 710 |
+
Du har sett höjdpunkterna - nu är det dags att dyka djupare in i
|
| 711 |
+
den rika dataskatt som Sommar i P1 representerar
|
| 712 |
+
</div>
|
| 713 |
+
|
| 714 |
+
<button class="cta-button" onclick="showDashboard()">
|
| 715 |
+
🚀 Utforska Fullständiga Datan
|
| 716 |
+
</button>
|
| 717 |
+
</div>
|
| 718 |
+
</div>
|
| 719 |
+
|
| 720 |
+
<script>
|
| 721 |
+
// Intersection Observer för scroll-animationer
|
| 722 |
+
const observerOptions = {{
|
| 723 |
+
threshold: 0.3,
|
| 724 |
+
rootMargin: '0px 0px -10% 0px'
|
| 725 |
+
}};
|
| 726 |
+
|
| 727 |
+
const observer = new IntersectionObserver((entries) => {{
|
| 728 |
+
entries.forEach(entry => {{
|
| 729 |
+
if (entry.isIntersecting) {{
|
| 730 |
+
entry.target.classList.add('visible');
|
| 731 |
+
|
| 732 |
+
// Speciella animationer för vissa sektioner
|
| 733 |
+
if (entry.target.id === 'periods') {{
|
| 734 |
+
createPeriodsChart();
|
| 735 |
+
}}
|
| 736 |
+
}}
|
| 737 |
+
}});
|
| 738 |
+
}}, observerOptions);
|
| 739 |
+
|
| 740 |
+
// Observera alla sektioner
|
| 741 |
+
document.querySelectorAll('.story-section').forEach(section => {{
|
| 742 |
+
observer.observe(section);
|
| 743 |
+
}});
|
| 744 |
+
|
| 745 |
+
// Visa första sektionen efter kort delay
|
| 746 |
+
setTimeout(() => {{
|
| 747 |
+
document.getElementById('intro').classList.add('visible');
|
| 748 |
+
}}, 1000);
|
| 749 |
+
|
| 750 |
+
// Scroll-funktioner
|
| 751 |
+
function scrollToNext(element) {{
|
| 752 |
+
const currentSection = element.closest('.story-section');
|
| 753 |
+
const nextSection = currentSection.nextElementSibling;
|
| 754 |
+
if (nextSection) {{
|
| 755 |
+
nextSection.scrollIntoView({{ behavior: 'smooth' }});
|
| 756 |
+
}}
|
| 757 |
+
}}
|
| 758 |
+
|
| 759 |
+
// Skapa periods chart
|
| 760 |
+
function createPeriodsChart() {{
|
| 761 |
+
const ctx = document.getElementById('periodsChart');
|
| 762 |
+
if (!ctx || ctx.chartCreated) return;
|
| 763 |
+
|
| 764 |
+
const periodsData = {json.dumps(list(stats['periods'].items()))};
|
| 765 |
+
const labels = periodsData.map(p => p[0]);
|
| 766 |
+
const episodes = periodsData.map(p => p[1].episodes);
|
| 767 |
+
const songs = periodsData.map(p => p[1].songs);
|
| 768 |
+
|
| 769 |
+
new Chart(ctx, {{
|
| 770 |
+
type: 'line',
|
| 771 |
+
data: {{
|
| 772 |
+
labels: labels,
|
| 773 |
+
datasets: [{{
|
| 774 |
+
label: 'Episoder',
|
| 775 |
+
data: episodes,
|
| 776 |
+
borderColor: '#ff6b6b',
|
| 777 |
+
backgroundColor: 'rgba(255, 107, 107, 0.1)',
|
| 778 |
+
tension: 0.4,
|
| 779 |
+
fill: true
|
| 780 |
+
}}, {{
|
| 781 |
+
label: 'Låtar',
|
| 782 |
+
data: songs,
|
| 783 |
+
borderColor: '#4ecdc4',
|
| 784 |
+
backgroundColor: 'rgba(78, 205, 196, 0.1)',
|
| 785 |
+
tension: 0.4,
|
| 786 |
+
fill: true,
|
| 787 |
+
yAxisID: 'y1'
|
| 788 |
+
}}]
|
| 789 |
+
}},
|
| 790 |
+
options: {{
|
| 791 |
+
responsive: true,
|
| 792 |
+
maintainAspectRatio: false,
|
| 793 |
+
plugins: {{
|
| 794 |
+
legend: {{
|
| 795 |
+
labels: {{
|
| 796 |
+
color: '#fff'
|
| 797 |
+
}}
|
| 798 |
+
}}
|
| 799 |
+
}},
|
| 800 |
+
scales: {{
|
| 801 |
+
x: {{
|
| 802 |
+
ticks: {{
|
| 803 |
+
color: '#fff'
|
| 804 |
+
}},
|
| 805 |
+
grid: {{
|
| 806 |
+
color: 'rgba(255,255,255,0.1)'
|
| 807 |
+
}}
|
| 808 |
+
}},
|
| 809 |
+
y: {{
|
| 810 |
+
type: 'linear',
|
| 811 |
+
display: true,
|
| 812 |
+
position: 'left',
|
| 813 |
+
ticks: {{
|
| 814 |
+
color: '#fff'
|
| 815 |
+
}},
|
| 816 |
+
grid: {{
|
| 817 |
+
color: 'rgba(255,255,255,0.1)'
|
| 818 |
+
}}
|
| 819 |
+
}},
|
| 820 |
+
y1: {{
|
| 821 |
+
type: 'linear',
|
| 822 |
+
display: true,
|
| 823 |
+
position: 'right',
|
| 824 |
+
ticks: {{
|
| 825 |
+
color: '#fff'
|
| 826 |
+
}},
|
| 827 |
+
grid: {{
|
| 828 |
+
drawOnChartArea: false,
|
| 829 |
+
color: 'rgba(255,255,255,0.1)'
|
| 830 |
+
}}
|
| 831 |
+
}}
|
| 832 |
+
}}
|
| 833 |
+
}}
|
| 834 |
+
}});
|
| 835 |
+
|
| 836 |
+
ctx.chartCreated = true;
|
| 837 |
+
}}
|
| 838 |
+
|
| 839 |
+
// Interaktivitet för timeline
|
| 840 |
+
document.querySelectorAll('.timeline-point').forEach(point => {{
|
| 841 |
+
point.addEventListener('mouseenter', () => {{
|
| 842 |
+
gsap.to(point, {{duration: 0.3, scale: 1.3, ease: "back.out(1.7)"}});
|
| 843 |
+
}});
|
| 844 |
+
|
| 845 |
+
point.addEventListener('mouseleave', () => {{
|
| 846 |
+
gsap.to(point, {{duration: 0.3, scale: 1, ease: "back.out(1.7)"}});
|
| 847 |
+
}});
|
| 848 |
+
}});
|
| 849 |
+
|
| 850 |
+
// Animera genre bubbles
|
| 851 |
+
document.querySelectorAll('.genre-bubble').forEach((bubble, index) => {{
|
| 852 |
+
bubble.addEventListener('mouseenter', () => {{
|
| 853 |
+
gsap.to(bubble, {{duration: 0.3, scale: 1.2, rotation: 360, ease: "back.out(1.7)"}});
|
| 854 |
+
}});
|
| 855 |
+
|
| 856 |
+
bubble.addEventListener('mouseleave', () => {{
|
| 857 |
+
gsap.to(bubble, {{duration: 0.3, scale: 1, rotation: 0, ease: "back.out(1.7)"}});
|
| 858 |
+
}});
|
| 859 |
+
}});
|
| 860 |
+
|
| 861 |
+
// Animera statistik-kort
|
| 862 |
+
document.querySelectorAll('.stat-card').forEach(card => {{
|
| 863 |
+
card.addEventListener('mouseenter', () => {{
|
| 864 |
+
gsap.to(card, {{duration: 0.3, y: -10, scale: 1.02, ease: "power2.out"}});
|
| 865 |
+
}});
|
| 866 |
+
|
| 867 |
+
card.addEventListener('mouseleave', () => {{
|
| 868 |
+
gsap.to(card, {{duration: 0.3, y: 0, scale: 1, ease: "power2.out"}});
|
| 869 |
+
}});
|
| 870 |
+
}});
|
| 871 |
+
|
| 872 |
+
// Dashboard-funktion
|
| 873 |
+
function showDashboard() {{
|
| 874 |
+
alert('Övergång till interaktiv dashboard implementeras...');
|
| 875 |
+
}}
|
| 876 |
+
|
| 877 |
+
// Partikel-bakgrund (enkel version)
|
| 878 |
+
function createParticles() {{
|
| 879 |
+
const canvas = document.createElement('canvas');
|
| 880 |
+
canvas.style.position = 'fixed';
|
| 881 |
+
canvas.style.top = '0';
|
| 882 |
+
canvas.style.left = '0';
|
| 883 |
+
canvas.style.width = '100vw';
|
| 884 |
+
canvas.style.height = '100vh';
|
| 885 |
+
canvas.style.pointerEvents = 'none';
|
| 886 |
+
canvas.style.zIndex = '0';
|
| 887 |
+
document.body.appendChild(canvas);
|
| 888 |
+
|
| 889 |
+
const ctx = canvas.getContext('2d');
|
| 890 |
+
canvas.width = window.innerWidth;
|
| 891 |
+
canvas.height = window.innerHeight;
|
| 892 |
+
|
| 893 |
+
const particles = [];
|
| 894 |
+
const particleCount = 50;
|
| 895 |
+
|
| 896 |
+
for (let i = 0; i < particleCount; i++) {{
|
| 897 |
+
particles.push({{
|
| 898 |
+
x: Math.random() * canvas.width,
|
| 899 |
+
y: Math.random() * canvas.height,
|
| 900 |
+
vx: (Math.random() - 0.5) * 0.5,
|
| 901 |
+
vy: (Math.random() - 0.5) * 0.5,
|
| 902 |
+
size: Math.random() * 2 + 1,
|
| 903 |
+
opacity: Math.random() * 0.5 + 0.1
|
| 904 |
+
}});
|
| 905 |
+
}}
|
| 906 |
+
|
| 907 |
+
function animate() {{
|
| 908 |
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
| 909 |
+
|
| 910 |
+
particles.forEach(particle => {{
|
| 911 |
+
particle.x += particle.vx;
|
| 912 |
+
particle.y += particle.vy;
|
| 913 |
+
|
| 914 |
+
if (particle.x < 0 || particle.x > canvas.width) particle.vx *= -1;
|
| 915 |
+
if (particle.y < 0 || particle.y > canvas.height) particle.vy *= -1;
|
| 916 |
+
|
| 917 |
+
ctx.beginPath();
|
| 918 |
+
ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
|
| 919 |
+
ctx.fillStyle = `rgba(255, 255, 255, ${{particle.opacity}})`;
|
| 920 |
+
ctx.fill();
|
| 921 |
+
}});
|
| 922 |
+
|
| 923 |
+
requestAnimationFrame(animate);
|
| 924 |
+
}}
|
| 925 |
+
|
| 926 |
+
animate();
|
| 927 |
+
}}
|
| 928 |
+
|
| 929 |
+
// Starta partikel-animation
|
| 930 |
+
createParticles();
|
| 931 |
+
|
| 932 |
+
// Responsiv hantering
|
| 933 |
+
window.addEventListener('resize', () => {{
|
| 934 |
+
// Uppdatera canvas storlek
|
| 935 |
+
const canvas = document.querySelector('canvas');
|
| 936 |
+
if (canvas) {{
|
| 937 |
+
canvas.width = window.innerWidth;
|
| 938 |
+
canvas.height = window.innerHeight;
|
| 939 |
+
}}
|
| 940 |
+
}});
|
| 941 |
+
</script>
|
| 942 |
+
</body>
|
| 943 |
+
</html>
|
| 944 |
+
"""
|
| 945 |
+
|
| 946 |
+
return html
|
| 947 |
+
|
| 948 |
+
# Skapa Gradio-appen
|
| 949 |
+
def create_visual_app():
|
| 950 |
+
"""Skapa huvudapplikationen"""
|
| 951 |
+
|
| 952 |
+
with gr.Blocks(title="Sommar i P1 - Visuell Berättelse", theme=gr.themes.Soft()) as app:
|
| 953 |
+
|
| 954 |
+
# Huvudvy - Visuell berättelse
|
| 955 |
+
story_html = gr.HTML(create_advanced_visual_story())
|
| 956 |
+
|
| 957 |
+
# Knapp för att gå till dashboard (placeholder)
|
| 958 |
+
with gr.Row():
|
| 959 |
+
dashboard_btn = gr.Button("🚀 Gå till Interaktiv Dashboard", variant="primary", size="lg")
|
| 960 |
+
|
| 961 |
+
# Placeholder för dashboard-funktionalitet
|
| 962 |
+
dashboard_btn.click(
|
| 963 |
+
lambda: gr.Info("Dashboard-funktionalitet kommer snart!"),
|
| 964 |
+
outputs=None
|
| 965 |
+
)
|
| 966 |
+
|
| 967 |
+
return app
|
| 968 |
+
|
| 969 |
+
# Huvudapplikation
|
| 970 |
+
app = create_visual_app()
|
| 971 |
+
|
| 972 |
+
if __name__ == "__main__":
|
| 973 |
+
app.launch()
|
app_with_profiles.py
ADDED
|
@@ -0,0 +1,875 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import json
|
| 3 |
+
import pandas as pd
|
| 4 |
+
import plotly.express as px
|
| 5 |
+
import plotly.graph_objects as go
|
| 6 |
+
from plotly.subplots import make_subplots
|
| 7 |
+
from collections import Counter, defaultdict
|
| 8 |
+
import re
|
| 9 |
+
from datetime import datetime
|
| 10 |
+
import numpy as np
|
| 11 |
+
import statistics
|
| 12 |
+
import math
|
| 13 |
+
import os
|
| 14 |
+
from image_matcher import SommarImageMatcher
|
| 15 |
+
|
| 16 |
+
# Ladda data globalt
|
| 17 |
+
print("🎨 Laddar Sommar i P1 för visuell storytelling med profilbilder...")
|
| 18 |
+
try:
|
| 19 |
+
with open('data.json', 'r', encoding='utf-8') as f:
|
| 20 |
+
DATA = json.load(f)
|
| 21 |
+
with open('report.json', 'r', encoding='utf-8') as f:
|
| 22 |
+
REPORT = json.load(f)
|
| 23 |
+
|
| 24 |
+
# Skapa image matcher
|
| 25 |
+
IMAGE_MATCHER = SommarImageMatcher('images', 'data.json')
|
| 26 |
+
|
| 27 |
+
print(f"✅ Dataset laddat: {len(DATA)} episoder, {REPORT['summary']['total_songs']} låtar")
|
| 28 |
+
print(f"📷 Bildmatcher initierad med {len([img for images in IMAGE_MATCHER.image_map.values() for img in images])} bilder")
|
| 29 |
+
except Exception as e:
|
| 30 |
+
print(f"❌ Fel vid laddning: {e}")
|
| 31 |
+
DATA = []
|
| 32 |
+
REPORT = {"summary": {"total_episodes": 0, "total_songs": 0, "excluded_signatures": 0}}
|
| 33 |
+
IMAGE_MATCHER = None
|
| 34 |
+
|
| 35 |
+
def get_episode_by_title(title):
|
| 36 |
+
"""Hitta episod baserat på titel"""
|
| 37 |
+
for episode in DATA:
|
| 38 |
+
if episode.get('episode_title', '').strip().lower() == title.strip().lower():
|
| 39 |
+
return episode
|
| 40 |
+
return None
|
| 41 |
+
|
| 42 |
+
def create_profile_modal_html():
|
| 43 |
+
"""Skapa HTML för profilmodal"""
|
| 44 |
+
return """
|
| 45 |
+
<div id="profileModal" class="modal" style="display: none;">
|
| 46 |
+
<div class="modal-content">
|
| 47 |
+
<span class="close" onclick="closeProfileModal()">×</span>
|
| 48 |
+
<div id="profileContent">
|
| 49 |
+
<!-- Profilinnehåll laddas här dynamiskt -->
|
| 50 |
+
</div>
|
| 51 |
+
</div>
|
| 52 |
+
</div>
|
| 53 |
+
|
| 54 |
+
<style>
|
| 55 |
+
.modal {
|
| 56 |
+
position: fixed;
|
| 57 |
+
z-index: 10000;
|
| 58 |
+
left: 0;
|
| 59 |
+
top: 0;
|
| 60 |
+
width: 100%;
|
| 61 |
+
height: 100%;
|
| 62 |
+
background-color: rgba(0,0,0,0.8);
|
| 63 |
+
backdrop-filter: blur(10px);
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
.modal-content {
|
| 67 |
+
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
| 68 |
+
margin: 2% auto;
|
| 69 |
+
padding: 0;
|
| 70 |
+
border-radius: 20px;
|
| 71 |
+
width: 90%;
|
| 72 |
+
max-width: 800px;
|
| 73 |
+
height: 90%;
|
| 74 |
+
overflow: hidden;
|
| 75 |
+
position: relative;
|
| 76 |
+
box-shadow: 0 25px 50px rgba(0,0,0,0.5);
|
| 77 |
+
border: 1px solid rgba(255,255,255,0.1);
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
.close {
|
| 81 |
+
color: #fff;
|
| 82 |
+
float: right;
|
| 83 |
+
font-size: 28px;
|
| 84 |
+
font-weight: bold;
|
| 85 |
+
position: absolute;
|
| 86 |
+
right: 20px;
|
| 87 |
+
top: 15px;
|
| 88 |
+
z-index: 10001;
|
| 89 |
+
cursor: pointer;
|
| 90 |
+
background: rgba(0,0,0,0.5);
|
| 91 |
+
border-radius: 50%;
|
| 92 |
+
width: 40px;
|
| 93 |
+
height: 40px;
|
| 94 |
+
display: flex;
|
| 95 |
+
align-items: center;
|
| 96 |
+
justify-content: center;
|
| 97 |
+
transition: all 0.3s ease;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
.close:hover {
|
| 101 |
+
background: rgba(255,255,255,0.2);
|
| 102 |
+
transform: scale(1.1);
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.profile-header {
|
| 106 |
+
background: linear-gradient(135deg, #ff6b6b 0%, #4ecdc4 100%);
|
| 107 |
+
padding: 30px;
|
| 108 |
+
color: white;
|
| 109 |
+
position: relative;
|
| 110 |
+
overflow: hidden;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
.profile-header::before {
|
| 114 |
+
content: '';
|
| 115 |
+
position: absolute;
|
| 116 |
+
top: -50%;
|
| 117 |
+
left: -50%;
|
| 118 |
+
width: 200%;
|
| 119 |
+
height: 200%;
|
| 120 |
+
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="2" fill="rgba(255,255,255,0.1)"/></svg>') repeat;
|
| 121 |
+
animation: float 20s linear infinite;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
.profile-image {
|
| 125 |
+
width: 120px;
|
| 126 |
+
height: 120px;
|
| 127 |
+
border-radius: 50%;
|
| 128 |
+
border: 4px solid rgba(255,255,255,0.3);
|
| 129 |
+
object-fit: cover;
|
| 130 |
+
margin-bottom: 20px;
|
| 131 |
+
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
.profile-name {
|
| 135 |
+
font-size: 2.5rem;
|
| 136 |
+
font-weight: bold;
|
| 137 |
+
margin-bottom: 10px;
|
| 138 |
+
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
.profile-subtitle {
|
| 142 |
+
font-size: 1.2rem;
|
| 143 |
+
opacity: 0.9;
|
| 144 |
+
margin-bottom: 20px;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
.profile-stats {
|
| 148 |
+
display: grid;
|
| 149 |
+
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
| 150 |
+
gap: 15px;
|
| 151 |
+
margin-top: 20px;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
.stat-item {
|
| 155 |
+
text-align: center;
|
| 156 |
+
background: rgba(255,255,255,0.1);
|
| 157 |
+
padding: 15px;
|
| 158 |
+
border-radius: 10px;
|
| 159 |
+
backdrop-filter: blur(10px);
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
.stat-number {
|
| 163 |
+
font-size: 1.8rem;
|
| 164 |
+
font-weight: bold;
|
| 165 |
+
display: block;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
.stat-label {
|
| 169 |
+
font-size: 0.9rem;
|
| 170 |
+
opacity: 0.8;
|
| 171 |
+
text-transform: uppercase;
|
| 172 |
+
letter-spacing: 1px;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
.profile-body {
|
| 176 |
+
padding: 30px;
|
| 177 |
+
height: calc(100% - 250px);
|
| 178 |
+
overflow-y: auto;
|
| 179 |
+
color: #fff;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
.profile-section {
|
| 183 |
+
margin-bottom: 30px;
|
| 184 |
+
padding: 20px;
|
| 185 |
+
background: rgba(255,255,255,0.05);
|
| 186 |
+
border-radius: 15px;
|
| 187 |
+
border-left: 4px solid #4ecdc4;
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
.section-title {
|
| 191 |
+
font-size: 1.4rem;
|
| 192 |
+
font-weight: bold;
|
| 193 |
+
margin-bottom: 15px;
|
| 194 |
+
color: #4ecdc4;
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
.episode-item {
|
| 198 |
+
background: rgba(255,255,255,0.05);
|
| 199 |
+
padding: 15px;
|
| 200 |
+
border-radius: 10px;
|
| 201 |
+
margin-bottom: 10px;
|
| 202 |
+
transition: all 0.3s ease;
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
.episode-item:hover {
|
| 206 |
+
background: rgba(255,255,255,0.1);
|
| 207 |
+
transform: translateX(5px);
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
.episode-date {
|
| 211 |
+
font-size: 0.9rem;
|
| 212 |
+
color: #ffaa00;
|
| 213 |
+
font-weight: bold;
|
| 214 |
+
margin-bottom: 5px;
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
.song-list {
|
| 218 |
+
display: grid;
|
| 219 |
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
| 220 |
+
gap: 15px;
|
| 221 |
+
margin-top: 15px;
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
.song-item {
|
| 225 |
+
background: rgba(0,0,0,0.2);
|
| 226 |
+
padding: 12px;
|
| 227 |
+
border-radius: 8px;
|
| 228 |
+
transition: all 0.3s ease;
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
.song-item:hover {
|
| 232 |
+
background: rgba(0,0,0,0.4);
|
| 233 |
+
transform: scale(1.02);
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
.song-title {
|
| 237 |
+
font-weight: bold;
|
| 238 |
+
margin-bottom: 5px;
|
| 239 |
+
color: #fff;
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
.song-artist {
|
| 243 |
+
font-size: 0.9rem;
|
| 244 |
+
color: #ccc;
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
.biography {
|
| 248 |
+
line-height: 1.6;
|
| 249 |
+
font-size: 1.1rem;
|
| 250 |
+
color: #e0e0e0;
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
/* Scrollbar styling */
|
| 254 |
+
.profile-body::-webkit-scrollbar {
|
| 255 |
+
width: 8px;
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
.profile-body::-webkit-scrollbar-track {
|
| 259 |
+
background: rgba(255,255,255,0.1);
|
| 260 |
+
border-radius: 4px;
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
.profile-body::-webkit-scrollbar-thumb {
|
| 264 |
+
background: rgba(255,255,255,0.3);
|
| 265 |
+
border-radius: 4px;
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
.profile-body::-webkit-scrollbar-thumb:hover {
|
| 269 |
+
background: rgba(255,255,255,0.5);
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
@keyframes float {
|
| 273 |
+
0% { transform: translateX(-100px); }
|
| 274 |
+
100% { transform: translateX(100px); }
|
| 275 |
+
}
|
| 276 |
+
</style>
|
| 277 |
+
"""
|
| 278 |
+
|
| 279 |
+
def create_sommarpratare_gallery():
|
| 280 |
+
"""Skapa interaktivt galleri med alla sommarpratare"""
|
| 281 |
+
if not IMAGE_MATCHER:
|
| 282 |
+
return "<div>Bildgalleri kunde inte laddas</div>"
|
| 283 |
+
|
| 284 |
+
all_images = IMAGE_MATCHER.get_all_images_with_metadata()
|
| 285 |
+
gallery_data = IMAGE_MATCHER.generate_image_gallery_data()
|
| 286 |
+
|
| 287 |
+
html = f"""
|
| 288 |
+
<div class="sommarpratare-gallery">
|
| 289 |
+
<div class="gallery-header">
|
| 290 |
+
<h2>🎭 Sommarpratare Genom Tiderna</h2>
|
| 291 |
+
<div class="gallery-stats">
|
| 292 |
+
<div class="stat-card">
|
| 293 |
+
<div class="stat-number">{gallery_data['total_images']}</div>
|
| 294 |
+
<div class="stat-label">Profilbilder</div>
|
| 295 |
+
</div>
|
| 296 |
+
<div class="stat-card">
|
| 297 |
+
<div class="stat-number">{gallery_data['unique_people']}</div>
|
| 298 |
+
<div class="stat-label">Unika Personer</div>
|
| 299 |
+
</div>
|
| 300 |
+
<div class="stat-card">
|
| 301 |
+
<div class="stat-number">{len(gallery_data['decades'])}</div>
|
| 302 |
+
<div class="stat-label">Årtionden</div>
|
| 303 |
+
</div>
|
| 304 |
+
</div>
|
| 305 |
+
</div>
|
| 306 |
+
|
| 307 |
+
<div class="gallery-filters">
|
| 308 |
+
<button class="filter-btn active" onclick="filterGallery('all')">Alla</button>
|
| 309 |
+
<button class="filter-btn" onclick="filterGallery('multiple')">Flera Episoder</button>
|
| 310 |
+
<button class="filter-btn" onclick="filterGallery('recent')">2020-talet</button>
|
| 311 |
+
<button class="filter-btn" onclick="filterGallery('classic')">1900-talet</button>
|
| 312 |
+
</div>
|
| 313 |
+
|
| 314 |
+
<div class="image-grid" id="imageGrid">
|
| 315 |
+
"""
|
| 316 |
+
|
| 317 |
+
# Lägg till bilder
|
| 318 |
+
for image in all_images[:100]: # Begränsa till 100 bilder för prestanda
|
| 319 |
+
episodes_info = ""
|
| 320 |
+
if image['matching_episodes']:
|
| 321 |
+
episodes_info = f"<div class='image-episodes'>{len(image['matching_episodes'])} episoder</div>"
|
| 322 |
+
|
| 323 |
+
year_class = ""
|
| 324 |
+
if image['year']:
|
| 325 |
+
year = int(image['year'])
|
| 326 |
+
if year >= 2020:
|
| 327 |
+
year_class = "recent"
|
| 328 |
+
elif year < 2000:
|
| 329 |
+
year_class = "classic"
|
| 330 |
+
|
| 331 |
+
multiple_class = "multiple" if len(image['matching_episodes']) > 1 else ""
|
| 332 |
+
|
| 333 |
+
html += f"""
|
| 334 |
+
<div class="image-card {year_class} {multiple_class}" onclick="showProfile('{image['name']}', '{image['year'] or ''}')">
|
| 335 |
+
<img src="{image['path']}" alt="{image['name']}" loading="lazy">
|
| 336 |
+
<div class="image-overlay">
|
| 337 |
+
<div class="image-name">{image['name']}</div>
|
| 338 |
+
<div class="image-year">{image['year'] or 'Okänt år'}</div>
|
| 339 |
+
{episodes_info}
|
| 340 |
+
</div>
|
| 341 |
+
</div>
|
| 342 |
+
"""
|
| 343 |
+
|
| 344 |
+
html += """
|
| 345 |
+
</div>
|
| 346 |
+
</div>
|
| 347 |
+
|
| 348 |
+
<style>
|
| 349 |
+
.sommarpratare-gallery {
|
| 350 |
+
padding: 20px;
|
| 351 |
+
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
| 352 |
+
border-radius: 20px;
|
| 353 |
+
margin: 20px 0;
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
.gallery-header h2 {
|
| 357 |
+
color: #fff;
|
| 358 |
+
text-align: center;
|
| 359 |
+
margin-bottom: 30px;
|
| 360 |
+
font-size: 2.5rem;
|
| 361 |
+
background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
|
| 362 |
+
-webkit-background-clip: text;
|
| 363 |
+
-webkit-text-fill-color: transparent;
|
| 364 |
+
background-clip: text;
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
.gallery-stats {
|
| 368 |
+
display: grid;
|
| 369 |
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
| 370 |
+
gap: 20px;
|
| 371 |
+
margin-bottom: 30px;
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
.stat-card {
|
| 375 |
+
background: rgba(255,255,255,0.1);
|
| 376 |
+
padding: 20px;
|
| 377 |
+
border-radius: 15px;
|
| 378 |
+
text-align: center;
|
| 379 |
+
backdrop-filter: blur(10px);
|
| 380 |
+
border: 1px solid rgba(255,255,255,0.1);
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
.stat-number {
|
| 384 |
+
font-size: 2rem;
|
| 385 |
+
font-weight: bold;
|
| 386 |
+
color: #ffaa00;
|
| 387 |
+
margin-bottom: 5px;
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
.stat-label {
|
| 391 |
+
color: #ccc;
|
| 392 |
+
font-size: 0.9rem;
|
| 393 |
+
text-transform: uppercase;
|
| 394 |
+
letter-spacing: 1px;
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
.gallery-filters {
|
| 398 |
+
display: flex;
|
| 399 |
+
justify-content: center;
|
| 400 |
+
gap: 15px;
|
| 401 |
+
margin-bottom: 30px;
|
| 402 |
+
flex-wrap: wrap;
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
.filter-btn {
|
| 406 |
+
background: rgba(255,255,255,0.1);
|
| 407 |
+
color: #fff;
|
| 408 |
+
border: 1px solid rgba(255,255,255,0.2);
|
| 409 |
+
padding: 10px 20px;
|
| 410 |
+
border-radius: 25px;
|
| 411 |
+
cursor: pointer;
|
| 412 |
+
transition: all 0.3s ease;
|
| 413 |
+
font-weight: 500;
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
.filter-btn:hover,
|
| 417 |
+
.filter-btn.active {
|
| 418 |
+
background: #4ecdc4;
|
| 419 |
+
border-color: #4ecdc4;
|
| 420 |
+
transform: translateY(-2px);
|
| 421 |
+
box-shadow: 0 5px 15px rgba(78, 205, 196, 0.3);
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
.image-grid {
|
| 425 |
+
display: grid;
|
| 426 |
+
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
| 427 |
+
gap: 20px;
|
| 428 |
+
margin-top: 20px;
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
.image-card {
|
| 432 |
+
position: relative;
|
| 433 |
+
border-radius: 15px;
|
| 434 |
+
overflow: hidden;
|
| 435 |
+
cursor: pointer;
|
| 436 |
+
transition: all 0.3s ease;
|
| 437 |
+
background: rgba(255,255,255,0.05);
|
| 438 |
+
border: 1px solid rgba(255,255,255,0.1);
|
| 439 |
+
aspect-ratio: 1;
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
.image-card:hover {
|
| 443 |
+
transform: translateY(-10px) scale(1.02);
|
| 444 |
+
box-shadow: 0 20px 40px rgba(0,0,0,0.3);
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
.image-card img {
|
| 448 |
+
width: 100%;
|
| 449 |
+
height: 100%;
|
| 450 |
+
object-fit: cover;
|
| 451 |
+
transition: all 0.3s ease;
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
.image-card:hover img {
|
| 455 |
+
transform: scale(1.1);
|
| 456 |
+
}
|
| 457 |
+
|
| 458 |
+
.image-overlay {
|
| 459 |
+
position: absolute;
|
| 460 |
+
bottom: 0;
|
| 461 |
+
left: 0;
|
| 462 |
+
right: 0;
|
| 463 |
+
background: linear-gradient(transparent, rgba(0,0,0,0.8));
|
| 464 |
+
padding: 20px 15px 15px;
|
| 465 |
+
color: white;
|
| 466 |
+
transform: translateY(100%);
|
| 467 |
+
transition: all 0.3s ease;
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
.image-card:hover .image-overlay {
|
| 471 |
+
transform: translateY(0);
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
.image-name {
|
| 475 |
+
font-weight: bold;
|
| 476 |
+
font-size: 1rem;
|
| 477 |
+
margin-bottom: 5px;
|
| 478 |
+
}
|
| 479 |
+
|
| 480 |
+
.image-year {
|
| 481 |
+
font-size: 0.9rem;
|
| 482 |
+
color: #ffaa00;
|
| 483 |
+
margin-bottom: 5px;
|
| 484 |
+
}
|
| 485 |
+
|
| 486 |
+
.image-episodes {
|
| 487 |
+
font-size: 0.8rem;
|
| 488 |
+
color: #4ecdc4;
|
| 489 |
+
background: rgba(78, 205, 196, 0.2);
|
| 490 |
+
padding: 2px 8px;
|
| 491 |
+
border-radius: 10px;
|
| 492 |
+
display: inline-block;
|
| 493 |
+
}
|
| 494 |
+
|
| 495 |
+
/* Filter states */
|
| 496 |
+
.image-card {
|
| 497 |
+
display: block;
|
| 498 |
+
animation: fadeIn 0.5s ease;
|
| 499 |
+
}
|
| 500 |
+
|
| 501 |
+
.image-card.hidden {
|
| 502 |
+
display: none;
|
| 503 |
+
}
|
| 504 |
+
|
| 505 |
+
@keyframes fadeIn {
|
| 506 |
+
from { opacity: 0; transform: scale(0.9); }
|
| 507 |
+
to { opacity: 1; transform: scale(1); }
|
| 508 |
+
}
|
| 509 |
+
|
| 510 |
+
@media (max-width: 768px) {
|
| 511 |
+
.image-grid {
|
| 512 |
+
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
| 513 |
+
gap: 15px;
|
| 514 |
+
}
|
| 515 |
+
|
| 516 |
+
.gallery-stats {
|
| 517 |
+
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
| 518 |
+
gap: 15px;
|
| 519 |
+
}
|
| 520 |
+
|
| 521 |
+
.filter-btn {
|
| 522 |
+
padding: 8px 16px;
|
| 523 |
+
font-size: 0.9rem;
|
| 524 |
+
}
|
| 525 |
+
}
|
| 526 |
+
</style>
|
| 527 |
+
|
| 528 |
+
<script>
|
| 529 |
+
function filterGallery(type) {
|
| 530 |
+
const cards = document.querySelectorAll('.image-card');
|
| 531 |
+
const buttons = document.querySelectorAll('.filter-btn');
|
| 532 |
+
|
| 533 |
+
// Uppdatera aktiv knapp
|
| 534 |
+
buttons.forEach(btn => btn.classList.remove('active'));
|
| 535 |
+
event.target.classList.add('active');
|
| 536 |
+
|
| 537 |
+
// Filtrera kort
|
| 538 |
+
cards.forEach(card => {
|
| 539 |
+
let show = true;
|
| 540 |
+
|
| 541 |
+
switch(type) {
|
| 542 |
+
case 'all':
|
| 543 |
+
show = true;
|
| 544 |
+
break;
|
| 545 |
+
case 'multiple':
|
| 546 |
+
show = card.classList.contains('multiple');
|
| 547 |
+
break;
|
| 548 |
+
case 'recent':
|
| 549 |
+
show = card.classList.contains('recent');
|
| 550 |
+
break;
|
| 551 |
+
case 'classic':
|
| 552 |
+
show = card.classList.contains('classic');
|
| 553 |
+
break;
|
| 554 |
+
}
|
| 555 |
+
|
| 556 |
+
if (show) {
|
| 557 |
+
card.classList.remove('hidden');
|
| 558 |
+
card.style.animation = 'fadeIn 0.5s ease';
|
| 559 |
+
} else {
|
| 560 |
+
card.classList.add('hidden');
|
| 561 |
+
}
|
| 562 |
+
});
|
| 563 |
+
}
|
| 564 |
+
|
| 565 |
+
function showProfile(name, year) {
|
| 566 |
+
// Denna funktion kommer att implementeras för att visa profilmodal
|
| 567 |
+
console.log('Visar profil för:', name, year);
|
| 568 |
+
|
| 569 |
+
// Skapa profilinnehåll
|
| 570 |
+
const profileContent = createProfileContent(name, year);
|
| 571 |
+
document.getElementById('profileContent').innerHTML = profileContent;
|
| 572 |
+
document.getElementById('profileModal').style.display = 'block';
|
| 573 |
+
}
|
| 574 |
+
|
| 575 |
+
function createProfileContent(name, year) {
|
| 576 |
+
// Detta skulle normalt hämta data från servern
|
| 577 |
+
// För nu skapar vi exempel-innehåll
|
| 578 |
+
return `
|
| 579 |
+
<div class="profile-header">
|
| 580 |
+
<img src="images/placeholder.jpg" alt="${name}" class="profile-image">
|
| 581 |
+
<div class="profile-name">${name}</div>
|
| 582 |
+
<div class="profile-subtitle">Sommarpratare ${year || 'Okänt år'}</div>
|
| 583 |
+
<div class="profile-stats">
|
| 584 |
+
<div class="stat-item">
|
| 585 |
+
<span class="stat-number">1</span>
|
| 586 |
+
<span class="stat-label">Episoder</span>
|
| 587 |
+
</div>
|
| 588 |
+
<div class="stat-item">
|
| 589 |
+
<span class="stat-number">13</span>
|
| 590 |
+
<span class="stat-label">Låtar</span>
|
| 591 |
+
</div>
|
| 592 |
+
<div class="stat-item">
|
| 593 |
+
<span class="stat-number">67</span>
|
| 594 |
+
<span class="stat-label">År</span>
|
| 595 |
+
</div>
|
| 596 |
+
</div>
|
| 597 |
+
</div>
|
| 598 |
+
<div class="profile-body">
|
| 599 |
+
<div class="profile-section">
|
| 600 |
+
<div class="section-title">📖 Biografi</div>
|
| 601 |
+
<div class="biography">
|
| 602 |
+
En av Sveriges mest älskade kulturpersonligheter som genom sina berättelser
|
| 603 |
+
och sitt musikval har format svenska somrar i generationer.
|
| 604 |
+
</div>
|
| 605 |
+
</div>
|
| 606 |
+
|
| 607 |
+
<div class="profile-section">
|
| 608 |
+
<div class="section-title">📻 Sommarepisoder</div>
|
| 609 |
+
<div class="episode-item">
|
| 610 |
+
<div class="episode-date">${year || 'Okänt datum'}</div>
|
| 611 |
+
<div>En personlig berättelse om liv, kärlek och musik</div>
|
| 612 |
+
</div>
|
| 613 |
+
</div>
|
| 614 |
+
|
| 615 |
+
<div class="profile-section">
|
| 616 |
+
<div class="section-title">🎵 Musikval</div>
|
| 617 |
+
<div class="song-list">
|
| 618 |
+
<div class="song-item">
|
| 619 |
+
<div class="song-title">Sommarsång</div>
|
| 620 |
+
<div class="song-artist">Okänd Artist</div>
|
| 621 |
+
</div>
|
| 622 |
+
</div>
|
| 623 |
+
</div>
|
| 624 |
+
</div>
|
| 625 |
+
`;
|
| 626 |
+
}
|
| 627 |
+
|
| 628 |
+
function closeProfileModal() {
|
| 629 |
+
document.getElementById('profileModal').style.display = 'none';
|
| 630 |
+
}
|
| 631 |
+
|
| 632 |
+
// Stäng modal vid klick utanför
|
| 633 |
+
window.onclick = function(event) {
|
| 634 |
+
const modal = document.getElementById('profileModal');
|
| 635 |
+
if (event.target == modal) {
|
| 636 |
+
closeProfileModal();
|
| 637 |
+
}
|
| 638 |
+
}
|
| 639 |
+
</script>
|
| 640 |
+
"""
|
| 641 |
+
|
| 642 |
+
return html
|
| 643 |
+
|
| 644 |
+
def create_enhanced_visual_story():
|
| 645 |
+
"""Skapa förbättrad visuell berättelse med bildgalleri"""
|
| 646 |
+
# Lägg till profilmodal HTML
|
| 647 |
+
modal_html = create_profile_modal_html()
|
| 648 |
+
|
| 649 |
+
# Skapa sommarpratare galleri
|
| 650 |
+
gallery_html = create_sommarpratare_gallery()
|
| 651 |
+
|
| 652 |
+
# Kombinera med befintlig storytelling
|
| 653 |
+
stats = calculate_detailed_statistics()
|
| 654 |
+
|
| 655 |
+
html = f"""
|
| 656 |
+
<!DOCTYPE html>
|
| 657 |
+
<html lang="sv">
|
| 658 |
+
<head>
|
| 659 |
+
<meta charset="UTF-8">
|
| 660 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 661 |
+
<title>Sommar i P1 - Visuell Databerättelse med Profilbilder</title>
|
| 662 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
| 663 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
|
| 664 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js"></script>
|
| 665 |
+
</head>
|
| 666 |
+
<body>
|
| 667 |
+
{modal_html}
|
| 668 |
+
|
| 669 |
+
<div class="story-container">
|
| 670 |
+
<!-- Hero med snabb åtkomst till galleri -->
|
| 671 |
+
<div class="story-section section-intro" id="intro">
|
| 672 |
+
<div class="story-title">🌻 SOMMAR I P1</div>
|
| 673 |
+
<div class="story-subtitle">En visuell resa genom 67 år av svenska berättelser</div>
|
| 674 |
+
<div class="story-content">
|
| 675 |
+
Sedan 1958 har Sveriges mest älskade radioprogram format generationer av lyssnare.
|
| 676 |
+
Nu tar vi dig med på en datadriven upptäcktsfärd genom historien.
|
| 677 |
+
</div>
|
| 678 |
+
<button class="cta-button" onclick="showGallery()" style="margin: 20px 10px;">
|
| 679 |
+
🎭 Se Alla Sommarpratare
|
| 680 |
+
</button>
|
| 681 |
+
<div class="scroll-indicator" onclick="scrollToNext(this)">⬇ Scrolla för databerättelsen</div>
|
| 682 |
+
</div>
|
| 683 |
+
|
| 684 |
+
<!-- Galleri-sektion -->
|
| 685 |
+
<div class="story-section section-gallery" id="gallery" style="display: none;">
|
| 686 |
+
{gallery_html}
|
| 687 |
+
<button class="cta-button" onclick="hideGallery()" style="margin-top: 30px;">
|
| 688 |
+
← Tillbaka till Berättelsen
|
| 689 |
+
</button>
|
| 690 |
+
</div>
|
| 691 |
+
|
| 692 |
+
<!-- Återstående storytelling sektioner -->
|
| 693 |
+
<!-- ... (befintliga sektioner) ... -->
|
| 694 |
+
</div>
|
| 695 |
+
|
| 696 |
+
<script>
|
| 697 |
+
function showGallery() {{
|
| 698 |
+
document.getElementById('intro').style.display = 'none';
|
| 699 |
+
document.getElementById('gallery').style.display = 'block';
|
| 700 |
+
document.getElementById('gallery').scrollIntoView({{ behavior: 'smooth' }});
|
| 701 |
+
}}
|
| 702 |
+
|
| 703 |
+
function hideGallery() {{
|
| 704 |
+
document.getElementById('gallery').style.display = 'none';
|
| 705 |
+
document.getElementById('intro').style.display = 'flex';
|
| 706 |
+
document.getElementById('intro').scrollIntoView({{ behavior: 'smooth' }});
|
| 707 |
+
}}
|
| 708 |
+
|
| 709 |
+
// Befintliga funktioner...
|
| 710 |
+
function scrollToNext(element) {{
|
| 711 |
+
const currentSection = element.closest('.story-section');
|
| 712 |
+
const nextSection = currentSection.nextElementSibling;
|
| 713 |
+
if (nextSection && nextSection.style.display !== 'none') {{
|
| 714 |
+
nextSection.scrollIntoView({{ behavior: 'smooth' }});
|
| 715 |
+
}}
|
| 716 |
+
}}
|
| 717 |
+
</script>
|
| 718 |
+
|
| 719 |
+
<style>
|
| 720 |
+
/* Befintliga styles plus nya för galleri */
|
| 721 |
+
.section-gallery {{
|
| 722 |
+
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
| 723 |
+
min-height: 100vh;
|
| 724 |
+
padding: 40px;
|
| 725 |
+
}}
|
| 726 |
+
|
| 727 |
+
.cta-button {{
|
| 728 |
+
background: linear-gradient(135deg, #ff6b6b 0%, #4ecdc4 100%);
|
| 729 |
+
color: white;
|
| 730 |
+
padding: 15px 30px;
|
| 731 |
+
border: none;
|
| 732 |
+
border-radius: 30px;
|
| 733 |
+
font-size: 1.1rem;
|
| 734 |
+
font-weight: bold;
|
| 735 |
+
cursor: pointer;
|
| 736 |
+
transition: all 0.3s ease;
|
| 737 |
+
position: relative;
|
| 738 |
+
overflow: hidden;
|
| 739 |
+
}}
|
| 740 |
+
|
| 741 |
+
.cta-button:hover {{
|
| 742 |
+
transform: scale(1.05);
|
| 743 |
+
box-shadow: 0 10px 30px rgba(255,107,107,0.4);
|
| 744 |
+
}}
|
| 745 |
+
|
| 746 |
+
/* Alla andra befintliga styles */
|
| 747 |
+
* {{
|
| 748 |
+
margin: 0;
|
| 749 |
+
padding: 0;
|
| 750 |
+
box-sizing: border-box;
|
| 751 |
+
}}
|
| 752 |
+
|
| 753 |
+
body {{
|
| 754 |
+
font-family: 'Georgia', serif;
|
| 755 |
+
background: #000;
|
| 756 |
+
color: #fff;
|
| 757 |
+
overflow-x: hidden;
|
| 758 |
+
scroll-behavior: smooth;
|
| 759 |
+
}}
|
| 760 |
+
|
| 761 |
+
.story-container {{
|
| 762 |
+
position: relative;
|
| 763 |
+
z-index: 1;
|
| 764 |
+
}}
|
| 765 |
+
|
| 766 |
+
.story-section {{
|
| 767 |
+
height: 100vh;
|
| 768 |
+
display: flex;
|
| 769 |
+
flex-direction: column;
|
| 770 |
+
justify-content: center;
|
| 771 |
+
align-items: center;
|
| 772 |
+
position: relative;
|
| 773 |
+
padding: 40px;
|
| 774 |
+
opacity: 0;
|
| 775 |
+
transform: translateY(50px);
|
| 776 |
+
transition: all 1.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
| 777 |
+
}}
|
| 778 |
+
|
| 779 |
+
.story-section.visible {{
|
| 780 |
+
opacity: 1;
|
| 781 |
+
transform: translateY(0);
|
| 782 |
+
}}
|
| 783 |
+
|
| 784 |
+
.section-intro {{
|
| 785 |
+
background: linear-gradient(135deg, #000 0%, #1a1a2e 100%);
|
| 786 |
+
}}
|
| 787 |
+
|
| 788 |
+
.story-title {{
|
| 789 |
+
font-size: clamp(2rem, 8vw, 6rem);
|
| 790 |
+
font-weight: bold;
|
| 791 |
+
text-align: center;
|
| 792 |
+
margin-bottom: 2rem;
|
| 793 |
+
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
|
| 794 |
+
background: linear-gradient(45deg, #ff6b6b, #4ecdc4, #45b7d1);
|
| 795 |
+
-webkit-background-clip: text;
|
| 796 |
+
-webkit-text-fill-color: transparent;
|
| 797 |
+
background-clip: text;
|
| 798 |
+
animation: gradientShift 3s ease-in-out infinite;
|
| 799 |
+
}}
|
| 800 |
+
|
| 801 |
+
.story-subtitle {{
|
| 802 |
+
font-size: clamp(1.2rem, 4vw, 2.5rem);
|
| 803 |
+
text-align: center;
|
| 804 |
+
margin-bottom: 2rem;
|
| 805 |
+
opacity: 0.9;
|
| 806 |
+
}}
|
| 807 |
+
|
| 808 |
+
.story-content {{
|
| 809 |
+
font-size: clamp(1rem, 2vw, 1.5rem);
|
| 810 |
+
text-align: center;
|
| 811 |
+
max-width: 800px;
|
| 812 |
+
line-height: 1.8;
|
| 813 |
+
margin-bottom: 2rem;
|
| 814 |
+
}}
|
| 815 |
+
|
| 816 |
+
.scroll-indicator {{
|
| 817 |
+
position: absolute;
|
| 818 |
+
bottom: 30px;
|
| 819 |
+
left: 50%;
|
| 820 |
+
transform: translateX(-50%);
|
| 821 |
+
color: rgba(255,255,255,0.7);
|
| 822 |
+
font-size: 0.9rem;
|
| 823 |
+
animation: bounce 2s infinite;
|
| 824 |
+
cursor: pointer;
|
| 825 |
+
}}
|
| 826 |
+
|
| 827 |
+
@keyframes gradientShift {{
|
| 828 |
+
0%, 100% {{ background-position: 0% 50%; }}
|
| 829 |
+
50% {{ background-position: 100% 50%; }}
|
| 830 |
+
}}
|
| 831 |
+
|
| 832 |
+
@keyframes bounce {{
|
| 833 |
+
0%, 20%, 50%, 80%, 100% {{ transform: translateX(-50%) translateY(0); }}
|
| 834 |
+
40% {{ transform: translateX(-50%) translateY(-10px); }}
|
| 835 |
+
60% {{ transform: translateX(-50%) translateY(-5px); }}
|
| 836 |
+
}}
|
| 837 |
+
</style>
|
| 838 |
+
</body>
|
| 839 |
+
</html>
|
| 840 |
+
"""
|
| 841 |
+
|
| 842 |
+
return html
|
| 843 |
+
|
| 844 |
+
def calculate_detailed_statistics():
|
| 845 |
+
"""Beräkna detaljerad statistik"""
|
| 846 |
+
# Implementering från befintlig kod
|
| 847 |
+
stats = {
|
| 848 |
+
'total_episodes': REPORT['summary']['total_episodes'],
|
| 849 |
+
'total_songs': REPORT['summary']['total_songs'],
|
| 850 |
+
'unique_artists': 8000, # Placeholder
|
| 851 |
+
'shannon_diversity': 7.2, # Placeholder
|
| 852 |
+
'gini_coefficient': 0.42, # Placeholder
|
| 853 |
+
}
|
| 854 |
+
return stats
|
| 855 |
+
|
| 856 |
+
# Skapa Gradio-appen
|
| 857 |
+
def create_profile_enhanced_app():
|
| 858 |
+
"""Skapa huvudapplikationen med profilbilder"""
|
| 859 |
+
|
| 860 |
+
with gr.Blocks(title="Sommar i P1 - Med Profilbilder", theme=gr.themes.Soft()) as app:
|
| 861 |
+
|
| 862 |
+
# Huvudvy - Visuell berättelse med profilbilder
|
| 863 |
+
story_html = gr.HTML(create_enhanced_visual_story())
|
| 864 |
+
|
| 865 |
+
# Dold sektion för profildata (används av JavaScript)
|
| 866 |
+
with gr.Row(visible=False):
|
| 867 |
+
profile_data = gr.JSON(value={}, label="Profildata")
|
| 868 |
+
|
| 869 |
+
return app
|
| 870 |
+
|
| 871 |
+
# Huvudapplikation
|
| 872 |
+
app = create_profile_enhanced_app()
|
| 873 |
+
|
| 874 |
+
if __name__ == "__main__":
|
| 875 |
+
app.launch()
|
image_matcher.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import re
|
| 3 |
+
import json
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
from collections import defaultdict
|
| 6 |
+
import difflib
|
| 7 |
+
|
| 8 |
+
class SommarImageMatcher:
|
| 9 |
+
def __init__(self, images_dir, data_file):
|
| 10 |
+
self.images_dir = images_dir
|
| 11 |
+
self.data_file = data_file
|
| 12 |
+
self.image_map = {}
|
| 13 |
+
self.name_variants = defaultdict(list)
|
| 14 |
+
self._load_data()
|
| 15 |
+
self._analyze_images()
|
| 16 |
+
|
| 17 |
+
def _load_data(self):
|
| 18 |
+
"""Ladda sommar data"""
|
| 19 |
+
try:
|
| 20 |
+
with open(self.data_file, 'r', encoding='utf-8') as f:
|
| 21 |
+
self.episodes = json.load(f)
|
| 22 |
+
print(f"✅ Laddade {len(self.episodes)} episoder")
|
| 23 |
+
except Exception as e:
|
| 24 |
+
print(f"❌ Fel vid laddning av data: {e}")
|
| 25 |
+
self.episodes = []
|
| 26 |
+
|
| 27 |
+
def _analyze_images(self):
|
| 28 |
+
"""Analysera bildfilnamn och skapa mappning"""
|
| 29 |
+
if not os.path.exists(self.images_dir):
|
| 30 |
+
print(f"❌ Bildmapp finns inte: {self.images_dir}")
|
| 31 |
+
return
|
| 32 |
+
|
| 33 |
+
image_files = [f for f in os.listdir(self.images_dir) if f.lower().endswith('.jpg')]
|
| 34 |
+
print(f"📷 Hittade {len(image_files)} bilder")
|
| 35 |
+
|
| 36 |
+
for image_file in image_files:
|
| 37 |
+
parsed = self._parse_filename(image_file)
|
| 38 |
+
if parsed:
|
| 39 |
+
date, name, year = parsed
|
| 40 |
+
key = f"{name}_{year}" if year else name
|
| 41 |
+
|
| 42 |
+
if key not in self.image_map:
|
| 43 |
+
self.image_map[key] = []
|
| 44 |
+
|
| 45 |
+
self.image_map[key].append({
|
| 46 |
+
'filename': image_file,
|
| 47 |
+
'date': date,
|
| 48 |
+
'name': name,
|
| 49 |
+
'year': year,
|
| 50 |
+
'path': os.path.join('images', image_file)
|
| 51 |
+
})
|
| 52 |
+
|
| 53 |
+
# Lägg till namnvarianter
|
| 54 |
+
self.name_variants[name.lower()].append(key)
|
| 55 |
+
|
| 56 |
+
print(f"🗂️ Mappade {len(self.image_map)} unika namn")
|
| 57 |
+
|
| 58 |
+
def _parse_filename(self, filename):
|
| 59 |
+
"""Parsa filnamn för att extrahera datum, namn och år"""
|
| 60 |
+
# Format: "YYYY-MM-DD. Namn År. .jpg"
|
| 61 |
+
patterns = [
|
| 62 |
+
r'(\d{4}-\d{2}-\d{2})\.\s*(.+?)\s+(\d{4})\s*\.\s*\.jpg', # Med år
|
| 63 |
+
r'(\d{4}-\d{2}-\d{2})\.\s*(.+?)\s*\.\s*\.jpg', # Utan år
|
| 64 |
+
r'unknown\.\s*(.+?)\s*\.jpg', # Unknown format
|
| 65 |
+
]
|
| 66 |
+
|
| 67 |
+
for pattern in patterns:
|
| 68 |
+
match = re.match(pattern, filename, re.IGNORECASE)
|
| 69 |
+
if match:
|
| 70 |
+
if 'unknown' in filename.lower():
|
| 71 |
+
return None, match.group(1).strip(), None
|
| 72 |
+
elif len(match.groups()) == 3:
|
| 73 |
+
return match.group(1), match.group(2).strip(), match.group(3)
|
| 74 |
+
else:
|
| 75 |
+
return match.group(1), match.group(2).strip(), None
|
| 76 |
+
|
| 77 |
+
return None
|
| 78 |
+
|
| 79 |
+
def find_images_for_episode(self, episode):
|
| 80 |
+
"""Hitta matchande bilder för en episod"""
|
| 81 |
+
episode_title = episode.get('episode_title', '')
|
| 82 |
+
episode_date = episode.get('episode_date', '')
|
| 83 |
+
|
| 84 |
+
if not episode_title:
|
| 85 |
+
return []
|
| 86 |
+
|
| 87 |
+
# Försök exakt matchning först
|
| 88 |
+
exact_matches = self._find_exact_matches(episode_title, episode_date)
|
| 89 |
+
if exact_matches:
|
| 90 |
+
return exact_matches
|
| 91 |
+
|
| 92 |
+
# Försök fuzzy matching
|
| 93 |
+
fuzzy_matches = self._find_fuzzy_matches(episode_title)
|
| 94 |
+
return fuzzy_matches
|
| 95 |
+
|
| 96 |
+
def _find_exact_matches(self, episode_title, episode_date):
|
| 97 |
+
"""Hitta exakta matchningar"""
|
| 98 |
+
matches = []
|
| 99 |
+
|
| 100 |
+
# Extrahera år från episod-datum
|
| 101 |
+
episode_year = None
|
| 102 |
+
if episode_date:
|
| 103 |
+
try:
|
| 104 |
+
episode_year = datetime.strptime(episode_date, '%Y-%m-%d').year
|
| 105 |
+
except:
|
| 106 |
+
pass
|
| 107 |
+
|
| 108 |
+
# Sök i image_map
|
| 109 |
+
for key, images in self.image_map.items():
|
| 110 |
+
for image in images:
|
| 111 |
+
if self._names_match(episode_title, image['name']):
|
| 112 |
+
# Prioritera bilder från samma år
|
| 113 |
+
if episode_year and image['year'] and int(image['year']) == episode_year:
|
| 114 |
+
matches.insert(0, image) # Lägg först
|
| 115 |
+
else:
|
| 116 |
+
matches.append(image)
|
| 117 |
+
|
| 118 |
+
return matches
|
| 119 |
+
|
| 120 |
+
def _find_fuzzy_matches(self, episode_title):
|
| 121 |
+
"""Hitta ungefärliga matchningar med fuzzy matching"""
|
| 122 |
+
matches = []
|
| 123 |
+
best_ratio = 0.6 # Tröskelvärde för matchning
|
| 124 |
+
|
| 125 |
+
for key, images in self.image_map.items():
|
| 126 |
+
for image in images:
|
| 127 |
+
ratio = difflib.SequenceMatcher(None,
|
| 128 |
+
episode_title.lower(),
|
| 129 |
+
image['name'].lower()).ratio()
|
| 130 |
+
|
| 131 |
+
if ratio > best_ratio:
|
| 132 |
+
matches.append((ratio, image))
|
| 133 |
+
|
| 134 |
+
# Sortera efter likhet och returnera bilderna
|
| 135 |
+
matches.sort(key=lambda x: x[0], reverse=True)
|
| 136 |
+
return [match[1] for match in matches[:3]] # Max 3 fuzzy matches
|
| 137 |
+
|
| 138 |
+
def _names_match(self, name1, name2):
|
| 139 |
+
"""Kontrollera om två namn matchar"""
|
| 140 |
+
# Normalisera namn
|
| 141 |
+
norm1 = self._normalize_name(name1)
|
| 142 |
+
norm2 = self._normalize_name(name2)
|
| 143 |
+
|
| 144 |
+
# Exakt matchning
|
| 145 |
+
if norm1 == norm2:
|
| 146 |
+
return True
|
| 147 |
+
|
| 148 |
+
# Kontrollera om det ena namnet innehåller det andra
|
| 149 |
+
if norm1 in norm2 or norm2 in norm1:
|
| 150 |
+
return True
|
| 151 |
+
|
| 152 |
+
# Kontrollera för- och efternamn separat
|
| 153 |
+
parts1 = norm1.split()
|
| 154 |
+
parts2 = norm2.split()
|
| 155 |
+
|
| 156 |
+
if len(parts1) >= 2 and len(parts2) >= 2:
|
| 157 |
+
# Jämför förnamn och efternamn
|
| 158 |
+
if parts1[0] == parts2[0] and parts1[-1] == parts2[-1]:
|
| 159 |
+
return True
|
| 160 |
+
|
| 161 |
+
return False
|
| 162 |
+
|
| 163 |
+
def _normalize_name(self, name):
|
| 164 |
+
"""Normalisera namn för jämförelse"""
|
| 165 |
+
# Ta bort extra tecken och gör lowercase
|
| 166 |
+
normalized = re.sub(r'[^\w\s]', '', name.lower())
|
| 167 |
+
# Ta bort extra whitespace
|
| 168 |
+
normalized = re.sub(r'\s+', ' ', normalized).strip()
|
| 169 |
+
return normalized
|
| 170 |
+
|
| 171 |
+
def get_all_images_with_metadata(self):
|
| 172 |
+
"""Hämta alla bilder med metadata"""
|
| 173 |
+
all_images = []
|
| 174 |
+
|
| 175 |
+
for key, images in self.image_map.items():
|
| 176 |
+
for image in images:
|
| 177 |
+
# Hitta matchande episoder
|
| 178 |
+
matching_episodes = []
|
| 179 |
+
for episode in self.episodes:
|
| 180 |
+
if self._names_match(episode.get('episode_title', ''), image['name']):
|
| 181 |
+
matching_episodes.append({
|
| 182 |
+
'title': episode.get('episode_title', ''),
|
| 183 |
+
'date': episode.get('episode_date', ''),
|
| 184 |
+
'songs_count': len(episode.get('songs', []))
|
| 185 |
+
})
|
| 186 |
+
|
| 187 |
+
image_data = image.copy()
|
| 188 |
+
image_data['matching_episodes'] = matching_episodes
|
| 189 |
+
image_data['episodes_count'] = len(matching_episodes)
|
| 190 |
+
all_images.append(image_data)
|
| 191 |
+
|
| 192 |
+
# Sortera efter antal matchande episoder (de med flest episoder först)
|
| 193 |
+
all_images.sort(key=lambda x: x['episodes_count'], reverse=True)
|
| 194 |
+
|
| 195 |
+
return all_images
|
| 196 |
+
|
| 197 |
+
def generate_image_gallery_data(self):
|
| 198 |
+
"""Generera data för bildgalleriet"""
|
| 199 |
+
gallery_data = {
|
| 200 |
+
'total_images': len([img for images in self.image_map.values() for img in images]),
|
| 201 |
+
'unique_people': len(self.image_map),
|
| 202 |
+
'decades': defaultdict(list),
|
| 203 |
+
'most_photographed': [],
|
| 204 |
+
'recent_additions': []
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
# Gruppera efter årtionden
|
| 208 |
+
for key, images in self.image_map.items():
|
| 209 |
+
for image in images:
|
| 210 |
+
if image['year']:
|
| 211 |
+
decade = f"{image['year'][:3]}0s"
|
| 212 |
+
gallery_data['decades'][decade].append(image)
|
| 213 |
+
|
| 214 |
+
# Hitta mest fotograferade personer
|
| 215 |
+
person_counts = {}
|
| 216 |
+
for key, images in self.image_map.items():
|
| 217 |
+
person_name = images[0]['name'] # Ta första bildens namn
|
| 218 |
+
person_counts[person_name] = len(images)
|
| 219 |
+
|
| 220 |
+
gallery_data['most_photographed'] = sorted(
|
| 221 |
+
person_counts.items(),
|
| 222 |
+
key=lambda x: x[1],
|
| 223 |
+
reverse=True
|
| 224 |
+
)[:10]
|
| 225 |
+
|
| 226 |
+
return gallery_data
|
| 227 |
+
|
| 228 |
+
def create_image_matcher():
|
| 229 |
+
"""Skapa och returnera en ImageMatcher instans"""
|
| 230 |
+
images_dir = '/Users/isakskogstad/Desktop/p1_sommar_space/hf_sommar_space/images'
|
| 231 |
+
data_file = '/Users/isakskogstad/Desktop/p1_sommar_space/hf_sommar_space/data.json'
|
| 232 |
+
|
| 233 |
+
return SommarImageMatcher(images_dir, data_file)
|
| 234 |
+
|
| 235 |
+
if __name__ == "__main__":
|
| 236 |
+
# Test funktionaliteten
|
| 237 |
+
matcher = create_image_matcher()
|
| 238 |
+
gallery_data = matcher.generate_image_gallery_data()
|
| 239 |
+
|
| 240 |
+
print(f"\n📊 Gallerstatistik:")
|
| 241 |
+
print(f"Totalt bilder: {gallery_data['total_images']}")
|
| 242 |
+
print(f"Unika personer: {gallery_data['unique_people']}")
|
| 243 |
+
print(f"Årtionden representerade: {len(gallery_data['decades'])}")
|
| 244 |
+
|
| 245 |
+
print(f"\n👥 Mest fotograferade:")
|
| 246 |
+
for name, count in gallery_data['most_photographed'][:5]:
|
| 247 |
+
print(f" {name}: {count} bilder")
|
images/1960-08-14. J/303/266rgen Cederberg 1960. .jpg
ADDED
|
|
images/1963-06-27. Torsten Ehrenmark 1963. .jpg
ADDED
|
|
images/1965-07-22. Torsten Ehrenmark 1965. .jpg
ADDED
|
|
images/1966-08-28. Torsten Ehrenmark 1966. .jpg
ADDED
|
|
images/1967-07-12. Tage Danielsson 1967. .jpg
ADDED
|
|
images/1967-08-15. Elisabeth S/303/266derstr/303/266m 1967. .jpg
ADDED
|
|
images/1968-07-22. Bo Setterlind 1968. .jpg
ADDED
|
|
images/1968-08-04. /303/205ke Falck 1968. .jpg
ADDED
|
|
images/1968-08-07. Bengt Feldreich 1968. .jpg
ADDED
|
|
images/1968-08-21. Sven Jerring 1968. .jpg
ADDED
|
|
images/1969-07-02. Bo Str/303/266mstedt 1969. .jpg
ADDED
|
|
images/1969-07-22. Anders Erik Malm 1969. .jpg
ADDED
|
|
images/1970-07-23. Torsten Jungstedt 1970. .jpg
ADDED
|
|
images/1970-08-21. Tage Danielsson 1970. .jpg
ADDED
|
|
images/1971-06-13. Beppe Wolgers 1971. .jpg
ADDED
|
|
images/1971-06-20. Magnus H/303/244renstam 1971. .jpg
ADDED
|
|
images/1971-06-24. Finn Zetterholm 1971. .jpg
ADDED
|
|
images/1971-07-07. Viveka Vogel 1971. .jpg
ADDED
|
|
images/1971-07-15. Ulla Trenter 1971. .jpg
ADDED
|
|
images/1971-08-03. Cilla Ingvar 1971. .jpg
ADDED
|
|
images/1971-08-10. Ulf Linde 1971. .jpg
ADDED
|
|
images/1971-08-13. Barbro Alving 1971. .jpg
ADDED
|
|
images/1972-07-09. G/303/266sta Knutsson 1972. .jpg
ADDED
|
|
images/1973-07-23. Tage Danielsson 1973. .jpg
ADDED
|
|
images/1974-06-19. /303/205ke Falck 1974. .jpg
ADDED
|
|
images/1974-08-06. Anders Gernandt 1974. .jpg
ADDED
|
|
images/1974-08-25. Barbro Lindgren 1974. .jpg
ADDED
|
|
images/1978-07-09. Maud Reutersw/303/244rd 1978. .jpg
ADDED
|
|
images/1979-06-21. Marit Paulsen 1979. .jpg
ADDED
|
|
images/1979-06-22. Lars Forssell 1979. .jpg
ADDED
|
|
images/1979-07-22. Kjell Alinge 1979. .jpg
ADDED
|
|
images/1979-08-08. Bj/303/266rn Skifs 1979. .jpg
ADDED
|
|
images/1979-08-12. Lars Ulvenstam 1979. .jpg
ADDED
|
|
images/1980-06-15. Tage Danielsson 1980. .jpg
ADDED
|
|
images/1980-06-19. Klas /303/226stergren 1980. .jpg
ADDED
|
|
images/1980-06-29. Olle Adolphson - 1980. .jpg
ADDED
|
|
images/1980-07-30. Channa Bankier 1980. .jpg
ADDED
|
|
images/1980-08-02. Lars Berghagen 1980. .jpg
ADDED
|
|
images/1980-08-17. Torsten Ehrenmark 1980. .jpg
ADDED
|
|
images/1981-06-20. Erna Tauro 1981. .jpg
ADDED
|
|
images/1981-06-21. Magnus H/303/244renstam 1981. .jpg
ADDED
|
|
images/1981-06-26. Robert Broberg 1981. .jpg
ADDED
|
|
images/1981-06-28. Lars Berghagen 1981. .jpg
ADDED
|
|