diff --git a/.devcontainer/README.md b/.devcontainer/README.md
new file mode 100644
index 0000000..05ee5ff
--- /dev/null
+++ b/.devcontainer/README.md
@@ -0,0 +1,207 @@
+# DevContainer Setup für Thats-Me
+
+## Was ist ein DevContainer?
+
+Ein DevContainer ist eine vollständige Entwicklungsumgebung in Docker. Cursor/VSCode läuft direkt im Container und Sie entwickeln mit allen Tools, die bereits vorinstalliert sind.
+
+## Voraussetzungen
+
+1. **Docker Desktop** muss laufen
+2. **Traefik Proxy** als externes Netzwerk:
+ ```bash
+ docker network create proxy
+ ```
+
+## DevContainer starten
+
+1. **In Cursor/VSCode:**
+
+ - Drücken Sie `Cmd+Shift+P` (macOS) oder `Ctrl+Shift+P` (Windows/Linux)
+ - Wählen Sie: **"Dev Containers: Reopen in Container"**
+ - Warten Sie, bis alle Container gebaut und gestartet sind
+
+2. **Beim ersten Start:**
+ - Der Laravel Container wird gebaut (kann einige Minuten dauern)
+ - Alle Services werden gestartet (MySQL, Redis, Mailpit, Quasar)
+ - Sie werden automatisch im Laravel Container eingeloggt
+
+## Was passiert beim Start?
+
+Der DevContainer startet folgende Services:
+
+- **laravel.test** - Laravel Backend (Sie arbeiten in diesem Container)
+- **mysql** - Datenbank
+- **redis** - Cache/Queue
+- **mailpit** - E-Mail Testing
+- **quasar.app** - Frontend App (läuft automatisch)
+
+## Arbeiten im Container
+
+### Terminal
+
+Das Terminal in Cursor/VSCode ist bereits im Container. Sie können direkt arbeiten:
+
+```bash
+# Sie sind im /var/www/html Verzeichnis (= ./backend vom Host)
+php artisan migrate
+php artisan serve
+composer install
+npm install
+npm run dev
+```
+
+### Verfügbare Services
+
+Im DevContainer können Sie direkt auf die anderen Services zugreifen:
+
+```bash
+# MySQL Verbindung
+mysql -h mysql -u sail -p
+# Passwort: password
+
+# Redis
+redis-cli -h redis
+
+# Composer
+composer install
+composer update
+
+# Artisan
+php artisan migrate
+php artisan tinker
+```
+
+## Zugriff von außerhalb
+
+### Von Ihrem Browser (Host)
+
+Die Ports werden automatisch weitergeleitet:
+
+- **Laravel App:** http://localhost (Port 80)
+- **Vite Dev Server:** http://localhost:5173
+- **Mailpit Dashboard:** http://localhost:8025
+- **Quasar App:** http://localhost:9000
+
+### Mit Traefik
+
+Wenn Traefik läuft, können Sie auch die Domains verwenden:
+
+- https://thats-me.test
+- https://portal.thats-me.test
+- https://api.thats-me.test
+- https://app.thats-me.test
+- https://assets.thats-me.test
+
+## Backend .env Konfiguration
+
+Stellen Sie sicher, dass `backend/.env` folgende Werte hat:
+
+```env
+DB_CONNECTION=mysql
+DB_HOST=mysql
+DB_PORT=3306
+DB_DATABASE=thats-me
+DB_USERNAME=sail
+DB_PASSWORD=password
+
+MAIL_MAILER=smtp
+MAIL_HOST=mailpit
+MAIL_PORT=1025
+
+REDIS_HOST=redis
+REDIS_PORT=6379
+```
+
+## Vite Dev Server starten
+
+```bash
+# Im DevContainer Terminal
+npm install
+npm run dev
+```
+
+Vite läuft dann auf Port 5173 und ist verfügbar unter:
+
+- http://localhost:5173 (vom Host)
+- https://assets.thats-me.test (mit Traefik)
+
+## Quasar App
+
+Die Quasar App läuft automatisch in einem separaten Container und ist verfügbar unter:
+
+- http://localhost:9000 (vom Host)
+- https://app.thats-me.test (mit Traefik)
+
+## Troubleshooting
+
+### Container startet nicht
+
+```bash
+# Schließen Sie den DevContainer
+# Öffnen Sie ein normales Terminal auf dem Host
+docker-compose down -v
+docker-compose build --no-cache
+# Dann neu starten: "Dev Containers: Reopen in Container"
+```
+
+### "Dockerfile not found" Fehler
+
+Stellen Sie sicher, dass Laravel Sail installiert ist:
+
+```bash
+cd backend
+composer require laravel/sail --dev
+```
+
+### Permission-Probleme
+
+Die User-IDs in `devcontainer.json` anpassen:
+
+```json
+"containerEnv": {
+ "WWWUSER": "1000", // Ihre User-ID
+ "WWWGROUP": "1000" // Ihre Group-ID
+}
+```
+
+Finden Sie Ihre IDs mit:
+
+```bash
+id -u # User ID
+id -g # Group ID
+```
+
+### Logs ansehen
+
+```bash
+# Im DevContainer Terminal
+docker-compose logs -f
+
+# Nur MySQL
+docker-compose logs -f mysql
+
+# Nur Quasar
+docker-compose logs -f quasar.app
+```
+
+## DevContainer verlassen
+
+1. Drücken Sie `Cmd+Shift+P` / `Ctrl+Shift+P`
+2. Wählen Sie: **"Dev Containers: Reopen Folder Locally"**
+
+## Container stoppen
+
+Nach dem Verlassen des DevContainers:
+
+```bash
+docker-compose down
+```
+
+## Vorteile des DevContainers
+
+✅ Alle arbeiten mit der gleichen Umgebung
+✅ Keine lokale PHP/MySQL Installation nötig
+✅ Automatische Service-Verwaltung
+✅ Isolierte Entwicklungsumgebung
+✅ Einfaches Onboarding für neue Entwickler
+
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 0000000..840f882
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,94 @@
+{
+ "name": "Thats-Me (Dev Container)",
+ // 1. DIES IST DER WICHTIGSTE TEIL:
+ // Wir verwenden Docker Compose für alle Services
+ "dockerComposeFile": [
+ "../docker-compose.yml"
+ ],
+ "service": "laravel.test",
+ // 3. WIR DEFINIEREN DEN ARBEITSBEREICH:
+ // Wir mounten das gesamte Projekt, damit Sie Backend UND Frontend sehen
+ "workspaceFolder": "/workspace",
+ // 4. WIR LEGEN DEN BENUTZER FEST:
+ // Laravel Sail führt Befehle standardmäßig als 'sail'-Benutzer aus, um Berechtigungsprobleme zu vermeiden.
+ "remoteUser": "sail",
+ // 5. ZUSÄTZLICHE ENTWICKLER-TOOLS (FEATURES):
+ // Features werden über postCreateCommand installiert um Kompatibilitätsprobleme zu vermeiden
+ "features": {},
+ // 6. BEFEHLE NACH DEM ERSTELLEN:
+ // Installiert nur die Tools die ohne Root-Rechte funktionieren
+ //"postCreateCommand": "composer install --no-interaction --prefer-dist --optimize-autoloader",
+ // 7. EDITOR-ANPASSUNGEN (Optional, aber sehr empfohlen):
+ "customizations": {
+ "vscode": {
+ "extensions": [
+ "bmewburn.vscode-intelephense-client",
+ "onecentlin.laravel-blade",
+ "shufo.vscode-blade-formatter",
+ "bradlc.vscode-tailwindcss"
+ ]
+ }
+ },
+ // 8. ZU STARTENDE DIENSTE:
+ // Legt fest, welche Dienste aus der docker-compose.yml gestartet werden sollen.
+ "runServices": [
+ "laravel.test",
+ "quasar.app",
+ "mysql",
+ "redis",
+ "mailpit"
+ ],
+ // 9. ZUSÄTZLICHE KONFIGURATION:
+ // Umgebungsvariablen für den DevContainer
+ "containerEnv": {
+ "WWWUSER": "501",
+ "WWWGROUP": "20",
+ "LARAVEL_SAIL": "1"
+ },
+ // 9b. MOUNTS:
+ // Mountet das gesamte Projekt (Root) nach /workspace, damit Sie Backend UND Frontend sehen
+ "mounts": [
+ "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=cached"
+ ],
+ // 10. FORWARD PORTS:
+ // Ports die automatisch weitergeleitet werden sollen (Container-Ports)
+ "forwardPorts": [
+ 80,
+ 5173,
+ 3306,
+ 6379,
+ 1025,
+ 8025,
+ 9000
+ ],
+ "portsAttributes": {
+ "80": {
+ "label": "Laravel App (HTTP)",
+ "onAutoForward": "notify"
+ },
+ "5173": {
+ "label": "Vite Dev Server",
+ "onAutoForward": "notify"
+ },
+ "3306": {
+ "label": "MySQL",
+ "onAutoForward": "silent"
+ },
+ "6379": {
+ "label": "Redis",
+ "onAutoForward": "silent"
+ },
+ "8025": {
+ "label": "Mailpit Dashboard",
+ "onAutoForward": "notify"
+ },
+ "1025": {
+ "label": "Mailpit SMTP",
+ "onAutoForward": "silent"
+ },
+ "9000": {
+ "label": "Quasar App",
+ "onAutoForward": "notify"
+ }
+ }
+}
diff --git a/.gitignore b/.gitignore
index df866ed..ce8cafa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,25 +1,58 @@
+# Laravel
+/.phpunit.cache
+/node_modules
+/public/build
+/public/hot
+/public/storage
+/public/vendor
+/storage/*.key
+/storage/app
+/storage/framework
+/storage/language
+/storage/logs
+/storage/pail
+/vendor
+.env
+.env.backup
+.env.production
+.phpactor.json
+.phpunit.result.cache
+Homestead.json
+Homestead.yaml
+auth.json
+npm-debug.log
+yarn-error.log
+
+# IDEs & Editors
+/.fleet
+/.idea
+/.nova
+/.vscode
+/.zed
+.claude/
+.cursor/
+
+# macOS
.DS_Store
-node_modules
-/dist
+.AppleDouble
+.LSOverride
+._*
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+Icon
-# local env files
-.env.local
-.env.*.local
-
-# Log files
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-pnpm-debug.log*
-
-# Editor directories and files
-.idea
-.vscode
-*.suo
-*.ntvs*
-*.njsproj
-*.sln
-*.sw?
-/.history
-/deployment
+# Project specific
+_static/
+_work/
+_storage/
diff --git a/DOCKER-SETUP.md b/DOCKER-SETUP.md
new file mode 100644
index 0000000..2a853c9
--- /dev/null
+++ b/DOCKER-SETUP.md
@@ -0,0 +1,296 @@
+# Docker Setup für Thats-Me Projekt
+
+## Übersicht
+
+Dieses Projekt verwendet Docker mit Laravel Sail für das Backend und einen Node-Container für die Quasar Frontend App.
+
+### Services
+
+- **laravel.test** - Laravel Backend (PHP 8.4)
+- **quasar.app** - Quasar Frontend App (Node 20)
+- **mysql** - MySQL 8.0 Database
+- **mailpit** - E-Mail Testing Tool
+- **redis** - Cache & Queue Service
+
+### Domains
+
+Das Setup konfiguriert 4 Domains über Traefik:
+
+1. **thats-me.test** - Laravel Webseite/Landingpage
+2. **portal.thats-me.test** - Laravel Admin Panel
+3. **api.thats-me.test** - Laravel API für Quasar App
+4. **app.thats-me.test** - Quasar Frontend App
+
+Zusätzlich:
+
+- **assets.thats-me.test** - Vite Dev Server für Laravel Assets
+
+## Voraussetzungen
+
+1. **Docker Desktop** installiert
+2. **Traefik Proxy** muss als externes Netzwerk verfügbar sein:
+ ```bash
+ docker network create proxy
+ ```
+3. **Laravel Sail** muss im Backend installiert sein:
+ ```bash
+ cd backend
+ composer require laravel/sail --dev
+ ```
+
+## Installation
+
+### 1. Environment Dateien einrichten
+
+**Root .env Datei:**
+
+```bash
+cp .env.docker .env
+```
+
+**Backend .env Datei:**
+
+```bash
+cd backend
+cp .env.example .env
+php artisan key:generate
+```
+
+Bearbeite `backend/.env` und stelle sicher, dass diese Einstellungen gesetzt sind:
+
+```env
+DB_CONNECTION=mysql
+DB_HOST=mysql
+DB_PORT=3306
+DB_DATABASE=thats-me
+DB_USERNAME=sail
+DB_PASSWORD=password
+
+MAIL_MAILER=smtp
+MAIL_HOST=mailpit
+MAIL_PORT=1025
+
+REDIS_HOST=redis
+REDIS_PORT=6379
+```
+
+### 2. Hosts-Datei konfigurieren
+
+Füge folgende Einträge zu deiner `/etc/hosts` Datei hinzu:
+
+```
+127.0.0.1 thats-me.test
+127.0.0.1 portal.thats-me.test
+127.0.0.1 api.thats-me.test
+127.0.0.1 app.thats-me.test
+127.0.0.1 assets.thats-me.test
+```
+
+### 3. Docker Container starten
+
+```bash
+docker-compose up -d
+```
+
+### 4. Laravel Installation abschließen
+
+Beim ersten Start:
+
+```bash
+# In den Laravel Container einsteigen
+docker-compose exec laravel.test bash
+
+# Composer Dependencies installieren
+composer install
+
+# Datenbank migrieren
+php artisan migrate
+
+# Optional: Seeder ausführen
+php artisan db:seed
+```
+
+### 5. Frontend Dependencies installieren
+
+Der Quasar Container installiert automatisch die Dependencies beim Start.
+Falls manuell nötig:
+
+```bash
+docker-compose exec quasar.app npm install
+```
+
+## Verwendung
+
+### Container starten
+
+```bash
+docker-compose up -d
+```
+
+### Container stoppen
+
+```bash
+docker-compose down
+```
+
+### Logs ansehen
+
+```bash
+# Alle Services
+docker-compose logs -f
+
+# Nur Laravel
+docker-compose logs -f laravel.test
+
+# Nur Quasar
+docker-compose logs -f quasar.app
+```
+
+### In Container einsteigen
+
+**Laravel:**
+
+```bash
+docker-compose exec laravel.test bash
+```
+
+**Quasar:**
+
+```bash
+docker-compose exec quasar.app sh
+```
+
+**MySQL:**
+
+```bash
+docker-compose exec mysql mysql -u sail -p
+# Passwort: password
+```
+
+### Artisan Commands
+
+```bash
+docker-compose exec laravel.test php artisan migrate
+docker-compose exec laravel.test php artisan cache:clear
+docker-compose exec laravel.test php artisan queue:work
+```
+
+### NPM Commands (Frontend)
+
+```bash
+docker-compose exec quasar.app npm run dev
+docker-compose exec quasar.app npm run build
+```
+
+## Zugriff auf die Anwendung
+
+### Mit Traefik (empfohlen)
+
+- **Hauptwebseite:** https://thats-me.test
+- **Admin Portal:** https://portal.thats-me.test
+- **API:** https://api.thats-me.test
+- **Frontend App:** https://app.thats-me.test
+- **Vite Assets:** https://assets.thats-me.test
+
+### Direkt über Ports
+
+- **Laravel App:** http://localhost (Port 80 über Traefik)
+- **Vite Dev Server:** http://localhost:5179 (Host) → 5173 (Container)
+- **Quasar App:** http://localhost:9000
+- **Mailpit Dashboard:** http://localhost:8028
+- **MySQL:** localhost:33070
+- **Redis:** localhost:6383
+
+## Vite Development Server
+
+Um den Vite Dev Server für Laravel zu starten:
+
+```bash
+docker-compose exec laravel.test npm install
+docker-compose exec laravel.test npm run dev
+```
+
+Dann ist HMR (Hot Module Replacement) unter https://assets.thats-me.test verfügbar.
+
+## Troubleshooting
+
+### Proxy-Netzwerk existiert nicht
+
+```bash
+docker network create proxy
+```
+
+### Port-Konflikte
+
+Ändere die Ports in der `.env` Datei:
+
+```env
+FORWARD_DB_PORT=33071
+FORWARD_MAILPIT_DASHBOARD_PORT=8029
+QUASAR_PORT=9001
+VITE_PORT=5180
+```
+
+### Permission-Probleme
+
+```bash
+# User/Group IDs in .env anpassen
+WWWUSER=1000
+WWWGROUP=1000
+```
+
+### Container neu bauen
+
+```bash
+docker-compose down -v
+docker-compose build --no-cache
+docker-compose up -d
+```
+
+### Quasar startet nicht
+
+```bash
+# Manuell im Container starten
+docker-compose exec quasar.app sh
+cd /app
+npm install
+npm run dev
+```
+
+## Entwicklung ohne Traefik
+
+Falls Sie kein Traefik haben, können Sie die Container auch direkt über Ports erreichen:
+
+1. Entfernen Sie die `labels` Sektion aus `docker-compose.yml`
+2. Aktivieren Sie den Port-Mapping für Laravel:
+ ```yaml
+ ports:
+ - "${APP_PORT:-80}:80"
+ - "${VITE_PORT:-5173}:5173"
+ ```
+3. Zugriff dann über:
+ - Laravel: http://localhost
+ - Vite: http://localhost:5173
+ - Quasar: http://localhost:9000
+
+## Nützliche Befehle
+
+```bash
+# Container Status
+docker-compose ps
+
+# Container neu starten
+docker-compose restart
+
+# Bestimmten Service neu starten
+docker-compose restart laravel.test
+
+# Container und Volumes löschen
+docker-compose down -v
+
+# Logs von heute
+docker-compose logs --since 24h
+
+# Ressourcen-Nutzung
+docker stats
+```
diff --git a/README.md b/README.md
index 159c8b4..ad4cd7b 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,13 @@
Eine kurze Beschreibung, was dieses Projekt macht (ein oder zwei Sätze).
+## Domains auf dem Testserver
+
+app.thats-me.test = frontend Quasar APP
+portal.thats-me.test = backend Laravel Admin Panel mit Tailwind CSS + FluxUI
+thats-me.test = backend Laravel Webseite / Landinpage mit Tailwind CSS + FluxUI
+api.thats-me.test = backend Laravel API für Quasar APP
+
## Inhaltsverzeichnis
- [Start des Projekts](#start)
@@ -93,4 +100,4 @@ Erkläre, wie andere zum Projekt beitragen können. Gibt es Richtlinien für Pul
Gib an, unter welcher Lizenz das Projekt veröffentlicht wird. Zum Beispiel:
-Dieses Projekt ist unter der MIT-Lizenz lizenziert - siehe die [LICENSE.md](LICENSE.md)-Datei für Details (falls vorhanden).
\ No newline at end of file
+Dieses Projekt ist unter der MIT-Lizenz lizenziert - siehe die [LICENSE.md](LICENSE.md)-Datei für Details (falls vorhanden).
diff --git a/backend/composer.json b/backend/composer.json
index e0a0e24..8915653 100644
--- a/backend/composer.json
+++ b/backend/composer.json
@@ -19,7 +19,7 @@
"fakerphp/faker": "^1.23",
"laravel/pail": "^1.2.2",
"laravel/pint": "^1.18",
- "laravel/sail": "^1.41",
+ "laravel/sail": "^1.46",
"mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.6",
"pestphp/pest": "^3.7",
@@ -74,4 +74,4 @@
},
"minimum-stability": "stable",
"prefer-stable": true
-}
\ No newline at end of file
+}
diff --git a/backend/composer.lock b/backend/composer.lock
index 1554f64..70d226f 100644
--- a/backend/composer.lock
+++ b/backend/composer.lock
@@ -4,29 +4,29 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "b17142323a68267a6e0b99a3d80c5654",
+ "content-hash": "9ec434279d6b443c6a5865f46b13b717",
"packages": [
{
"name": "brick/math",
- "version": "0.12.3",
+ "version": "0.14.0",
"source": {
"type": "git",
"url": "https://github.com/brick/math.git",
- "reference": "866551da34e9a618e64a819ee1e01c20d8a588ba"
+ "reference": "113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/brick/math/zipball/866551da34e9a618e64a819ee1e01c20d8a588ba",
- "reference": "866551da34e9a618e64a819ee1e01c20d8a588ba",
+ "url": "https://api.github.com/repos/brick/math/zipball/113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2",
+ "reference": "113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2",
"shasum": ""
},
"require": {
- "php": "^8.1"
+ "php": "^8.2"
},
"require-dev": {
"php-coveralls/php-coveralls": "^2.2",
- "phpunit/phpunit": "^10.1",
- "vimeo/psalm": "6.8.8"
+ "phpstan/phpstan": "2.1.22",
+ "phpunit/phpunit": "^11.5"
},
"type": "library",
"autoload": {
@@ -56,7 +56,7 @@
],
"support": {
"issues": "https://github.com/brick/math/issues",
- "source": "https://github.com/brick/math/tree/0.12.3"
+ "source": "https://github.com/brick/math/tree/0.14.0"
},
"funding": [
{
@@ -64,7 +64,7 @@
"type": "github"
}
],
- "time": "2025-02-28T13:11:00+00:00"
+ "time": "2025-08-29T12:40:03+00:00"
},
{
"name": "carbonphp/carbon-doctrine-types",
@@ -212,33 +212,32 @@
},
{
"name": "doctrine/inflector",
- "version": "2.0.10",
+ "version": "2.1.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/inflector.git",
- "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc"
+ "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/inflector/zipball/5817d0659c5b50c9b950feb9af7b9668e2c436bc",
- "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc",
+ "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b",
+ "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0"
},
"require-dev": {
- "doctrine/coding-standard": "^11.0",
- "phpstan/phpstan": "^1.8",
- "phpstan/phpstan-phpunit": "^1.1",
- "phpstan/phpstan-strict-rules": "^1.3",
- "phpunit/phpunit": "^8.5 || ^9.5",
- "vimeo/psalm": "^4.25 || ^5.4"
+ "doctrine/coding-standard": "^12.0 || ^13.0",
+ "phpstan/phpstan": "^1.12 || ^2.0",
+ "phpstan/phpstan-phpunit": "^1.4 || ^2.0",
+ "phpstan/phpstan-strict-rules": "^1.6 || ^2.0",
+ "phpunit/phpunit": "^8.5 || ^12.2"
},
"type": "library",
"autoload": {
"psr-4": {
- "Doctrine\\Inflector\\": "lib/Doctrine/Inflector"
+ "Doctrine\\Inflector\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -283,7 +282,7 @@
],
"support": {
"issues": "https://github.com/doctrine/inflector/issues",
- "source": "https://github.com/doctrine/inflector/tree/2.0.10"
+ "source": "https://github.com/doctrine/inflector/tree/2.1.0"
},
"funding": [
{
@@ -299,7 +298,7 @@
"type": "tidelift"
}
],
- "time": "2024-02-18T20:23:39+00:00"
+ "time": "2025-08-10T19:31:58+00:00"
},
{
"name": "doctrine/lexer",
@@ -645,22 +644,22 @@
},
{
"name": "guzzlehttp/guzzle",
- "version": "7.9.3",
+ "version": "7.10.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
- "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77"
+ "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7b2f29fe81dc4da0ca0ea7d42107a0845946ea77",
- "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77",
+ "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4",
+ "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4",
"shasum": ""
},
"require": {
"ext-json": "*",
- "guzzlehttp/promises": "^1.5.3 || ^2.0.3",
- "guzzlehttp/psr7": "^2.7.0",
+ "guzzlehttp/promises": "^2.3",
+ "guzzlehttp/psr7": "^2.8",
"php": "^7.2.5 || ^8.0",
"psr/http-client": "^1.0",
"symfony/deprecation-contracts": "^2.2 || ^3.0"
@@ -751,7 +750,7 @@
],
"support": {
"issues": "https://github.com/guzzle/guzzle/issues",
- "source": "https://github.com/guzzle/guzzle/tree/7.9.3"
+ "source": "https://github.com/guzzle/guzzle/tree/7.10.0"
},
"funding": [
{
@@ -767,20 +766,20 @@
"type": "tidelift"
}
],
- "time": "2025-03-27T13:37:11+00:00"
+ "time": "2025-08-23T22:36:01+00:00"
},
{
"name": "guzzlehttp/promises",
- "version": "2.2.0",
+ "version": "2.3.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/promises.git",
- "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c"
+ "reference": "481557b130ef3790cf82b713667b43030dc9c957"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/promises/zipball/7c69f28996b0a6920945dd20b3857e499d9ca96c",
- "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c",
+ "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957",
+ "reference": "481557b130ef3790cf82b713667b43030dc9c957",
"shasum": ""
},
"require": {
@@ -788,7 +787,7 @@
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
- "phpunit/phpunit": "^8.5.39 || ^9.6.20"
+ "phpunit/phpunit": "^8.5.44 || ^9.6.25"
},
"type": "library",
"extra": {
@@ -834,7 +833,7 @@
],
"support": {
"issues": "https://github.com/guzzle/promises/issues",
- "source": "https://github.com/guzzle/promises/tree/2.2.0"
+ "source": "https://github.com/guzzle/promises/tree/2.3.0"
},
"funding": [
{
@@ -850,20 +849,20 @@
"type": "tidelift"
}
],
- "time": "2025-03-27T13:27:01+00:00"
+ "time": "2025-08-22T14:34:08+00:00"
},
{
"name": "guzzlehttp/psr7",
- "version": "2.7.1",
+ "version": "2.8.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
- "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16"
+ "reference": "21dc724a0583619cd1652f673303492272778051"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/psr7/zipball/c2270caaabe631b3b44c85f99e5a04bbb8060d16",
- "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16",
+ "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051",
+ "reference": "21dc724a0583619cd1652f673303492272778051",
"shasum": ""
},
"require": {
@@ -879,7 +878,7 @@
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"http-interop/http-factory-tests": "0.9.0",
- "phpunit/phpunit": "^8.5.39 || ^9.6.20"
+ "phpunit/phpunit": "^8.5.44 || ^9.6.25"
},
"suggest": {
"laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
@@ -950,7 +949,7 @@
],
"support": {
"issues": "https://github.com/guzzle/psr7/issues",
- "source": "https://github.com/guzzle/psr7/tree/2.7.1"
+ "source": "https://github.com/guzzle/psr7/tree/2.8.0"
},
"funding": [
{
@@ -966,20 +965,20 @@
"type": "tidelift"
}
],
- "time": "2025-03-27T12:30:47+00:00"
+ "time": "2025-08-23T21:21:41+00:00"
},
{
"name": "guzzlehttp/uri-template",
- "version": "v1.0.4",
+ "version": "v1.0.5",
"source": {
"type": "git",
"url": "https://github.com/guzzle/uri-template.git",
- "reference": "30e286560c137526eccd4ce21b2de477ab0676d2"
+ "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/uri-template/zipball/30e286560c137526eccd4ce21b2de477ab0676d2",
- "reference": "30e286560c137526eccd4ce21b2de477ab0676d2",
+ "url": "https://api.github.com/repos/guzzle/uri-template/zipball/4f4bbd4e7172148801e76e3decc1e559bdee34e1",
+ "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1",
"shasum": ""
},
"require": {
@@ -988,7 +987,7 @@
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
- "phpunit/phpunit": "^8.5.36 || ^9.6.15",
+ "phpunit/phpunit": "^8.5.44 || ^9.6.25",
"uri-template/tests": "1.0.0"
},
"type": "library",
@@ -1036,7 +1035,7 @@
],
"support": {
"issues": "https://github.com/guzzle/uri-template/issues",
- "source": "https://github.com/guzzle/uri-template/tree/v1.0.4"
+ "source": "https://github.com/guzzle/uri-template/tree/v1.0.5"
},
"funding": [
{
@@ -1052,24 +1051,24 @@
"type": "tidelift"
}
],
- "time": "2025-02-03T10:55:03+00:00"
+ "time": "2025-08-22T14:27:06+00:00"
},
{
"name": "laravel/framework",
- "version": "v12.4.1",
+ "version": "v12.34.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
- "reference": "cdefd852ecb459a65392cd6ccb578c92a15b8e2b"
+ "reference": "f9ec5a5d88bc8c468f17b59f88e05c8ac3c8d687"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/framework/zipball/cdefd852ecb459a65392cd6ccb578c92a15b8e2b",
- "reference": "cdefd852ecb459a65392cd6ccb578c92a15b8e2b",
+ "url": "https://api.github.com/repos/laravel/framework/zipball/f9ec5a5d88bc8c468f17b59f88e05c8ac3c8d687",
+ "reference": "f9ec5a5d88bc8c468f17b59f88e05c8ac3c8d687",
"shasum": ""
},
"require": {
- "brick/math": "^0.11|^0.12",
+ "brick/math": "^0.11|^0.12|^0.13|^0.14",
"composer-runtime-api": "^2.2",
"doctrine/inflector": "^2.0.5",
"dragonmantank/cron-expression": "^3.4",
@@ -1086,7 +1085,7 @@
"guzzlehttp/uri-template": "^1.0",
"laravel/prompts": "^0.3.0",
"laravel/serializable-closure": "^1.3|^2.0",
- "league/commonmark": "^2.6",
+ "league/commonmark": "^2.7",
"league/flysystem": "^3.25.1",
"league/flysystem-local": "^3.25.1",
"league/uri": "^7.5.1",
@@ -1105,7 +1104,9 @@
"symfony/http-kernel": "^7.2.0",
"symfony/mailer": "^7.2.0",
"symfony/mime": "^7.2.0",
- "symfony/polyfill-php83": "^1.31",
+ "symfony/polyfill-php83": "^1.33",
+ "symfony/polyfill-php84": "^1.33",
+ "symfony/polyfill-php85": "^1.33",
"symfony/process": "^7.2.0",
"symfony/routing": "^7.2.0",
"symfony/uid": "^7.2.0",
@@ -1141,6 +1142,7 @@
"illuminate/filesystem": "self.version",
"illuminate/hashing": "self.version",
"illuminate/http": "self.version",
+ "illuminate/json-schema": "self.version",
"illuminate/log": "self.version",
"illuminate/macroable": "self.version",
"illuminate/mail": "self.version",
@@ -1173,12 +1175,13 @@
"league/flysystem-read-only": "^3.25.1",
"league/flysystem-sftp-v3": "^3.25.1",
"mockery/mockery": "^1.6.10",
- "orchestra/testbench-core": "^10.0.0",
- "pda/pheanstalk": "^5.0.6",
+ "opis/json-schema": "^2.4.1",
+ "orchestra/testbench-core": "^10.7.0",
+ "pda/pheanstalk": "^5.0.6|^7.0.0",
"php-http/discovery": "^1.15",
"phpstan/phpstan": "^2.0",
"phpunit/phpunit": "^10.5.35|^11.5.3|^12.0.1",
- "predis/predis": "^2.3",
+ "predis/predis": "^2.3|^3.0",
"resend/resend-php": "^0.10.0",
"symfony/cache": "^7.2.0",
"symfony/http-client": "^7.2.0",
@@ -1198,7 +1201,7 @@
"ext-pdo": "Required to use all database features.",
"ext-posix": "Required to use all features of the queue worker.",
"ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0|^6.0).",
- "fakerphp/faker": "Required to use the eloquent factory builder (^1.9.1).",
+ "fakerphp/faker": "Required to generate fake data using the fake() helper (^1.23).",
"filp/whoops": "Required for friendly error pages in development (^2.14.3).",
"laravel/tinker": "Required to use the tinker console command (^2.0).",
"league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.25.1).",
@@ -1210,7 +1213,7 @@
"pda/pheanstalk": "Required to use the beanstalk queue driver (^5.0).",
"php-http/discovery": "Required to use PSR-7 bridging features (^1.15).",
"phpunit/phpunit": "Required to use assertions and run tests (^10.5.35|^11.5.3|^12.0.1).",
- "predis/predis": "Required to use the predis connector (^2.3).",
+ "predis/predis": "Required to use the predis connector (^2.3|^3.0).",
"psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).",
"pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).",
"resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0).",
@@ -1267,20 +1270,20 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
- "time": "2025-03-30T16:27:26+00:00"
+ "time": "2025-10-14T13:58:31+00:00"
},
{
"name": "laravel/prompts",
- "version": "v0.3.5",
+ "version": "v0.3.7",
"source": {
"type": "git",
"url": "https://github.com/laravel/prompts.git",
- "reference": "57b8f7efe40333cdb925700891c7d7465325d3b1"
+ "reference": "a1891d362714bc40c8d23b0b1d7090f022ea27cc"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/prompts/zipball/57b8f7efe40333cdb925700891c7d7465325d3b1",
- "reference": "57b8f7efe40333cdb925700891c7d7465325d3b1",
+ "url": "https://api.github.com/repos/laravel/prompts/zipball/a1891d362714bc40c8d23b0b1d7090f022ea27cc",
+ "reference": "a1891d362714bc40c8d23b0b1d7090f022ea27cc",
"shasum": ""
},
"require": {
@@ -1297,8 +1300,8 @@
"illuminate/collections": "^10.0|^11.0|^12.0",
"mockery/mockery": "^1.5",
"pestphp/pest": "^2.3|^3.4",
- "phpstan/phpstan": "^1.11",
- "phpstan/phpstan-mockery": "^1.1"
+ "phpstan/phpstan": "^1.12.28",
+ "phpstan/phpstan-mockery": "^1.1.3"
},
"suggest": {
"ext-pcntl": "Required for the spinner to be animated."
@@ -1324,22 +1327,22 @@
"description": "Add beautiful and user-friendly forms to your command-line applications.",
"support": {
"issues": "https://github.com/laravel/prompts/issues",
- "source": "https://github.com/laravel/prompts/tree/v0.3.5"
+ "source": "https://github.com/laravel/prompts/tree/v0.3.7"
},
- "time": "2025-02-11T13:34:40+00:00"
+ "time": "2025-09-19T13:47:56+00:00"
},
{
"name": "laravel/serializable-closure",
- "version": "v2.0.4",
+ "version": "v2.0.6",
"source": {
"type": "git",
"url": "https://github.com/laravel/serializable-closure.git",
- "reference": "b352cf0534aa1ae6b4d825d1e762e35d43f8a841"
+ "reference": "038ce42edee619599a1debb7e81d7b3759492819"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/b352cf0534aa1ae6b4d825d1e762e35d43f8a841",
- "reference": "b352cf0534aa1ae6b4d825d1e762e35d43f8a841",
+ "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/038ce42edee619599a1debb7e81d7b3759492819",
+ "reference": "038ce42edee619599a1debb7e81d7b3759492819",
"shasum": ""
},
"require": {
@@ -1387,7 +1390,7 @@
"issues": "https://github.com/laravel/serializable-closure/issues",
"source": "https://github.com/laravel/serializable-closure"
},
- "time": "2025-03-19T13:51:03+00:00"
+ "time": "2025-10-09T13:42:30+00:00"
},
{
"name": "laravel/tinker",
@@ -1457,16 +1460,16 @@
},
{
"name": "league/commonmark",
- "version": "2.6.1",
+ "version": "2.7.1",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/commonmark.git",
- "reference": "d990688c91cedfb69753ffc2512727ec646df2ad"
+ "reference": "10732241927d3971d28e7ea7b5712721fa2296ca"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/d990688c91cedfb69753ffc2512727ec646df2ad",
- "reference": "d990688c91cedfb69753ffc2512727ec646df2ad",
+ "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/10732241927d3971d28e7ea7b5712721fa2296ca",
+ "reference": "10732241927d3971d28e7ea7b5712721fa2296ca",
"shasum": ""
},
"require": {
@@ -1495,7 +1498,7 @@
"symfony/process": "^5.4 | ^6.0 | ^7.0",
"symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0",
"unleashedtech/php-coding-standard": "^3.1.1",
- "vimeo/psalm": "^4.24.0 || ^5.0.0"
+ "vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0"
},
"suggest": {
"symfony/yaml": "v2.3+ required if using the Front Matter extension"
@@ -1503,7 +1506,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "2.7-dev"
+ "dev-main": "2.8-dev"
}
},
"autoload": {
@@ -1560,7 +1563,7 @@
"type": "tidelift"
}
],
- "time": "2024-12-29T14:10:59+00:00"
+ "time": "2025-07-20T12:47:49+00:00"
},
{
"name": "league/config",
@@ -1646,16 +1649,16 @@
},
{
"name": "league/flysystem",
- "version": "3.29.1",
+ "version": "3.30.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem.git",
- "reference": "edc1bb7c86fab0776c3287dbd19b5fa278347319"
+ "reference": "2203e3151755d874bb2943649dae1eb8533ac93e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/edc1bb7c86fab0776c3287dbd19b5fa278347319",
- "reference": "edc1bb7c86fab0776c3287dbd19b5fa278347319",
+ "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/2203e3151755d874bb2943649dae1eb8533ac93e",
+ "reference": "2203e3151755d874bb2943649dae1eb8533ac93e",
"shasum": ""
},
"require": {
@@ -1679,13 +1682,13 @@
"composer/semver": "^3.0",
"ext-fileinfo": "*",
"ext-ftp": "*",
- "ext-mongodb": "^1.3",
+ "ext-mongodb": "^1.3|^2",
"ext-zip": "*",
"friendsofphp/php-cs-fixer": "^3.5",
"google/cloud-storage": "^1.23",
"guzzlehttp/psr7": "^2.6",
"microsoft/azure-storage-blob": "^1.1",
- "mongodb/mongodb": "^1.2",
+ "mongodb/mongodb": "^1.2|^2",
"phpseclib/phpseclib": "^3.0.36",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^9.5.11|^10.0",
@@ -1723,22 +1726,22 @@
],
"support": {
"issues": "https://github.com/thephpleague/flysystem/issues",
- "source": "https://github.com/thephpleague/flysystem/tree/3.29.1"
+ "source": "https://github.com/thephpleague/flysystem/tree/3.30.0"
},
- "time": "2024-10-08T08:58:34+00:00"
+ "time": "2025-06-25T13:29:59+00:00"
},
{
"name": "league/flysystem-local",
- "version": "3.29.0",
+ "version": "3.30.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem-local.git",
- "reference": "e0e8d52ce4b2ed154148453d321e97c8e931bd27"
+ "reference": "6691915f77c7fb69adfb87dcd550052dc184ee10"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/e0e8d52ce4b2ed154148453d321e97c8e931bd27",
- "reference": "e0e8d52ce4b2ed154148453d321e97c8e931bd27",
+ "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/6691915f77c7fb69adfb87dcd550052dc184ee10",
+ "reference": "6691915f77c7fb69adfb87dcd550052dc184ee10",
"shasum": ""
},
"require": {
@@ -1772,9 +1775,9 @@
"local"
],
"support": {
- "source": "https://github.com/thephpleague/flysystem-local/tree/3.29.0"
+ "source": "https://github.com/thephpleague/flysystem-local/tree/3.30.0"
},
- "time": "2024-08-09T21:24:39+00:00"
+ "time": "2025-05-21T10:34:19+00:00"
},
{
"name": "league/mime-type-detection",
@@ -2008,16 +2011,16 @@
},
{
"name": "livewire/flux",
- "version": "v2.1.1",
+ "version": "v2.6.0",
"source": {
"type": "git",
"url": "https://github.com/livewire/flux.git",
- "reference": "f5b7169e4538039d59a750cdcc64494e2fc0729c"
+ "reference": "3cb2ea40978449da74b3814eeef75f0388124224"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/livewire/flux/zipball/f5b7169e4538039d59a750cdcc64494e2fc0729c",
- "reference": "f5b7169e4538039d59a750cdcc64494e2fc0729c",
+ "url": "https://api.github.com/repos/livewire/flux/zipball/3cb2ea40978449da74b3814eeef75f0388124224",
+ "reference": "3cb2ea40978449da74b3814eeef75f0388124224",
"shasum": ""
},
"require": {
@@ -2029,6 +2032,9 @@
"php": "^8.1",
"symfony/console": "^6.0|^7.0"
},
+ "conflict": {
+ "livewire/blaze": "<0.1.0"
+ },
"type": "library",
"extra": {
"laravel": {
@@ -2065,22 +2071,22 @@
],
"support": {
"issues": "https://github.com/livewire/flux/issues",
- "source": "https://github.com/livewire/flux/tree/v2.1.1"
+ "source": "https://github.com/livewire/flux/tree/v2.6.0"
},
- "time": "2025-03-20T21:34:31+00:00"
+ "time": "2025-10-13T23:17:18+00:00"
},
{
"name": "livewire/livewire",
- "version": "v3.6.2",
+ "version": "v3.6.4",
"source": {
"type": "git",
"url": "https://github.com/livewire/livewire.git",
- "reference": "8f8914731f5eb43b6bb145d87c8d5a9edfc89313"
+ "reference": "ef04be759da41b14d2d129e670533180a44987dc"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/livewire/livewire/zipball/8f8914731f5eb43b6bb145d87c8d5a9edfc89313",
- "reference": "8f8914731f5eb43b6bb145d87c8d5a9edfc89313",
+ "url": "https://api.github.com/repos/livewire/livewire/zipball/ef04be759da41b14d2d129e670533180a44987dc",
+ "reference": "ef04be759da41b14d2d129e670533180a44987dc",
"shasum": ""
},
"require": {
@@ -2135,7 +2141,7 @@
"description": "A front-end framework for Laravel.",
"support": {
"issues": "https://github.com/livewire/livewire/issues",
- "source": "https://github.com/livewire/livewire/tree/v3.6.2"
+ "source": "https://github.com/livewire/livewire/tree/v3.6.4"
},
"funding": [
{
@@ -2143,20 +2149,20 @@
"type": "github"
}
],
- "time": "2025-03-12T20:24:15+00:00"
+ "time": "2025-07-17T05:12:15+00:00"
},
{
"name": "livewire/volt",
- "version": "v1.7.0",
+ "version": "v1.7.2",
"source": {
"type": "git",
"url": "https://github.com/livewire/volt.git",
- "reference": "94091094aa745c8636f9c7bed1e2da2d2a3f32b3"
+ "reference": "91ba934e72bbd162442840862959ade24dbe728a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/livewire/volt/zipball/94091094aa745c8636f9c7bed1e2da2d2a3f32b3",
- "reference": "94091094aa745c8636f9c7bed1e2da2d2a3f32b3",
+ "url": "https://api.github.com/repos/livewire/volt/zipball/91ba934e72bbd162442840862959ade24dbe728a",
+ "reference": "91ba934e72bbd162442840862959ade24dbe728a",
"shasum": ""
},
"require": {
@@ -2215,7 +2221,7 @@
"issues": "https://github.com/livewire/volt/issues",
"source": "https://github.com/livewire/volt"
},
- "time": "2025-03-05T15:20:55+00:00"
+ "time": "2025-08-06T15:40:50+00:00"
},
{
"name": "monolog/monolog",
@@ -2322,16 +2328,16 @@
},
{
"name": "nesbot/carbon",
- "version": "3.8.6",
+ "version": "3.10.3",
"source": {
"type": "git",
"url": "https://github.com/CarbonPHP/carbon.git",
- "reference": "ff2f20cf83bd4d503720632ce8a426dc747bf7fd"
+ "reference": "8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/ff2f20cf83bd4d503720632ce8a426dc747bf7fd",
- "reference": "ff2f20cf83bd4d503720632ce8a426dc747bf7fd",
+ "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f",
+ "reference": "8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f",
"shasum": ""
},
"require": {
@@ -2339,9 +2345,9 @@
"ext-json": "*",
"php": "^8.1",
"psr/clock": "^1.0",
- "symfony/clock": "^6.3 || ^7.0",
+ "symfony/clock": "^6.3.12 || ^7.0",
"symfony/polyfill-mbstring": "^1.0",
- "symfony/translation": "^4.4.18 || ^5.2.1|| ^6.0 || ^7.0"
+ "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0"
},
"provide": {
"psr/clock-implementation": "1.0"
@@ -2349,14 +2355,13 @@
"require-dev": {
"doctrine/dbal": "^3.6.3 || ^4.0",
"doctrine/orm": "^2.15.2 || ^3.0",
- "friendsofphp/php-cs-fixer": "^3.57.2",
+ "friendsofphp/php-cs-fixer": "^v3.87.1",
"kylekatarnls/multi-tester": "^2.5.3",
- "ondrejmirtes/better-reflection": "^6.25.0.4",
"phpmd/phpmd": "^2.15.0",
- "phpstan/extension-installer": "^1.3.1",
- "phpstan/phpstan": "^1.11.2",
- "phpunit/phpunit": "^10.5.20",
- "squizlabs/php_codesniffer": "^3.9.0"
+ "phpstan/extension-installer": "^1.4.3",
+ "phpstan/phpstan": "^2.1.22",
+ "phpunit/phpunit": "^10.5.53",
+ "squizlabs/php_codesniffer": "^3.13.4"
},
"bin": [
"bin/carbon"
@@ -2424,7 +2429,7 @@
"type": "tidelift"
}
],
- "time": "2025-02-20T17:33:38+00:00"
+ "time": "2025-09-06T13:39:36+00:00"
},
{
"name": "nette/schema",
@@ -2490,29 +2495,29 @@
},
{
"name": "nette/utils",
- "version": "v4.0.6",
+ "version": "v4.0.8",
"source": {
"type": "git",
"url": "https://github.com/nette/utils.git",
- "reference": "ce708655043c7050eb050df361c5e313cf708309"
+ "reference": "c930ca4e3cf4f17dcfb03037703679d2396d2ede"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nette/utils/zipball/ce708655043c7050eb050df361c5e313cf708309",
- "reference": "ce708655043c7050eb050df361c5e313cf708309",
+ "url": "https://api.github.com/repos/nette/utils/zipball/c930ca4e3cf4f17dcfb03037703679d2396d2ede",
+ "reference": "c930ca4e3cf4f17dcfb03037703679d2396d2ede",
"shasum": ""
},
"require": {
- "php": "8.0 - 8.4"
+ "php": "8.0 - 8.5"
},
"conflict": {
"nette/finder": "<3",
"nette/schema": "<1.2.2"
},
"require-dev": {
- "jetbrains/phpstorm-attributes": "dev-master",
+ "jetbrains/phpstorm-attributes": "^1.2",
"nette/tester": "^2.5",
- "phpstan/phpstan": "^1.0",
+ "phpstan/phpstan-nette": "^2.0@stable",
"tracy/tracy": "^2.9"
},
"suggest": {
@@ -2530,6 +2535,9 @@
}
},
"autoload": {
+ "psr-4": {
+ "Nette\\": "src"
+ },
"classmap": [
"src/"
]
@@ -2570,22 +2578,22 @@
],
"support": {
"issues": "https://github.com/nette/utils/issues",
- "source": "https://github.com/nette/utils/tree/v4.0.6"
+ "source": "https://github.com/nette/utils/tree/v4.0.8"
},
- "time": "2025-03-30T21:06:30+00:00"
+ "time": "2025-08-06T21:43:34+00:00"
},
{
"name": "nikic/php-parser",
- "version": "v5.4.0",
+ "version": "v5.6.1",
"source": {
"type": "git",
"url": "https://github.com/nikic/PHP-Parser.git",
- "reference": "447a020a1f875a434d62f2a401f53b82a396e494"
+ "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494",
- "reference": "447a020a1f875a434d62f2a401f53b82a396e494",
+ "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2",
+ "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2",
"shasum": ""
},
"require": {
@@ -2604,7 +2612,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "5.0-dev"
+ "dev-master": "5.x-dev"
}
},
"autoload": {
@@ -2628,37 +2636,37 @@
],
"support": {
"issues": "https://github.com/nikic/PHP-Parser/issues",
- "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0"
+ "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.1"
},
- "time": "2024-12-30T11:07:19+00:00"
+ "time": "2025-08-13T20:13:15+00:00"
},
{
"name": "nunomaduro/termwind",
- "version": "v2.3.0",
+ "version": "v2.3.1",
"source": {
"type": "git",
"url": "https://github.com/nunomaduro/termwind.git",
- "reference": "52915afe6a1044e8b9cee1bcff836fb63acf9cda"
+ "reference": "dfa08f390e509967a15c22493dc0bac5733d9123"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/52915afe6a1044e8b9cee1bcff836fb63acf9cda",
- "reference": "52915afe6a1044e8b9cee1bcff836fb63acf9cda",
+ "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/dfa08f390e509967a15c22493dc0bac5733d9123",
+ "reference": "dfa08f390e509967a15c22493dc0bac5733d9123",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": "^8.2",
- "symfony/console": "^7.1.8"
+ "symfony/console": "^7.2.6"
},
"require-dev": {
- "illuminate/console": "^11.33.2",
- "laravel/pint": "^1.18.2",
+ "illuminate/console": "^11.44.7",
+ "laravel/pint": "^1.22.0",
"mockery/mockery": "^1.6.12",
- "pestphp/pest": "^2.36.0",
- "phpstan/phpstan": "^1.12.11",
- "phpstan/phpstan-strict-rules": "^1.6.1",
- "symfony/var-dumper": "^7.1.8",
+ "pestphp/pest": "^2.36.0 || ^3.8.2",
+ "phpstan/phpstan": "^1.12.25",
+ "phpstan/phpstan-strict-rules": "^1.6.2",
+ "symfony/var-dumper": "^7.2.6",
"thecodingmachine/phpstan-strict-rules": "^1.0.0"
},
"type": "library",
@@ -2701,7 +2709,7 @@
],
"support": {
"issues": "https://github.com/nunomaduro/termwind/issues",
- "source": "https://github.com/nunomaduro/termwind/tree/v2.3.0"
+ "source": "https://github.com/nunomaduro/termwind/tree/v2.3.1"
},
"funding": [
{
@@ -2717,20 +2725,20 @@
"type": "github"
}
],
- "time": "2024-11-21T10:39:51+00:00"
+ "time": "2025-05-08T08:14:37+00:00"
},
{
"name": "phpoption/phpoption",
- "version": "1.9.3",
+ "version": "1.9.4",
"source": {
"type": "git",
"url": "https://github.com/schmittjoh/php-option.git",
- "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54"
+ "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/e3fac8b24f56113f7cb96af14958c0dd16330f54",
- "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54",
+ "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d",
+ "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d",
"shasum": ""
},
"require": {
@@ -2738,7 +2746,7 @@
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
- "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28"
+ "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34"
},
"type": "library",
"extra": {
@@ -2780,7 +2788,7 @@
],
"support": {
"issues": "https://github.com/schmittjoh/php-option/issues",
- "source": "https://github.com/schmittjoh/php-option/tree/1.9.3"
+ "source": "https://github.com/schmittjoh/php-option/tree/1.9.4"
},
"funding": [
{
@@ -2792,7 +2800,7 @@
"type": "tidelift"
}
],
- "time": "2024-07-20T21:41:07+00:00"
+ "time": "2025-08-21T11:53:16+00:00"
},
{
"name": "psr/clock",
@@ -3208,16 +3216,16 @@
},
{
"name": "psy/psysh",
- "version": "v0.12.8",
+ "version": "v0.12.12",
"source": {
"type": "git",
"url": "https://github.com/bobthecow/psysh.git",
- "reference": "85057ceedee50c49d4f6ecaff73ee96adb3b3625"
+ "reference": "cd23863404a40ccfaf733e3af4db2b459837f7e7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/bobthecow/psysh/zipball/85057ceedee50c49d4f6ecaff73ee96adb3b3625",
- "reference": "85057ceedee50c49d4f6ecaff73ee96adb3b3625",
+ "url": "https://api.github.com/repos/bobthecow/psysh/zipball/cd23863404a40ccfaf733e3af4db2b459837f7e7",
+ "reference": "cd23863404a40ccfaf733e3af4db2b459837f7e7",
"shasum": ""
},
"require": {
@@ -3267,12 +3275,11 @@
"authors": [
{
"name": "Justin Hileman",
- "email": "justin@justinhileman.info",
- "homepage": "http://justinhileman.com"
+ "email": "justin@justinhileman.info"
}
],
"description": "An interactive shell for modern PHP.",
- "homepage": "http://psysh.org",
+ "homepage": "https://psysh.org",
"keywords": [
"REPL",
"console",
@@ -3281,9 +3288,9 @@
],
"support": {
"issues": "https://github.com/bobthecow/psysh/issues",
- "source": "https://github.com/bobthecow/psysh/tree/v0.12.8"
+ "source": "https://github.com/bobthecow/psysh/tree/v0.12.12"
},
- "time": "2025-03-16T03:05:19+00:00"
+ "time": "2025-09-20T13:46:31+00:00"
},
{
"name": "ralouphie/getallheaders",
@@ -3407,21 +3414,20 @@
},
{
"name": "ramsey/uuid",
- "version": "4.7.6",
+ "version": "4.9.1",
"source": {
"type": "git",
"url": "https://github.com/ramsey/uuid.git",
- "reference": "91039bc1faa45ba123c4328958e620d382ec7088"
+ "reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/ramsey/uuid/zipball/91039bc1faa45ba123c4328958e620d382ec7088",
- "reference": "91039bc1faa45ba123c4328958e620d382ec7088",
+ "url": "https://api.github.com/repos/ramsey/uuid/zipball/81f941f6f729b1e3ceea61d9d014f8b6c6800440",
+ "reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440",
"shasum": ""
},
"require": {
- "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12",
- "ext-json": "*",
+ "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14",
"php": "^8.0",
"ramsey/collection": "^1.2 || ^2.0"
},
@@ -3429,26 +3435,23 @@
"rhumsaa/uuid": "self.version"
},
"require-dev": {
- "captainhook/captainhook": "^5.10",
+ "captainhook/captainhook": "^5.25",
"captainhook/plugin-composer": "^5.3",
- "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0",
- "doctrine/annotations": "^1.8",
- "ergebnis/composer-normalize": "^2.15",
- "mockery/mockery": "^1.3",
+ "dealerdirect/phpcodesniffer-composer-installer": "^1.0",
+ "ergebnis/composer-normalize": "^2.47",
+ "mockery/mockery": "^1.6",
"paragonie/random-lib": "^2",
- "php-mock/php-mock": "^2.2",
- "php-mock/php-mock-mockery": "^1.3",
- "php-parallel-lint/php-parallel-lint": "^1.1",
- "phpbench/phpbench": "^1.0",
- "phpstan/extension-installer": "^1.1",
- "phpstan/phpstan": "^1.8",
- "phpstan/phpstan-mockery": "^1.1",
- "phpstan/phpstan-phpunit": "^1.1",
- "phpunit/phpunit": "^8.5 || ^9",
- "ramsey/composer-repl": "^1.4",
- "slevomat/coding-standard": "^8.4",
- "squizlabs/php_codesniffer": "^3.5",
- "vimeo/psalm": "^4.9"
+ "php-mock/php-mock": "^2.6",
+ "php-mock/php-mock-mockery": "^1.5",
+ "php-parallel-lint/php-parallel-lint": "^1.4.0",
+ "phpbench/phpbench": "^1.2.14",
+ "phpstan/extension-installer": "^1.4",
+ "phpstan/phpstan": "^2.1",
+ "phpstan/phpstan-mockery": "^2.0",
+ "phpstan/phpstan-phpunit": "^2.0",
+ "phpunit/phpunit": "^9.6",
+ "slevomat/coding-standard": "^8.18",
+ "squizlabs/php_codesniffer": "^3.13"
},
"suggest": {
"ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.",
@@ -3483,23 +3486,13 @@
],
"support": {
"issues": "https://github.com/ramsey/uuid/issues",
- "source": "https://github.com/ramsey/uuid/tree/4.7.6"
+ "source": "https://github.com/ramsey/uuid/tree/4.9.1"
},
- "funding": [
- {
- "url": "https://github.com/ramsey",
- "type": "github"
- },
- {
- "url": "https://tidelift.com/funding/github/packagist/ramsey/uuid",
- "type": "tidelift"
- }
- ],
- "time": "2024-04-27T21:32:50+00:00"
+ "time": "2025-09-04T20:59:21+00:00"
},
{
"name": "symfony/clock",
- "version": "v7.2.0",
+ "version": "v7.3.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/clock.git",
@@ -3553,7 +3546,7 @@
"time"
],
"support": {
- "source": "https://github.com/symfony/clock/tree/v7.2.0"
+ "source": "https://github.com/symfony/clock/tree/v7.3.0"
},
"funding": [
{
@@ -3573,23 +3566,24 @@
},
{
"name": "symfony/console",
- "version": "v7.2.5",
+ "version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
- "reference": "e51498ea18570c062e7df29d05a7003585b19b88"
+ "reference": "2b9c5fafbac0399a20a2e82429e2bd735dcfb7db"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/console/zipball/e51498ea18570c062e7df29d05a7003585b19b88",
- "reference": "e51498ea18570c062e7df29d05a7003585b19b88",
+ "url": "https://api.github.com/repos/symfony/console/zipball/2b9c5fafbac0399a20a2e82429e2bd735dcfb7db",
+ "reference": "2b9c5fafbac0399a20a2e82429e2bd735dcfb7db",
"shasum": ""
},
"require": {
"php": ">=8.2",
+ "symfony/deprecation-contracts": "^2.5|^3",
"symfony/polyfill-mbstring": "~1.0",
"symfony/service-contracts": "^2.5|^3",
- "symfony/string": "^6.4|^7.0"
+ "symfony/string": "^7.2"
},
"conflict": {
"symfony/dependency-injection": "<6.4",
@@ -3646,7 +3640,7 @@
"terminal"
],
"support": {
- "source": "https://github.com/symfony/console/tree/v7.2.5"
+ "source": "https://github.com/symfony/console/tree/v7.3.4"
},
"funding": [
{
@@ -3657,16 +3651,20 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-03-12T08:11:12+00:00"
+ "time": "2025-09-22T15:31:00+00:00"
},
{
"name": "symfony/css-selector",
- "version": "v7.2.0",
+ "version": "v7.3.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/css-selector.git",
@@ -3711,7 +3709,7 @@
"description": "Converts CSS selectors to XPath expressions",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/css-selector/tree/v7.2.0"
+ "source": "https://github.com/symfony/css-selector/tree/v7.3.0"
},
"funding": [
{
@@ -3731,16 +3729,16 @@
},
{
"name": "symfony/deprecation-contracts",
- "version": "v3.5.1",
+ "version": "v3.6.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
- "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6"
+ "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6",
- "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6",
+ "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62",
+ "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62",
"shasum": ""
},
"require": {
@@ -3753,7 +3751,7 @@
"name": "symfony/contracts"
},
"branch-alias": {
- "dev-main": "3.5-dev"
+ "dev-main": "3.6-dev"
}
},
"autoload": {
@@ -3778,7 +3776,7 @@
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1"
+ "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0"
},
"funding": [
{
@@ -3794,20 +3792,20 @@
"type": "tidelift"
}
],
- "time": "2024-09-25T14:20:29+00:00"
+ "time": "2024-09-25T14:21:43+00:00"
},
{
"name": "symfony/error-handler",
- "version": "v7.2.5",
+ "version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/error-handler.git",
- "reference": "102be5e6a8e4f4f3eb3149bcbfa33a80d1ee374b"
+ "reference": "99f81bc944ab8e5dae4f21b4ca9972698bbad0e4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/error-handler/zipball/102be5e6a8e4f4f3eb3149bcbfa33a80d1ee374b",
- "reference": "102be5e6a8e4f4f3eb3149bcbfa33a80d1ee374b",
+ "url": "https://api.github.com/repos/symfony/error-handler/zipball/99f81bc944ab8e5dae4f21b4ca9972698bbad0e4",
+ "reference": "99f81bc944ab8e5dae4f21b4ca9972698bbad0e4",
"shasum": ""
},
"require": {
@@ -3820,9 +3818,11 @@
"symfony/http-kernel": "<6.4"
},
"require-dev": {
+ "symfony/console": "^6.4|^7.0",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/http-kernel": "^6.4|^7.0",
- "symfony/serializer": "^6.4|^7.0"
+ "symfony/serializer": "^6.4|^7.0",
+ "symfony/webpack-encore-bundle": "^1.0|^2.0"
},
"bin": [
"Resources/bin/patch-type-declarations"
@@ -3853,7 +3853,7 @@
"description": "Provides tools to manage errors and ease debugging PHP code",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/error-handler/tree/v7.2.5"
+ "source": "https://github.com/symfony/error-handler/tree/v7.3.4"
},
"funding": [
{
@@ -3864,25 +3864,29 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-03-03T07:12:39+00:00"
+ "time": "2025-09-11T10:12:26+00:00"
},
{
"name": "symfony/event-dispatcher",
- "version": "v7.2.0",
+ "version": "v7.3.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher.git",
- "reference": "910c5db85a5356d0fea57680defec4e99eb9c8c1"
+ "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/910c5db85a5356d0fea57680defec4e99eb9c8c1",
- "reference": "910c5db85a5356d0fea57680defec4e99eb9c8c1",
+ "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b7dc69e71de420ac04bc9ab830cf3ffebba48191",
+ "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191",
"shasum": ""
},
"require": {
@@ -3933,7 +3937,7 @@
"description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/event-dispatcher/tree/v7.2.0"
+ "source": "https://github.com/symfony/event-dispatcher/tree/v7.3.3"
},
"funding": [
{
@@ -3944,25 +3948,29 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2024-09-25T14:21:43+00:00"
+ "time": "2025-08-13T11:49:31+00:00"
},
{
"name": "symfony/event-dispatcher-contracts",
- "version": "v3.5.1",
+ "version": "v3.6.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher-contracts.git",
- "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f"
+ "reference": "59eb412e93815df44f05f342958efa9f46b1e586"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/7642f5e970b672283b7823222ae8ef8bbc160b9f",
- "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f",
+ "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586",
+ "reference": "59eb412e93815df44f05f342958efa9f46b1e586",
"shasum": ""
},
"require": {
@@ -3976,7 +3984,7 @@
"name": "symfony/contracts"
},
"branch-alias": {
- "dev-main": "3.5-dev"
+ "dev-main": "3.6-dev"
}
},
"autoload": {
@@ -4009,7 +4017,7 @@
"standards"
],
"support": {
- "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.1"
+ "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0"
},
"funding": [
{
@@ -4025,20 +4033,20 @@
"type": "tidelift"
}
],
- "time": "2024-09-25T14:20:29+00:00"
+ "time": "2024-09-25T14:21:43+00:00"
},
{
"name": "symfony/finder",
- "version": "v7.2.2",
+ "version": "v7.3.2",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
- "reference": "87a71856f2f56e4100373e92529eed3171695cfb"
+ "reference": "2a6614966ba1074fa93dae0bc804227422df4dfe"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/finder/zipball/87a71856f2f56e4100373e92529eed3171695cfb",
- "reference": "87a71856f2f56e4100373e92529eed3171695cfb",
+ "url": "https://api.github.com/repos/symfony/finder/zipball/2a6614966ba1074fa93dae0bc804227422df4dfe",
+ "reference": "2a6614966ba1074fa93dae0bc804227422df4dfe",
"shasum": ""
},
"require": {
@@ -4073,7 +4081,7 @@
"description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/finder/tree/v7.2.2"
+ "source": "https://github.com/symfony/finder/tree/v7.3.2"
},
"funding": [
{
@@ -4084,25 +4092,29 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2024-12-30T19:00:17+00:00"
+ "time": "2025-07-15T13:41:35+00:00"
},
{
"name": "symfony/http-foundation",
- "version": "v7.2.5",
+ "version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-foundation.git",
- "reference": "371272aeb6286f8135e028ca535f8e4d6f114126"
+ "reference": "c061c7c18918b1b64268771aad04b40be41dd2e6"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/http-foundation/zipball/371272aeb6286f8135e028ca535f8e4d6f114126",
- "reference": "371272aeb6286f8135e028ca535f8e4d6f114126",
+ "url": "https://api.github.com/repos/symfony/http-foundation/zipball/c061c7c18918b1b64268771aad04b40be41dd2e6",
+ "reference": "c061c7c18918b1b64268771aad04b40be41dd2e6",
"shasum": ""
},
"require": {
@@ -4119,6 +4131,7 @@
"doctrine/dbal": "^3.6|^4",
"predis/predis": "^1.1|^2.0",
"symfony/cache": "^6.4.12|^7.1.5",
+ "symfony/clock": "^6.4|^7.0",
"symfony/dependency-injection": "^6.4|^7.0",
"symfony/expression-language": "^6.4|^7.0",
"symfony/http-kernel": "^6.4|^7.0",
@@ -4151,7 +4164,7 @@
"description": "Defines an object-oriented layer for the HTTP specification",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/http-foundation/tree/v7.2.5"
+ "source": "https://github.com/symfony/http-foundation/tree/v7.3.4"
},
"funding": [
{
@@ -4162,25 +4175,29 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-03-25T15:54:33+00:00"
+ "time": "2025-09-16T08:38:17+00:00"
},
{
"name": "symfony/http-kernel",
- "version": "v7.2.5",
+ "version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-kernel.git",
- "reference": "b1fe91bc1fa454a806d3f98db4ba826eb9941a54"
+ "reference": "b796dffea7821f035047235e076b60ca2446e3cf"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/http-kernel/zipball/b1fe91bc1fa454a806d3f98db4ba826eb9941a54",
- "reference": "b1fe91bc1fa454a806d3f98db4ba826eb9941a54",
+ "url": "https://api.github.com/repos/symfony/http-kernel/zipball/b796dffea7821f035047235e076b60ca2446e3cf",
+ "reference": "b796dffea7821f035047235e076b60ca2446e3cf",
"shasum": ""
},
"require": {
@@ -4188,8 +4205,8 @@
"psr/log": "^1|^2|^3",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/error-handler": "^6.4|^7.0",
- "symfony/event-dispatcher": "^6.4|^7.0",
- "symfony/http-foundation": "^6.4|^7.0",
+ "symfony/event-dispatcher": "^7.3",
+ "symfony/http-foundation": "^7.3",
"symfony/polyfill-ctype": "^1.8"
},
"conflict": {
@@ -4265,7 +4282,7 @@
"description": "Provides a structured process for converting a Request into a Response",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/http-kernel/tree/v7.2.5"
+ "source": "https://github.com/symfony/http-kernel/tree/v7.3.4"
},
"funding": [
{
@@ -4276,25 +4293,29 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-03-28T13:32:50+00:00"
+ "time": "2025-09-27T12:32:17+00:00"
},
{
"name": "symfony/mailer",
- "version": "v7.2.3",
+ "version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/mailer.git",
- "reference": "f3871b182c44997cf039f3b462af4a48fb85f9d3"
+ "reference": "ab97ef2f7acf0216955f5845484235113047a31d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/mailer/zipball/f3871b182c44997cf039f3b462af4a48fb85f9d3",
- "reference": "f3871b182c44997cf039f3b462af4a48fb85f9d3",
+ "url": "https://api.github.com/repos/symfony/mailer/zipball/ab97ef2f7acf0216955f5845484235113047a31d",
+ "reference": "ab97ef2f7acf0216955f5845484235113047a31d",
"shasum": ""
},
"require": {
@@ -4345,7 +4366,7 @@
"description": "Helps sending emails",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/mailer/tree/v7.2.3"
+ "source": "https://github.com/symfony/mailer/tree/v7.3.4"
},
"funding": [
{
@@ -4356,25 +4377,29 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-01-27T11:08:17+00:00"
+ "time": "2025-09-17T05:51:54+00:00"
},
{
"name": "symfony/mime",
- "version": "v7.2.4",
+ "version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/mime.git",
- "reference": "87ca22046b78c3feaff04b337f33b38510fd686b"
+ "reference": "b1b828f69cbaf887fa835a091869e55df91d0e35"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/mime/zipball/87ca22046b78c3feaff04b337f33b38510fd686b",
- "reference": "87ca22046b78c3feaff04b337f33b38510fd686b",
+ "url": "https://api.github.com/repos/symfony/mime/zipball/b1b828f69cbaf887fa835a091869e55df91d0e35",
+ "reference": "b1b828f69cbaf887fa835a091869e55df91d0e35",
"shasum": ""
},
"require": {
@@ -4429,7 +4454,7 @@
"mime-type"
],
"support": {
- "source": "https://github.com/symfony/mime/tree/v7.2.4"
+ "source": "https://github.com/symfony/mime/tree/v7.3.4"
},
"funding": [
{
@@ -4440,16 +4465,20 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-02-19T08:51:20+00:00"
+ "time": "2025-09-16T08:38:17+00:00"
},
{
"name": "symfony/polyfill-ctype",
- "version": "v1.31.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
@@ -4508,7 +4537,7 @@
"portable"
],
"support": {
- "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0"
+ "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0"
},
"funding": [
{
@@ -4519,6 +4548,10 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
@@ -4528,16 +4561,16 @@
},
{
"name": "symfony/polyfill-intl-grapheme",
- "version": "v1.31.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
- "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe"
+ "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe",
- "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70",
+ "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70",
"shasum": ""
},
"require": {
@@ -4586,7 +4619,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0"
+ "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0"
},
"funding": [
{
@@ -4597,25 +4630,29 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2024-09-09T11:45:10+00:00"
+ "time": "2025-06-27T09:58:17+00:00"
},
{
"name": "symfony/polyfill-intl-idn",
- "version": "v1.31.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-idn.git",
- "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773"
+ "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/c36586dcf89a12315939e00ec9b4474adcb1d773",
- "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3",
+ "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3",
"shasum": ""
},
"require": {
@@ -4669,7 +4706,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.31.0"
+ "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0"
},
"funding": [
{
@@ -4680,16 +4717,20 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2024-09-09T11:45:10+00:00"
+ "time": "2024-09-10T14:38:51+00:00"
},
{
"name": "symfony/polyfill-intl-normalizer",
- "version": "v1.31.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
@@ -4750,7 +4791,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0"
+ "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0"
},
"funding": [
{
@@ -4761,6 +4802,10 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
@@ -4770,19 +4815,20 @@
},
{
"name": "symfony/polyfill-mbstring",
- "version": "v1.31.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
- "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341"
+ "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341",
- "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493",
+ "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493",
"shasum": ""
},
"require": {
+ "ext-iconv": "*",
"php": ">=7.2"
},
"provide": {
@@ -4830,7 +4876,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0"
+ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0"
},
"funding": [
{
@@ -4841,25 +4887,29 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2024-09-09T11:45:10+00:00"
+ "time": "2024-12-23T08:48:59+00:00"
},
{
"name": "symfony/polyfill-php80",
- "version": "v1.31.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
- "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8"
+ "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8",
- "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8",
+ "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608",
+ "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608",
"shasum": ""
},
"require": {
@@ -4910,7 +4960,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0"
+ "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0"
},
"funding": [
{
@@ -4921,25 +4971,29 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2024-09-09T11:45:10+00:00"
+ "time": "2025-01-02T08:10:11+00:00"
},
{
"name": "symfony/polyfill-php83",
- "version": "v1.31.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php83.git",
- "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491"
+ "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491",
- "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491",
+ "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5",
+ "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5",
"shasum": ""
},
"require": {
@@ -4986,7 +5040,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-php83/tree/v1.31.0"
+ "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0"
},
"funding": [
{
@@ -4997,16 +5051,180 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2024-09-09T11:45:10+00:00"
+ "time": "2025-07-08T02:45:35+00:00"
+ },
+ {
+ "name": "symfony/polyfill-php84",
+ "version": "v1.33.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php84.git",
+ "reference": "d8ced4d875142b6a7426000426b8abc631d6b191"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191",
+ "reference": "d8ced4d875142b6a7426000426b8abc631d6b191",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Php84\\": ""
+ },
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-06-24T13:30:11+00:00"
+ },
+ {
+ "name": "symfony/polyfill-php85",
+ "version": "v1.33.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php85.git",
+ "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91",
+ "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Php85\\": ""
+ },
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php85/tree/v1.33.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-06-23T16:12:55+00:00"
},
{
"name": "symfony/polyfill-uuid",
- "version": "v1.31.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-uuid.git",
@@ -5065,7 +5283,7 @@
"uuid"
],
"support": {
- "source": "https://github.com/symfony/polyfill-uuid/tree/v1.31.0"
+ "source": "https://github.com/symfony/polyfill-uuid/tree/v1.33.0"
},
"funding": [
{
@@ -5076,6 +5294,10 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
@@ -5085,16 +5307,16 @@
},
{
"name": "symfony/process",
- "version": "v7.2.5",
+ "version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
- "reference": "87b7c93e57df9d8e39a093d32587702380ff045d"
+ "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/process/zipball/87b7c93e57df9d8e39a093d32587702380ff045d",
- "reference": "87b7c93e57df9d8e39a093d32587702380ff045d",
+ "url": "https://api.github.com/repos/symfony/process/zipball/f24f8f316367b30810810d4eb30c543d7003ff3b",
+ "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b",
"shasum": ""
},
"require": {
@@ -5126,7 +5348,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/process/tree/v7.2.5"
+ "source": "https://github.com/symfony/process/tree/v7.3.4"
},
"funding": [
{
@@ -5137,25 +5359,29 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-03-13T12:21:46+00:00"
+ "time": "2025-09-11T10:12:26+00:00"
},
{
"name": "symfony/routing",
- "version": "v7.2.3",
+ "version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/routing.git",
- "reference": "ee9a67edc6baa33e5fae662f94f91fd262930996"
+ "reference": "8dc648e159e9bac02b703b9fbd937f19ba13d07c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/routing/zipball/ee9a67edc6baa33e5fae662f94f91fd262930996",
- "reference": "ee9a67edc6baa33e5fae662f94f91fd262930996",
+ "url": "https://api.github.com/repos/symfony/routing/zipball/8dc648e159e9bac02b703b9fbd937f19ba13d07c",
+ "reference": "8dc648e159e9bac02b703b9fbd937f19ba13d07c",
"shasum": ""
},
"require": {
@@ -5207,7 +5433,7 @@
"url"
],
"support": {
- "source": "https://github.com/symfony/routing/tree/v7.2.3"
+ "source": "https://github.com/symfony/routing/tree/v7.3.4"
},
"funding": [
{
@@ -5218,25 +5444,29 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-01-17T10:56:55+00:00"
+ "time": "2025-09-11T10:12:26+00:00"
},
{
"name": "symfony/service-contracts",
- "version": "v3.5.1",
+ "version": "v3.6.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/service-contracts.git",
- "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0"
+ "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0",
- "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0",
+ "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4",
+ "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4",
"shasum": ""
},
"require": {
@@ -5254,7 +5484,7 @@
"name": "symfony/contracts"
},
"branch-alias": {
- "dev-main": "3.5-dev"
+ "dev-main": "3.6-dev"
}
},
"autoload": {
@@ -5290,7 +5520,7 @@
"standards"
],
"support": {
- "source": "https://github.com/symfony/service-contracts/tree/v3.5.1"
+ "source": "https://github.com/symfony/service-contracts/tree/v3.6.0"
},
"funding": [
{
@@ -5306,20 +5536,20 @@
"type": "tidelift"
}
],
- "time": "2024-09-25T14:20:29+00:00"
+ "time": "2025-04-25T09:37:31+00:00"
},
{
"name": "symfony/string",
- "version": "v7.2.0",
+ "version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
- "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82"
+ "reference": "f96476035142921000338bad71e5247fbc138872"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/string/zipball/446e0d146f991dde3e73f45f2c97a9faad773c82",
- "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82",
+ "url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872",
+ "reference": "f96476035142921000338bad71e5247fbc138872",
"shasum": ""
},
"require": {
@@ -5334,7 +5564,6 @@
},
"require-dev": {
"symfony/emoji": "^7.1",
- "symfony/error-handler": "^6.4|^7.0",
"symfony/http-client": "^6.4|^7.0",
"symfony/intl": "^6.4|^7.0",
"symfony/translation-contracts": "^2.5|^3.0",
@@ -5377,7 +5606,7 @@
"utf8"
],
"support": {
- "source": "https://github.com/symfony/string/tree/v7.2.0"
+ "source": "https://github.com/symfony/string/tree/v7.3.4"
},
"funding": [
{
@@ -5388,25 +5617,29 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2024-11-13T13:31:26+00:00"
+ "time": "2025-09-11T14:36:48+00:00"
},
{
"name": "symfony/translation",
- "version": "v7.2.4",
+ "version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation.git",
- "reference": "283856e6981286cc0d800b53bd5703e8e363f05a"
+ "reference": "ec25870502d0c7072d086e8ffba1420c85965174"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/translation/zipball/283856e6981286cc0d800b53bd5703e8e363f05a",
- "reference": "283856e6981286cc0d800b53bd5703e8e363f05a",
+ "url": "https://api.github.com/repos/symfony/translation/zipball/ec25870502d0c7072d086e8ffba1420c85965174",
+ "reference": "ec25870502d0c7072d086e8ffba1420c85965174",
"shasum": ""
},
"require": {
@@ -5416,6 +5649,7 @@
"symfony/translation-contracts": "^2.5|^3.0"
},
"conflict": {
+ "nikic/php-parser": "<5.0",
"symfony/config": "<6.4",
"symfony/console": "<6.4",
"symfony/dependency-injection": "<6.4",
@@ -5429,7 +5663,7 @@
"symfony/translation-implementation": "2.3|3.0"
},
"require-dev": {
- "nikic/php-parser": "^4.18|^5.0",
+ "nikic/php-parser": "^5.0",
"psr/log": "^1|^2|^3",
"symfony/config": "^6.4|^7.0",
"symfony/console": "^6.4|^7.0",
@@ -5472,7 +5706,7 @@
"description": "Provides tools to internationalize your application",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/translation/tree/v7.2.4"
+ "source": "https://github.com/symfony/translation/tree/v7.3.4"
},
"funding": [
{
@@ -5483,25 +5717,29 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-02-13T10:27:23+00:00"
+ "time": "2025-09-07T11:39:36+00:00"
},
{
"name": "symfony/translation-contracts",
- "version": "v3.5.1",
+ "version": "v3.6.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation-contracts.git",
- "reference": "4667ff3bd513750603a09c8dedbea942487fb07c"
+ "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/4667ff3bd513750603a09c8dedbea942487fb07c",
- "reference": "4667ff3bd513750603a09c8dedbea942487fb07c",
+ "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/df210c7a2573f1913b2d17cc95f90f53a73d8f7d",
+ "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d",
"shasum": ""
},
"require": {
@@ -5514,7 +5752,7 @@
"name": "symfony/contracts"
},
"branch-alias": {
- "dev-main": "3.5-dev"
+ "dev-main": "3.6-dev"
}
},
"autoload": {
@@ -5550,7 +5788,7 @@
"standards"
],
"support": {
- "source": "https://github.com/symfony/translation-contracts/tree/v3.5.1"
+ "source": "https://github.com/symfony/translation-contracts/tree/v3.6.0"
},
"funding": [
{
@@ -5566,20 +5804,20 @@
"type": "tidelift"
}
],
- "time": "2024-09-25T14:20:29+00:00"
+ "time": "2024-09-27T08:32:26+00:00"
},
{
"name": "symfony/uid",
- "version": "v7.2.0",
+ "version": "v7.3.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/uid.git",
- "reference": "2d294d0c48df244c71c105a169d0190bfb080426"
+ "reference": "a69f69f3159b852651a6bf45a9fdd149520525bb"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/uid/zipball/2d294d0c48df244c71c105a169d0190bfb080426",
- "reference": "2d294d0c48df244c71c105a169d0190bfb080426",
+ "url": "https://api.github.com/repos/symfony/uid/zipball/a69f69f3159b852651a6bf45a9fdd149520525bb",
+ "reference": "a69f69f3159b852651a6bf45a9fdd149520525bb",
"shasum": ""
},
"require": {
@@ -5624,7 +5862,7 @@
"uuid"
],
"support": {
- "source": "https://github.com/symfony/uid/tree/v7.2.0"
+ "source": "https://github.com/symfony/uid/tree/v7.3.1"
},
"funding": [
{
@@ -5640,31 +5878,31 @@
"type": "tidelift"
}
],
- "time": "2024-09-25T14:21:43+00:00"
+ "time": "2025-06-27T19:55:54+00:00"
},
{
"name": "symfony/var-dumper",
- "version": "v7.2.3",
+ "version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-dumper.git",
- "reference": "82b478c69745d8878eb60f9a049a4d584996f73a"
+ "reference": "b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/var-dumper/zipball/82b478c69745d8878eb60f9a049a4d584996f73a",
- "reference": "82b478c69745d8878eb60f9a049a4d584996f73a",
+ "url": "https://api.github.com/repos/symfony/var-dumper/zipball/b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb",
+ "reference": "b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb",
"shasum": ""
},
"require": {
"php": ">=8.2",
+ "symfony/deprecation-contracts": "^2.5|^3",
"symfony/polyfill-mbstring": "~1.0"
},
"conflict": {
"symfony/console": "<6.4"
},
"require-dev": {
- "ext-iconv": "*",
"symfony/console": "^6.4|^7.0",
"symfony/http-kernel": "^6.4|^7.0",
"symfony/process": "^6.4|^7.0",
@@ -5707,7 +5945,7 @@
"dump"
],
"support": {
- "source": "https://github.com/symfony/var-dumper/tree/v7.2.3"
+ "source": "https://github.com/symfony/var-dumper/tree/v7.3.4"
},
"funding": [
{
@@ -5718,12 +5956,16 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-01-17T11:39:41+00:00"
+ "time": "2025-09-11T10:12:26+00:00"
},
{
"name": "tijsverkoyen/css-to-inline-styles",
@@ -5782,16 +6024,16 @@
},
{
"name": "vlucas/phpdotenv",
- "version": "v5.6.1",
+ "version": "v5.6.2",
"source": {
"type": "git",
"url": "https://github.com/vlucas/phpdotenv.git",
- "reference": "a59a13791077fe3d44f90e7133eb68e7d22eaff2"
+ "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/a59a13791077fe3d44f90e7133eb68e7d22eaff2",
- "reference": "a59a13791077fe3d44f90e7133eb68e7d22eaff2",
+ "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/24ac4c74f91ee2c193fa1aaa5c249cb0822809af",
+ "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af",
"shasum": ""
},
"require": {
@@ -5850,7 +6092,7 @@
],
"support": {
"issues": "https://github.com/vlucas/phpdotenv/issues",
- "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.1"
+ "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.2"
},
"funding": [
{
@@ -5862,7 +6104,7 @@
"type": "tidelift"
}
],
- "time": "2024-07-20T21:52:34+00:00"
+ "time": "2025-04-30T23:37:27+00:00"
},
{
"name": "voku/portable-ascii",
@@ -6000,16 +6242,16 @@
"packages-dev": [
{
"name": "brianium/paratest",
- "version": "v7.8.3",
+ "version": "v7.8.4",
"source": {
"type": "git",
"url": "https://github.com/paratestphp/paratest.git",
- "reference": "a585c346ddf1bec22e51e20b5387607905604a71"
+ "reference": "130a9bf0e269ee5f5b320108f794ad03e275cad4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/paratestphp/paratest/zipball/a585c346ddf1bec22e51e20b5387607905604a71",
- "reference": "a585c346ddf1bec22e51e20b5387607905604a71",
+ "url": "https://api.github.com/repos/paratestphp/paratest/zipball/130a9bf0e269ee5f5b320108f794ad03e275cad4",
+ "reference": "130a9bf0e269ee5f5b320108f794ad03e275cad4",
"shasum": ""
},
"require": {
@@ -6018,26 +6260,26 @@
"ext-reflection": "*",
"ext-simplexml": "*",
"fidry/cpu-core-counter": "^1.2.0",
- "jean85/pretty-package-versions": "^2.1.0",
+ "jean85/pretty-package-versions": "^2.1.1",
"php": "~8.2.0 || ~8.3.0 || ~8.4.0",
- "phpunit/php-code-coverage": "^11.0.9 || ^12.0.4",
- "phpunit/php-file-iterator": "^5.1.0 || ^6",
- "phpunit/php-timer": "^7.0.1 || ^8",
- "phpunit/phpunit": "^11.5.11 || ^12.0.6",
- "sebastian/environment": "^7.2.0 || ^8",
- "symfony/console": "^6.4.17 || ^7.2.1",
- "symfony/process": "^6.4.19 || ^7.2.4"
+ "phpunit/php-code-coverage": "^11.0.10",
+ "phpunit/php-file-iterator": "^5.1.0",
+ "phpunit/php-timer": "^7.0.1",
+ "phpunit/phpunit": "^11.5.24",
+ "sebastian/environment": "^7.2.1",
+ "symfony/console": "^6.4.22 || ^7.3.0",
+ "symfony/process": "^6.4.20 || ^7.3.0"
},
"require-dev": {
"doctrine/coding-standard": "^12.0.0",
"ext-pcov": "*",
"ext-posix": "*",
- "phpstan/phpstan": "^2.1.6",
- "phpstan/phpstan-deprecation-rules": "^2.0.1",
- "phpstan/phpstan-phpunit": "^2.0.4",
- "phpstan/phpstan-strict-rules": "^2.0.3",
- "squizlabs/php_codesniffer": "^3.11.3",
- "symfony/filesystem": "^6.4.13 || ^7.2.0"
+ "phpstan/phpstan": "^2.1.17",
+ "phpstan/phpstan-deprecation-rules": "^2.0.3",
+ "phpstan/phpstan-phpunit": "^2.0.6",
+ "phpstan/phpstan-strict-rules": "^2.0.4",
+ "squizlabs/php_codesniffer": "^3.13.2",
+ "symfony/filesystem": "^6.4.13 || ^7.3.0"
},
"bin": [
"bin/paratest",
@@ -6077,7 +6319,7 @@
],
"support": {
"issues": "https://github.com/paratestphp/paratest/issues",
- "source": "https://github.com/paratestphp/paratest/tree/v7.8.3"
+ "source": "https://github.com/paratestphp/paratest/tree/v7.8.4"
},
"funding": [
{
@@ -6089,30 +6331,33 @@
"type": "paypal"
}
],
- "time": "2025-03-05T08:29:11+00:00"
+ "time": "2025-06-23T06:07:21+00:00"
},
{
"name": "doctrine/deprecations",
- "version": "1.1.4",
+ "version": "1.1.5",
"source": {
"type": "git",
"url": "https://github.com/doctrine/deprecations.git",
- "reference": "31610dbb31faa98e6b5447b62340826f54fbc4e9"
+ "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/deprecations/zipball/31610dbb31faa98e6b5447b62340826f54fbc4e9",
- "reference": "31610dbb31faa98e6b5447b62340826f54fbc4e9",
+ "url": "https://api.github.com/repos/doctrine/deprecations/zipball/459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38",
+ "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
+ "conflict": {
+ "phpunit/phpunit": "<=7.5 || >=13"
+ },
"require-dev": {
- "doctrine/coding-standard": "^9 || ^12",
- "phpstan/phpstan": "1.4.10 || 2.0.3",
+ "doctrine/coding-standard": "^9 || ^12 || ^13",
+ "phpstan/phpstan": "1.4.10 || 2.1.11",
"phpstan/phpstan-phpunit": "^1.0 || ^2",
- "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5",
+ "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12",
"psr/log": "^1 || ^2 || ^3"
},
"suggest": {
@@ -6132,9 +6377,9 @@
"homepage": "https://www.doctrine-project.org/",
"support": {
"issues": "https://github.com/doctrine/deprecations/issues",
- "source": "https://github.com/doctrine/deprecations/tree/1.1.4"
+ "source": "https://github.com/doctrine/deprecations/tree/1.1.5"
},
- "time": "2024-12-07T21:18:45+00:00"
+ "time": "2025-04-07T20:06:18+00:00"
},
{
"name": "fakerphp/faker",
@@ -6201,16 +6446,16 @@
},
{
"name": "fidry/cpu-core-counter",
- "version": "1.2.0",
+ "version": "1.3.0",
"source": {
"type": "git",
"url": "https://github.com/theofidry/cpu-core-counter.git",
- "reference": "8520451a140d3f46ac33042715115e290cf5785f"
+ "reference": "db9508f7b1474469d9d3c53b86f817e344732678"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/8520451a140d3f46ac33042715115e290cf5785f",
- "reference": "8520451a140d3f46ac33042715115e290cf5785f",
+ "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/db9508f7b1474469d9d3c53b86f817e344732678",
+ "reference": "db9508f7b1474469d9d3c53b86f817e344732678",
"shasum": ""
},
"require": {
@@ -6220,10 +6465,10 @@
"fidry/makefile": "^0.2.0",
"fidry/php-cs-fixer-config": "^1.1.2",
"phpstan/extension-installer": "^1.2.0",
- "phpstan/phpstan": "^1.9.2",
- "phpstan/phpstan-deprecation-rules": "^1.0.0",
- "phpstan/phpstan-phpunit": "^1.2.2",
- "phpstan/phpstan-strict-rules": "^1.4.4",
+ "phpstan/phpstan": "^2.0",
+ "phpstan/phpstan-deprecation-rules": "^2.0.0",
+ "phpstan/phpstan-phpunit": "^2.0",
+ "phpstan/phpstan-strict-rules": "^2.0",
"phpunit/phpunit": "^8.5.31 || ^9.5.26",
"webmozarts/strict-phpunit": "^7.5"
},
@@ -6250,7 +6495,7 @@
],
"support": {
"issues": "https://github.com/theofidry/cpu-core-counter/issues",
- "source": "https://github.com/theofidry/cpu-core-counter/tree/1.2.0"
+ "source": "https://github.com/theofidry/cpu-core-counter/tree/1.3.0"
},
"funding": [
{
@@ -6258,20 +6503,20 @@
"type": "github"
}
],
- "time": "2024-08-06T10:04:20+00:00"
+ "time": "2025-08-14T07:29:31+00:00"
},
{
"name": "filp/whoops",
- "version": "2.18.0",
+ "version": "2.18.4",
"source": {
"type": "git",
"url": "https://github.com/filp/whoops.git",
- "reference": "a7de6c3c6c3c022f5cfc337f8ede6a14460cf77e"
+ "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/filp/whoops/zipball/a7de6c3c6c3c022f5cfc337f8ede6a14460cf77e",
- "reference": "a7de6c3c6c3c022f5cfc337f8ede6a14460cf77e",
+ "url": "https://api.github.com/repos/filp/whoops/zipball/d2102955e48b9fd9ab24280a7ad12ed552752c4d",
+ "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d",
"shasum": ""
},
"require": {
@@ -6321,7 +6566,7 @@
],
"support": {
"issues": "https://github.com/filp/whoops/issues",
- "source": "https://github.com/filp/whoops/tree/2.18.0"
+ "source": "https://github.com/filp/whoops/tree/2.18.4"
},
"funding": [
{
@@ -6329,24 +6574,24 @@
"type": "github"
}
],
- "time": "2025-03-15T12:00:00+00:00"
+ "time": "2025-08-08T12:00:00+00:00"
},
{
"name": "hamcrest/hamcrest-php",
- "version": "v2.0.1",
+ "version": "v2.1.1",
"source": {
"type": "git",
"url": "https://github.com/hamcrest/hamcrest-php.git",
- "reference": "8c3d0a3f6af734494ad8f6fbbee0ba92422859f3"
+ "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/8c3d0a3f6af734494ad8f6fbbee0ba92422859f3",
- "reference": "8c3d0a3f6af734494ad8f6fbbee0ba92422859f3",
+ "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487",
+ "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487",
"shasum": ""
},
"require": {
- "php": "^5.3|^7.0|^8.0"
+ "php": "^7.4|^8.0"
},
"replace": {
"cordoval/hamcrest-php": "*",
@@ -6354,8 +6599,8 @@
"kodova/hamcrest-php": "*"
},
"require-dev": {
- "phpunit/php-file-iterator": "^1.4 || ^2.0",
- "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0"
+ "phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0",
+ "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0"
},
"type": "library",
"extra": {
@@ -6378,9 +6623,9 @@
],
"support": {
"issues": "https://github.com/hamcrest/hamcrest-php/issues",
- "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.0.1"
+ "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.1.1"
},
- "time": "2020-07-09T08:09:16+00:00"
+ "time": "2025-04-30T06:54:44+00:00"
},
{
"name": "jean85/pretty-package-versions",
@@ -6444,16 +6689,16 @@
},
{
"name": "laravel/pail",
- "version": "v1.2.2",
+ "version": "v1.2.3",
"source": {
"type": "git",
"url": "https://github.com/laravel/pail.git",
- "reference": "f31f4980f52be17c4667f3eafe034e6826787db2"
+ "reference": "8cc3d575c1f0e57eeb923f366a37528c50d2385a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/pail/zipball/f31f4980f52be17c4667f3eafe034e6826787db2",
- "reference": "f31f4980f52be17c4667f3eafe034e6826787db2",
+ "url": "https://api.github.com/repos/laravel/pail/zipball/8cc3d575c1f0e57eeb923f366a37528c50d2385a",
+ "reference": "8cc3d575c1f0e57eeb923f366a37528c50d2385a",
"shasum": ""
},
"require": {
@@ -6473,7 +6718,7 @@
"orchestra/testbench-core": "^8.13|^9.0|^10.0",
"pestphp/pest": "^2.20|^3.0",
"pestphp/pest-plugin-type-coverage": "^2.3|^3.0",
- "phpstan/phpstan": "^1.10",
+ "phpstan/phpstan": "^1.12.27",
"symfony/var-dumper": "^6.3|^7.0"
},
"type": "library",
@@ -6509,6 +6754,7 @@
"description": "Easily delve into your Laravel application's log files directly from the command line.",
"homepage": "https://github.com/laravel/pail",
"keywords": [
+ "dev",
"laravel",
"logs",
"php",
@@ -6518,20 +6764,20 @@
"issues": "https://github.com/laravel/pail/issues",
"source": "https://github.com/laravel/pail"
},
- "time": "2025-01-28T15:15:15+00:00"
+ "time": "2025-06-05T13:55:57+00:00"
},
{
"name": "laravel/pint",
- "version": "v1.21.2",
+ "version": "v1.25.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/pint.git",
- "reference": "370772e7d9e9da087678a0edf2b11b6960e40558"
+ "reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/pint/zipball/370772e7d9e9da087678a0edf2b11b6960e40558",
- "reference": "370772e7d9e9da087678a0edf2b11b6960e40558",
+ "url": "https://api.github.com/repos/laravel/pint/zipball/5016e263f95d97670d71b9a987bd8996ade6d8d9",
+ "reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9",
"shasum": ""
},
"require": {
@@ -6542,12 +6788,12 @@
"php": "^8.2.0"
},
"require-dev": {
- "friendsofphp/php-cs-fixer": "^3.72.0",
- "illuminate/view": "^11.44.2",
- "larastan/larastan": "^3.2.0",
- "laravel-zero/framework": "^11.36.1",
+ "friendsofphp/php-cs-fixer": "^3.87.2",
+ "illuminate/view": "^11.46.0",
+ "larastan/larastan": "^3.7.1",
+ "laravel-zero/framework": "^11.45.0",
"mockery/mockery": "^1.6.12",
- "nunomaduro/termwind": "^2.3",
+ "nunomaduro/termwind": "^2.3.1",
"pestphp/pest": "^2.36.0"
},
"bin": [
@@ -6584,20 +6830,20 @@
"issues": "https://github.com/laravel/pint/issues",
"source": "https://github.com/laravel/pint"
},
- "time": "2025-03-14T22:31:42+00:00"
+ "time": "2025-09-19T02:57:12+00:00"
},
{
"name": "laravel/sail",
- "version": "v1.41.0",
+ "version": "v1.46.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/sail.git",
- "reference": "fe1a4ada0abb5e4bd99eb4e4b0d87906c00cdeec"
+ "reference": "eb90c4f113c4a9637b8fdd16e24cfc64f2b0ae6e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/sail/zipball/fe1a4ada0abb5e4bd99eb4e4b0d87906c00cdeec",
- "reference": "fe1a4ada0abb5e4bd99eb4e4b0d87906c00cdeec",
+ "url": "https://api.github.com/repos/laravel/sail/zipball/eb90c4f113c4a9637b8fdd16e24cfc64f2b0ae6e",
+ "reference": "eb90c4f113c4a9637b8fdd16e24cfc64f2b0ae6e",
"shasum": ""
},
"require": {
@@ -6647,7 +6893,7 @@
"issues": "https://github.com/laravel/sail/issues",
"source": "https://github.com/laravel/sail"
},
- "time": "2025-01-24T15:45:36+00:00"
+ "time": "2025-09-23T13:44:39+00:00"
},
{
"name": "mockery/mockery",
@@ -6734,16 +6980,16 @@
},
{
"name": "myclabs/deep-copy",
- "version": "1.13.0",
+ "version": "1.13.4",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
- "reference": "024473a478be9df5fdaca2c793f2232fe788e414"
+ "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/024473a478be9df5fdaca2c793f2232fe788e414",
- "reference": "024473a478be9df5fdaca2c793f2232fe788e414",
+ "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a",
+ "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a",
"shasum": ""
},
"require": {
@@ -6782,7 +7028,7 @@
],
"support": {
"issues": "https://github.com/myclabs/DeepCopy/issues",
- "source": "https://github.com/myclabs/DeepCopy/tree/1.13.0"
+ "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4"
},
"funding": [
{
@@ -6790,42 +7036,43 @@
"type": "tidelift"
}
],
- "time": "2025-02-12T12:17:51+00:00"
+ "time": "2025-08-01T08:46:24+00:00"
},
{
"name": "nunomaduro/collision",
- "version": "v8.7.0",
+ "version": "v8.8.2",
"source": {
"type": "git",
"url": "https://github.com/nunomaduro/collision.git",
- "reference": "586cb8181a257a2152b6a855ca8d9598878a1a26"
+ "reference": "60207965f9b7b7a4ce15a0f75d57f9dadb105bdb"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nunomaduro/collision/zipball/586cb8181a257a2152b6a855ca8d9598878a1a26",
- "reference": "586cb8181a257a2152b6a855ca8d9598878a1a26",
+ "url": "https://api.github.com/repos/nunomaduro/collision/zipball/60207965f9b7b7a4ce15a0f75d57f9dadb105bdb",
+ "reference": "60207965f9b7b7a4ce15a0f75d57f9dadb105bdb",
"shasum": ""
},
"require": {
- "filp/whoops": "^2.17.0",
- "nunomaduro/termwind": "^2.3.0",
+ "filp/whoops": "^2.18.1",
+ "nunomaduro/termwind": "^2.3.1",
"php": "^8.2.0",
- "symfony/console": "^7.2.1"
+ "symfony/console": "^7.3.0"
},
"conflict": {
- "laravel/framework": "<11.39.1 || >=13.0.0",
- "phpunit/phpunit": "<11.5.3 || >=12.0.0"
+ "laravel/framework": "<11.44.2 || >=13.0.0",
+ "phpunit/phpunit": "<11.5.15 || >=13.0.0"
},
"require-dev": {
- "larastan/larastan": "^2.10.0",
- "laravel/framework": "^11.44.2",
- "laravel/pint": "^1.21.2",
- "laravel/sail": "^1.41.0",
- "laravel/sanctum": "^4.0.8",
+ "brianium/paratest": "^7.8.3",
+ "larastan/larastan": "^3.4.2",
+ "laravel/framework": "^11.44.2 || ^12.18",
+ "laravel/pint": "^1.22.1",
+ "laravel/sail": "^1.43.1",
+ "laravel/sanctum": "^4.1.1",
"laravel/tinker": "^2.10.1",
- "orchestra/testbench-core": "^9.12.0",
- "pestphp/pest": "^3.7.4",
- "sebastian/environment": "^6.1.0 || ^7.2.0"
+ "orchestra/testbench-core": "^9.12.0 || ^10.4",
+ "pestphp/pest": "^3.8.2",
+ "sebastian/environment": "^7.2.1 || ^8.0"
},
"type": "library",
"extra": {
@@ -6888,42 +7135,42 @@
"type": "patreon"
}
],
- "time": "2025-03-14T22:37:40+00:00"
+ "time": "2025-06-25T02:12:12+00:00"
},
{
"name": "pestphp/pest",
- "version": "v3.8.0",
+ "version": "v3.8.4",
"source": {
"type": "git",
"url": "https://github.com/pestphp/pest.git",
- "reference": "42e1b9f17fc2b2036701f4b968158264bde542d4"
+ "reference": "72cf695554420e21858cda831d5db193db102574"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/pestphp/pest/zipball/42e1b9f17fc2b2036701f4b968158264bde542d4",
- "reference": "42e1b9f17fc2b2036701f4b968158264bde542d4",
+ "url": "https://api.github.com/repos/pestphp/pest/zipball/72cf695554420e21858cda831d5db193db102574",
+ "reference": "72cf695554420e21858cda831d5db193db102574",
"shasum": ""
},
"require": {
- "brianium/paratest": "^7.8.3",
- "nunomaduro/collision": "^8.7.0",
- "nunomaduro/termwind": "^2.3.0",
+ "brianium/paratest": "^7.8.4",
+ "nunomaduro/collision": "^8.8.2",
+ "nunomaduro/termwind": "^2.3.1",
"pestphp/pest-plugin": "^3.0.0",
- "pestphp/pest-plugin-arch": "^3.1.0",
+ "pestphp/pest-plugin-arch": "^3.1.1",
"pestphp/pest-plugin-mutate": "^3.0.5",
"php": "^8.2.0",
- "phpunit/phpunit": "^11.5.15"
+ "phpunit/phpunit": "^11.5.33"
},
"conflict": {
"filp/whoops": "<2.16.0",
- "phpunit/phpunit": ">11.5.15",
+ "phpunit/phpunit": ">11.5.33",
"sebastian/exporter": "<6.0.0",
"webmozart/assert": "<1.11.0"
},
"require-dev": {
"pestphp/pest-dev-tools": "^3.4.0",
- "pestphp/pest-plugin-type-coverage": "^3.5.0",
- "symfony/process": "^7.2.5"
+ "pestphp/pest-plugin-type-coverage": "^3.6.1",
+ "symfony/process": "^7.3.0"
},
"bin": [
"bin/pest"
@@ -6988,7 +7235,7 @@
],
"support": {
"issues": "https://github.com/pestphp/pest/issues",
- "source": "https://github.com/pestphp/pest/tree/v3.8.0"
+ "source": "https://github.com/pestphp/pest/tree/v3.8.4"
},
"funding": [
{
@@ -7000,7 +7247,7 @@
"type": "github"
}
],
- "time": "2025-03-30T17:49:10+00:00"
+ "time": "2025-08-20T19:12:42+00:00"
},
{
"name": "pestphp/pest-plugin",
@@ -7074,16 +7321,16 @@
},
{
"name": "pestphp/pest-plugin-arch",
- "version": "v3.1.0",
+ "version": "v3.1.1",
"source": {
"type": "git",
"url": "https://github.com/pestphp/pest-plugin-arch.git",
- "reference": "ebec636b97ee73936ee8485e15a59c3f5a4c21b2"
+ "reference": "db7bd9cb1612b223e16618d85475c6f63b9c8daa"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/pestphp/pest-plugin-arch/zipball/ebec636b97ee73936ee8485e15a59c3f5a4c21b2",
- "reference": "ebec636b97ee73936ee8485e15a59c3f5a4c21b2",
+ "url": "https://api.github.com/repos/pestphp/pest-plugin-arch/zipball/db7bd9cb1612b223e16618d85475c6f63b9c8daa",
+ "reference": "db7bd9cb1612b223e16618d85475c6f63b9c8daa",
"shasum": ""
},
"require": {
@@ -7092,7 +7339,7 @@
"ta-tikoma/phpunit-architecture-test": "^0.8.4"
},
"require-dev": {
- "pestphp/pest": "^3.7.5",
+ "pestphp/pest": "^3.8.1",
"pestphp/pest-dev-tools": "^3.4.0"
},
"type": "library",
@@ -7128,7 +7375,7 @@
"unit"
],
"support": {
- "source": "https://github.com/pestphp/pest-plugin-arch/tree/v3.1.0"
+ "source": "https://github.com/pestphp/pest-plugin-arch/tree/v3.1.1"
},
"funding": [
{
@@ -7140,31 +7387,31 @@
"type": "github"
}
],
- "time": "2025-03-30T17:28:50+00:00"
+ "time": "2025-04-16T22:59:48+00:00"
},
{
"name": "pestphp/pest-plugin-laravel",
- "version": "v3.1.0",
+ "version": "v3.2.0",
"source": {
"type": "git",
"url": "https://github.com/pestphp/pest-plugin-laravel.git",
- "reference": "1c4e994476375c72aa7aebaaa97aa98f5d5378cd"
+ "reference": "6801be82fd92b96e82dd72e563e5674b1ce365fc"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/pestphp/pest-plugin-laravel/zipball/1c4e994476375c72aa7aebaaa97aa98f5d5378cd",
- "reference": "1c4e994476375c72aa7aebaaa97aa98f5d5378cd",
+ "url": "https://api.github.com/repos/pestphp/pest-plugin-laravel/zipball/6801be82fd92b96e82dd72e563e5674b1ce365fc",
+ "reference": "6801be82fd92b96e82dd72e563e5674b1ce365fc",
"shasum": ""
},
"require": {
- "laravel/framework": "^11.39.1|^12.0.0",
- "pestphp/pest": "^3.7.4",
+ "laravel/framework": "^11.39.1|^12.9.2",
+ "pestphp/pest": "^3.8.2",
"php": "^8.2.0"
},
"require-dev": {
"laravel/dusk": "^8.2.13|dev-develop",
- "orchestra/testbench": "^9.9.0|^10.0.0",
- "pestphp/pest-dev-tools": "^3.3.0"
+ "orchestra/testbench": "^9.9.0|^10.2.1",
+ "pestphp/pest-dev-tools": "^3.4.0"
},
"type": "library",
"extra": {
@@ -7202,7 +7449,7 @@
"unit"
],
"support": {
- "source": "https://github.com/pestphp/pest-plugin-laravel/tree/v3.1.0"
+ "source": "https://github.com/pestphp/pest-plugin-laravel/tree/v3.2.0"
},
"funding": [
{
@@ -7214,7 +7461,7 @@
"type": "github"
}
],
- "time": "2025-01-24T13:22:39+00:00"
+ "time": "2025-04-21T07:40:53+00:00"
},
{
"name": "pestphp/pest-plugin-mutate",
@@ -7461,16 +7708,16 @@
},
{
"name": "phpdocumentor/reflection-docblock",
- "version": "5.6.1",
+ "version": "5.6.3",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
- "reference": "e5e784149a09bd69d9a5e3b01c5cbd2e2bd653d8"
+ "reference": "94f8051919d1b0369a6bcc7931d679a511c03fe9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/e5e784149a09bd69d9a5e3b01c5cbd2e2bd653d8",
- "reference": "e5e784149a09bd69d9a5e3b01c5cbd2e2bd653d8",
+ "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/94f8051919d1b0369a6bcc7931d679a511c03fe9",
+ "reference": "94f8051919d1b0369a6bcc7931d679a511c03fe9",
"shasum": ""
},
"require": {
@@ -7519,9 +7766,9 @@
"description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
"support": {
"issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues",
- "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.1"
+ "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.3"
},
- "time": "2024-12-07T09:39:29+00:00"
+ "time": "2025-08-01T19:43:32+00:00"
},
{
"name": "phpdocumentor/type-resolver",
@@ -7583,16 +7830,16 @@
},
{
"name": "phpstan/phpdoc-parser",
- "version": "2.1.0",
+ "version": "2.3.0",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpdoc-parser.git",
- "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68"
+ "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/9b30d6fd026b2c132b3985ce6b23bec09ab3aa68",
- "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68",
+ "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/1e0cd5370df5dd2e556a36b9c62f62e555870495",
+ "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495",
"shasum": ""
},
"require": {
@@ -7624,22 +7871,22 @@
"description": "PHPDoc parser with support for nullable, intersection and generic types",
"support": {
"issues": "https://github.com/phpstan/phpdoc-parser/issues",
- "source": "https://github.com/phpstan/phpdoc-parser/tree/2.1.0"
+ "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.0"
},
- "time": "2025-02-19T13:28:12+00:00"
+ "time": "2025-08-30T15:50:23+00:00"
},
{
"name": "phpunit/php-code-coverage",
- "version": "11.0.9",
+ "version": "11.0.11",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
- "reference": "14d63fbcca18457e49c6f8bebaa91a87e8e188d7"
+ "reference": "4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/14d63fbcca18457e49c6f8bebaa91a87e8e188d7",
- "reference": "14d63fbcca18457e49c6f8bebaa91a87e8e188d7",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4",
+ "reference": "4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4",
"shasum": ""
},
"require": {
@@ -7696,15 +7943,27 @@
"support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
- "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.9"
+ "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.11"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage",
+ "type": "tidelift"
}
],
- "time": "2025-02-25T13:26:39+00:00"
+ "time": "2025-08-27T14:37:49+00:00"
},
{
"name": "phpunit/php-file-iterator",
@@ -7953,16 +8212,16 @@
},
{
"name": "phpunit/phpunit",
- "version": "11.5.15",
+ "version": "11.5.33",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
- "reference": "4b6a4ee654e5e0c5e1f17e2f83c0f4c91dee1f9c"
+ "reference": "5965e9ff57546cb9137c0ff6aa78cb7442b05cf6"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4b6a4ee654e5e0c5e1f17e2f83c0f4c91dee1f9c",
- "reference": "4b6a4ee654e5e0c5e1f17e2f83c0f4c91dee1f9c",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/5965e9ff57546cb9137c0ff6aa78cb7442b05cf6",
+ "reference": "5965e9ff57546cb9137c0ff6aa78cb7442b05cf6",
"shasum": ""
},
"require": {
@@ -7972,24 +8231,24 @@
"ext-mbstring": "*",
"ext-xml": "*",
"ext-xmlwriter": "*",
- "myclabs/deep-copy": "^1.13.0",
+ "myclabs/deep-copy": "^1.13.4",
"phar-io/manifest": "^2.0.4",
"phar-io/version": "^3.2.1",
"php": ">=8.2",
- "phpunit/php-code-coverage": "^11.0.9",
+ "phpunit/php-code-coverage": "^11.0.10",
"phpunit/php-file-iterator": "^5.1.0",
"phpunit/php-invoker": "^5.0.1",
"phpunit/php-text-template": "^4.0.1",
"phpunit/php-timer": "^7.0.1",
"sebastian/cli-parser": "^3.0.2",
"sebastian/code-unit": "^3.0.3",
- "sebastian/comparator": "^6.3.1",
+ "sebastian/comparator": "^6.3.2",
"sebastian/diff": "^6.0.2",
- "sebastian/environment": "^7.2.0",
+ "sebastian/environment": "^7.2.1",
"sebastian/exporter": "^6.3.0",
"sebastian/global-state": "^7.0.2",
"sebastian/object-enumerator": "^6.0.1",
- "sebastian/type": "^5.1.2",
+ "sebastian/type": "^5.1.3",
"sebastian/version": "^5.0.2",
"staabm/side-effects-detector": "^1.0.5"
},
@@ -8034,7 +8293,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
- "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.15"
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.33"
},
"funding": [
{
@@ -8045,12 +8304,20 @@
"url": "https://github.com/sebastianbergmann",
"type": "github"
},
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit",
"type": "tidelift"
}
],
- "time": "2025-03-23T16:02:11+00:00"
+ "time": "2025-08-16T05:19:02+00:00"
},
{
"name": "sebastian/cli-parser",
@@ -8224,16 +8491,16 @@
},
{
"name": "sebastian/comparator",
- "version": "6.3.1",
+ "version": "6.3.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/comparator.git",
- "reference": "24b8fbc2c8e201bb1308e7b05148d6ab393b6959"
+ "reference": "85c77556683e6eee4323e4c5468641ca0237e2e8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/24b8fbc2c8e201bb1308e7b05148d6ab393b6959",
- "reference": "24b8fbc2c8e201bb1308e7b05148d6ab393b6959",
+ "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/85c77556683e6eee4323e4c5468641ca0237e2e8",
+ "reference": "85c77556683e6eee4323e4c5468641ca0237e2e8",
"shasum": ""
},
"require": {
@@ -8292,15 +8559,27 @@
"support": {
"issues": "https://github.com/sebastianbergmann/comparator/issues",
"security": "https://github.com/sebastianbergmann/comparator/security/policy",
- "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.1"
+ "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.2"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator",
+ "type": "tidelift"
}
],
- "time": "2025-03-07T06:57:01+00:00"
+ "time": "2025-08-10T08:07:46+00:00"
},
{
"name": "sebastian/complexity",
@@ -8429,23 +8708,23 @@
},
{
"name": "sebastian/environment",
- "version": "7.2.0",
+ "version": "7.2.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/environment.git",
- "reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5"
+ "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5",
- "reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5",
+ "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4",
+ "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4",
"shasum": ""
},
"require": {
"php": ">=8.2"
},
"require-dev": {
- "phpunit/phpunit": "^11.0"
+ "phpunit/phpunit": "^11.3"
},
"suggest": {
"ext-posix": "*"
@@ -8481,28 +8760,40 @@
"support": {
"issues": "https://github.com/sebastianbergmann/environment/issues",
"security": "https://github.com/sebastianbergmann/environment/security/policy",
- "source": "https://github.com/sebastianbergmann/environment/tree/7.2.0"
+ "source": "https://github.com/sebastianbergmann/environment/tree/7.2.1"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/environment",
+ "type": "tidelift"
}
],
- "time": "2024-07-03T04:54:44+00:00"
+ "time": "2025-05-21T11:55:47+00:00"
},
{
"name": "sebastian/exporter",
- "version": "6.3.0",
+ "version": "6.3.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/exporter.git",
- "reference": "3473f61172093b2da7de1fb5782e1f24cc036dc3"
+ "reference": "70a298763b40b213ec087c51c739efcaa90bcd74"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/3473f61172093b2da7de1fb5782e1f24cc036dc3",
- "reference": "3473f61172093b2da7de1fb5782e1f24cc036dc3",
+ "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/70a298763b40b213ec087c51c739efcaa90bcd74",
+ "reference": "70a298763b40b213ec087c51c739efcaa90bcd74",
"shasum": ""
},
"require": {
@@ -8516,7 +8807,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "6.1-dev"
+ "dev-main": "6.3-dev"
}
},
"autoload": {
@@ -8559,15 +8850,27 @@
"support": {
"issues": "https://github.com/sebastianbergmann/exporter/issues",
"security": "https://github.com/sebastianbergmann/exporter/security/policy",
- "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.0"
+ "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.2"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter",
+ "type": "tidelift"
}
],
- "time": "2024-12-05T09:17:50+00:00"
+ "time": "2025-09-24T06:12:51+00:00"
},
{
"name": "sebastian/global-state",
@@ -8805,23 +9108,23 @@
},
{
"name": "sebastian/recursion-context",
- "version": "6.0.2",
+ "version": "6.0.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/recursion-context.git",
- "reference": "694d156164372abbd149a4b85ccda2e4670c0e16"
+ "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/694d156164372abbd149a4b85ccda2e4670c0e16",
- "reference": "694d156164372abbd149a4b85ccda2e4670c0e16",
+ "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/f6458abbf32a6c8174f8f26261475dc133b3d9dc",
+ "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc",
"shasum": ""
},
"require": {
"php": ">=8.2"
},
"require-dev": {
- "phpunit/phpunit": "^11.0"
+ "phpunit/phpunit": "^11.3"
},
"type": "library",
"extra": {
@@ -8857,28 +9160,40 @@
"support": {
"issues": "https://github.com/sebastianbergmann/recursion-context/issues",
"security": "https://github.com/sebastianbergmann/recursion-context/security/policy",
- "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.2"
+ "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.3"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context",
+ "type": "tidelift"
}
],
- "time": "2024-07-03T05:10:34+00:00"
+ "time": "2025-08-13T04:42:22+00:00"
},
{
"name": "sebastian/type",
- "version": "5.1.2",
+ "version": "5.1.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/type.git",
- "reference": "a8a7e30534b0eb0c77cd9d07e82de1a114389f5e"
+ "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/a8a7e30534b0eb0c77cd9d07e82de1a114389f5e",
- "reference": "a8a7e30534b0eb0c77cd9d07e82de1a114389f5e",
+ "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/f77d2d4e78738c98d9a68d2596fe5e8fa380f449",
+ "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449",
"shasum": ""
},
"require": {
@@ -8914,15 +9229,27 @@
"support": {
"issues": "https://github.com/sebastianbergmann/type/issues",
"security": "https://github.com/sebastianbergmann/type/security/policy",
- "source": "https://github.com/sebastianbergmann/type/tree/5.1.2"
+ "source": "https://github.com/sebastianbergmann/type/tree/5.1.3"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/type",
+ "type": "tidelift"
}
],
- "time": "2025-03-18T13:35:50+00:00"
+ "time": "2025-08-09T06:55:48+00:00"
},
{
"name": "sebastian/version",
@@ -9032,16 +9359,16 @@
},
{
"name": "symfony/yaml",
- "version": "v7.2.5",
+ "version": "v7.3.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
- "reference": "4c4b6f4cfcd7e52053f0c8bfad0f7f30fb924912"
+ "reference": "d4f4a66866fe2451f61296924767280ab5732d9d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/yaml/zipball/4c4b6f4cfcd7e52053f0c8bfad0f7f30fb924912",
- "reference": "4c4b6f4cfcd7e52053f0c8bfad0f7f30fb924912",
+ "url": "https://api.github.com/repos/symfony/yaml/zipball/d4f4a66866fe2451f61296924767280ab5732d9d",
+ "reference": "d4f4a66866fe2451f61296924767280ab5732d9d",
"shasum": ""
},
"require": {
@@ -9084,7 +9411,7 @@
"description": "Loads and dumps YAML files",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/yaml/tree/v7.2.5"
+ "source": "https://github.com/symfony/yaml/tree/v7.3.3"
},
"funding": [
{
@@ -9095,32 +9422,36 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-03-03T07:12:39+00:00"
+ "time": "2025-08-27T11:34:33+00:00"
},
{
"name": "ta-tikoma/phpunit-architecture-test",
- "version": "0.8.4",
+ "version": "0.8.5",
"source": {
"type": "git",
"url": "https://github.com/ta-tikoma/phpunit-architecture-test.git",
- "reference": "89f0dea1cb0f0d5744d3ec1764a286af5e006636"
+ "reference": "cf6fb197b676ba716837c886baca842e4db29005"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/ta-tikoma/phpunit-architecture-test/zipball/89f0dea1cb0f0d5744d3ec1764a286af5e006636",
- "reference": "89f0dea1cb0f0d5744d3ec1764a286af5e006636",
+ "url": "https://api.github.com/repos/ta-tikoma/phpunit-architecture-test/zipball/cf6fb197b676ba716837c886baca842e4db29005",
+ "reference": "cf6fb197b676ba716837c886baca842e4db29005",
"shasum": ""
},
"require": {
"nikic/php-parser": "^4.18.0 || ^5.0.0",
"php": "^8.1.0",
"phpdocumentor/reflection-docblock": "^5.3.0",
- "phpunit/phpunit": "^10.5.5 || ^11.0.0",
+ "phpunit/phpunit": "^10.5.5 || ^11.0.0 || ^12.0.0",
"symfony/finder": "^6.4.0 || ^7.0.0"
},
"require-dev": {
@@ -9157,9 +9488,9 @@
],
"support": {
"issues": "https://github.com/ta-tikoma/phpunit-architecture-test/issues",
- "source": "https://github.com/ta-tikoma/phpunit-architecture-test/tree/0.8.4"
+ "source": "https://github.com/ta-tikoma/phpunit-architecture-test/tree/0.8.5"
},
- "time": "2024-01-05T14:10:56+00:00"
+ "time": "2025-04-20T20:23:40+00:00"
},
{
"name": "theseer/tokenizer",
@@ -9214,12 +9545,12 @@
],
"aliases": [],
"minimum-stability": "stable",
- "stability-flags": [],
+ "stability-flags": {},
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
"php": "^8.2"
},
- "platform-dev": [],
- "plugin-api-version": "2.3.0"
+ "platform-dev": {},
+ "plugin-api-version": "2.6.0"
}
diff --git a/backend/package-lock.json b/backend/package-lock.json
index d2371c6..29e5849 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -1,9 +1,10 @@
{
- "name": "backend",
+ "name": "html",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
+ "name": "html",
"dependencies": {
"@tailwindcss/vite": "^4.0.7",
"autoprefixer": "^10.4.20",
@@ -420,9 +421,9 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.34.8",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.8.tgz",
- "integrity": "sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw==",
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz",
+ "integrity": "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==",
"cpu": [
"arm"
],
@@ -433,9 +434,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
- "version": "4.34.8",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.8.tgz",
- "integrity": "sha512-Gigjz7mNWaOL9wCggvoK3jEIUUbGul656opstjaUSGC3eT0BM7PofdAJaBfPFWWkXNVAXbaQtC99OCg4sJv70Q==",
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.4.tgz",
+ "integrity": "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==",
"cpu": [
"arm64"
],
@@ -446,9 +447,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.34.8",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.8.tgz",
- "integrity": "sha512-02rVdZ5tgdUNRxIUrFdcMBZQoaPMrxtwSb+/hOfBdqkatYHR3lZ2A2EGyHq2sGOd0Owk80oV3snlDASC24He3Q==",
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.4.tgz",
+ "integrity": "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==",
"cpu": [
"arm64"
],
@@ -459,9 +460,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.34.8",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.8.tgz",
- "integrity": "sha512-qIP/elwR/tq/dYRx3lgwK31jkZvMiD6qUtOycLhTzCvrjbZ3LjQnEM9rNhSGpbLXVJYQ3rq39A6Re0h9tU2ynw==",
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.4.tgz",
+ "integrity": "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==",
"cpu": [
"x64"
],
@@ -472,9 +473,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
- "version": "4.34.8",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.8.tgz",
- "integrity": "sha512-IQNVXL9iY6NniYbTaOKdrlVP3XIqazBgJOVkddzJlqnCpRi/yAeSOa8PLcECFSQochzqApIOE1GHNu3pCz+BDA==",
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.4.tgz",
+ "integrity": "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==",
"cpu": [
"arm64"
],
@@ -485,9 +486,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
- "version": "4.34.8",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.8.tgz",
- "integrity": "sha512-TYXcHghgnCqYFiE3FT5QwXtOZqDj5GmaFNTNt3jNC+vh22dc/ukG2cG+pi75QO4kACohZzidsq7yKTKwq/Jq7Q==",
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.4.tgz",
+ "integrity": "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==",
"cpu": [
"x64"
],
@@ -498,9 +499,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.34.8",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.8.tgz",
- "integrity": "sha512-A4iphFGNkWRd+5m3VIGuqHnG3MVnqKe7Al57u9mwgbyZ2/xF9Jio72MaY7xxh+Y87VAHmGQr73qoKL9HPbXj1g==",
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.4.tgz",
+ "integrity": "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==",
"cpu": [
"arm"
],
@@ -511,9 +512,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
- "version": "4.34.8",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.8.tgz",
- "integrity": "sha512-S0lqKLfTm5u+QTxlFiAnb2J/2dgQqRy/XvziPtDd1rKZFXHTyYLoVL58M/XFwDI01AQCDIevGLbQrMAtdyanpA==",
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.4.tgz",
+ "integrity": "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==",
"cpu": [
"arm"
],
@@ -524,9 +525,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.34.8",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.8.tgz",
- "integrity": "sha512-jpz9YOuPiSkL4G4pqKrus0pn9aYwpImGkosRKwNi+sJSkz+WU3anZe6hi73StLOQdfXYXC7hUfsQlTnjMd3s1A==",
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.4.tgz",
+ "integrity": "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==",
"cpu": [
"arm64"
],
@@ -537,9 +538,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.34.8",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.8.tgz",
- "integrity": "sha512-KdSfaROOUJXgTVxJNAZ3KwkRc5nggDk+06P6lgi1HLv1hskgvxHUKZ4xtwHkVYJ1Rep4GNo+uEfycCRRxht7+Q==",
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.4.tgz",
+ "integrity": "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==",
"cpu": [
"arm64"
],
@@ -549,10 +550,10 @@
"linux"
]
},
- "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
- "version": "4.34.8",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.8.tgz",
- "integrity": "sha512-NyF4gcxwkMFRjgXBM6g2lkT58OWztZvw5KkV2K0qqSnUEqCVcqdh2jN4gQrTn/YUpAcNKyFHfoOZEer9nwo6uQ==",
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.4.tgz",
+ "integrity": "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==",
"cpu": [
"loong64"
],
@@ -562,10 +563,10 @@
"linux"
]
},
- "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
- "version": "4.34.8",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.8.tgz",
- "integrity": "sha512-LMJc999GkhGvktHU85zNTDImZVUCJ1z/MbAJTnviiWmmjyckP5aQsHtcujMjpNdMZPT2rQEDBlJfubhs3jsMfw==",
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.4.tgz",
+ "integrity": "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==",
"cpu": [
"ppc64"
],
@@ -576,9 +577,22 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.34.8",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.8.tgz",
- "integrity": "sha512-xAQCAHPj8nJq1PI3z8CIZzXuXCstquz7cIOL73HHdXiRcKk8Ywwqtx2wrIy23EcTn4aZ2fLJNBB8d0tQENPCmw==",
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.4.tgz",
+ "integrity": "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.4.tgz",
+ "integrity": "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==",
"cpu": [
"riscv64"
],
@@ -589,9 +603,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
- "version": "4.34.8",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.8.tgz",
- "integrity": "sha512-DdePVk1NDEuc3fOe3dPPTb+rjMtuFw89gw6gVWxQFAuEqqSdDKnrwzZHrUYdac7A7dXl9Q2Vflxpme15gUWQFA==",
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.4.tgz",
+ "integrity": "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==",
"cpu": [
"s390x"
],
@@ -615,9 +629,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.34.8",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.8.tgz",
- "integrity": "sha512-SCXcP0ZpGFIe7Ge+McxY5zKxiEI5ra+GT3QRxL0pMMtxPfpyLAKleZODi1zdRHkz5/BhueUrYtYVgubqe9JBNQ==",
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.4.tgz",
+ "integrity": "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==",
"cpu": [
"x64"
],
@@ -627,10 +641,23 @@
"linux"
]
},
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.4.tgz",
+ "integrity": "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
"node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.34.8",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.8.tgz",
- "integrity": "sha512-YHYsgzZgFJzTRbth4h7Or0m5O74Yda+hLin0irAIobkLQFRQd1qWmnoVfwmKm9TXIZVAD0nZ+GEb2ICicLyCnQ==",
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.4.tgz",
+ "integrity": "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==",
"cpu": [
"arm64"
],
@@ -641,9 +668,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.34.8",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.8.tgz",
- "integrity": "sha512-r3NRQrXkHr4uWy5TOjTpTYojR9XmF0j/RYgKCef+Ag46FWUTltm5ziticv8LdNsDMehjJ543x/+TJAek/xBA2w==",
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.4.tgz",
+ "integrity": "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==",
"cpu": [
"ia32"
],
@@ -653,10 +680,23 @@
"win32"
]
},
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.4.tgz",
+ "integrity": "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
"node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.34.8",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.8.tgz",
- "integrity": "sha512-U0FaE5O1BCpZSeE6gBl3c5ObhePQSfk9vDRToMmTkbhCOgW4jqvtS5LGyQ76L1fH8sM0keRp4uDTsbjiUyjk0g==",
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.4.tgz",
+ "integrity": "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==",
"cpu": [
"x64"
],
@@ -891,9 +931,9 @@
}
},
"node_modules/@types/estree": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
- "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"license": "MIT"
},
"node_modules/ansi-regex": {
@@ -964,13 +1004,13 @@
}
},
"node_modules/axios": {
- "version": "1.7.9",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
- "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
+ "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
- "form-data": "^4.0.0",
+ "form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
@@ -993,6 +1033,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001688",
"electron-to-chromium": "^1.5.73",
@@ -1311,14 +1352,15 @@
}
},
"node_modules/form-data": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
- "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
+ "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
@@ -1844,6 +1886,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"nanoid": "^3.3.8",
"picocolors": "^1.1.1",
@@ -1875,12 +1918,12 @@
}
},
"node_modules/rollup": {
- "version": "4.34.8",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.8.tgz",
- "integrity": "sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ==",
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz",
+ "integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==",
"license": "MIT",
"dependencies": {
- "@types/estree": "1.0.6"
+ "@types/estree": "1.0.8"
},
"bin": {
"rollup": "dist/bin/rollup"
@@ -1890,32 +1933,35 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.34.8",
- "@rollup/rollup-android-arm64": "4.34.8",
- "@rollup/rollup-darwin-arm64": "4.34.8",
- "@rollup/rollup-darwin-x64": "4.34.8",
- "@rollup/rollup-freebsd-arm64": "4.34.8",
- "@rollup/rollup-freebsd-x64": "4.34.8",
- "@rollup/rollup-linux-arm-gnueabihf": "4.34.8",
- "@rollup/rollup-linux-arm-musleabihf": "4.34.8",
- "@rollup/rollup-linux-arm64-gnu": "4.34.8",
- "@rollup/rollup-linux-arm64-musl": "4.34.8",
- "@rollup/rollup-linux-loongarch64-gnu": "4.34.8",
- "@rollup/rollup-linux-powerpc64le-gnu": "4.34.8",
- "@rollup/rollup-linux-riscv64-gnu": "4.34.8",
- "@rollup/rollup-linux-s390x-gnu": "4.34.8",
- "@rollup/rollup-linux-x64-gnu": "4.34.8",
- "@rollup/rollup-linux-x64-musl": "4.34.8",
- "@rollup/rollup-win32-arm64-msvc": "4.34.8",
- "@rollup/rollup-win32-ia32-msvc": "4.34.8",
- "@rollup/rollup-win32-x64-msvc": "4.34.8",
+ "@rollup/rollup-android-arm-eabi": "4.52.4",
+ "@rollup/rollup-android-arm64": "4.52.4",
+ "@rollup/rollup-darwin-arm64": "4.52.4",
+ "@rollup/rollup-darwin-x64": "4.52.4",
+ "@rollup/rollup-freebsd-arm64": "4.52.4",
+ "@rollup/rollup-freebsd-x64": "4.52.4",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.52.4",
+ "@rollup/rollup-linux-arm-musleabihf": "4.52.4",
+ "@rollup/rollup-linux-arm64-gnu": "4.52.4",
+ "@rollup/rollup-linux-arm64-musl": "4.52.4",
+ "@rollup/rollup-linux-loong64-gnu": "4.52.4",
+ "@rollup/rollup-linux-ppc64-gnu": "4.52.4",
+ "@rollup/rollup-linux-riscv64-gnu": "4.52.4",
+ "@rollup/rollup-linux-riscv64-musl": "4.52.4",
+ "@rollup/rollup-linux-s390x-gnu": "4.52.4",
+ "@rollup/rollup-linux-x64-gnu": "4.52.4",
+ "@rollup/rollup-linux-x64-musl": "4.52.4",
+ "@rollup/rollup-openharmony-arm64": "4.52.4",
+ "@rollup/rollup-win32-arm64-msvc": "4.52.4",
+ "@rollup/rollup-win32-ia32-msvc": "4.52.4",
+ "@rollup/rollup-win32-x64-gnu": "4.52.4",
+ "@rollup/rollup-win32-x64-msvc": "4.52.4",
"fsevents": "~2.3.2"
}
},
"node_modules/rollup/node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.34.8",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.8.tgz",
- "integrity": "sha512-8y7ED8gjxITUltTUEJLQdgpbPh1sUQ0kMTmufRF/Ns5tI9TNMNlhWtmPKKHCU0SilX+3MJkZ0zERYYGIVBYHIA==",
+ "version": "4.52.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.4.tgz",
+ "integrity": "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==",
"cpu": [
"x64"
],
@@ -2011,6 +2057,52 @@
"node": ">=6"
}
},
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyglobby/node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/tree-kill": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
@@ -2057,14 +2149,18 @@
}
},
"node_modules/vite": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz",
- "integrity": "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==",
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.0.tgz",
+ "integrity": "sha512-oLnWs9Hak/LOlKjeSpOwD6JMks8BeICEdYMJBf6P4Lac/pO9tKiv/XhXnAM7nNfSkZahjlCZu9sS50zL8fSnsw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"esbuild": "^0.25.0",
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2",
"postcss": "^8.5.3",
- "rollup": "^4.30.1"
+ "rollup": "^4.34.9",
+ "tinyglobby": "^0.2.13"
},
"bin": {
"vite": "bin/vite.js"
@@ -2137,6 +2233,36 @@
"picomatch": "^2.3.1"
}
},
+ "node_modules/vite/node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
diff --git a/backend/vite.config.js b/backend/vite.config.js
index 8299591..dc94106 100644
--- a/backend/vite.config.js
+++ b/backend/vite.config.js
@@ -1,15 +1,24 @@
-// npm run dev # dev server
-// npm run build # build for production
-// npm run preview # preview production build
-
+/**
+ * Vite-Konfiguration für Backend (Thats-Me)
+ * - Domain: thats-me.test
+ * - Port: 5173
+ * - Verwendet FluxUI
+ * - Build-Verzeichnis: public/build/thats-me
+ *
+ * Starten mit: npm run dev
+ */
import { defineConfig } from "vite";
import laravel from "laravel-vite-plugin";
import tailwindcss from "@tailwindcss/vite";
-import fs from "fs";
-// Pfade zu deinen MAMP-Zertifikaten
-// const certPath = "/Applications/MAMP/Library/OpenSSL/certs/thats-me.test.crt";
-// const keyPath = "/Applications/MAMP/Library/OpenSSL/certs/thats-me.test.key";
+const httpsConfig =
+ process.env.NODE_ENV === "production"
+ ? {
+ // In Produktion: echte Zertifikate verwenden
+ key: process.env.SSL_KEY_PATH,
+ cert: process.env.SSL_CERT_PATH,
+ }
+ : true; // Self-signed für Entwicklung
export default defineConfig({
plugins: [
@@ -20,17 +29,34 @@ export default defineConfig({
tailwindcss(),
],
server: {
- // https: {
- // key: fs.readFileSync(keyPath),
- // cert: fs.readFileSync(certPath),
- // },
- cors: true, // Ergänze diese Zeile
- host: "192.168.1.8",
- port: 5173,
- hmr: {
- host: "192.168.1.8",
- protocol: "https",
+ https: false, // Traefik übernimmt SSL
+ cors: {
+ origin: ["https://thats-me.test", "https://assets.thats-me.test"],
+ credentials: true,
+ },
+ host: "0.0.0.0",
+ port: 5173,
+ strictPort: true,
+ allowedHosts: [
+ "assets.thats-me.test",
+ "thats-me.test",
+ "localhost",
+ "0.0.0.0",
+ ],
+ hmr: {
+ host: "assets.thats-me.test",
+ protocol: "wss",
+ },
+ origin: "https://assets.thats-me.test", // Ohne Port!
+ },
+ build: {
+ outDir: "public/build/thats-me",
+ assetsDir: "",
+ manifest: "manifest.json",
+ rollupOptions: {
+ output: {
+ manualChunks: undefined,
+ },
},
- origin: "https://192.168.1.8:5173",
},
});
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..3ab9c9e
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,162 @@
+services:
+ # Laravel Backend Service
+ laravel.test:
+ build:
+ context: './backend/vendor/laravel/sail/runtimes/8.4'
+ dockerfile: Dockerfile
+ args:
+ WWWGROUP: '${WWWGROUP:-20}'
+ WWWUSER: '${WWWUSER:-501}'
+ image: 'sail-8.4/app'
+ extra_hosts:
+ - 'host.docker.internal:host-gateway'
+ ports:
+ - '${VITE_PORT:-5179}:5173'
+ environment:
+ WWWUSER: '${WWWUSER:-501}'
+ WWWGROUP: '${WWWGROUP:-20}'
+ LARAVEL_SAIL: 1
+ XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}'
+ XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}'
+ IGNITION_LOCAL_SITES_PATH: '${PWD}'
+ DB_CONNECTION: mysql
+ DB_HOST: mysql
+ DB_PORT: 3306
+ DB_DATABASE: thats-me
+ DB_USERNAME: sail
+ DB_PASSWORD: password
+ MAIL_HOST: mailpit
+ MAIL_PORT: 1025
+ REDIS_HOST: redis
+ volumes:
+ - './backend:/var/www/html'
+ networks:
+ - sail
+ - proxy
+ depends_on:
+ - mysql
+ - mailpit
+ - redis
+ labels:
+ - "traefik.enable=true"
+
+ # Domain 1: Hauptdomain thats-me.test (Webseite/Landingpage)
+ - "traefik.http.routers.thatsme-main.rule=Host(`thats-me.test`)"
+ - "traefik.http.routers.thatsme-main.entrypoints=websecure"
+ - "traefik.http.routers.thatsme-main.tls=true"
+ - "traefik.http.routers.thatsme-main.service=thatsme-service"
+
+ # Domain 2: portal.thats-me.test (Admin Panel)
+ - "traefik.http.routers.thatsme-portal.rule=Host(`portal.thats-me.test`)"
+ - "traefik.http.routers.thatsme-portal.entrypoints=websecure"
+ - "traefik.http.routers.thatsme-portal.tls=true"
+ - "traefik.http.routers.thatsme-portal.service=thatsme-service"
+
+ # Domain 3: api.thats-me.test (API für Quasar App)
+ - "traefik.http.routers.thatsme-api.rule=Host(`api.thats-me.test`)"
+ - "traefik.http.routers.thatsme-api.entrypoints=websecure"
+ - "traefik.http.routers.thatsme-api.tls=true"
+ - "traefik.http.routers.thatsme-api.service=thatsme-service"
+
+ # Vite Asset Domain für Backend Development
+ - "traefik.http.routers.thatsme-assets.rule=Host(`assets.thats-me.test`)"
+ - "traefik.http.routers.thatsme-assets.entrypoints=websecure"
+ - "traefik.http.routers.thatsme-assets.tls=true"
+ - "traefik.http.routers.thatsme-assets.service=thatsme-assets-service"
+
+ # Service Definitions
+ - "traefik.http.services.thatsme-service.loadbalancer.server.port=80"
+ - "traefik.http.services.thatsme-assets-service.loadbalancer.server.port=5173"
+ - "traefik.http.services.thatsme-assets-service.loadbalancer.server.scheme=http"
+ - "traefik.docker.network=proxy"
+
+ # Quasar Frontend Service
+ quasar.app:
+ image: 'node:20-alpine'
+ working_dir: /app
+ command: sh -c "npm install && npm run dev"
+ ports:
+ - '${QUASAR_PORT:-9000}:9000'
+ environment:
+ NODE_ENV: development
+ volumes:
+ - './frontend:/app'
+ networks:
+ - sail
+ - proxy
+ labels:
+ - "traefik.enable=true"
+
+ # Domain 4: app.thats-me.test (Quasar Frontend App)
+ - "traefik.http.routers.thatsme-app.rule=Host(`app.thats-me.test`)"
+ - "traefik.http.routers.thatsme-app.entrypoints=websecure"
+ - "traefik.http.routers.thatsme-app.tls=true"
+ - "traefik.http.routers.thatsme-app.service=thatsme-app-service"
+
+ # Service Definition
+ - "traefik.http.services.thatsme-app-service.loadbalancer.server.port=9000"
+ - "traefik.http.services.thatsme-app-service.loadbalancer.server.scheme=http"
+ - "traefik.docker.network=proxy"
+
+ # MySQL Database
+ mysql:
+ image: 'mysql/mysql-server:8.0'
+ ports:
+ - '${FORWARD_DB_PORT:-33070}:3306'
+ environment:
+ MYSQL_ROOT_PASSWORD: '${DB_PASSWORD:-password}'
+ MYSQL_ROOT_HOST: '%'
+ MYSQL_DATABASE: '${DB_DATABASE:-thats-me}'
+ MYSQL_USER: '${DB_USERNAME:-sail}'
+ MYSQL_PASSWORD: '${DB_PASSWORD:-password}'
+ MYSQL_ALLOW_EMPTY_PASSWORD: 1
+ volumes:
+ - 'sail-mysql:/var/lib/mysql'
+ - './backend/vendor/laravel/sail/database/mysql/create-testing-database.sh:/docker-entrypoint-initdb.d/10-create-testing-database.sh'
+ networks:
+ - sail
+ healthcheck:
+ test:
+ - CMD
+ - mysqladmin
+ - ping
+ - '-p${DB_PASSWORD:-password}'
+ retries: 3
+ timeout: 5s
+
+ # Mailpit für E-Mail Testing
+ mailpit:
+ image: 'axllent/mailpit:latest'
+ ports:
+ - '${FORWARD_MAILPIT_PORT:-1028}:1025'
+ - '${FORWARD_MAILPIT_DASHBOARD_PORT:-8028}:8025'
+ networks:
+ - sail
+
+ # Redis Cache/Queue
+ redis:
+ image: 'redis:alpine'
+ ports:
+ - '${FORWARD_REDIS_PORT:-6383}:6379'
+ volumes:
+ - 'sail-redis:/data'
+ networks:
+ - sail
+ healthcheck:
+ test:
+ - CMD
+ - redis-cli
+ - ping
+ retries: 3
+ timeout: 5s
+
+networks:
+ sail:
+ driver: bridge
+ proxy:
+ external: true
+volumes:
+ sail-mysql:
+ driver: local
+ sail-redis:
+ driver: local
diff --git a/dot-line-system/.gitignore b/dot-line-system/.gitignore
new file mode 100644
index 0000000..a547bf3
--- /dev/null
+++ b/dot-line-system/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/dot-line-system/.history/index_20250515080205.html b/dot-line-system/.history/index_20250515080205.html
new file mode 100644
index 0000000..a991726
--- /dev/null
+++ b/dot-line-system/.history/index_20250515080205.html
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+ Connected Dots Visualization
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dot-line-system/.history/index_20250515093839.html b/dot-line-system/.history/index_20250515093839.html
new file mode 100644
index 0000000..8020d80
--- /dev/null
+++ b/dot-line-system/.history/index_20250515093839.html
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+ Connected Dots Visualization
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dot-line-system/.history/index_20250522083017.html b/dot-line-system/.history/index_20250522083017.html
new file mode 100644
index 0000000..1e0cd44
--- /dev/null
+++ b/dot-line-system/.history/index_20250522083017.html
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+
+ Connected Dots Visualization
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dot-line-system/.history/index_20250522083042.html b/dot-line-system/.history/index_20250522083042.html
new file mode 100644
index 0000000..31c10e6
--- /dev/null
+++ b/dot-line-system/.history/index_20250522083042.html
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+ Connected Dots Visualization
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dot-line-system/.history/index_20250522083155.html b/dot-line-system/.history/index_20250522083155.html
new file mode 100644
index 0000000..a4f6d5a
--- /dev/null
+++ b/dot-line-system/.history/index_20250522083155.html
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
+ Connected Dots Visualization
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dot-line-system/.history/index_20250522085953.html b/dot-line-system/.history/index_20250522085953.html
new file mode 100644
index 0000000..bc2c92b
--- /dev/null
+++ b/dot-line-system/.history/index_20250522085953.html
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+ Connected Dots Visualization
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dot-line-system/.history/index_20250522090004.html b/dot-line-system/.history/index_20250522090004.html
new file mode 100644
index 0000000..fa0ba23
--- /dev/null
+++ b/dot-line-system/.history/index_20250522090004.html
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+ Connected Dots Visualization
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dot-line-system/.history/index_20250522090012.html b/dot-line-system/.history/index_20250522090012.html
new file mode 100644
index 0000000..f556919
--- /dev/null
+++ b/dot-line-system/.history/index_20250522090012.html
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+ Connected Dots Visualization
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dot-line-system/.history/index_20250522090611.html b/dot-line-system/.history/index_20250522090611.html
new file mode 100644
index 0000000..014c674
--- /dev/null
+++ b/dot-line-system/.history/index_20250522090611.html
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+ Connected Dots Visualization
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dot-line-system/.history/index_20250522090656.html b/dot-line-system/.history/index_20250522090656.html
new file mode 100644
index 0000000..014c674
--- /dev/null
+++ b/dot-line-system/.history/index_20250522090656.html
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+ Connected Dots Visualization
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dot-line-system/.history/index_20250522090717.html b/dot-line-system/.history/index_20250522090717.html
new file mode 100644
index 0000000..bc7028c
--- /dev/null
+++ b/dot-line-system/.history/index_20250522090717.html
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+ Connected Dots Visualization
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dot-line-system/.history/index_20250522090730.html b/dot-line-system/.history/index_20250522090730.html
new file mode 100644
index 0000000..014c674
--- /dev/null
+++ b/dot-line-system/.history/index_20250522090730.html
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+ Connected Dots Visualization
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dot-line-system/.history/index_20250522095109.html b/dot-line-system/.history/index_20250522095109.html
new file mode 100644
index 0000000..d03862f
--- /dev/null
+++ b/dot-line-system/.history/index_20250522095109.html
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+ Connected Dots Visualization
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dot-line-system/.history/index_20250522114322.html b/dot-line-system/.history/index_20250522114322.html
new file mode 100644
index 0000000..d03862f
--- /dev/null
+++ b/dot-line-system/.history/index_20250522114322.html
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+ Connected Dots Visualization
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dot-line-system/.history/index_20250522114359.html b/dot-line-system/.history/index_20250522114359.html
new file mode 100644
index 0000000..4112aaf
--- /dev/null
+++ b/dot-line-system/.history/index_20250522114359.html
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+ Connected Dots Visualization
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dot-line-system/.history/index_20250522114425.html b/dot-line-system/.history/index_20250522114425.html
new file mode 100644
index 0000000..d03862f
--- /dev/null
+++ b/dot-line-system/.history/index_20250522114425.html
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+ Connected Dots Visualization
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dot-line-system/.history/index_20250522114819.html b/dot-line-system/.history/index_20250522114819.html
new file mode 100644
index 0000000..d03862f
--- /dev/null
+++ b/dot-line-system/.history/index_20250522114819.html
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+ Connected Dots Visualization
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dot-line-system/.history/index_20250522130240.html b/dot-line-system/.history/index_20250522130240.html
new file mode 100644
index 0000000..d03862f
--- /dev/null
+++ b/dot-line-system/.history/index_20250522130240.html
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+ Connected Dots Visualization
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dot-line-system/.history/index_20250522130258.html b/dot-line-system/.history/index_20250522130258.html
new file mode 100644
index 0000000..d03862f
--- /dev/null
+++ b/dot-line-system/.history/index_20250522130258.html
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+ Connected Dots Visualization
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dot-line-system/.history/index_20250522130455.html b/dot-line-system/.history/index_20250522130455.html
new file mode 100644
index 0000000..d03862f
--- /dev/null
+++ b/dot-line-system/.history/index_20250522130455.html
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+ Connected Dots Visualization
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dot-line-system/.history/index_20250522130505.html b/dot-line-system/.history/index_20250522130505.html
new file mode 100644
index 0000000..6adfe7f
--- /dev/null
+++ b/dot-line-system/.history/index_20250522130505.html
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+ Connected Dots Visualization
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dot-line-system/.history/index_20250522130518.html b/dot-line-system/.history/index_20250522130518.html
new file mode 100644
index 0000000..9309940
--- /dev/null
+++ b/dot-line-system/.history/index_20250522130518.html
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+ Connected Dots Visualization
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dot-line-system/.history/index_20250522130539.html b/dot-line-system/.history/index_20250522130539.html
new file mode 100644
index 0000000..9309940
--- /dev/null
+++ b/dot-line-system/.history/index_20250522130539.html
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+ Connected Dots Visualization
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dot-line-system/.history/index_20250522130613.html b/dot-line-system/.history/index_20250522130613.html
new file mode 100644
index 0000000..9309940
--- /dev/null
+++ b/dot-line-system/.history/index_20250522130613.html
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+ Connected Dots Visualization
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dot-line-system/.history/index_20250522132851.html b/dot-line-system/.history/index_20250522132851.html
new file mode 100644
index 0000000..77ca0c9
--- /dev/null
+++ b/dot-line-system/.history/index_20250522132851.html
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+ Connected Dots Visualization
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dot-line-system/.history/index_20250522133036.html b/dot-line-system/.history/index_20250522133036.html
new file mode 100644
index 0000000..77ca0c9
--- /dev/null
+++ b/dot-line-system/.history/index_20250522133036.html
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+ Connected Dots Visualization
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dot-line-system/.history/index_20250522222202.html b/dot-line-system/.history/index_20250522222202.html
new file mode 100644
index 0000000..77ca0c9
--- /dev/null
+++ b/dot-line-system/.history/index_20250522222202.html
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+ Connected Dots Visualization
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dot-line-system/.history/index_20250522222233.html b/dot-line-system/.history/index_20250522222233.html
new file mode 100644
index 0000000..415baa6
--- /dev/null
+++ b/dot-line-system/.history/index_20250522222233.html
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+ Life Line
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dot-line-system/.history/package_20250515093313.json b/dot-line-system/.history/package_20250515093313.json
new file mode 100644
index 0000000..6d93786
--- /dev/null
+++ b/dot-line-system/.history/package_20250515093313.json
@@ -0,0 +1,15 @@
+{
+ "name": "dot-line-system",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "devDependencies": {
+ "typescript": "~5.7.2",
+ "vite": "^6.3.5"
+ }
+}
diff --git a/dot-line-system/.history/package_20250515093412.json b/dot-line-system/.history/package_20250515093412.json
new file mode 100644
index 0000000..9c7aefd
--- /dev/null
+++ b/dot-line-system/.history/package_20250515093412.json
@@ -0,0 +1,20 @@
+{
+ "name": "dot-line-system",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "devDependencies": {
+ "typescript": "~5.7.2",
+ "vite": "^6.3.5"
+ },
+ "scripts": {
+ "build": "vite build",
+ "start": "vite",
+ "tsc": "tsc"
+}
+}
diff --git a/dot-line-system/.history/package_20250515093415.json b/dot-line-system/.history/package_20250515093415.json
new file mode 100644
index 0000000..882780f
--- /dev/null
+++ b/dot-line-system/.history/package_20250515093415.json
@@ -0,0 +1,20 @@
+{
+ "name": "dot-line-system",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "devDependencies": {
+ "typescript": "~5.7.2",
+ "vite": "^6.3.5"
+ },
+ "scripts": {
+ "build": "vite build",
+ "start": "vite",
+ "tsc": "tsc"
+ }
+}
diff --git a/dot-line-system/.history/readme_20250521221324.md b/dot-line-system/.history/readme_20250521221324.md
new file mode 100644
index 0000000..e69de29
diff --git a/dot-line-system/.history/readme_20250521221329.md b/dot-line-system/.history/readme_20250521221329.md
new file mode 100644
index 0000000..b896a08
--- /dev/null
+++ b/dot-line-system/.history/readme_20250521221329.md
@@ -0,0 +1 @@
+npm run dev
\ No newline at end of file
diff --git a/dot-line-system/.history/readme_20250521221343.md b/dot-line-system/.history/readme_20250521221343.md
new file mode 100644
index 0000000..c2d989b
--- /dev/null
+++ b/dot-line-system/.history/readme_20250521221343.md
@@ -0,0 +1,2 @@
+*Start the project*
+npm run dev
\ No newline at end of file
diff --git a/dot-line-system/.history/readme_20250521221558.md b/dot-line-system/.history/readme_20250521221558.md
new file mode 100644
index 0000000..509d6b0
--- /dev/null
+++ b/dot-line-system/.history/readme_20250521221558.md
@@ -0,0 +1,3 @@
+**Start the project**
+
+npm run dev
\ No newline at end of file
diff --git a/dot-line-system/.history/readme_20250521221603.md b/dot-line-system/.history/readme_20250521221603.md
new file mode 100644
index 0000000..ca40a30
--- /dev/null
+++ b/dot-line-system/.history/readme_20250521221603.md
@@ -0,0 +1,2 @@
+**Start the project**
+npm run dev
\ No newline at end of file
diff --git a/dot-line-system/.history/readme_20250521221932.md b/dot-line-system/.history/readme_20250521221932.md
new file mode 100644
index 0000000..46e160d
--- /dev/null
+++ b/dot-line-system/.history/readme_20250521221932.md
@@ -0,0 +1,4 @@
+**Start the project**
+npm install
+
+npm run
\ No newline at end of file
diff --git a/dot-line-system/.history/readme_20250522081835.md b/dot-line-system/.history/readme_20250522081835.md
new file mode 100644
index 0000000..16dac6d
--- /dev/null
+++ b/dot-line-system/.history/readme_20250522081835.md
@@ -0,0 +1,7 @@
+**Prepare the project**
+npm install
+
+**Start the project**
+npm start
+
+http://localhost:5173/
\ No newline at end of file
diff --git a/dot-line-system/.history/readme_20250522081843.md b/dot-line-system/.history/readme_20250522081843.md
new file mode 100644
index 0000000..2d357b2
--- /dev/null
+++ b/dot-line-system/.history/readme_20250522081843.md
@@ -0,0 +1,8 @@
+**Prepare the project**
+npm install
+
+**Start the project**
+npm start
+
+**Aufrufen**
+http://localhost:5173/
\ No newline at end of file
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250515080205.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250515080205.ts
new file mode 100644
index 0000000..67a6b1d
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250515080205.ts
@@ -0,0 +1,545 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+
+ private preloadedImages: Map = new Map();
+
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+
+ constructor(containerId: string, dots: DotConfig[], config?: Partial) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map(dot => dot.x));
+ const maxX = Math.max(...this.dots.map(dot => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 200,
+ tooltipHeight: 150,
+ ...config
+ };
+
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+
+ // Create SVG elements
+ this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+ this.gridGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ this.curvePath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ this.dotsGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ this.tooltipGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter(dot => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map(dot => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+
+ // Create a loading indicator (optional)
+ const loadingCount = {current: 0, total: imageUrls.length};
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log('All images preloaded successfully');
+ }
+ };
+
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+
+ // Set src to start loading
+ img.src = url;
+
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ };
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = 'connected-dots-styles';
+
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement('style');
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute('width', `${this.config.totalWidth}`);
+ this.svg.setAttribute('height', `${this.config.height}`);
+ this.svg.style.overflow = 'visible';
+ this.scrollContainer.appendChild(this.svg);
+
+ // Configure grid group
+ this.gridGroup.classList.add('grid');
+ this.svg.appendChild(this.gridGroup);
+
+ // Configure curve path
+ this.curvePath.setAttribute('fill', 'none');
+ this.curvePath.setAttribute('stroke', 'white');
+ this.curvePath.setAttribute('stroke-width', '2');
+ this.curvePath.setAttribute('stroke-linecap', 'round');
+ this.curvePath.classList.add('curve-path');
+ this.svg.appendChild(this.curvePath);
+
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add('tooltips');
+ this.svg.appendChild(this.tooltipGroup);
+ }
+
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * (this.config.height / 2 * 0.8);
+
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+
+ private calculateBezierControlPoints(dots: DotConfig[], index: number): ControlPoints {
+ const tension = this.config.tension * 150; // Scale tension for Bezier curve
+
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+
+ return { x1, y1, x2, y2 };
+ }
+
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return '';
+
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(this.dots[0].value)}`;
+
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(this.dots, i);
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+
+ return path;
+ }
+
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+
+ if (!this.config.showGrid) return;
+
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+ line.setAttribute('x1', '0');
+ line.setAttribute('y1', this.getDotY(value).toString());
+ line.setAttribute('x2', this.config.totalWidth.toString());
+ line.setAttribute('y2', this.getDotY(value).toString());
+ line.setAttribute('stroke', 'rgba(219, 39, 119, 0.4)');
+ line.setAttribute('stroke-width', '1');
+ this.gridGroup.appendChild(line);
+
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ text.setAttribute('x', '10');
+ text.setAttribute('y', (this.getDotY(value) + 4).toString());
+ text.setAttribute('fill', 'rgba(219, 39, 119, 0.8)');
+ text.setAttribute('font-size', '12');
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+
+ // Vertical grid lines
+ const numVertLines = Math.ceil(this.config.totalWidth / this.config.xUnitSize);
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+ line.setAttribute('x1', x.toString());
+ line.setAttribute('y1', '0');
+ line.setAttribute('x2', x.toString());
+ line.setAttribute('y2', this.config.height.toString());
+ line.setAttribute('stroke', 'rgba(219, 39, 119, 0.4)');
+ line.setAttribute('stroke-width', '1');
+ this.gridGroup.appendChild(line);
+
+ if (xValue !== 0) {
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ text.setAttribute('x', x.toString());
+ text.setAttribute('y', (this.config.height / 2 + 20).toString());
+ text.setAttribute('fill', 'rgba(219, 39, 119, 0.8)');
+ text.setAttribute('font-size', '12');
+ text.setAttribute('text-anchor', 'middle');
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ tooltip.classList.add('dot-tooltip');
+ tooltip.setAttribute('data-dot-id', dot.id.toString());
+
+ // Calculate tooltip position
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipHeight = this.config.tooltipHeight;
+ const tooltipX = x - tooltipWidth / 2;
+
+ // Calculate tooltip Y position, ensuring it stays within the container
+ let tooltipY = y - tooltipHeight - 20; // Position above the dot with some spacing
+
+ // Ensure tooltip doesn't go above the container
+ tooltipY = Math.max(tooltipY, 10); // Keep at least 10px from the top
+
+ // Background rectangle
+ const bg = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+ bg.setAttribute('x', tooltipX.toString());
+ bg.setAttribute('y', tooltipY.toString());
+ bg.setAttribute('width', tooltipWidth.toString());
+ bg.setAttribute('height', tooltipHeight.toString());
+ bg.setAttribute('rx', '5');
+ bg.setAttribute('ry', '5');
+ bg.setAttribute('fill', 'rgba(0, 0, 0, 0.8)');
+ tooltip.appendChild(bg);
+
+ // Tooltip arrow (pointing to the dot)
+ const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ arrow.setAttribute('d', `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${tooltipY + tooltipHeight - 10} L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`);
+ arrow.setAttribute('fill', 'rgba(0, 0, 0, 0.8)');
+ tooltip.appendChild(arrow);
+
+ // Image (if provided)
+ if (dot.imageUrl) {
+ const imgContainer = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ imgContainer.setAttribute('x', (tooltipX + 10).toString());
+ imgContainer.setAttribute('y', (tooltipY + 10).toString());
+ imgContainer.setAttribute('width', (tooltipWidth - 20).toString());
+ imgContainer.setAttribute('height', (tooltipHeight / 2).toString());
+
+ const img = document.createElement('img');
+ img.src = dot.imageUrl;
+ img.className = 'tooltip-img';
+
+ imgContainer.appendChild(img);
+ tooltip.appendChild(imgContainer);
+ }
+
+ // Title (if provided)
+ if (dot.title) {
+ const title = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ title.setAttribute('x', (tooltipX + 10).toString());
+ title.setAttribute('y', dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 26).toString()
+ : (tooltipY + 25).toString());
+ title.setAttribute('fill', 'white');
+ title.setAttribute('font-size', '14');
+ title.setAttribute('font-weight', 'bold');
+ title.textContent = dot.title;
+ tooltip.appendChild(title);
+ }
+
+ // Description (if provided)
+ if (dot.description) {
+ const descriptionFO = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ descriptionFO.setAttribute('x', (tooltipX + 10).toString());
+ descriptionFO.setAttribute('y', dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 32).toString()
+ : dot.title
+ ? (tooltipY + 35).toString()
+ : (tooltipY + 15).toString());
+ descriptionFO.setAttribute('width', (tooltipWidth - 20).toString());
+ descriptionFO.setAttribute('height', (tooltipHeight / 2 - 10).toString());
+
+ const descriptionDiv = document.createElement('div');
+ descriptionDiv.style.color = 'white';
+ descriptionDiv.style.fontSize = '12px';
+ descriptionDiv.style.overflow = 'hidden';
+ descriptionDiv.textContent = dot.description;
+
+ descriptionFO.appendChild(descriptionDiv);
+ tooltip.appendChild(descriptionFO);
+ }
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute('d', pathData);
+ }
+
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ return { leftmost, rightmost };
+ }
+
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+
+ const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
+ circle.setAttribute('cx', x.toString());
+ circle.setAttribute('cy', y.toString());
+ circle.setAttribute('r', this.config.dotRadius.toString());
+ circle.setAttribute('fill', 'white');
+ circle.setAttribute('data-dot-id', dot.id.toString());
+ circle.classList.add('dot');
+
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener('click', () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error('Dot has no link');
+ throw new Error('Dot has no link');
+ }
+ });
+ }
+
+ this.dotsGroup.appendChild(circle);
+ };
+ }
+
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute('width', `${this.config.totalWidth}`);
+
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map(dot => dot.x));
+ const maxX = Math.max(...this.dots.map(dot => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute('height', `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250515094002.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250515094002.ts
new file mode 100644
index 0000000..55596ee
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250515094002.ts
@@ -0,0 +1,545 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+
+ private preloadedImages: Map = new Map();
+
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+
+ constructor(containerId: string, dots: DotConfig[], config?: Partial) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 200;
+ let calculatedWidth = 0;
+
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map(dot => dot.x));
+ const maxX = Math.max(...this.dots.map(dot => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 200,
+ tooltipHeight: 150,
+ ...config
+ };
+
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+
+ // Create SVG elements
+ this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+ this.gridGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ this.curvePath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ this.dotsGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ this.tooltipGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter(dot => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map(dot => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+
+ // Create a loading indicator (optional)
+ const loadingCount = {current: 0, total: imageUrls.length};
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log('All images preloaded successfully');
+ }
+ };
+
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+
+ // Set src to start loading
+ img.src = url;
+
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ };
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = 'connected-dots-styles';
+
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement('style');
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute('width', `${this.config.totalWidth}`);
+ this.svg.setAttribute('height', `${this.config.height}`);
+ this.svg.style.overflow = 'visible';
+ this.scrollContainer.appendChild(this.svg);
+
+ // Configure grid group
+ this.gridGroup.classList.add('grid');
+ this.svg.appendChild(this.gridGroup);
+
+ // Configure curve path
+ this.curvePath.setAttribute('fill', 'none');
+ this.curvePath.setAttribute('stroke', 'white');
+ this.curvePath.setAttribute('stroke-width', '2');
+ this.curvePath.setAttribute('stroke-linecap', 'round');
+ this.curvePath.classList.add('curve-path');
+ this.svg.appendChild(this.curvePath);
+
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add('tooltips');
+ this.svg.appendChild(this.tooltipGroup);
+ }
+
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * (this.config.height / 2 * 0.8);
+
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+
+ private calculateBezierControlPoints(dots: DotConfig[], index: number): ControlPoints {
+ const tension = this.config.tension * 150; // Scale tension for Bezier curve
+
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+
+ return { x1, y1, x2, y2 };
+ }
+
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return '';
+
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(this.dots[0].value)}`;
+
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(this.dots, i);
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+
+ return path;
+ }
+
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+
+ if (!this.config.showGrid) return;
+
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+ line.setAttribute('x1', '0');
+ line.setAttribute('y1', this.getDotY(value).toString());
+ line.setAttribute('x2', this.config.totalWidth.toString());
+ line.setAttribute('y2', this.getDotY(value).toString());
+ line.setAttribute('stroke', 'rgba(219, 39, 119, 0.4)');
+ line.setAttribute('stroke-width', '1');
+ this.gridGroup.appendChild(line);
+
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ text.setAttribute('x', '10');
+ text.setAttribute('y', (this.getDotY(value) + 4).toString());
+ text.setAttribute('fill', 'rgba(219, 39, 119, 0.8)');
+ text.setAttribute('font-size', '12');
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+
+ // Vertical grid lines
+ const numVertLines = Math.ceil(this.config.totalWidth / this.config.xUnitSize);
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+ line.setAttribute('x1', x.toString());
+ line.setAttribute('y1', '0');
+ line.setAttribute('x2', x.toString());
+ line.setAttribute('y2', this.config.height.toString());
+ line.setAttribute('stroke', 'rgba(219, 39, 119, 0.4)');
+ line.setAttribute('stroke-width', '1');
+ this.gridGroup.appendChild(line);
+
+ if (xValue !== 0) {
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ text.setAttribute('x', x.toString());
+ text.setAttribute('y', (this.config.height / 2 + 20).toString());
+ text.setAttribute('fill', 'rgba(219, 39, 119, 0.8)');
+ text.setAttribute('font-size', '12');
+ text.setAttribute('text-anchor', 'middle');
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ tooltip.classList.add('dot-tooltip');
+ tooltip.setAttribute('data-dot-id', dot.id.toString());
+
+ // Calculate tooltip position
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipHeight = this.config.tooltipHeight;
+ const tooltipX = x - tooltipWidth / 2;
+
+ // Calculate tooltip Y position, ensuring it stays within the container
+ let tooltipY = y - tooltipHeight - 20; // Position above the dot with some spacing
+
+ // Ensure tooltip doesn't go above the container
+ tooltipY = Math.max(tooltipY, 10); // Keep at least 10px from the top
+
+ // Background rectangle
+ const bg = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+ bg.setAttribute('x', tooltipX.toString());
+ bg.setAttribute('y', tooltipY.toString());
+ bg.setAttribute('width', tooltipWidth.toString());
+ bg.setAttribute('height', tooltipHeight.toString());
+ bg.setAttribute('rx', '5');
+ bg.setAttribute('ry', '5');
+ bg.setAttribute('fill', 'rgba(0, 0, 0, 0.8)');
+ tooltip.appendChild(bg);
+
+ // Tooltip arrow (pointing to the dot)
+ const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ arrow.setAttribute('d', `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${tooltipY + tooltipHeight - 10} L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`);
+ arrow.setAttribute('fill', 'rgba(0, 0, 0, 0.8)');
+ tooltip.appendChild(arrow);
+
+ // Image (if provided)
+ if (dot.imageUrl) {
+ const imgContainer = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ imgContainer.setAttribute('x', (tooltipX + 10).toString());
+ imgContainer.setAttribute('y', (tooltipY + 10).toString());
+ imgContainer.setAttribute('width', (tooltipWidth - 20).toString());
+ imgContainer.setAttribute('height', (tooltipHeight / 2).toString());
+
+ const img = document.createElement('img');
+ img.src = dot.imageUrl;
+ img.className = 'tooltip-img';
+
+ imgContainer.appendChild(img);
+ tooltip.appendChild(imgContainer);
+ }
+
+ // Title (if provided)
+ if (dot.title) {
+ const title = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ title.setAttribute('x', (tooltipX + 10).toString());
+ title.setAttribute('y', dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 26).toString()
+ : (tooltipY + 25).toString());
+ title.setAttribute('fill', 'white');
+ title.setAttribute('font-size', '14');
+ title.setAttribute('font-weight', 'bold');
+ title.textContent = dot.title;
+ tooltip.appendChild(title);
+ }
+
+ // Description (if provided)
+ if (dot.description) {
+ const descriptionFO = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ descriptionFO.setAttribute('x', (tooltipX + 10).toString());
+ descriptionFO.setAttribute('y', dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 32).toString()
+ : dot.title
+ ? (tooltipY + 35).toString()
+ : (tooltipY + 15).toString());
+ descriptionFO.setAttribute('width', (tooltipWidth - 20).toString());
+ descriptionFO.setAttribute('height', (tooltipHeight / 2 - 10).toString());
+
+ const descriptionDiv = document.createElement('div');
+ descriptionDiv.style.color = 'white';
+ descriptionDiv.style.fontSize = '12px';
+ descriptionDiv.style.overflow = 'hidden';
+ descriptionDiv.textContent = dot.description;
+
+ descriptionFO.appendChild(descriptionDiv);
+ tooltip.appendChild(descriptionFO);
+ }
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute('d', pathData);
+ }
+
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ return { leftmost, rightmost };
+ }
+
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+
+ const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
+ circle.setAttribute('cx', x.toString());
+ circle.setAttribute('cy', y.toString());
+ circle.setAttribute('r', this.config.dotRadius.toString());
+ circle.setAttribute('fill', 'white');
+ circle.setAttribute('data-dot-id', dot.id.toString());
+ circle.classList.add('dot');
+
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener('click', () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error('Dot has no link');
+ throw new Error('Dot has no link');
+ }
+ });
+ }
+
+ this.dotsGroup.appendChild(circle);
+ };
+ }
+
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute('width', `${this.config.totalWidth}`);
+
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map(dot => dot.x));
+ const maxX = Math.max(...this.dots.map(dot => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute('height', `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250515094010.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250515094010.ts
new file mode 100644
index 0000000..430cc96
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250515094010.ts
@@ -0,0 +1,545 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+
+ private preloadedImages: Map = new Map();
+
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+
+ constructor(containerId: string, dots: DotConfig[], config?: Partial) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 160;
+ let calculatedWidth = 0;
+
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map(dot => dot.x));
+ const maxX = Math.max(...this.dots.map(dot => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 200,
+ tooltipHeight: 150,
+ ...config
+ };
+
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+
+ // Create SVG elements
+ this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+ this.gridGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ this.curvePath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ this.dotsGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ this.tooltipGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter(dot => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map(dot => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+
+ // Create a loading indicator (optional)
+ const loadingCount = {current: 0, total: imageUrls.length};
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log('All images preloaded successfully');
+ }
+ };
+
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+
+ // Set src to start loading
+ img.src = url;
+
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ };
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = 'connected-dots-styles';
+
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement('style');
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute('width', `${this.config.totalWidth}`);
+ this.svg.setAttribute('height', `${this.config.height}`);
+ this.svg.style.overflow = 'visible';
+ this.scrollContainer.appendChild(this.svg);
+
+ // Configure grid group
+ this.gridGroup.classList.add('grid');
+ this.svg.appendChild(this.gridGroup);
+
+ // Configure curve path
+ this.curvePath.setAttribute('fill', 'none');
+ this.curvePath.setAttribute('stroke', 'white');
+ this.curvePath.setAttribute('stroke-width', '2');
+ this.curvePath.setAttribute('stroke-linecap', 'round');
+ this.curvePath.classList.add('curve-path');
+ this.svg.appendChild(this.curvePath);
+
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add('tooltips');
+ this.svg.appendChild(this.tooltipGroup);
+ }
+
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * (this.config.height / 2 * 0.8);
+
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+
+ private calculateBezierControlPoints(dots: DotConfig[], index: number): ControlPoints {
+ const tension = this.config.tension * 150; // Scale tension for Bezier curve
+
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+
+ return { x1, y1, x2, y2 };
+ }
+
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return '';
+
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(this.dots[0].value)}`;
+
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(this.dots, i);
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+
+ return path;
+ }
+
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+
+ if (!this.config.showGrid) return;
+
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+ line.setAttribute('x1', '0');
+ line.setAttribute('y1', this.getDotY(value).toString());
+ line.setAttribute('x2', this.config.totalWidth.toString());
+ line.setAttribute('y2', this.getDotY(value).toString());
+ line.setAttribute('stroke', 'rgba(219, 39, 119, 0.4)');
+ line.setAttribute('stroke-width', '1');
+ this.gridGroup.appendChild(line);
+
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ text.setAttribute('x', '10');
+ text.setAttribute('y', (this.getDotY(value) + 4).toString());
+ text.setAttribute('fill', 'rgba(219, 39, 119, 0.8)');
+ text.setAttribute('font-size', '12');
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+
+ // Vertical grid lines
+ const numVertLines = Math.ceil(this.config.totalWidth / this.config.xUnitSize);
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+ line.setAttribute('x1', x.toString());
+ line.setAttribute('y1', '0');
+ line.setAttribute('x2', x.toString());
+ line.setAttribute('y2', this.config.height.toString());
+ line.setAttribute('stroke', 'rgba(219, 39, 119, 0.4)');
+ line.setAttribute('stroke-width', '1');
+ this.gridGroup.appendChild(line);
+
+ if (xValue !== 0) {
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ text.setAttribute('x', x.toString());
+ text.setAttribute('y', (this.config.height / 2 + 20).toString());
+ text.setAttribute('fill', 'rgba(219, 39, 119, 0.8)');
+ text.setAttribute('font-size', '12');
+ text.setAttribute('text-anchor', 'middle');
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ tooltip.classList.add('dot-tooltip');
+ tooltip.setAttribute('data-dot-id', dot.id.toString());
+
+ // Calculate tooltip position
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipHeight = this.config.tooltipHeight;
+ const tooltipX = x - tooltipWidth / 2;
+
+ // Calculate tooltip Y position, ensuring it stays within the container
+ let tooltipY = y - tooltipHeight - 20; // Position above the dot with some spacing
+
+ // Ensure tooltip doesn't go above the container
+ tooltipY = Math.max(tooltipY, 10); // Keep at least 10px from the top
+
+ // Background rectangle
+ const bg = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+ bg.setAttribute('x', tooltipX.toString());
+ bg.setAttribute('y', tooltipY.toString());
+ bg.setAttribute('width', tooltipWidth.toString());
+ bg.setAttribute('height', tooltipHeight.toString());
+ bg.setAttribute('rx', '5');
+ bg.setAttribute('ry', '5');
+ bg.setAttribute('fill', 'rgba(0, 0, 0, 0.8)');
+ tooltip.appendChild(bg);
+
+ // Tooltip arrow (pointing to the dot)
+ const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ arrow.setAttribute('d', `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${tooltipY + tooltipHeight - 10} L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`);
+ arrow.setAttribute('fill', 'rgba(0, 0, 0, 0.8)');
+ tooltip.appendChild(arrow);
+
+ // Image (if provided)
+ if (dot.imageUrl) {
+ const imgContainer = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ imgContainer.setAttribute('x', (tooltipX + 10).toString());
+ imgContainer.setAttribute('y', (tooltipY + 10).toString());
+ imgContainer.setAttribute('width', (tooltipWidth - 20).toString());
+ imgContainer.setAttribute('height', (tooltipHeight / 2).toString());
+
+ const img = document.createElement('img');
+ img.src = dot.imageUrl;
+ img.className = 'tooltip-img';
+
+ imgContainer.appendChild(img);
+ tooltip.appendChild(imgContainer);
+ }
+
+ // Title (if provided)
+ if (dot.title) {
+ const title = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ title.setAttribute('x', (tooltipX + 10).toString());
+ title.setAttribute('y', dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 26).toString()
+ : (tooltipY + 25).toString());
+ title.setAttribute('fill', 'white');
+ title.setAttribute('font-size', '14');
+ title.setAttribute('font-weight', 'bold');
+ title.textContent = dot.title;
+ tooltip.appendChild(title);
+ }
+
+ // Description (if provided)
+ if (dot.description) {
+ const descriptionFO = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ descriptionFO.setAttribute('x', (tooltipX + 10).toString());
+ descriptionFO.setAttribute('y', dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 32).toString()
+ : dot.title
+ ? (tooltipY + 35).toString()
+ : (tooltipY + 15).toString());
+ descriptionFO.setAttribute('width', (tooltipWidth - 20).toString());
+ descriptionFO.setAttribute('height', (tooltipHeight / 2 - 10).toString());
+
+ const descriptionDiv = document.createElement('div');
+ descriptionDiv.style.color = 'white';
+ descriptionDiv.style.fontSize = '12px';
+ descriptionDiv.style.overflow = 'hidden';
+ descriptionDiv.textContent = dot.description;
+
+ descriptionFO.appendChild(descriptionDiv);
+ tooltip.appendChild(descriptionFO);
+ }
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute('d', pathData);
+ }
+
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ return { leftmost, rightmost };
+ }
+
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+
+ const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
+ circle.setAttribute('cx', x.toString());
+ circle.setAttribute('cy', y.toString());
+ circle.setAttribute('r', this.config.dotRadius.toString());
+ circle.setAttribute('fill', 'white');
+ circle.setAttribute('data-dot-id', dot.id.toString());
+ circle.classList.add('dot');
+
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener('click', () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error('Dot has no link');
+ throw new Error('Dot has no link');
+ }
+ });
+ }
+
+ this.dotsGroup.appendChild(circle);
+ };
+ }
+
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute('width', `${this.config.totalWidth}`);
+
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map(dot => dot.x));
+ const maxX = Math.max(...this.dots.map(dot => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute('height', `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250515094030.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250515094030.ts
new file mode 100644
index 0000000..c9bbcbb
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250515094030.ts
@@ -0,0 +1,545 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+
+ private preloadedImages: Map = new Map();
+
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+
+ constructor(containerId: string, dots: DotConfig[], config?: Partial) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 160;
+ let calculatedWidth = 0;
+
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map(dot => dot.x));
+ const maxX = Math.max(...this.dots.map(dot => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 10) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 200,
+ tooltipHeight: 150,
+ ...config
+ };
+
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+
+ // Create SVG elements
+ this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+ this.gridGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ this.curvePath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ this.dotsGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ this.tooltipGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter(dot => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map(dot => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+
+ // Create a loading indicator (optional)
+ const loadingCount = {current: 0, total: imageUrls.length};
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log('All images preloaded successfully');
+ }
+ };
+
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+
+ // Set src to start loading
+ img.src = url;
+
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ };
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = 'connected-dots-styles';
+
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement('style');
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute('width', `${this.config.totalWidth}`);
+ this.svg.setAttribute('height', `${this.config.height}`);
+ this.svg.style.overflow = 'visible';
+ this.scrollContainer.appendChild(this.svg);
+
+ // Configure grid group
+ this.gridGroup.classList.add('grid');
+ this.svg.appendChild(this.gridGroup);
+
+ // Configure curve path
+ this.curvePath.setAttribute('fill', 'none');
+ this.curvePath.setAttribute('stroke', 'white');
+ this.curvePath.setAttribute('stroke-width', '2');
+ this.curvePath.setAttribute('stroke-linecap', 'round');
+ this.curvePath.classList.add('curve-path');
+ this.svg.appendChild(this.curvePath);
+
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add('tooltips');
+ this.svg.appendChild(this.tooltipGroup);
+ }
+
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * (this.config.height / 2 * 0.8);
+
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+
+ private calculateBezierControlPoints(dots: DotConfig[], index: number): ControlPoints {
+ const tension = this.config.tension * 150; // Scale tension for Bezier curve
+
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+
+ return { x1, y1, x2, y2 };
+ }
+
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return '';
+
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(this.dots[0].value)}`;
+
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(this.dots, i);
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+
+ return path;
+ }
+
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+
+ if (!this.config.showGrid) return;
+
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+ line.setAttribute('x1', '0');
+ line.setAttribute('y1', this.getDotY(value).toString());
+ line.setAttribute('x2', this.config.totalWidth.toString());
+ line.setAttribute('y2', this.getDotY(value).toString());
+ line.setAttribute('stroke', 'rgba(219, 39, 119, 0.4)');
+ line.setAttribute('stroke-width', '1');
+ this.gridGroup.appendChild(line);
+
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ text.setAttribute('x', '10');
+ text.setAttribute('y', (this.getDotY(value) + 4).toString());
+ text.setAttribute('fill', 'rgba(219, 39, 119, 0.8)');
+ text.setAttribute('font-size', '12');
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+
+ // Vertical grid lines
+ const numVertLines = Math.ceil(this.config.totalWidth / this.config.xUnitSize);
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+ line.setAttribute('x1', x.toString());
+ line.setAttribute('y1', '0');
+ line.setAttribute('x2', x.toString());
+ line.setAttribute('y2', this.config.height.toString());
+ line.setAttribute('stroke', 'rgba(219, 39, 119, 0.4)');
+ line.setAttribute('stroke-width', '1');
+ this.gridGroup.appendChild(line);
+
+ if (xValue !== 0) {
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ text.setAttribute('x', x.toString());
+ text.setAttribute('y', (this.config.height / 2 + 20).toString());
+ text.setAttribute('fill', 'rgba(219, 39, 119, 0.8)');
+ text.setAttribute('font-size', '12');
+ text.setAttribute('text-anchor', 'middle');
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ tooltip.classList.add('dot-tooltip');
+ tooltip.setAttribute('data-dot-id', dot.id.toString());
+
+ // Calculate tooltip position
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipHeight = this.config.tooltipHeight;
+ const tooltipX = x - tooltipWidth / 2;
+
+ // Calculate tooltip Y position, ensuring it stays within the container
+ let tooltipY = y - tooltipHeight - 20; // Position above the dot with some spacing
+
+ // Ensure tooltip doesn't go above the container
+ tooltipY = Math.max(tooltipY, 10); // Keep at least 10px from the top
+
+ // Background rectangle
+ const bg = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+ bg.setAttribute('x', tooltipX.toString());
+ bg.setAttribute('y', tooltipY.toString());
+ bg.setAttribute('width', tooltipWidth.toString());
+ bg.setAttribute('height', tooltipHeight.toString());
+ bg.setAttribute('rx', '5');
+ bg.setAttribute('ry', '5');
+ bg.setAttribute('fill', 'rgba(0, 0, 0, 0.8)');
+ tooltip.appendChild(bg);
+
+ // Tooltip arrow (pointing to the dot)
+ const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ arrow.setAttribute('d', `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${tooltipY + tooltipHeight - 10} L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`);
+ arrow.setAttribute('fill', 'rgba(0, 0, 0, 0.8)');
+ tooltip.appendChild(arrow);
+
+ // Image (if provided)
+ if (dot.imageUrl) {
+ const imgContainer = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ imgContainer.setAttribute('x', (tooltipX + 10).toString());
+ imgContainer.setAttribute('y', (tooltipY + 10).toString());
+ imgContainer.setAttribute('width', (tooltipWidth - 20).toString());
+ imgContainer.setAttribute('height', (tooltipHeight / 2).toString());
+
+ const img = document.createElement('img');
+ img.src = dot.imageUrl;
+ img.className = 'tooltip-img';
+
+ imgContainer.appendChild(img);
+ tooltip.appendChild(imgContainer);
+ }
+
+ // Title (if provided)
+ if (dot.title) {
+ const title = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ title.setAttribute('x', (tooltipX + 10).toString());
+ title.setAttribute('y', dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 26).toString()
+ : (tooltipY + 25).toString());
+ title.setAttribute('fill', 'white');
+ title.setAttribute('font-size', '14');
+ title.setAttribute('font-weight', 'bold');
+ title.textContent = dot.title;
+ tooltip.appendChild(title);
+ }
+
+ // Description (if provided)
+ if (dot.description) {
+ const descriptionFO = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ descriptionFO.setAttribute('x', (tooltipX + 10).toString());
+ descriptionFO.setAttribute('y', dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 32).toString()
+ : dot.title
+ ? (tooltipY + 35).toString()
+ : (tooltipY + 15).toString());
+ descriptionFO.setAttribute('width', (tooltipWidth - 20).toString());
+ descriptionFO.setAttribute('height', (tooltipHeight / 2 - 10).toString());
+
+ const descriptionDiv = document.createElement('div');
+ descriptionDiv.style.color = 'white';
+ descriptionDiv.style.fontSize = '12px';
+ descriptionDiv.style.overflow = 'hidden';
+ descriptionDiv.textContent = dot.description;
+
+ descriptionFO.appendChild(descriptionDiv);
+ tooltip.appendChild(descriptionFO);
+ }
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute('d', pathData);
+ }
+
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ return { leftmost, rightmost };
+ }
+
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+
+ const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
+ circle.setAttribute('cx', x.toString());
+ circle.setAttribute('cy', y.toString());
+ circle.setAttribute('r', this.config.dotRadius.toString());
+ circle.setAttribute('fill', 'white');
+ circle.setAttribute('data-dot-id', dot.id.toString());
+ circle.classList.add('dot');
+
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener('click', () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error('Dot has no link');
+ throw new Error('Dot has no link');
+ }
+ });
+ }
+
+ this.dotsGroup.appendChild(circle);
+ };
+ }
+
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute('width', `${this.config.totalWidth}`);
+
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map(dot => dot.x));
+ const maxX = Math.max(...this.dots.map(dot => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute('height', `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250515094033.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250515094033.ts
new file mode 100644
index 0000000..430cc96
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250515094033.ts
@@ -0,0 +1,545 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+
+ private preloadedImages: Map = new Map();
+
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+
+ constructor(containerId: string, dots: DotConfig[], config?: Partial) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 160;
+ let calculatedWidth = 0;
+
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map(dot => dot.x));
+ const maxX = Math.max(...this.dots.map(dot => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 200,
+ tooltipHeight: 150,
+ ...config
+ };
+
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+
+ // Create SVG elements
+ this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+ this.gridGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ this.curvePath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ this.dotsGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ this.tooltipGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter(dot => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map(dot => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+
+ // Create a loading indicator (optional)
+ const loadingCount = {current: 0, total: imageUrls.length};
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log('All images preloaded successfully');
+ }
+ };
+
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+
+ // Set src to start loading
+ img.src = url;
+
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ };
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = 'connected-dots-styles';
+
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement('style');
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute('width', `${this.config.totalWidth}`);
+ this.svg.setAttribute('height', `${this.config.height}`);
+ this.svg.style.overflow = 'visible';
+ this.scrollContainer.appendChild(this.svg);
+
+ // Configure grid group
+ this.gridGroup.classList.add('grid');
+ this.svg.appendChild(this.gridGroup);
+
+ // Configure curve path
+ this.curvePath.setAttribute('fill', 'none');
+ this.curvePath.setAttribute('stroke', 'white');
+ this.curvePath.setAttribute('stroke-width', '2');
+ this.curvePath.setAttribute('stroke-linecap', 'round');
+ this.curvePath.classList.add('curve-path');
+ this.svg.appendChild(this.curvePath);
+
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add('tooltips');
+ this.svg.appendChild(this.tooltipGroup);
+ }
+
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * (this.config.height / 2 * 0.8);
+
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+
+ private calculateBezierControlPoints(dots: DotConfig[], index: number): ControlPoints {
+ const tension = this.config.tension * 150; // Scale tension for Bezier curve
+
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+
+ return { x1, y1, x2, y2 };
+ }
+
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return '';
+
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(this.dots[0].value)}`;
+
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(this.dots, i);
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+
+ return path;
+ }
+
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+
+ if (!this.config.showGrid) return;
+
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+ line.setAttribute('x1', '0');
+ line.setAttribute('y1', this.getDotY(value).toString());
+ line.setAttribute('x2', this.config.totalWidth.toString());
+ line.setAttribute('y2', this.getDotY(value).toString());
+ line.setAttribute('stroke', 'rgba(219, 39, 119, 0.4)');
+ line.setAttribute('stroke-width', '1');
+ this.gridGroup.appendChild(line);
+
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ text.setAttribute('x', '10');
+ text.setAttribute('y', (this.getDotY(value) + 4).toString());
+ text.setAttribute('fill', 'rgba(219, 39, 119, 0.8)');
+ text.setAttribute('font-size', '12');
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+
+ // Vertical grid lines
+ const numVertLines = Math.ceil(this.config.totalWidth / this.config.xUnitSize);
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+ line.setAttribute('x1', x.toString());
+ line.setAttribute('y1', '0');
+ line.setAttribute('x2', x.toString());
+ line.setAttribute('y2', this.config.height.toString());
+ line.setAttribute('stroke', 'rgba(219, 39, 119, 0.4)');
+ line.setAttribute('stroke-width', '1');
+ this.gridGroup.appendChild(line);
+
+ if (xValue !== 0) {
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ text.setAttribute('x', x.toString());
+ text.setAttribute('y', (this.config.height / 2 + 20).toString());
+ text.setAttribute('fill', 'rgba(219, 39, 119, 0.8)');
+ text.setAttribute('font-size', '12');
+ text.setAttribute('text-anchor', 'middle');
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ tooltip.classList.add('dot-tooltip');
+ tooltip.setAttribute('data-dot-id', dot.id.toString());
+
+ // Calculate tooltip position
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipHeight = this.config.tooltipHeight;
+ const tooltipX = x - tooltipWidth / 2;
+
+ // Calculate tooltip Y position, ensuring it stays within the container
+ let tooltipY = y - tooltipHeight - 20; // Position above the dot with some spacing
+
+ // Ensure tooltip doesn't go above the container
+ tooltipY = Math.max(tooltipY, 10); // Keep at least 10px from the top
+
+ // Background rectangle
+ const bg = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+ bg.setAttribute('x', tooltipX.toString());
+ bg.setAttribute('y', tooltipY.toString());
+ bg.setAttribute('width', tooltipWidth.toString());
+ bg.setAttribute('height', tooltipHeight.toString());
+ bg.setAttribute('rx', '5');
+ bg.setAttribute('ry', '5');
+ bg.setAttribute('fill', 'rgba(0, 0, 0, 0.8)');
+ tooltip.appendChild(bg);
+
+ // Tooltip arrow (pointing to the dot)
+ const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ arrow.setAttribute('d', `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${tooltipY + tooltipHeight - 10} L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`);
+ arrow.setAttribute('fill', 'rgba(0, 0, 0, 0.8)');
+ tooltip.appendChild(arrow);
+
+ // Image (if provided)
+ if (dot.imageUrl) {
+ const imgContainer = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ imgContainer.setAttribute('x', (tooltipX + 10).toString());
+ imgContainer.setAttribute('y', (tooltipY + 10).toString());
+ imgContainer.setAttribute('width', (tooltipWidth - 20).toString());
+ imgContainer.setAttribute('height', (tooltipHeight / 2).toString());
+
+ const img = document.createElement('img');
+ img.src = dot.imageUrl;
+ img.className = 'tooltip-img';
+
+ imgContainer.appendChild(img);
+ tooltip.appendChild(imgContainer);
+ }
+
+ // Title (if provided)
+ if (dot.title) {
+ const title = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ title.setAttribute('x', (tooltipX + 10).toString());
+ title.setAttribute('y', dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 26).toString()
+ : (tooltipY + 25).toString());
+ title.setAttribute('fill', 'white');
+ title.setAttribute('font-size', '14');
+ title.setAttribute('font-weight', 'bold');
+ title.textContent = dot.title;
+ tooltip.appendChild(title);
+ }
+
+ // Description (if provided)
+ if (dot.description) {
+ const descriptionFO = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ descriptionFO.setAttribute('x', (tooltipX + 10).toString());
+ descriptionFO.setAttribute('y', dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 32).toString()
+ : dot.title
+ ? (tooltipY + 35).toString()
+ : (tooltipY + 15).toString());
+ descriptionFO.setAttribute('width', (tooltipWidth - 20).toString());
+ descriptionFO.setAttribute('height', (tooltipHeight / 2 - 10).toString());
+
+ const descriptionDiv = document.createElement('div');
+ descriptionDiv.style.color = 'white';
+ descriptionDiv.style.fontSize = '12px';
+ descriptionDiv.style.overflow = 'hidden';
+ descriptionDiv.textContent = dot.description;
+
+ descriptionFO.appendChild(descriptionDiv);
+ tooltip.appendChild(descriptionFO);
+ }
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute('d', pathData);
+ }
+
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ return { leftmost, rightmost };
+ }
+
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+
+ const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
+ circle.setAttribute('cx', x.toString());
+ circle.setAttribute('cy', y.toString());
+ circle.setAttribute('r', this.config.dotRadius.toString());
+ circle.setAttribute('fill', 'white');
+ circle.setAttribute('data-dot-id', dot.id.toString());
+ circle.classList.add('dot');
+
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener('click', () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error('Dot has no link');
+ throw new Error('Dot has no link');
+ }
+ });
+ }
+
+ this.dotsGroup.appendChild(circle);
+ };
+ }
+
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute('width', `${this.config.totalWidth}`);
+
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map(dot => dot.x));
+ const maxX = Math.max(...this.dots.map(dot => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute('height', `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250515094036.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250515094036.ts
new file mode 100644
index 0000000..67a6b1d
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250515094036.ts
@@ -0,0 +1,545 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+
+ private preloadedImages: Map = new Map();
+
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+
+ constructor(containerId: string, dots: DotConfig[], config?: Partial) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map(dot => dot.x));
+ const maxX = Math.max(...this.dots.map(dot => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 200,
+ tooltipHeight: 150,
+ ...config
+ };
+
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+
+ // Create SVG elements
+ this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+ this.gridGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ this.curvePath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ this.dotsGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ this.tooltipGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter(dot => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map(dot => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+
+ // Create a loading indicator (optional)
+ const loadingCount = {current: 0, total: imageUrls.length};
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log('All images preloaded successfully');
+ }
+ };
+
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+
+ // Set src to start loading
+ img.src = url;
+
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ };
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = 'connected-dots-styles';
+
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement('style');
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute('width', `${this.config.totalWidth}`);
+ this.svg.setAttribute('height', `${this.config.height}`);
+ this.svg.style.overflow = 'visible';
+ this.scrollContainer.appendChild(this.svg);
+
+ // Configure grid group
+ this.gridGroup.classList.add('grid');
+ this.svg.appendChild(this.gridGroup);
+
+ // Configure curve path
+ this.curvePath.setAttribute('fill', 'none');
+ this.curvePath.setAttribute('stroke', 'white');
+ this.curvePath.setAttribute('stroke-width', '2');
+ this.curvePath.setAttribute('stroke-linecap', 'round');
+ this.curvePath.classList.add('curve-path');
+ this.svg.appendChild(this.curvePath);
+
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add('tooltips');
+ this.svg.appendChild(this.tooltipGroup);
+ }
+
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * (this.config.height / 2 * 0.8);
+
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+
+ private calculateBezierControlPoints(dots: DotConfig[], index: number): ControlPoints {
+ const tension = this.config.tension * 150; // Scale tension for Bezier curve
+
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+
+ return { x1, y1, x2, y2 };
+ }
+
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return '';
+
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(this.dots[0].value)}`;
+
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(this.dots, i);
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+
+ return path;
+ }
+
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+
+ if (!this.config.showGrid) return;
+
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+ line.setAttribute('x1', '0');
+ line.setAttribute('y1', this.getDotY(value).toString());
+ line.setAttribute('x2', this.config.totalWidth.toString());
+ line.setAttribute('y2', this.getDotY(value).toString());
+ line.setAttribute('stroke', 'rgba(219, 39, 119, 0.4)');
+ line.setAttribute('stroke-width', '1');
+ this.gridGroup.appendChild(line);
+
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ text.setAttribute('x', '10');
+ text.setAttribute('y', (this.getDotY(value) + 4).toString());
+ text.setAttribute('fill', 'rgba(219, 39, 119, 0.8)');
+ text.setAttribute('font-size', '12');
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+
+ // Vertical grid lines
+ const numVertLines = Math.ceil(this.config.totalWidth / this.config.xUnitSize);
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+ line.setAttribute('x1', x.toString());
+ line.setAttribute('y1', '0');
+ line.setAttribute('x2', x.toString());
+ line.setAttribute('y2', this.config.height.toString());
+ line.setAttribute('stroke', 'rgba(219, 39, 119, 0.4)');
+ line.setAttribute('stroke-width', '1');
+ this.gridGroup.appendChild(line);
+
+ if (xValue !== 0) {
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ text.setAttribute('x', x.toString());
+ text.setAttribute('y', (this.config.height / 2 + 20).toString());
+ text.setAttribute('fill', 'rgba(219, 39, 119, 0.8)');
+ text.setAttribute('font-size', '12');
+ text.setAttribute('text-anchor', 'middle');
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ tooltip.classList.add('dot-tooltip');
+ tooltip.setAttribute('data-dot-id', dot.id.toString());
+
+ // Calculate tooltip position
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipHeight = this.config.tooltipHeight;
+ const tooltipX = x - tooltipWidth / 2;
+
+ // Calculate tooltip Y position, ensuring it stays within the container
+ let tooltipY = y - tooltipHeight - 20; // Position above the dot with some spacing
+
+ // Ensure tooltip doesn't go above the container
+ tooltipY = Math.max(tooltipY, 10); // Keep at least 10px from the top
+
+ // Background rectangle
+ const bg = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+ bg.setAttribute('x', tooltipX.toString());
+ bg.setAttribute('y', tooltipY.toString());
+ bg.setAttribute('width', tooltipWidth.toString());
+ bg.setAttribute('height', tooltipHeight.toString());
+ bg.setAttribute('rx', '5');
+ bg.setAttribute('ry', '5');
+ bg.setAttribute('fill', 'rgba(0, 0, 0, 0.8)');
+ tooltip.appendChild(bg);
+
+ // Tooltip arrow (pointing to the dot)
+ const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ arrow.setAttribute('d', `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${tooltipY + tooltipHeight - 10} L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`);
+ arrow.setAttribute('fill', 'rgba(0, 0, 0, 0.8)');
+ tooltip.appendChild(arrow);
+
+ // Image (if provided)
+ if (dot.imageUrl) {
+ const imgContainer = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ imgContainer.setAttribute('x', (tooltipX + 10).toString());
+ imgContainer.setAttribute('y', (tooltipY + 10).toString());
+ imgContainer.setAttribute('width', (tooltipWidth - 20).toString());
+ imgContainer.setAttribute('height', (tooltipHeight / 2).toString());
+
+ const img = document.createElement('img');
+ img.src = dot.imageUrl;
+ img.className = 'tooltip-img';
+
+ imgContainer.appendChild(img);
+ tooltip.appendChild(imgContainer);
+ }
+
+ // Title (if provided)
+ if (dot.title) {
+ const title = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ title.setAttribute('x', (tooltipX + 10).toString());
+ title.setAttribute('y', dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 26).toString()
+ : (tooltipY + 25).toString());
+ title.setAttribute('fill', 'white');
+ title.setAttribute('font-size', '14');
+ title.setAttribute('font-weight', 'bold');
+ title.textContent = dot.title;
+ tooltip.appendChild(title);
+ }
+
+ // Description (if provided)
+ if (dot.description) {
+ const descriptionFO = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ descriptionFO.setAttribute('x', (tooltipX + 10).toString());
+ descriptionFO.setAttribute('y', dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 32).toString()
+ : dot.title
+ ? (tooltipY + 35).toString()
+ : (tooltipY + 15).toString());
+ descriptionFO.setAttribute('width', (tooltipWidth - 20).toString());
+ descriptionFO.setAttribute('height', (tooltipHeight / 2 - 10).toString());
+
+ const descriptionDiv = document.createElement('div');
+ descriptionDiv.style.color = 'white';
+ descriptionDiv.style.fontSize = '12px';
+ descriptionDiv.style.overflow = 'hidden';
+ descriptionDiv.textContent = dot.description;
+
+ descriptionFO.appendChild(descriptionDiv);
+ tooltip.appendChild(descriptionFO);
+ }
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute('d', pathData);
+ }
+
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ return { leftmost, rightmost };
+ }
+
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+
+ const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
+ circle.setAttribute('cx', x.toString());
+ circle.setAttribute('cy', y.toString());
+ circle.setAttribute('r', this.config.dotRadius.toString());
+ circle.setAttribute('fill', 'white');
+ circle.setAttribute('data-dot-id', dot.id.toString());
+ circle.classList.add('dot');
+
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener('click', () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error('Dot has no link');
+ throw new Error('Dot has no link');
+ }
+ });
+ }
+
+ this.dotsGroup.appendChild(circle);
+ };
+ }
+
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute('width', `${this.config.totalWidth}`);
+
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map(dot => dot.x));
+ const maxX = Math.max(...this.dots.map(dot => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute('height', `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250515094059.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250515094059.ts
new file mode 100644
index 0000000..4a059d8
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250515094059.ts
@@ -0,0 +1,545 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+
+ private preloadedImages: Map = new Map();
+
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+
+ constructor(containerId: string, dots: DotConfig[], config?: Partial) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map(dot => dot.x));
+ const maxX = Math.max(...this.dots.map(dot => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config
+ };
+
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+
+ // Create SVG elements
+ this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+ this.gridGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ this.curvePath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ this.dotsGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ this.tooltipGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter(dot => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map(dot => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+
+ // Create a loading indicator (optional)
+ const loadingCount = {current: 0, total: imageUrls.length};
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log('All images preloaded successfully');
+ }
+ };
+
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+
+ // Set src to start loading
+ img.src = url;
+
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ };
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = 'connected-dots-styles';
+
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement('style');
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute('width', `${this.config.totalWidth}`);
+ this.svg.setAttribute('height', `${this.config.height}`);
+ this.svg.style.overflow = 'visible';
+ this.scrollContainer.appendChild(this.svg);
+
+ // Configure grid group
+ this.gridGroup.classList.add('grid');
+ this.svg.appendChild(this.gridGroup);
+
+ // Configure curve path
+ this.curvePath.setAttribute('fill', 'none');
+ this.curvePath.setAttribute('stroke', 'white');
+ this.curvePath.setAttribute('stroke-width', '2');
+ this.curvePath.setAttribute('stroke-linecap', 'round');
+ this.curvePath.classList.add('curve-path');
+ this.svg.appendChild(this.curvePath);
+
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add('tooltips');
+ this.svg.appendChild(this.tooltipGroup);
+ }
+
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * (this.config.height / 2 * 0.8);
+
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+
+ private calculateBezierControlPoints(dots: DotConfig[], index: number): ControlPoints {
+ const tension = this.config.tension * 150; // Scale tension for Bezier curve
+
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+
+ return { x1, y1, x2, y2 };
+ }
+
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return '';
+
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(this.dots[0].value)}`;
+
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(this.dots, i);
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+
+ return path;
+ }
+
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+
+ if (!this.config.showGrid) return;
+
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+ line.setAttribute('x1', '0');
+ line.setAttribute('y1', this.getDotY(value).toString());
+ line.setAttribute('x2', this.config.totalWidth.toString());
+ line.setAttribute('y2', this.getDotY(value).toString());
+ line.setAttribute('stroke', 'rgba(219, 39, 119, 0.4)');
+ line.setAttribute('stroke-width', '1');
+ this.gridGroup.appendChild(line);
+
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ text.setAttribute('x', '10');
+ text.setAttribute('y', (this.getDotY(value) + 4).toString());
+ text.setAttribute('fill', 'rgba(219, 39, 119, 0.8)');
+ text.setAttribute('font-size', '12');
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+
+ // Vertical grid lines
+ const numVertLines = Math.ceil(this.config.totalWidth / this.config.xUnitSize);
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+ line.setAttribute('x1', x.toString());
+ line.setAttribute('y1', '0');
+ line.setAttribute('x2', x.toString());
+ line.setAttribute('y2', this.config.height.toString());
+ line.setAttribute('stroke', 'rgba(219, 39, 119, 0.4)');
+ line.setAttribute('stroke-width', '1');
+ this.gridGroup.appendChild(line);
+
+ if (xValue !== 0) {
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ text.setAttribute('x', x.toString());
+ text.setAttribute('y', (this.config.height / 2 + 20).toString());
+ text.setAttribute('fill', 'rgba(219, 39, 119, 0.8)');
+ text.setAttribute('font-size', '12');
+ text.setAttribute('text-anchor', 'middle');
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ tooltip.classList.add('dot-tooltip');
+ tooltip.setAttribute('data-dot-id', dot.id.toString());
+
+ // Calculate tooltip position
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipHeight = this.config.tooltipHeight;
+ const tooltipX = x - tooltipWidth / 2;
+
+ // Calculate tooltip Y position, ensuring it stays within the container
+ let tooltipY = y - tooltipHeight - 20; // Position above the dot with some spacing
+
+ // Ensure tooltip doesn't go above the container
+ tooltipY = Math.max(tooltipY, 10); // Keep at least 10px from the top
+
+ // Background rectangle
+ const bg = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+ bg.setAttribute('x', tooltipX.toString());
+ bg.setAttribute('y', tooltipY.toString());
+ bg.setAttribute('width', tooltipWidth.toString());
+ bg.setAttribute('height', tooltipHeight.toString());
+ bg.setAttribute('rx', '5');
+ bg.setAttribute('ry', '5');
+ bg.setAttribute('fill', 'rgba(0, 0, 0, 0.8)');
+ tooltip.appendChild(bg);
+
+ // Tooltip arrow (pointing to the dot)
+ const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ arrow.setAttribute('d', `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${tooltipY + tooltipHeight - 10} L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`);
+ arrow.setAttribute('fill', 'rgba(0, 0, 0, 0.8)');
+ tooltip.appendChild(arrow);
+
+ // Image (if provided)
+ if (dot.imageUrl) {
+ const imgContainer = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ imgContainer.setAttribute('x', (tooltipX + 10).toString());
+ imgContainer.setAttribute('y', (tooltipY + 10).toString());
+ imgContainer.setAttribute('width', (tooltipWidth - 20).toString());
+ imgContainer.setAttribute('height', (tooltipHeight / 2).toString());
+
+ const img = document.createElement('img');
+ img.src = dot.imageUrl;
+ img.className = 'tooltip-img';
+
+ imgContainer.appendChild(img);
+ tooltip.appendChild(imgContainer);
+ }
+
+ // Title (if provided)
+ if (dot.title) {
+ const title = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ title.setAttribute('x', (tooltipX + 10).toString());
+ title.setAttribute('y', dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 26).toString()
+ : (tooltipY + 25).toString());
+ title.setAttribute('fill', 'white');
+ title.setAttribute('font-size', '14');
+ title.setAttribute('font-weight', 'bold');
+ title.textContent = dot.title;
+ tooltip.appendChild(title);
+ }
+
+ // Description (if provided)
+ if (dot.description) {
+ const descriptionFO = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ descriptionFO.setAttribute('x', (tooltipX + 10).toString());
+ descriptionFO.setAttribute('y', dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 32).toString()
+ : dot.title
+ ? (tooltipY + 35).toString()
+ : (tooltipY + 15).toString());
+ descriptionFO.setAttribute('width', (tooltipWidth - 20).toString());
+ descriptionFO.setAttribute('height', (tooltipHeight / 2 - 10).toString());
+
+ const descriptionDiv = document.createElement('div');
+ descriptionDiv.style.color = 'white';
+ descriptionDiv.style.fontSize = '12px';
+ descriptionDiv.style.overflow = 'hidden';
+ descriptionDiv.textContent = dot.description;
+
+ descriptionFO.appendChild(descriptionDiv);
+ tooltip.appendChild(descriptionFO);
+ }
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute('d', pathData);
+ }
+
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ return { leftmost, rightmost };
+ }
+
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+
+ const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
+ circle.setAttribute('cx', x.toString());
+ circle.setAttribute('cy', y.toString());
+ circle.setAttribute('r', this.config.dotRadius.toString());
+ circle.setAttribute('fill', 'white');
+ circle.setAttribute('data-dot-id', dot.id.toString());
+ circle.classList.add('dot');
+
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener('click', () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error('Dot has no link');
+ throw new Error('Dot has no link');
+ }
+ });
+ }
+
+ this.dotsGroup.appendChild(circle);
+ };
+ }
+
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute('width', `${this.config.totalWidth}`);
+
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map(dot => dot.x));
+ const maxX = Math.max(...this.dots.map(dot => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute('height', `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250515094215.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250515094215.ts
new file mode 100644
index 0000000..2af0be1
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250515094215.ts
@@ -0,0 +1,545 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+
+ private preloadedImages: Map = new Map();
+
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+
+ constructor(containerId: string, dots: DotConfig[], config?: Partial) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map(dot => dot.x));
+ const maxX = Math.max(...this.dots.map(dot => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config
+ };
+
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+
+ // Create SVG elements
+ this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+ this.gridGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ this.curvePath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ this.dotsGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ this.tooltipGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter(dot => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map(dot => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+
+ // Create a loading indicator (optional)
+ const loadingCount = {current: 0, total: imageUrls.length};
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log('All images preloaded successfully');
+ }
+ };
+
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+
+ // Set src to start loading
+ img.src = url;
+
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ };
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = 'connected-dots-styles';
+
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement('style');
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute('width', `${this.config.totalWidth}`);
+ this.svg.setAttribute('height', `${this.config.height}`);
+ this.svg.style.overflow = 'visible';
+ this.scrollContainer.appendChild(this.svg);
+
+ // Configure grid group
+ this.gridGroup.classList.add('grid');
+ this.svg.appendChild(this.gridGroup);
+
+ // Configure curve path
+ this.curvePath.setAttribute('fill', 'none');
+ this.curvePath.setAttribute('stroke', 'white');
+ this.curvePath.setAttribute('stroke-width', '2');
+ this.curvePath.setAttribute('stroke-linecap', 'round');
+ this.curvePath.classList.add('curve-path');
+ this.svg.appendChild(this.curvePath);
+
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add('tooltips');
+ this.svg.appendChild(this.tooltipGroup);
+ }
+
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * (this.config.height / 2 * 0.8);
+
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+
+ private calculateBezierControlPoints(dots: DotConfig[], index: number): ControlPoints {
+ const tension = this.config.tension * 250; // Scale tension for Bezier curve
+
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+
+ return { x1, y1, x2, y2 };
+ }
+
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return '';
+
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(this.dots[0].value)}`;
+
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(this.dots, i);
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+
+ return path;
+ }
+
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+
+ if (!this.config.showGrid) return;
+
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+ line.setAttribute('x1', '0');
+ line.setAttribute('y1', this.getDotY(value).toString());
+ line.setAttribute('x2', this.config.totalWidth.toString());
+ line.setAttribute('y2', this.getDotY(value).toString());
+ line.setAttribute('stroke', 'rgba(219, 39, 119, 0.4)');
+ line.setAttribute('stroke-width', '1');
+ this.gridGroup.appendChild(line);
+
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ text.setAttribute('x', '10');
+ text.setAttribute('y', (this.getDotY(value) + 4).toString());
+ text.setAttribute('fill', 'rgba(219, 39, 119, 0.8)');
+ text.setAttribute('font-size', '12');
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+
+ // Vertical grid lines
+ const numVertLines = Math.ceil(this.config.totalWidth / this.config.xUnitSize);
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+ line.setAttribute('x1', x.toString());
+ line.setAttribute('y1', '0');
+ line.setAttribute('x2', x.toString());
+ line.setAttribute('y2', this.config.height.toString());
+ line.setAttribute('stroke', 'rgba(219, 39, 119, 0.4)');
+ line.setAttribute('stroke-width', '1');
+ this.gridGroup.appendChild(line);
+
+ if (xValue !== 0) {
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ text.setAttribute('x', x.toString());
+ text.setAttribute('y', (this.config.height / 2 + 20).toString());
+ text.setAttribute('fill', 'rgba(219, 39, 119, 0.8)');
+ text.setAttribute('font-size', '12');
+ text.setAttribute('text-anchor', 'middle');
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ tooltip.classList.add('dot-tooltip');
+ tooltip.setAttribute('data-dot-id', dot.id.toString());
+
+ // Calculate tooltip position
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipHeight = this.config.tooltipHeight;
+ const tooltipX = x - tooltipWidth / 2;
+
+ // Calculate tooltip Y position, ensuring it stays within the container
+ let tooltipY = y - tooltipHeight - 20; // Position above the dot with some spacing
+
+ // Ensure tooltip doesn't go above the container
+ tooltipY = Math.max(tooltipY, 10); // Keep at least 10px from the top
+
+ // Background rectangle
+ const bg = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+ bg.setAttribute('x', tooltipX.toString());
+ bg.setAttribute('y', tooltipY.toString());
+ bg.setAttribute('width', tooltipWidth.toString());
+ bg.setAttribute('height', tooltipHeight.toString());
+ bg.setAttribute('rx', '5');
+ bg.setAttribute('ry', '5');
+ bg.setAttribute('fill', 'rgba(0, 0, 0, 0.8)');
+ tooltip.appendChild(bg);
+
+ // Tooltip arrow (pointing to the dot)
+ const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ arrow.setAttribute('d', `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${tooltipY + tooltipHeight - 10} L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`);
+ arrow.setAttribute('fill', 'rgba(0, 0, 0, 0.8)');
+ tooltip.appendChild(arrow);
+
+ // Image (if provided)
+ if (dot.imageUrl) {
+ const imgContainer = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ imgContainer.setAttribute('x', (tooltipX + 10).toString());
+ imgContainer.setAttribute('y', (tooltipY + 10).toString());
+ imgContainer.setAttribute('width', (tooltipWidth - 20).toString());
+ imgContainer.setAttribute('height', (tooltipHeight / 2).toString());
+
+ const img = document.createElement('img');
+ img.src = dot.imageUrl;
+ img.className = 'tooltip-img';
+
+ imgContainer.appendChild(img);
+ tooltip.appendChild(imgContainer);
+ }
+
+ // Title (if provided)
+ if (dot.title) {
+ const title = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ title.setAttribute('x', (tooltipX + 10).toString());
+ title.setAttribute('y', dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 26).toString()
+ : (tooltipY + 25).toString());
+ title.setAttribute('fill', 'white');
+ title.setAttribute('font-size', '14');
+ title.setAttribute('font-weight', 'bold');
+ title.textContent = dot.title;
+ tooltip.appendChild(title);
+ }
+
+ // Description (if provided)
+ if (dot.description) {
+ const descriptionFO = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ descriptionFO.setAttribute('x', (tooltipX + 10).toString());
+ descriptionFO.setAttribute('y', dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 32).toString()
+ : dot.title
+ ? (tooltipY + 35).toString()
+ : (tooltipY + 15).toString());
+ descriptionFO.setAttribute('width', (tooltipWidth - 20).toString());
+ descriptionFO.setAttribute('height', (tooltipHeight / 2 - 10).toString());
+
+ const descriptionDiv = document.createElement('div');
+ descriptionDiv.style.color = 'white';
+ descriptionDiv.style.fontSize = '12px';
+ descriptionDiv.style.overflow = 'hidden';
+ descriptionDiv.textContent = dot.description;
+
+ descriptionFO.appendChild(descriptionDiv);
+ tooltip.appendChild(descriptionFO);
+ }
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute('d', pathData);
+ }
+
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ return { leftmost, rightmost };
+ }
+
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+
+ const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
+ circle.setAttribute('cx', x.toString());
+ circle.setAttribute('cy', y.toString());
+ circle.setAttribute('r', this.config.dotRadius.toString());
+ circle.setAttribute('fill', 'white');
+ circle.setAttribute('data-dot-id', dot.id.toString());
+ circle.classList.add('dot');
+
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener('click', () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error('Dot has no link');
+ throw new Error('Dot has no link');
+ }
+ });
+ }
+
+ this.dotsGroup.appendChild(circle);
+ };
+ }
+
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute('width', `${this.config.totalWidth}`);
+
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map(dot => dot.x));
+ const maxX = Math.max(...this.dots.map(dot => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute('height', `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250515094221.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250515094221.ts
new file mode 100644
index 0000000..04e0114
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250515094221.ts
@@ -0,0 +1,545 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+
+ private preloadedImages: Map = new Map();
+
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+
+ constructor(containerId: string, dots: DotConfig[], config?: Partial) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map(dot => dot.x));
+ const maxX = Math.max(...this.dots.map(dot => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config
+ };
+
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+
+ // Create SVG elements
+ this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+ this.gridGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ this.curvePath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ this.dotsGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ this.tooltipGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter(dot => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map(dot => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+
+ // Create a loading indicator (optional)
+ const loadingCount = {current: 0, total: imageUrls.length};
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log('All images preloaded successfully');
+ }
+ };
+
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+
+ // Set src to start loading
+ img.src = url;
+
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ };
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = 'connected-dots-styles';
+
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement('style');
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute('width', `${this.config.totalWidth}`);
+ this.svg.setAttribute('height', `${this.config.height}`);
+ this.svg.style.overflow = 'visible';
+ this.scrollContainer.appendChild(this.svg);
+
+ // Configure grid group
+ this.gridGroup.classList.add('grid');
+ this.svg.appendChild(this.gridGroup);
+
+ // Configure curve path
+ this.curvePath.setAttribute('fill', 'none');
+ this.curvePath.setAttribute('stroke', 'white');
+ this.curvePath.setAttribute('stroke-width', '2');
+ this.curvePath.setAttribute('stroke-linecap', 'round');
+ this.curvePath.classList.add('curve-path');
+ this.svg.appendChild(this.curvePath);
+
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add('tooltips');
+ this.svg.appendChild(this.tooltipGroup);
+ }
+
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * (this.config.height / 2 * 0.8);
+
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+
+ private calculateBezierControlPoints(dots: DotConfig[], index: number): ControlPoints {
+ const tension = this.config.tension * 300; // Scale tension for Bezier curve
+
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+
+ return { x1, y1, x2, y2 };
+ }
+
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return '';
+
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(this.dots[0].value)}`;
+
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(this.dots, i);
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+
+ return path;
+ }
+
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+
+ if (!this.config.showGrid) return;
+
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+ line.setAttribute('x1', '0');
+ line.setAttribute('y1', this.getDotY(value).toString());
+ line.setAttribute('x2', this.config.totalWidth.toString());
+ line.setAttribute('y2', this.getDotY(value).toString());
+ line.setAttribute('stroke', 'rgba(219, 39, 119, 0.4)');
+ line.setAttribute('stroke-width', '1');
+ this.gridGroup.appendChild(line);
+
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ text.setAttribute('x', '10');
+ text.setAttribute('y', (this.getDotY(value) + 4).toString());
+ text.setAttribute('fill', 'rgba(219, 39, 119, 0.8)');
+ text.setAttribute('font-size', '12');
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+
+ // Vertical grid lines
+ const numVertLines = Math.ceil(this.config.totalWidth / this.config.xUnitSize);
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+ line.setAttribute('x1', x.toString());
+ line.setAttribute('y1', '0');
+ line.setAttribute('x2', x.toString());
+ line.setAttribute('y2', this.config.height.toString());
+ line.setAttribute('stroke', 'rgba(219, 39, 119, 0.4)');
+ line.setAttribute('stroke-width', '1');
+ this.gridGroup.appendChild(line);
+
+ if (xValue !== 0) {
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ text.setAttribute('x', x.toString());
+ text.setAttribute('y', (this.config.height / 2 + 20).toString());
+ text.setAttribute('fill', 'rgba(219, 39, 119, 0.8)');
+ text.setAttribute('font-size', '12');
+ text.setAttribute('text-anchor', 'middle');
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ tooltip.classList.add('dot-tooltip');
+ tooltip.setAttribute('data-dot-id', dot.id.toString());
+
+ // Calculate tooltip position
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipHeight = this.config.tooltipHeight;
+ const tooltipX = x - tooltipWidth / 2;
+
+ // Calculate tooltip Y position, ensuring it stays within the container
+ let tooltipY = y - tooltipHeight - 20; // Position above the dot with some spacing
+
+ // Ensure tooltip doesn't go above the container
+ tooltipY = Math.max(tooltipY, 10); // Keep at least 10px from the top
+
+ // Background rectangle
+ const bg = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+ bg.setAttribute('x', tooltipX.toString());
+ bg.setAttribute('y', tooltipY.toString());
+ bg.setAttribute('width', tooltipWidth.toString());
+ bg.setAttribute('height', tooltipHeight.toString());
+ bg.setAttribute('rx', '5');
+ bg.setAttribute('ry', '5');
+ bg.setAttribute('fill', 'rgba(0, 0, 0, 0.8)');
+ tooltip.appendChild(bg);
+
+ // Tooltip arrow (pointing to the dot)
+ const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ arrow.setAttribute('d', `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${tooltipY + tooltipHeight - 10} L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`);
+ arrow.setAttribute('fill', 'rgba(0, 0, 0, 0.8)');
+ tooltip.appendChild(arrow);
+
+ // Image (if provided)
+ if (dot.imageUrl) {
+ const imgContainer = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ imgContainer.setAttribute('x', (tooltipX + 10).toString());
+ imgContainer.setAttribute('y', (tooltipY + 10).toString());
+ imgContainer.setAttribute('width', (tooltipWidth - 20).toString());
+ imgContainer.setAttribute('height', (tooltipHeight / 2).toString());
+
+ const img = document.createElement('img');
+ img.src = dot.imageUrl;
+ img.className = 'tooltip-img';
+
+ imgContainer.appendChild(img);
+ tooltip.appendChild(imgContainer);
+ }
+
+ // Title (if provided)
+ if (dot.title) {
+ const title = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ title.setAttribute('x', (tooltipX + 10).toString());
+ title.setAttribute('y', dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 26).toString()
+ : (tooltipY + 25).toString());
+ title.setAttribute('fill', 'white');
+ title.setAttribute('font-size', '14');
+ title.setAttribute('font-weight', 'bold');
+ title.textContent = dot.title;
+ tooltip.appendChild(title);
+ }
+
+ // Description (if provided)
+ if (dot.description) {
+ const descriptionFO = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ descriptionFO.setAttribute('x', (tooltipX + 10).toString());
+ descriptionFO.setAttribute('y', dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 32).toString()
+ : dot.title
+ ? (tooltipY + 35).toString()
+ : (tooltipY + 15).toString());
+ descriptionFO.setAttribute('width', (tooltipWidth - 20).toString());
+ descriptionFO.setAttribute('height', (tooltipHeight / 2 - 10).toString());
+
+ const descriptionDiv = document.createElement('div');
+ descriptionDiv.style.color = 'white';
+ descriptionDiv.style.fontSize = '12px';
+ descriptionDiv.style.overflow = 'hidden';
+ descriptionDiv.textContent = dot.description;
+
+ descriptionFO.appendChild(descriptionDiv);
+ tooltip.appendChild(descriptionFO);
+ }
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute('d', pathData);
+ }
+
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ return { leftmost, rightmost };
+ }
+
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+
+ const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
+ circle.setAttribute('cx', x.toString());
+ circle.setAttribute('cy', y.toString());
+ circle.setAttribute('r', this.config.dotRadius.toString());
+ circle.setAttribute('fill', 'white');
+ circle.setAttribute('data-dot-id', dot.id.toString());
+ circle.classList.add('dot');
+
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener('click', () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error('Dot has no link');
+ throw new Error('Dot has no link');
+ }
+ });
+ }
+
+ this.dotsGroup.appendChild(circle);
+ };
+ }
+
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute('width', `${this.config.totalWidth}`);
+
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map(dot => dot.x));
+ const maxX = Math.max(...this.dots.map(dot => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute('height', `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250515094419.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250515094419.ts
new file mode 100644
index 0000000..6b48da2
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250515094419.ts
@@ -0,0 +1,545 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+
+ private preloadedImages: Map = new Map();
+
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+
+ constructor(containerId: string, dots: DotConfig[], config?: Partial) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map(dot => dot.x));
+ const maxX = Math.max(...this.dots.map(dot => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config
+ };
+
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+
+ // Create SVG elements
+ this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+ this.gridGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ this.curvePath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ this.dotsGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ this.tooltipGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter(dot => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map(dot => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+
+ // Create a loading indicator (optional)
+ const loadingCount = {current: 0, total: imageUrls.length};
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log('All images preloaded successfully');
+ }
+ };
+
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+
+ // Set src to start loading
+ img.src = url;
+
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ };
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = 'connected-dots-styles';
+
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement('style');
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute('width', `${this.config.totalWidth}`);
+ this.svg.setAttribute('height', `${this.config.height}`);
+ this.svg.style.overflow = 'visible';
+ this.scrollContainer.appendChild(this.svg);
+
+ // Configure grid group
+ this.gridGroup.classList.add('grid');
+ this.svg.appendChild(this.gridGroup);
+
+ // Configure curve path
+ this.curvePath.setAttribute('fill', 'none');
+ this.curvePath.setAttribute('stroke', 'white');
+ this.curvePath.setAttribute('stroke-width', '2');
+ this.curvePath.setAttribute('stroke-linecap', 'round');
+ this.curvePath.classList.add('curve-path');
+ this.svg.appendChild(this.curvePath);
+
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add('tooltips');
+ this.svg.appendChild(this.tooltipGroup);
+ }
+
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * (this.config.height / 2 * 0.8);
+
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+
+ private calculateBezierControlPoints(dots: DotConfig[], index: number): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+
+ return { x1, y1, x2, y2 };
+ }
+
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return '';
+
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(this.dots[0].value)}`;
+
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(this.dots, i);
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+
+ return path;
+ }
+
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+
+ if (!this.config.showGrid) return;
+
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+ line.setAttribute('x1', '0');
+ line.setAttribute('y1', this.getDotY(value).toString());
+ line.setAttribute('x2', this.config.totalWidth.toString());
+ line.setAttribute('y2', this.getDotY(value).toString());
+ line.setAttribute('stroke', 'rgba(219, 39, 119, 0.4)');
+ line.setAttribute('stroke-width', '1');
+ this.gridGroup.appendChild(line);
+
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ text.setAttribute('x', '10');
+ text.setAttribute('y', (this.getDotY(value) + 4).toString());
+ text.setAttribute('fill', 'rgba(219, 39, 119, 0.8)');
+ text.setAttribute('font-size', '12');
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+
+ // Vertical grid lines
+ const numVertLines = Math.ceil(this.config.totalWidth / this.config.xUnitSize);
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+ line.setAttribute('x1', x.toString());
+ line.setAttribute('y1', '0');
+ line.setAttribute('x2', x.toString());
+ line.setAttribute('y2', this.config.height.toString());
+ line.setAttribute('stroke', 'rgba(219, 39, 119, 0.4)');
+ line.setAttribute('stroke-width', '1');
+ this.gridGroup.appendChild(line);
+
+ if (xValue !== 0) {
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ text.setAttribute('x', x.toString());
+ text.setAttribute('y', (this.config.height / 2 + 20).toString());
+ text.setAttribute('fill', 'rgba(219, 39, 119, 0.8)');
+ text.setAttribute('font-size', '12');
+ text.setAttribute('text-anchor', 'middle');
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ tooltip.classList.add('dot-tooltip');
+ tooltip.setAttribute('data-dot-id', dot.id.toString());
+
+ // Calculate tooltip position
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipHeight = this.config.tooltipHeight;
+ const tooltipX = x - tooltipWidth / 2;
+
+ // Calculate tooltip Y position, ensuring it stays within the container
+ let tooltipY = y - tooltipHeight - 20; // Position above the dot with some spacing
+
+ // Ensure tooltip doesn't go above the container
+ tooltipY = Math.max(tooltipY, 10); // Keep at least 10px from the top
+
+ // Background rectangle
+ const bg = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+ bg.setAttribute('x', tooltipX.toString());
+ bg.setAttribute('y', tooltipY.toString());
+ bg.setAttribute('width', tooltipWidth.toString());
+ bg.setAttribute('height', tooltipHeight.toString());
+ bg.setAttribute('rx', '5');
+ bg.setAttribute('ry', '5');
+ bg.setAttribute('fill', 'rgba(0, 0, 0, 0.8)');
+ tooltip.appendChild(bg);
+
+ // Tooltip arrow (pointing to the dot)
+ const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ arrow.setAttribute('d', `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${tooltipY + tooltipHeight - 10} L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`);
+ arrow.setAttribute('fill', 'rgba(0, 0, 0, 0.8)');
+ tooltip.appendChild(arrow);
+
+ // Image (if provided)
+ if (dot.imageUrl) {
+ const imgContainer = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ imgContainer.setAttribute('x', (tooltipX + 10).toString());
+ imgContainer.setAttribute('y', (tooltipY + 10).toString());
+ imgContainer.setAttribute('width', (tooltipWidth - 20).toString());
+ imgContainer.setAttribute('height', (tooltipHeight / 2).toString());
+
+ const img = document.createElement('img');
+ img.src = dot.imageUrl;
+ img.className = 'tooltip-img';
+
+ imgContainer.appendChild(img);
+ tooltip.appendChild(imgContainer);
+ }
+
+ // Title (if provided)
+ if (dot.title) {
+ const title = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ title.setAttribute('x', (tooltipX + 10).toString());
+ title.setAttribute('y', dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 26).toString()
+ : (tooltipY + 25).toString());
+ title.setAttribute('fill', 'white');
+ title.setAttribute('font-size', '14');
+ title.setAttribute('font-weight', 'bold');
+ title.textContent = dot.title;
+ tooltip.appendChild(title);
+ }
+
+ // Description (if provided)
+ if (dot.description) {
+ const descriptionFO = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ descriptionFO.setAttribute('x', (tooltipX + 10).toString());
+ descriptionFO.setAttribute('y', dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 32).toString()
+ : dot.title
+ ? (tooltipY + 35).toString()
+ : (tooltipY + 15).toString());
+ descriptionFO.setAttribute('width', (tooltipWidth - 20).toString());
+ descriptionFO.setAttribute('height', (tooltipHeight / 2 - 10).toString());
+
+ const descriptionDiv = document.createElement('div');
+ descriptionDiv.style.color = 'white';
+ descriptionDiv.style.fontSize = '12px';
+ descriptionDiv.style.overflow = 'hidden';
+ descriptionDiv.textContent = dot.description;
+
+ descriptionFO.appendChild(descriptionDiv);
+ tooltip.appendChild(descriptionFO);
+ }
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute('d', pathData);
+ }
+
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ return { leftmost, rightmost };
+ }
+
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+
+ const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
+ circle.setAttribute('cx', x.toString());
+ circle.setAttribute('cy', y.toString());
+ circle.setAttribute('r', this.config.dotRadius.toString());
+ circle.setAttribute('fill', 'white');
+ circle.setAttribute('data-dot-id', dot.id.toString());
+ circle.classList.add('dot');
+
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener('click', () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error('Dot has no link');
+ throw new Error('Dot has no link');
+ }
+ });
+ }
+
+ this.dotsGroup.appendChild(circle);
+ };
+ }
+
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute('width', `${this.config.totalWidth}`);
+
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map(dot => dot.x));
+ const maxX = Math.max(...this.dots.map(dot => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute('height', `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522091556.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522091556.ts
new file mode 100644
index 0000000..5374d09
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522091556.ts
@@ -0,0 +1,553 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+
+ private preloadedImages: Map = new Map();
+
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+
+ constructor(containerId: string, dots: DotConfig[], config?: Partial) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map(dot => dot.x));
+ const maxX = Math.max(...this.dots.map(dot => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config
+ };
+
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+
+ // Create SVG elements
+ this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+ this.gridGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ this.curvePath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ this.dotsGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ this.tooltipGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter(dot => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map(dot => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+
+ // Create a loading indicator (optional)
+ const loadingCount = {current: 0, total: imageUrls.length};
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log('All images preloaded successfully');
+ }
+ };
+
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+
+ // Set src to start loading
+ img.src = url;
+
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ };
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = 'connected-dots-styles';
+
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement('style');
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute('width', `${this.config.totalWidth}`);
+ this.svg.setAttribute('height', `${this.config.height}`);
+ this.svg.style.overflow = 'visible';
+ this.scrollContainer.appendChild(this.svg);
+
+ // Configure grid group
+ this.gridGroup.classList.add('grid');
+ this.svg.appendChild(this.gridGroup);
+
+ // Configure curve path
+ this.curvePath.setAttribute('fill', 'none');
+ this.curvePath.setAttribute('stroke', 'white');
+ this.curvePath.setAttribute('stroke-width', '2');
+ this.curvePath.setAttribute('stroke-linecap', 'round');
+ this.curvePath.classList.add('curve-path');
+ this.svg.appendChild(this.curvePath);
+
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add('tooltips');
+ this.svg.appendChild(this.tooltipGroup);
+ }
+
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * (this.config.height / 2 * 0.8);
+
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+
+ private calculateBezierControlPoints(dots: DotConfig[], index: number): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+
+ return { x1, y1, x2, y2 };
+ }
+
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return '';
+
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(this.dots[0].value)}`;
+
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(this.dots, i);
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+
+ return path;
+ }
+
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+
+ if (!this.config.showGrid) return;
+
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+ line.setAttribute('x1', '0');
+ line.setAttribute('y1', this.getDotY(value).toString());
+ line.setAttribute('x2', this.config.totalWidth.toString());
+ line.setAttribute('y2', this.getDotY(value).toString());
+ line.setAttribute('stroke', 'rgba(219, 39, 119, 0.4)');
+ line.setAttribute('stroke-width', '1');
+ this.gridGroup.appendChild(line);
+
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ text.setAttribute('x', '10');
+ text.setAttribute('y', (this.getDotY(value) + 4).toString());
+ text.setAttribute('fill', 'rgba(219, 39, 119, 0.8)');
+ text.setAttribute('font-size', '12');
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+
+ // Vertical grid lines
+ const numVertLines = Math.ceil(this.config.totalWidth / this.config.xUnitSize);
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+ line.setAttribute('x1', x.toString());
+ line.setAttribute('y1', '0');
+ line.setAttribute('x2', x.toString());
+ line.setAttribute('y2', this.config.height.toString());
+ line.setAttribute('stroke', 'rgba(219, 39, 119, 0.4)');
+ line.setAttribute('stroke-width', '1');
+ this.gridGroup.appendChild(line);
+
+ if (xValue !== 0) {
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ text.setAttribute('x', x.toString());
+ text.setAttribute('y', (this.config.height / 2 + 20).toString());
+ text.setAttribute('fill', 'rgba(219, 39, 119, 0.8)');
+ text.setAttribute('font-size', '12');
+ text.setAttribute('text-anchor', 'middle');
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ tooltip.classList.add('dot-tooltip');
+ tooltip.setAttribute('data-dot-id', dot.id.toString());
+
+ // Calculate tooltip position
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipHeight = this.config.tooltipHeight;
+ const tooltipX = x - tooltipWidth / 2;
+
+ // Calculate tooltip Y position, ensuring it stays within the container
+ let tooltipY = y - tooltipHeight - 20; // Position above the dot with some spacing
+
+ // Ensure tooltip doesn't go above the container
+ tooltipY = Math.max(tooltipY, 10); // Keep at least 10px from the top
+
+ // Background rectangle
+ const bg = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+ bg.setAttribute('x', tooltipX.toString());
+ bg.setAttribute('y', tooltipY.toString());
+ bg.setAttribute('width', tooltipWidth.toString());
+ bg.setAttribute('height', tooltipHeight.toString());
+ bg.setAttribute('rx', '5');
+ bg.setAttribute('ry', '5');
+ bg.setAttribute('fill', 'rgba(0, 0, 0, 0.8)');
+ tooltip.appendChild(bg);
+
+ // Tooltip arrow (pointing to the dot)
+ const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ arrow.setAttribute('d', `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${tooltipY + tooltipHeight - 10} L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`);
+ arrow.setAttribute('fill', 'rgba(0, 0, 0, 0.8)');
+ tooltip.appendChild(arrow);
+
+ // Image (if provided)
+ // Image (if provided)
+if (dot.imageUrl) {
+ const imgContainer = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ imgContainer.setAttribute('x', (tooltipX + 10).toString());
+ imgContainer.setAttribute('y', (tooltipY + 10).toString());
+
+ // Set width and height to the same value for a square aspect
+ const imageSize = Math.min((tooltipWidth - 20), (tooltipHeight / 2));
+ imgContainer.setAttribute('width', imageSize.toString());
+ imgContainer.setAttribute('height', imageSize.toString());
+
+ const img = document.createElement('img');
+ img.src = dot.imageUrl;
+ img.className = 'tooltip-img';
+ img.style.width = '100%';
+ img.style.height = '100%';
+ img.style.objectFit = 'cover'; // Ensure the image covers the space while maintaining aspect ratio
+
+ imgContainer.appendChild(img);
+ tooltip.appendChild(imgContainer);
+}
+
+
+ // Title (if provided)
+ if (dot.title) {
+ const title = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ title.setAttribute('x', (tooltipX + 10).toString());
+ title.setAttribute('y', dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 26).toString()
+ : (tooltipY + 25).toString());
+ title.setAttribute('fill', 'white');
+ title.setAttribute('font-size', '14');
+ title.setAttribute('font-weight', 'bold');
+ title.textContent = dot.title;
+ tooltip.appendChild(title);
+ }
+
+ // Description (if provided)
+ if (dot.description) {
+ const descriptionFO = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ descriptionFO.setAttribute('x', (tooltipX + 10).toString());
+ descriptionFO.setAttribute('y', dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 32).toString()
+ : dot.title
+ ? (tooltipY + 35).toString()
+ : (tooltipY + 15).toString());
+ descriptionFO.setAttribute('width', (tooltipWidth - 20).toString());
+ descriptionFO.setAttribute('height', (tooltipHeight / 2 - 10).toString());
+
+ const descriptionDiv = document.createElement('div');
+ descriptionDiv.style.color = 'white';
+ descriptionDiv.style.fontSize = '12px';
+ descriptionDiv.style.overflow = 'hidden';
+ descriptionDiv.textContent = dot.description;
+
+ descriptionFO.appendChild(descriptionDiv);
+ tooltip.appendChild(descriptionFO);
+ }
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute('d', pathData);
+ }
+
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ return { leftmost, rightmost };
+ }
+
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+
+ const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
+ circle.setAttribute('cx', x.toString());
+ circle.setAttribute('cy', y.toString());
+ circle.setAttribute('r', this.config.dotRadius.toString());
+ circle.setAttribute('fill', 'white');
+ circle.setAttribute('data-dot-id', dot.id.toString());
+ circle.classList.add('dot');
+
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener('click', () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error('Dot has no link');
+ throw new Error('Dot has no link');
+ }
+ });
+ }
+
+ this.dotsGroup.appendChild(circle);
+ };
+ }
+
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute('width', `${this.config.totalWidth}`);
+
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map(dot => dot.x));
+ const maxX = Math.max(...this.dots.map(dot => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute('height', `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522091605.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522091605.ts
new file mode 100644
index 0000000..96417a8
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522091605.ts
@@ -0,0 +1,553 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+
+ private preloadedImages: Map = new Map();
+
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+
+ constructor(containerId: string, dots: DotConfig[], config?: Partial) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map(dot => dot.x));
+ const maxX = Math.max(...this.dots.map(dot => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config
+ };
+
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+
+ // Create SVG elements
+ this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+ this.gridGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ this.curvePath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ this.dotsGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ this.tooltipGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter(dot => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map(dot => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+
+ // Create a loading indicator (optional)
+ const loadingCount = {current: 0, total: imageUrls.length};
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log('All images preloaded successfully');
+ }
+ };
+
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+
+ // Set src to start loading
+ img.src = url;
+
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ };
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = 'connected-dots-styles';
+
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement('style');
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute('width', `${this.config.totalWidth}`);
+ this.svg.setAttribute('height', `${this.config.height}`);
+ this.svg.style.overflow = 'visible';
+ this.scrollContainer.appendChild(this.svg);
+
+ // Configure grid group
+ this.gridGroup.classList.add('grid');
+ this.svg.appendChild(this.gridGroup);
+
+ // Configure curve path
+ this.curvePath.setAttribute('fill', 'none');
+ this.curvePath.setAttribute('stroke', 'white');
+ this.curvePath.setAttribute('stroke-width', '2');
+ this.curvePath.setAttribute('stroke-linecap', 'round');
+ this.curvePath.classList.add('curve-path');
+ this.svg.appendChild(this.curvePath);
+
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add('tooltips');
+ this.svg.appendChild(this.tooltipGroup);
+ }
+
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * (this.config.height / 2 * 0.8);
+
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+
+ private calculateBezierControlPoints(dots: DotConfig[], index: number): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+
+ return { x1, y1, x2, y2 };
+ }
+
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return '';
+
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(this.dots[0].value)}`;
+
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(this.dots, i);
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+
+ return path;
+ }
+
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+
+ if (!this.config.showGrid) return;
+
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+ line.setAttribute('x1', '0');
+ line.setAttribute('y1', this.getDotY(value).toString());
+ line.setAttribute('x2', this.config.totalWidth.toString());
+ line.setAttribute('y2', this.getDotY(value).toString());
+ line.setAttribute('stroke', 'rgba(219, 39, 119, 0.4)');
+ line.setAttribute('stroke-width', '1');
+ this.gridGroup.appendChild(line);
+
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ text.setAttribute('x', '10');
+ text.setAttribute('y', (this.getDotY(value) + 4).toString());
+ text.setAttribute('fill', 'rgba(219, 39, 119, 0.8)');
+ text.setAttribute('font-size', '12');
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+
+ // Vertical grid lines
+ const numVertLines = Math.ceil(this.config.totalWidth / this.config.xUnitSize);
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+ line.setAttribute('x1', x.toString());
+ line.setAttribute('y1', '0');
+ line.setAttribute('x2', x.toString());
+ line.setAttribute('y2', this.config.height.toString());
+ line.setAttribute('stroke', 'rgba(219, 39, 119, 0.4)');
+ line.setAttribute('stroke-width', '1');
+ this.gridGroup.appendChild(line);
+
+ if (xValue !== 0) {
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ text.setAttribute('x', x.toString());
+ text.setAttribute('y', (this.config.height / 2 + 20).toString());
+ text.setAttribute('fill', 'rgba(219, 39, 119, 0.8)');
+ text.setAttribute('font-size', '12');
+ text.setAttribute('text-anchor', 'middle');
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ tooltip.classList.add('dot-tooltip');
+ tooltip.setAttribute('data-dot-id', dot.id.toString());
+
+ // Calculate tooltip position
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipHeight = this.config.tooltipHeight;
+ const tooltipX = x - tooltipWidth / 2;
+
+ // Calculate tooltip Y position, ensuring it stays within the container
+ let tooltipY = y - tooltipHeight - 20; // Position above the dot with some spacing
+
+ // Ensure tooltip doesn't go above the container
+ tooltipY = Math.max(tooltipY, 10); // Keep at least 10px from the top
+
+ // Background rectangle
+ const bg = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+ bg.setAttribute('x', tooltipX.toString());
+ bg.setAttribute('y', tooltipY.toString());
+ bg.setAttribute('width', tooltipWidth.toString());
+ bg.setAttribute('height', tooltipHeight.toString());
+ bg.setAttribute('rx', '5');
+ bg.setAttribute('ry', '5');
+ bg.setAttribute('fill', 'rgba(0, 0, 0, 0.8)');
+ tooltip.appendChild(bg);
+
+ // Tooltip arrow (pointing to the dot)
+ const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ arrow.setAttribute('d', `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${tooltipY + tooltipHeight - 10} L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`);
+ arrow.setAttribute('fill', 'rgba(0, 0, 0, 0.8)');
+ tooltip.appendChild(arrow);
+
+ // Image (if provided)
+ // Image (if provided)
+ if (dot.imageUrl) {
+ const imgContainer = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ imgContainer.setAttribute('x', (tooltipX + 10).toString());
+ imgContainer.setAttribute('y', (tooltipY + 10).toString());
+
+ // Set width and height to the same value for a square aspect
+ const imageSize = Math.min((tooltipWidth - 20), (tooltipHeight / 2));
+ imgContainer.setAttribute('width', imageSize.toString());
+ imgContainer.setAttribute('height', imageSize.toString());
+
+ const img = document.createElement('img');
+ img.src = dot.imageUrl;
+ img.className = 'tooltip-img';
+ img.style.width = '100%';
+ img.style.height = '100%';
+ img.style.objectFit = 'cover'; // Ensure the image covers the space while maintaining aspect ratio
+
+ imgContainer.appendChild(img);
+ tooltip.appendChild(imgContainer);
+ }
+
+
+ // Title (if provided)
+ if (dot.title) {
+ const title = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ title.setAttribute('x', (tooltipX + 10).toString());
+ title.setAttribute('y', dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 26).toString()
+ : (tooltipY + 25).toString());
+ title.setAttribute('fill', 'white');
+ title.setAttribute('font-size', '14');
+ title.setAttribute('font-weight', 'bold');
+ title.textContent = dot.title;
+ tooltip.appendChild(title);
+ }
+
+ // Description (if provided)
+ if (dot.description) {
+ const descriptionFO = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ descriptionFO.setAttribute('x', (tooltipX + 10).toString());
+ descriptionFO.setAttribute('y', dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 32).toString()
+ : dot.title
+ ? (tooltipY + 35).toString()
+ : (tooltipY + 15).toString());
+ descriptionFO.setAttribute('width', (tooltipWidth - 20).toString());
+ descriptionFO.setAttribute('height', (tooltipHeight / 2 - 10).toString());
+
+ const descriptionDiv = document.createElement('div');
+ descriptionDiv.style.color = 'white';
+ descriptionDiv.style.fontSize = '12px';
+ descriptionDiv.style.overflow = 'hidden';
+ descriptionDiv.textContent = dot.description;
+
+ descriptionFO.appendChild(descriptionDiv);
+ tooltip.appendChild(descriptionFO);
+ }
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute('d', pathData);
+ }
+
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ return { leftmost, rightmost };
+ }
+
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+
+ const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
+ circle.setAttribute('cx', x.toString());
+ circle.setAttribute('cy', y.toString());
+ circle.setAttribute('r', this.config.dotRadius.toString());
+ circle.setAttribute('fill', 'white');
+ circle.setAttribute('data-dot-id', dot.id.toString());
+ circle.classList.add('dot');
+
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener('click', () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error('Dot has no link');
+ throw new Error('Dot has no link');
+ }
+ });
+ }
+
+ this.dotsGroup.appendChild(circle);
+ };
+ }
+
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute('width', `${this.config.totalWidth}`);
+
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map(dot => dot.x));
+ const maxX = Math.max(...this.dots.map(dot => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute('height', `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522091611.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522091611.ts
new file mode 100644
index 0000000..92dfc80
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522091611.ts
@@ -0,0 +1,551 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+
+ private preloadedImages: Map = new Map();
+
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+
+ constructor(containerId: string, dots: DotConfig[], config?: Partial) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map(dot => dot.x));
+ const maxX = Math.max(...this.dots.map(dot => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config
+ };
+
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+
+ // Create SVG elements
+ this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+ this.gridGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ this.curvePath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ this.dotsGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ this.tooltipGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter(dot => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map(dot => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+
+ // Create a loading indicator (optional)
+ const loadingCount = {current: 0, total: imageUrls.length};
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log('All images preloaded successfully');
+ }
+ };
+
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+
+ // Set src to start loading
+ img.src = url;
+
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ };
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = 'connected-dots-styles';
+
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement('style');
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute('width', `${this.config.totalWidth}`);
+ this.svg.setAttribute('height', `${this.config.height}`);
+ this.svg.style.overflow = 'visible';
+ this.scrollContainer.appendChild(this.svg);
+
+ // Configure grid group
+ this.gridGroup.classList.add('grid');
+ this.svg.appendChild(this.gridGroup);
+
+ // Configure curve path
+ this.curvePath.setAttribute('fill', 'none');
+ this.curvePath.setAttribute('stroke', 'white');
+ this.curvePath.setAttribute('stroke-width', '2');
+ this.curvePath.setAttribute('stroke-linecap', 'round');
+ this.curvePath.classList.add('curve-path');
+ this.svg.appendChild(this.curvePath);
+
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add('tooltips');
+ this.svg.appendChild(this.tooltipGroup);
+ }
+
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * (this.config.height / 2 * 0.8);
+
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+
+ private calculateBezierControlPoints(dots: DotConfig[], index: number): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+
+ return { x1, y1, x2, y2 };
+ }
+
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return '';
+
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(this.dots[0].value)}`;
+
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(this.dots, i);
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+
+ return path;
+ }
+
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+
+ if (!this.config.showGrid) return;
+
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+ line.setAttribute('x1', '0');
+ line.setAttribute('y1', this.getDotY(value).toString());
+ line.setAttribute('x2', this.config.totalWidth.toString());
+ line.setAttribute('y2', this.getDotY(value).toString());
+ line.setAttribute('stroke', 'rgba(219, 39, 119, 0.4)');
+ line.setAttribute('stroke-width', '1');
+ this.gridGroup.appendChild(line);
+
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ text.setAttribute('x', '10');
+ text.setAttribute('y', (this.getDotY(value) + 4).toString());
+ text.setAttribute('fill', 'rgba(219, 39, 119, 0.8)');
+ text.setAttribute('font-size', '12');
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+
+ // Vertical grid lines
+ const numVertLines = Math.ceil(this.config.totalWidth / this.config.xUnitSize);
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+ line.setAttribute('x1', x.toString());
+ line.setAttribute('y1', '0');
+ line.setAttribute('x2', x.toString());
+ line.setAttribute('y2', this.config.height.toString());
+ line.setAttribute('stroke', 'rgba(219, 39, 119, 0.4)');
+ line.setAttribute('stroke-width', '1');
+ this.gridGroup.appendChild(line);
+
+ if (xValue !== 0) {
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ text.setAttribute('x', x.toString());
+ text.setAttribute('y', (this.config.height / 2 + 20).toString());
+ text.setAttribute('fill', 'rgba(219, 39, 119, 0.8)');
+ text.setAttribute('font-size', '12');
+ text.setAttribute('text-anchor', 'middle');
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ tooltip.classList.add('dot-tooltip');
+ tooltip.setAttribute('data-dot-id', dot.id.toString());
+
+ // Calculate tooltip position
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipHeight = this.config.tooltipHeight;
+ const tooltipX = x - tooltipWidth / 2;
+
+ // Calculate tooltip Y position, ensuring it stays within the container
+ let tooltipY = y - tooltipHeight - 20; // Position above the dot with some spacing
+
+ // Ensure tooltip doesn't go above the container
+ tooltipY = Math.max(tooltipY, 10); // Keep at least 10px from the top
+
+ // Background rectangle
+ const bg = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+ bg.setAttribute('x', tooltipX.toString());
+ bg.setAttribute('y', tooltipY.toString());
+ bg.setAttribute('width', tooltipWidth.toString());
+ bg.setAttribute('height', tooltipHeight.toString());
+ bg.setAttribute('rx', '5');
+ bg.setAttribute('ry', '5');
+ bg.setAttribute('fill', 'rgba(0, 0, 0, 0.8)');
+ tooltip.appendChild(bg);
+
+ // Tooltip arrow (pointing to the dot)
+ const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ arrow.setAttribute('d', `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${tooltipY + tooltipHeight - 10} L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`);
+ arrow.setAttribute('fill', 'rgba(0, 0, 0, 0.8)');
+ tooltip.appendChild(arrow);
+
+ // Image (if provided)
+ if (dot.imageUrl) {
+ const imgContainer = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ imgContainer.setAttribute('x', (tooltipX + 10).toString());
+ imgContainer.setAttribute('y', (tooltipY + 10).toString());
+
+ // Set width and height to the same value for a square aspect
+ const imageSize = Math.min((tooltipWidth - 20), (tooltipHeight / 2));
+ imgContainer.setAttribute('width', imageSize.toString());
+ imgContainer.setAttribute('height', imageSize.toString());
+
+ const img = document.createElement('img');
+ img.src = dot.imageUrl;
+ img.className = 'tooltip-img';
+ img.style.width = '100%';
+ img.style.height = '100%';
+ img.style.objectFit = 'cover'; // Ensure the image covers the space while maintaining aspect ratio
+
+ imgContainer.appendChild(img);
+ tooltip.appendChild(imgContainer);
+ }
+
+ // Title (if provided)
+ if (dot.title) {
+ const title = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ title.setAttribute('x', (tooltipX + 10).toString());
+ title.setAttribute('y', dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 26).toString()
+ : (tooltipY + 25).toString());
+ title.setAttribute('fill', 'white');
+ title.setAttribute('font-size', '14');
+ title.setAttribute('font-weight', 'bold');
+ title.textContent = dot.title;
+ tooltip.appendChild(title);
+ }
+
+ // Description (if provided)
+ if (dot.description) {
+ const descriptionFO = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ descriptionFO.setAttribute('x', (tooltipX + 10).toString());
+ descriptionFO.setAttribute('y', dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 32).toString()
+ : dot.title
+ ? (tooltipY + 35).toString()
+ : (tooltipY + 15).toString());
+ descriptionFO.setAttribute('width', (tooltipWidth - 20).toString());
+ descriptionFO.setAttribute('height', (tooltipHeight / 2 - 10).toString());
+
+ const descriptionDiv = document.createElement('div');
+ descriptionDiv.style.color = 'white';
+ descriptionDiv.style.fontSize = '12px';
+ descriptionDiv.style.overflow = 'hidden';
+ descriptionDiv.textContent = dot.description;
+
+ descriptionFO.appendChild(descriptionDiv);
+ tooltip.appendChild(descriptionFO);
+ }
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute('d', pathData);
+ }
+
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ return { leftmost, rightmost };
+ }
+
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+
+ const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
+ circle.setAttribute('cx', x.toString());
+ circle.setAttribute('cy', y.toString());
+ circle.setAttribute('r', this.config.dotRadius.toString());
+ circle.setAttribute('fill', 'white');
+ circle.setAttribute('data-dot-id', dot.id.toString());
+ circle.classList.add('dot');
+
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener('click', () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error('Dot has no link');
+ throw new Error('Dot has no link');
+ }
+ });
+ }
+
+ this.dotsGroup.appendChild(circle);
+ };
+ }
+
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute('width', `${this.config.totalWidth}`);
+
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map(dot => dot.x));
+ const maxX = Math.max(...this.dots.map(dot => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute('height', `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522091723.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522091723.ts
new file mode 100644
index 0000000..cc5d6e1
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522091723.ts
@@ -0,0 +1,561 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+
+ private preloadedImages: Map = new Map();
+
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+
+ constructor(containerId: string, dots: DotConfig[], config?: Partial) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map(dot => dot.x));
+ const maxX = Math.max(...this.dots.map(dot => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config
+ };
+
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+
+ // Create SVG elements
+ this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+ this.gridGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ this.curvePath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ this.dotsGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ this.tooltipGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter(dot => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map(dot => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+
+ // Create a loading indicator (optional)
+ const loadingCount = {current: 0, total: imageUrls.length};
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log('All images preloaded successfully');
+ }
+ };
+
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+
+ // Set src to start loading
+ img.src = url;
+
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ };
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = 'connected-dots-styles';
+
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement('style');
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute('width', `${this.config.totalWidth}`);
+ this.svg.setAttribute('height', `${this.config.height}`);
+ this.svg.style.overflow = 'visible';
+ this.scrollContainer.appendChild(this.svg);
+
+ // Configure grid group
+ this.gridGroup.classList.add('grid');
+ this.svg.appendChild(this.gridGroup);
+
+ // Configure curve path
+ this.curvePath.setAttribute('fill', 'none');
+ this.curvePath.setAttribute('stroke', 'white');
+ this.curvePath.setAttribute('stroke-width', '2');
+ this.curvePath.setAttribute('stroke-linecap', 'round');
+ this.curvePath.classList.add('curve-path');
+ this.svg.appendChild(this.curvePath);
+
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add('tooltips');
+ this.svg.appendChild(this.tooltipGroup);
+ }
+
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * (this.config.height / 2 * 0.8);
+
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+
+ private calculateBezierControlPoints(dots: DotConfig[], index: number): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+
+ return { x1, y1, x2, y2 };
+ }
+
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return '';
+
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(this.dots[0].value)}`;
+
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(this.dots, i);
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+
+ return path;
+ }
+
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+
+ if (!this.config.showGrid) return;
+
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+ line.setAttribute('x1', '0');
+ line.setAttribute('y1', this.getDotY(value).toString());
+ line.setAttribute('x2', this.config.totalWidth.toString());
+ line.setAttribute('y2', this.getDotY(value).toString());
+ line.setAttribute('stroke', 'rgba(219, 39, 119, 0.4)');
+ line.setAttribute('stroke-width', '1');
+ this.gridGroup.appendChild(line);
+
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ text.setAttribute('x', '10');
+ text.setAttribute('y', (this.getDotY(value) + 4).toString());
+ text.setAttribute('fill', 'rgba(219, 39, 119, 0.8)');
+ text.setAttribute('font-size', '12');
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+
+ // Vertical grid lines
+ const numVertLines = Math.ceil(this.config.totalWidth / this.config.xUnitSize);
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+ line.setAttribute('x1', x.toString());
+ line.setAttribute('y1', '0');
+ line.setAttribute('x2', x.toString());
+ line.setAttribute('y2', this.config.height.toString());
+ line.setAttribute('stroke', 'rgba(219, 39, 119, 0.4)');
+ line.setAttribute('stroke-width', '1');
+ this.gridGroup.appendChild(line);
+
+ if (xValue !== 0) {
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ text.setAttribute('x', x.toString());
+ text.setAttribute('y', (this.config.height / 2 + 20).toString());
+ text.setAttribute('fill', 'rgba(219, 39, 119, 0.8)');
+ text.setAttribute('font-size', '12');
+ text.setAttribute('text-anchor', 'middle');
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ tooltip.classList.add('dot-tooltip');
+ tooltip.setAttribute('data-dot-id', dot.id.toString());
+
+ // Calculate tooltip position
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipHeight = this.config.tooltipHeight;
+ const tooltipX = x - tooltipWidth / 2;
+
+ // Calculate tooltip Y position, ensuring it stays within the container
+ let tooltipY = y - tooltipHeight - 20; // Position above the dot with some spacing
+
+ // Ensure tooltip doesn't go above the container
+ tooltipY = Math.max(tooltipY, 10); // Keep at least 10px from the top
+
+ // Background rectangle
+ // Create a square background for the tooltip
+ const bg = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+ bg.setAttribute('x', tooltipX.toString());
+ bg.setAttribute('y', tooltipY.toString());
+
+ // Make the width and height equal for a square shape
+ const squareSize = Math.min(tooltipWidth, tooltipHeight);
+ bg.setAttribute('width', squareSize.toString());
+ bg.setAttribute('height', squareSize.toString());
+
+ // Optional: Adjust corner rounding if needed
+ bg.setAttribute('rx', '5'); // You can change this value for rounder corners
+ bg.setAttribute('ry', '5');
+
+ // Set the fill color to white
+ bg.setAttribute('fill', 'white');
+
+ tooltip.appendChild(bg);
+
+
+ // Tooltip arrow (pointing to the dot)
+ const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ arrow.setAttribute('d', `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${tooltipY + tooltipHeight - 10} L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`);
+ arrow.setAttribute('fill', 'rgba(0, 0, 0, 0.8)');
+ tooltip.appendChild(arrow);
+
+ // Image (if provided)
+ if (dot.imageUrl) {
+ const imgContainer = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ imgContainer.setAttribute('x', (tooltipX + 10).toString());
+ imgContainer.setAttribute('y', (tooltipY + 10).toString());
+
+ // Set width and height to the same value for a square aspect
+ const imageSize = Math.min((tooltipWidth - 20), (tooltipHeight / 2));
+ imgContainer.setAttribute('width', imageSize.toString());
+ imgContainer.setAttribute('height', imageSize.toString());
+
+ const img = document.createElement('img');
+ img.src = dot.imageUrl;
+ img.className = 'tooltip-img';
+ img.style.width = '100%';
+ img.style.height = '100%';
+ img.style.objectFit = 'cover'; // Ensure the image covers the space while maintaining aspect ratio
+
+ imgContainer.appendChild(img);
+ tooltip.appendChild(imgContainer);
+ }
+
+ // Title (if provided)
+ if (dot.title) {
+ const title = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ title.setAttribute('x', (tooltipX + 10).toString());
+ title.setAttribute('y', dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 26).toString()
+ : (tooltipY + 25).toString());
+ title.setAttribute('fill', 'white');
+ title.setAttribute('font-size', '14');
+ title.setAttribute('font-weight', 'bold');
+ title.textContent = dot.title;
+ tooltip.appendChild(title);
+ }
+
+ // Description (if provided)
+ if (dot.description) {
+ const descriptionFO = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ descriptionFO.setAttribute('x', (tooltipX + 10).toString());
+ descriptionFO.setAttribute('y', dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 32).toString()
+ : dot.title
+ ? (tooltipY + 35).toString()
+ : (tooltipY + 15).toString());
+ descriptionFO.setAttribute('width', (tooltipWidth - 20).toString());
+ descriptionFO.setAttribute('height', (tooltipHeight / 2 - 10).toString());
+
+ const descriptionDiv = document.createElement('div');
+ descriptionDiv.style.color = 'white';
+ descriptionDiv.style.fontSize = '12px';
+ descriptionDiv.style.overflow = 'hidden';
+ descriptionDiv.textContent = dot.description;
+
+ descriptionFO.appendChild(descriptionDiv);
+ tooltip.appendChild(descriptionFO);
+ }
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute('d', pathData);
+ }
+
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ return { leftmost, rightmost };
+ }
+
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+
+ const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
+ circle.setAttribute('cx', x.toString());
+ circle.setAttribute('cy', y.toString());
+ circle.setAttribute('r', this.config.dotRadius.toString());
+ circle.setAttribute('fill', 'white');
+ circle.setAttribute('data-dot-id', dot.id.toString());
+ circle.classList.add('dot');
+
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener('click', () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error('Dot has no link');
+ throw new Error('Dot has no link');
+ }
+ });
+ }
+
+ this.dotsGroup.appendChild(circle);
+ };
+ }
+
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute('width', `${this.config.totalWidth}`);
+
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map(dot => dot.x));
+ const maxX = Math.max(...this.dots.map(dot => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute('height', `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522091732.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522091732.ts
new file mode 100644
index 0000000..1f2397f
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522091732.ts
@@ -0,0 +1,561 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+
+ private preloadedImages: Map = new Map();
+
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+
+ constructor(containerId: string, dots: DotConfig[], config?: Partial) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map(dot => dot.x));
+ const maxX = Math.max(...this.dots.map(dot => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config
+ };
+
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+
+ // Create SVG elements
+ this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+ this.gridGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ this.curvePath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ this.dotsGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ this.tooltipGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter(dot => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map(dot => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+
+ // Create a loading indicator (optional)
+ const loadingCount = {current: 0, total: imageUrls.length};
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log('All images preloaded successfully');
+ }
+ };
+
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+
+ // Set src to start loading
+ img.src = url;
+
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ };
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = 'connected-dots-styles';
+
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement('style');
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute('width', `${this.config.totalWidth}`);
+ this.svg.setAttribute('height', `${this.config.height}`);
+ this.svg.style.overflow = 'visible';
+ this.scrollContainer.appendChild(this.svg);
+
+ // Configure grid group
+ this.gridGroup.classList.add('grid');
+ this.svg.appendChild(this.gridGroup);
+
+ // Configure curve path
+ this.curvePath.setAttribute('fill', 'none');
+ this.curvePath.setAttribute('stroke', 'white');
+ this.curvePath.setAttribute('stroke-width', '2');
+ this.curvePath.setAttribute('stroke-linecap', 'round');
+ this.curvePath.classList.add('curve-path');
+ this.svg.appendChild(this.curvePath);
+
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add('tooltips');
+ this.svg.appendChild(this.tooltipGroup);
+ }
+
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * (this.config.height / 2 * 0.8);
+
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+
+ private calculateBezierControlPoints(dots: DotConfig[], index: number): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+
+ return { x1, y1, x2, y2 };
+ }
+
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return '';
+
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(this.dots[0].value)}`;
+
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(this.dots, i);
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+
+ return path;
+ }
+
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+
+ if (!this.config.showGrid) return;
+
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+ line.setAttribute('x1', '0');
+ line.setAttribute('y1', this.getDotY(value).toString());
+ line.setAttribute('x2', this.config.totalWidth.toString());
+ line.setAttribute('y2', this.getDotY(value).toString());
+ line.setAttribute('stroke', 'rgba(219, 39, 119, 0.4)');
+ line.setAttribute('stroke-width', '1');
+ this.gridGroup.appendChild(line);
+
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ text.setAttribute('x', '10');
+ text.setAttribute('y', (this.getDotY(value) + 4).toString());
+ text.setAttribute('fill', 'rgba(219, 39, 119, 0.8)');
+ text.setAttribute('font-size', '12');
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+
+ // Vertical grid lines
+ const numVertLines = Math.ceil(this.config.totalWidth / this.config.xUnitSize);
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+ line.setAttribute('x1', x.toString());
+ line.setAttribute('y1', '0');
+ line.setAttribute('x2', x.toString());
+ line.setAttribute('y2', this.config.height.toString());
+ line.setAttribute('stroke', 'rgba(219, 39, 119, 0.4)');
+ line.setAttribute('stroke-width', '1');
+ this.gridGroup.appendChild(line);
+
+ if (xValue !== 0) {
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ text.setAttribute('x', x.toString());
+ text.setAttribute('y', (this.config.height / 2 + 20).toString());
+ text.setAttribute('fill', 'rgba(219, 39, 119, 0.8)');
+ text.setAttribute('font-size', '12');
+ text.setAttribute('text-anchor', 'middle');
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ tooltip.classList.add('dot-tooltip');
+ tooltip.setAttribute('data-dot-id', dot.id.toString());
+
+ // Calculate tooltip position
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipHeight = this.config.tooltipHeight;
+ const tooltipX = x - tooltipWidth / 2;
+
+ // Calculate tooltip Y position, ensuring it stays within the container
+ let tooltipY = y - tooltipHeight - 20; // Position above the dot with some spacing
+
+ // Ensure tooltip doesn't go above the container
+ tooltipY = Math.max(tooltipY, 10); // Keep at least 10px from the top
+
+ // Background rectangle
+ // Create a square background for the tooltip
+ const bg = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+ bg.setAttribute('x', tooltipX.toString());
+ bg.setAttribute('y', tooltipY.toString());
+
+ // Make the width and height equal for a square shape
+ const squareSize = Math.min(tooltipWidth, tooltipHeight);
+ bg.setAttribute('width', squareSize.toString());
+ bg.setAttribute('height', squareSize.toString());
+
+ // Optional: Adjust corner rounding if needed
+ bg.setAttribute('rx', '2'); // You can change this value for rounder corners
+ bg.setAttribute('ry', '2');
+
+ // Set the fill color to white
+ bg.setAttribute('fill', 'white');
+
+ tooltip.appendChild(bg);
+
+
+ // Tooltip arrow (pointing to the dot)
+ const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ arrow.setAttribute('d', `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${tooltipY + tooltipHeight - 10} L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`);
+ arrow.setAttribute('fill', 'rgba(0, 0, 0, 0.8)');
+ tooltip.appendChild(arrow);
+
+ // Image (if provided)
+ if (dot.imageUrl) {
+ const imgContainer = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ imgContainer.setAttribute('x', (tooltipX + 10).toString());
+ imgContainer.setAttribute('y', (tooltipY + 10).toString());
+
+ // Set width and height to the same value for a square aspect
+ const imageSize = Math.min((tooltipWidth - 20), (tooltipHeight / 2));
+ imgContainer.setAttribute('width', imageSize.toString());
+ imgContainer.setAttribute('height', imageSize.toString());
+
+ const img = document.createElement('img');
+ img.src = dot.imageUrl;
+ img.className = 'tooltip-img';
+ img.style.width = '100%';
+ img.style.height = '100%';
+ img.style.objectFit = 'cover'; // Ensure the image covers the space while maintaining aspect ratio
+
+ imgContainer.appendChild(img);
+ tooltip.appendChild(imgContainer);
+ }
+
+ // Title (if provided)
+ if (dot.title) {
+ const title = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ title.setAttribute('x', (tooltipX + 10).toString());
+ title.setAttribute('y', dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 26).toString()
+ : (tooltipY + 25).toString());
+ title.setAttribute('fill', 'white');
+ title.setAttribute('font-size', '14');
+ title.setAttribute('font-weight', 'bold');
+ title.textContent = dot.title;
+ tooltip.appendChild(title);
+ }
+
+ // Description (if provided)
+ if (dot.description) {
+ const descriptionFO = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ descriptionFO.setAttribute('x', (tooltipX + 10).toString());
+ descriptionFO.setAttribute('y', dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 32).toString()
+ : dot.title
+ ? (tooltipY + 35).toString()
+ : (tooltipY + 15).toString());
+ descriptionFO.setAttribute('width', (tooltipWidth - 20).toString());
+ descriptionFO.setAttribute('height', (tooltipHeight / 2 - 10).toString());
+
+ const descriptionDiv = document.createElement('div');
+ descriptionDiv.style.color = 'white';
+ descriptionDiv.style.fontSize = '12px';
+ descriptionDiv.style.overflow = 'hidden';
+ descriptionDiv.textContent = dot.description;
+
+ descriptionFO.appendChild(descriptionDiv);
+ tooltip.appendChild(descriptionFO);
+ }
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute('d', pathData);
+ }
+
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ return { leftmost, rightmost };
+ }
+
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+
+ const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
+ circle.setAttribute('cx', x.toString());
+ circle.setAttribute('cy', y.toString());
+ circle.setAttribute('r', this.config.dotRadius.toString());
+ circle.setAttribute('fill', 'white');
+ circle.setAttribute('data-dot-id', dot.id.toString());
+ circle.classList.add('dot');
+
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener('click', () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error('Dot has no link');
+ throw new Error('Dot has no link');
+ }
+ });
+ }
+
+ this.dotsGroup.appendChild(circle);
+ };
+ }
+
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute('width', `${this.config.totalWidth}`);
+
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map(dot => dot.x));
+ const maxX = Math.max(...this.dots.map(dot => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute('height', `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522091747.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522091747.ts
new file mode 100644
index 0000000..fe1a619
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522091747.ts
@@ -0,0 +1,561 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+
+ private preloadedImages: Map = new Map();
+
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+
+ constructor(containerId: string, dots: DotConfig[], config?: Partial) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map(dot => dot.x));
+ const maxX = Math.max(...this.dots.map(dot => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config
+ };
+
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+
+ // Create SVG elements
+ this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+ this.gridGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ this.curvePath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ this.dotsGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ this.tooltipGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter(dot => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map(dot => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+
+ // Create a loading indicator (optional)
+ const loadingCount = {current: 0, total: imageUrls.length};
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log('All images preloaded successfully');
+ }
+ };
+
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+
+ // Set src to start loading
+ img.src = url;
+
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ };
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = 'connected-dots-styles';
+
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement('style');
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute('width', `${this.config.totalWidth}`);
+ this.svg.setAttribute('height', `${this.config.height}`);
+ this.svg.style.overflow = 'visible';
+ this.scrollContainer.appendChild(this.svg);
+
+ // Configure grid group
+ this.gridGroup.classList.add('grid');
+ this.svg.appendChild(this.gridGroup);
+
+ // Configure curve path
+ this.curvePath.setAttribute('fill', 'none');
+ this.curvePath.setAttribute('stroke', 'white');
+ this.curvePath.setAttribute('stroke-width', '2');
+ this.curvePath.setAttribute('stroke-linecap', 'round');
+ this.curvePath.classList.add('curve-path');
+ this.svg.appendChild(this.curvePath);
+
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add('tooltips');
+ this.svg.appendChild(this.tooltipGroup);
+ }
+
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * (this.config.height / 2 * 0.8);
+
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+
+ private calculateBezierControlPoints(dots: DotConfig[], index: number): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+
+ return { x1, y1, x2, y2 };
+ }
+
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return '';
+
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(this.dots[0].value)}`;
+
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(this.dots, i);
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+
+ return path;
+ }
+
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+
+ if (!this.config.showGrid) return;
+
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+ line.setAttribute('x1', '0');
+ line.setAttribute('y1', this.getDotY(value).toString());
+ line.setAttribute('x2', this.config.totalWidth.toString());
+ line.setAttribute('y2', this.getDotY(value).toString());
+ line.setAttribute('stroke', 'rgba(219, 39, 119, 0.4)');
+ line.setAttribute('stroke-width', '1');
+ this.gridGroup.appendChild(line);
+
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ text.setAttribute('x', '10');
+ text.setAttribute('y', (this.getDotY(value) + 4).toString());
+ text.setAttribute('fill', 'rgba(219, 39, 119, 0.8)');
+ text.setAttribute('font-size', '12');
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+
+ // Vertical grid lines
+ const numVertLines = Math.ceil(this.config.totalWidth / this.config.xUnitSize);
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+ line.setAttribute('x1', x.toString());
+ line.setAttribute('y1', '0');
+ line.setAttribute('x2', x.toString());
+ line.setAttribute('y2', this.config.height.toString());
+ line.setAttribute('stroke', 'rgba(219, 39, 119, 0.4)');
+ line.setAttribute('stroke-width', '1');
+ this.gridGroup.appendChild(line);
+
+ if (xValue !== 0) {
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ text.setAttribute('x', x.toString());
+ text.setAttribute('y', (this.config.height / 2 + 20).toString());
+ text.setAttribute('fill', 'rgba(219, 39, 119, 0.8)');
+ text.setAttribute('font-size', '12');
+ text.setAttribute('text-anchor', 'middle');
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ tooltip.classList.add('dot-tooltip');
+ tooltip.setAttribute('data-dot-id', dot.id.toString());
+
+ // Calculate tooltip position
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipHeight = this.config.tooltipHeight;
+ const tooltipX = x - tooltipWidth / 2;
+
+ // Calculate tooltip Y position, ensuring it stays within the container
+ let tooltipY = y - tooltipHeight - 20; // Position above the dot with some spacing
+
+ // Ensure tooltip doesn't go above the container
+ tooltipY = Math.max(tooltipY, 10); // Keep at least 10px from the top
+
+ // Background rectangle
+ // Create a square background for the tooltip
+ const bg = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+ bg.setAttribute('x', tooltipX.toString());
+ bg.setAttribute('y', tooltipY.toString());
+
+ // Make the width and height equal for a square shape
+ const squareSize = Math.min(tooltipWidth, tooltipHeight);
+ bg.setAttribute('width', squareSize.toString());
+ bg.setAttribute('height', squareSize.toString());
+
+ // Optional: Adjust corner rounding if needed
+ bg.setAttribute('rx', '10'); // You can change this value for rounder corners
+ bg.setAttribute('ry', '10');
+
+ // Set the fill color to white
+ bg.setAttribute('fill', 'white');
+
+ tooltip.appendChild(bg);
+
+
+ // Tooltip arrow (pointing to the dot)
+ const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ arrow.setAttribute('d', `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${tooltipY + tooltipHeight - 10} L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`);
+ arrow.setAttribute('fill', 'rgba(0, 0, 0, 0.8)');
+ tooltip.appendChild(arrow);
+
+ // Image (if provided)
+ if (dot.imageUrl) {
+ const imgContainer = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ imgContainer.setAttribute('x', (tooltipX + 10).toString());
+ imgContainer.setAttribute('y', (tooltipY + 10).toString());
+
+ // Set width and height to the same value for a square aspect
+ const imageSize = Math.min((tooltipWidth - 20), (tooltipHeight / 2));
+ imgContainer.setAttribute('width', imageSize.toString());
+ imgContainer.setAttribute('height', imageSize.toString());
+
+ const img = document.createElement('img');
+ img.src = dot.imageUrl;
+ img.className = 'tooltip-img';
+ img.style.width = '100%';
+ img.style.height = '100%';
+ img.style.objectFit = 'cover'; // Ensure the image covers the space while maintaining aspect ratio
+
+ imgContainer.appendChild(img);
+ tooltip.appendChild(imgContainer);
+ }
+
+ // Title (if provided)
+ if (dot.title) {
+ const title = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ title.setAttribute('x', (tooltipX + 10).toString());
+ title.setAttribute('y', dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 26).toString()
+ : (tooltipY + 25).toString());
+ title.setAttribute('fill', 'white');
+ title.setAttribute('font-size', '14');
+ title.setAttribute('font-weight', 'bold');
+ title.textContent = dot.title;
+ tooltip.appendChild(title);
+ }
+
+ // Description (if provided)
+ if (dot.description) {
+ const descriptionFO = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ descriptionFO.setAttribute('x', (tooltipX + 10).toString());
+ descriptionFO.setAttribute('y', dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 32).toString()
+ : dot.title
+ ? (tooltipY + 35).toString()
+ : (tooltipY + 15).toString());
+ descriptionFO.setAttribute('width', (tooltipWidth - 20).toString());
+ descriptionFO.setAttribute('height', (tooltipHeight / 2 - 10).toString());
+
+ const descriptionDiv = document.createElement('div');
+ descriptionDiv.style.color = 'white';
+ descriptionDiv.style.fontSize = '12px';
+ descriptionDiv.style.overflow = 'hidden';
+ descriptionDiv.textContent = dot.description;
+
+ descriptionFO.appendChild(descriptionDiv);
+ tooltip.appendChild(descriptionFO);
+ }
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute('d', pathData);
+ }
+
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ return { leftmost, rightmost };
+ }
+
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+
+ const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
+ circle.setAttribute('cx', x.toString());
+ circle.setAttribute('cy', y.toString());
+ circle.setAttribute('r', this.config.dotRadius.toString());
+ circle.setAttribute('fill', 'white');
+ circle.setAttribute('data-dot-id', dot.id.toString());
+ circle.classList.add('dot');
+
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener('click', () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error('Dot has no link');
+ throw new Error('Dot has no link');
+ }
+ });
+ }
+
+ this.dotsGroup.appendChild(circle);
+ };
+ }
+
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute('width', `${this.config.totalWidth}`);
+
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map(dot => dot.x));
+ const maxX = Math.max(...this.dots.map(dot => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute('height', `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522091858.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522091858.ts
new file mode 100644
index 0000000..3dde4dc
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522091858.ts
@@ -0,0 +1,565 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+
+ private preloadedImages: Map = new Map();
+
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+
+ constructor(containerId: string, dots: DotConfig[], config?: Partial) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map(dot => dot.x));
+ const maxX = Math.max(...this.dots.map(dot => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config
+ };
+
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+
+ // Create SVG elements
+ this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+ this.gridGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ this.curvePath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ this.dotsGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ this.tooltipGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter(dot => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map(dot => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+
+ // Create a loading indicator (optional)
+ const loadingCount = {current: 0, total: imageUrls.length};
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log('All images preloaded successfully');
+ }
+ };
+
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+
+ // Set src to start loading
+ img.src = url;
+
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ };
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = 'connected-dots-styles';
+
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement('style');
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute('width', `${this.config.totalWidth}`);
+ this.svg.setAttribute('height', `${this.config.height}`);
+ this.svg.style.overflow = 'visible';
+ this.scrollContainer.appendChild(this.svg);
+
+ // Configure grid group
+ this.gridGroup.classList.add('grid');
+ this.svg.appendChild(this.gridGroup);
+
+ // Configure curve path
+ this.curvePath.setAttribute('fill', 'none');
+ this.curvePath.setAttribute('stroke', 'white');
+ this.curvePath.setAttribute('stroke-width', '2');
+ this.curvePath.setAttribute('stroke-linecap', 'round');
+ this.curvePath.classList.add('curve-path');
+ this.svg.appendChild(this.curvePath);
+
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add('tooltips');
+ this.svg.appendChild(this.tooltipGroup);
+ }
+
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * (this.config.height / 2 * 0.8);
+
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+
+ private calculateBezierControlPoints(dots: DotConfig[], index: number): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+
+ return { x1, y1, x2, y2 };
+ }
+
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return '';
+
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(this.dots[0].value)}`;
+
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(this.dots, i);
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+
+ return path;
+ }
+
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+
+ if (!this.config.showGrid) return;
+
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+ line.setAttribute('x1', '0');
+ line.setAttribute('y1', this.getDotY(value).toString());
+ line.setAttribute('x2', this.config.totalWidth.toString());
+ line.setAttribute('y2', this.getDotY(value).toString());
+ line.setAttribute('stroke', 'rgba(219, 39, 119, 0.4)');
+ line.setAttribute('stroke-width', '1');
+ this.gridGroup.appendChild(line);
+
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ text.setAttribute('x', '10');
+ text.setAttribute('y', (this.getDotY(value) + 4).toString());
+ text.setAttribute('fill', 'rgba(219, 39, 119, 0.8)');
+ text.setAttribute('font-size', '12');
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+
+ // Vertical grid lines
+ const numVertLines = Math.ceil(this.config.totalWidth / this.config.xUnitSize);
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+ line.setAttribute('x1', x.toString());
+ line.setAttribute('y1', '0');
+ line.setAttribute('x2', x.toString());
+ line.setAttribute('y2', this.config.height.toString());
+ line.setAttribute('stroke', 'rgba(219, 39, 119, 0.4)');
+ line.setAttribute('stroke-width', '1');
+ this.gridGroup.appendChild(line);
+
+ if (xValue !== 0) {
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ text.setAttribute('x', x.toString());
+ text.setAttribute('y', (this.config.height / 2 + 20).toString());
+ text.setAttribute('fill', 'rgba(219, 39, 119, 0.8)');
+ text.setAttribute('font-size', '12');
+ text.setAttribute('text-anchor', 'middle');
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ tooltip.classList.add('dot-tooltip');
+ tooltip.setAttribute('data-dot-id', dot.id.toString());
+
+ // Calculate tooltip position
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipHeight = this.config.tooltipHeight;
+ const tooltipX = x - tooltipWidth / 2;
+
+ // Calculate tooltip Y position, ensuring it stays within the container
+ let tooltipY = y - tooltipHeight - 20; // Position above the dot with some spacing
+
+ // Ensure tooltip doesn't go above the container
+ tooltipY = Math.max(tooltipY, 10); // Keep at least 10px from the top
+
+ // Background rectangle
+ // Create a rectangle for the tooltip with a 9:16 aspect ratio
+ const bg = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+ bg.setAttribute('x', tooltipX.toString());
+ bg.setAttribute('y', tooltipY.toString());
+
+ // Calculate width and height based on ratio
+ const height = tooltipHeight;
+ const width = (9 / 16) * height;
+
+ // Set the width and height for a 9:16 aspect ratio
+ bg.setAttribute('width', width.toString());
+ bg.setAttribute('height', height.toString());
+
+ // Remove any background fill
+ bg.setAttribute('fill', 'none');
+
+ // Optional: Adjust corner rounding if needed
+ bg.setAttribute('rx', '5'); // You can set this to 0 for sharp corners
+ bg.setAttribute('ry', '5');
+
+ tooltip.appendChild(bg);
+
+
+
+ // Tooltip arrow (pointing to the dot)
+ const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ arrow.setAttribute('d', `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${tooltipY + tooltipHeight - 10} L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`);
+ arrow.setAttribute('fill', 'rgba(0, 0, 0, 0.8)');
+ tooltip.appendChild(arrow);
+
+ // Image (if provided)
+ if (dot.imageUrl) {
+ const imgContainer = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ imgContainer.setAttribute('x', (tooltipX + 10).toString());
+ imgContainer.setAttribute('y', (tooltipY + 10).toString());
+
+ // Set width and height to the same value for a square aspect
+ const imageSize = Math.min((tooltipWidth - 20), (tooltipHeight / 2));
+ imgContainer.setAttribute('width', imageSize.toString());
+ imgContainer.setAttribute('height', imageSize.toString());
+
+ const img = document.createElement('img');
+ img.src = dot.imageUrl;
+ img.className = 'tooltip-img';
+ img.style.width = '100%';
+ img.style.height = '100%';
+ img.style.objectFit = 'cover'; // Ensure the image covers the space while maintaining aspect ratio
+
+ imgContainer.appendChild(img);
+ tooltip.appendChild(imgContainer);
+ }
+
+ // Title (if provided)
+ if (dot.title) {
+ const title = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ title.setAttribute('x', (tooltipX + 10).toString());
+ title.setAttribute('y', dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 26).toString()
+ : (tooltipY + 25).toString());
+ title.setAttribute('fill', 'white');
+ title.setAttribute('font-size', '14');
+ title.setAttribute('font-weight', 'bold');
+ title.textContent = dot.title;
+ tooltip.appendChild(title);
+ }
+
+ // Description (if provided)
+ if (dot.description) {
+ const descriptionFO = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ descriptionFO.setAttribute('x', (tooltipX + 10).toString());
+ descriptionFO.setAttribute('y', dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 32).toString()
+ : dot.title
+ ? (tooltipY + 35).toString()
+ : (tooltipY + 15).toString());
+ descriptionFO.setAttribute('width', (tooltipWidth - 20).toString());
+ descriptionFO.setAttribute('height', (tooltipHeight / 2 - 10).toString());
+
+ const descriptionDiv = document.createElement('div');
+ descriptionDiv.style.color = 'white';
+ descriptionDiv.style.fontSize = '12px';
+ descriptionDiv.style.overflow = 'hidden';
+ descriptionDiv.textContent = dot.description;
+
+ descriptionFO.appendChild(descriptionDiv);
+ tooltip.appendChild(descriptionFO);
+ }
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute('d', pathData);
+ }
+
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ return { leftmost, rightmost };
+ }
+
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+
+ const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
+ circle.setAttribute('cx', x.toString());
+ circle.setAttribute('cy', y.toString());
+ circle.setAttribute('r', this.config.dotRadius.toString());
+ circle.setAttribute('fill', 'white');
+ circle.setAttribute('data-dot-id', dot.id.toString());
+ circle.classList.add('dot');
+
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener('click', () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error('Dot has no link');
+ throw new Error('Dot has no link');
+ }
+ });
+ }
+
+ this.dotsGroup.appendChild(circle);
+ };
+ }
+
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute('width', `${this.config.totalWidth}`);
+
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map(dot => dot.x));
+ const maxX = Math.max(...this.dots.map(dot => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute('height', `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522091957.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522091957.ts
new file mode 100644
index 0000000..8ab4815
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522091957.ts
@@ -0,0 +1,570 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+
+ private preloadedImages: Map = new Map();
+
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+
+ constructor(containerId: string, dots: DotConfig[], config?: Partial) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map(dot => dot.x));
+ const maxX = Math.max(...this.dots.map(dot => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config
+ };
+
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+
+ // Create SVG elements
+ this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+ this.gridGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ this.curvePath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ this.dotsGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ this.tooltipGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter(dot => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map(dot => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+
+ // Create a loading indicator (optional)
+ const loadingCount = {current: 0, total: imageUrls.length};
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log('All images preloaded successfully');
+ }
+ };
+
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+
+ // Set src to start loading
+ img.src = url;
+
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ };
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = 'connected-dots-styles';
+
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement('style');
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute('width', `${this.config.totalWidth}`);
+ this.svg.setAttribute('height', `${this.config.height}`);
+ this.svg.style.overflow = 'visible';
+ this.scrollContainer.appendChild(this.svg);
+
+ // Configure grid group
+ this.gridGroup.classList.add('grid');
+ this.svg.appendChild(this.gridGroup);
+
+ // Configure curve path
+ this.curvePath.setAttribute('fill', 'none');
+ this.curvePath.setAttribute('stroke', 'white');
+ this.curvePath.setAttribute('stroke-width', '2');
+ this.curvePath.setAttribute('stroke-linecap', 'round');
+ this.curvePath.classList.add('curve-path');
+ this.svg.appendChild(this.curvePath);
+
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add('tooltips');
+ this.svg.appendChild(this.tooltipGroup);
+ }
+
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * (this.config.height / 2 * 0.8);
+
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+
+ private calculateBezierControlPoints(dots: DotConfig[], index: number): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+
+ return { x1, y1, x2, y2 };
+ }
+
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return '';
+
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(this.dots[0].value)}`;
+
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(this.dots, i);
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+
+ return path;
+ }
+
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+
+ if (!this.config.showGrid) return;
+
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+ line.setAttribute('x1', '0');
+ line.setAttribute('y1', this.getDotY(value).toString());
+ line.setAttribute('x2', this.config.totalWidth.toString());
+ line.setAttribute('y2', this.getDotY(value).toString());
+ line.setAttribute('stroke', 'rgba(219, 39, 119, 0.4)');
+ line.setAttribute('stroke-width', '1');
+ this.gridGroup.appendChild(line);
+
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ text.setAttribute('x', '10');
+ text.setAttribute('y', (this.getDotY(value) + 4).toString());
+ text.setAttribute('fill', 'rgba(219, 39, 119, 0.8)');
+ text.setAttribute('font-size', '12');
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+
+ // Vertical grid lines
+ const numVertLines = Math.ceil(this.config.totalWidth / this.config.xUnitSize);
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+ line.setAttribute('x1', x.toString());
+ line.setAttribute('y1', '0');
+ line.setAttribute('x2', x.toString());
+ line.setAttribute('y2', this.config.height.toString());
+ line.setAttribute('stroke', 'rgba(219, 39, 119, 0.4)');
+ line.setAttribute('stroke-width', '1');
+ this.gridGroup.appendChild(line);
+
+ if (xValue !== 0) {
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ text.setAttribute('x', x.toString());
+ text.setAttribute('y', (this.config.height / 2 + 20).toString());
+ text.setAttribute('fill', 'rgba(219, 39, 119, 0.8)');
+ text.setAttribute('font-size', '12');
+ text.setAttribute('text-anchor', 'middle');
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ tooltip.classList.add('dot-tooltip');
+ tooltip.setAttribute('data-dot-id', dot.id.toString());
+
+ // Calculate tooltip position
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipHeight = this.config.tooltipHeight;
+ const tooltipX = x - tooltipWidth / 2;
+
+ // Calculate tooltip Y position, ensuring it stays within the container
+ let tooltipY = y - tooltipHeight - 20; // Position above the dot with some spacing
+
+ // Ensure tooltip doesn't go above the container
+ tooltipY = Math.max(tooltipY, 10); // Keep at least 10px from the top
+
+ // Background rectangle
+ // Create a rectangle for the tooltip with a 9:16 aspect ratio
+ const bg = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+ bg.setAttribute('x', tooltipX.toString());
+ bg.setAttribute('y', tooltipY.toString());
+
+ // Calculate width and height based on ratio
+ const height = tooltipHeight;
+ const width = (9 / 16) * height;
+
+ // Set the width and height for a 9:16 aspect ratio
+ bg.setAttribute('width', width.toString());
+ bg.setAttribute('height', height.toString());
+
+ // Remove any background fill
+ bg.setAttribute('fill', 'none');
+
+ // Optional: Adjust corner rounding if needed
+ bg.setAttribute('rx', '5'); // You can set this to 0 for sharp corners
+ bg.setAttribute('ry', '5');
+
+ tooltip.appendChild(bg);
+
+
+
+ // Tooltip arrow (pointing to the dot)
+ const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ arrow.setAttribute('d', `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${tooltipY + tooltipHeight - 10} L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`);
+ arrow.setAttribute('fill', 'rgba(0, 0, 0, 0.8)');
+ tooltip.appendChild(arrow);
+
+ // Image (if provided)
+ if (dot.imageUrl) {
+ const imgContainer = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ imgContainer.setAttribute('x', (tooltipX + 10).toString());
+ imgContainer.setAttribute('y', (tooltipY + 10).toString());
+
+ // Set width and height to the same value for a square aspect ratio
+ const imageSize = Math.min((tooltipWidth - 20), (tooltipHeight / 2));
+ imgContainer.setAttribute('width', imageSize.toString());
+ imgContainer.setAttribute('height', imageSize.toString());
+
+ const img = document.createElement('img');
+ img.src = dot.imageUrl;
+ img.className = 'tooltip-img';
+ img.style.width = '100%';
+ img.style.height = '100%';
+ img.style.objectFit = 'cover'; // Ensure the image covers the space while maintaining aspect ratio
+
+ // Make the image circular and add a white border
+ img.style.borderRadius = '50%'; // Makes the image round
+ img.style.border = '1px solid white'; // Adds a 1px white border around the image
+
+ imgContainer.appendChild(img);
+ tooltip.appendChild(imgContainer);
+ }
+
+
+ // Title (if provided)
+ if (dot.title) {
+ const title = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ title.setAttribute('x', (tooltipX + 10).toString());
+ title.setAttribute('y', dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 26).toString()
+ : (tooltipY + 25).toString());
+ title.setAttribute('fill', 'white');
+ title.setAttribute('font-size', '14');
+ title.setAttribute('font-weight', 'bold');
+ title.textContent = dot.title;
+ tooltip.appendChild(title);
+ }
+
+ // Description (if provided)
+ if (dot.description) {
+ const descriptionFO = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ descriptionFO.setAttribute('x', (tooltipX + 10).toString());
+ descriptionFO.setAttribute('y', dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 32).toString()
+ : dot.title
+ ? (tooltipY + 35).toString()
+ : (tooltipY + 15).toString());
+ descriptionFO.setAttribute('width', (tooltipWidth - 20).toString());
+ descriptionFO.setAttribute('height', (tooltipHeight / 2 - 10).toString());
+
+ const descriptionDiv = document.createElement('div');
+ descriptionDiv.style.color = 'white';
+ descriptionDiv.style.fontSize = '12px';
+ descriptionDiv.style.overflow = 'hidden';
+ descriptionDiv.textContent = dot.description;
+
+ descriptionFO.appendChild(descriptionDiv);
+ tooltip.appendChild(descriptionFO);
+ }
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute('d', pathData);
+ }
+
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ return { leftmost, rightmost };
+ }
+
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+
+ const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
+ circle.setAttribute('cx', x.toString());
+ circle.setAttribute('cy', y.toString());
+ circle.setAttribute('r', this.config.dotRadius.toString());
+ circle.setAttribute('fill', 'white');
+ circle.setAttribute('data-dot-id', dot.id.toString());
+ circle.classList.add('dot');
+
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener('click', () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error('Dot has no link');
+ throw new Error('Dot has no link');
+ }
+ });
+ }
+
+ this.dotsGroup.appendChild(circle);
+ };
+ }
+
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute('width', `${this.config.totalWidth}`);
+
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map(dot => dot.x));
+ const maxX = Math.max(...this.dots.map(dot => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute('height', `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522092004.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522092004.ts
new file mode 100644
index 0000000..c8fde55
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522092004.ts
@@ -0,0 +1,570 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+
+ private preloadedImages: Map = new Map();
+
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+
+ constructor(containerId: string, dots: DotConfig[], config?: Partial) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map(dot => dot.x));
+ const maxX = Math.max(...this.dots.map(dot => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config
+ };
+
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+
+ // Create SVG elements
+ this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+ this.gridGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ this.curvePath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ this.dotsGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ this.tooltipGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter(dot => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map(dot => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+
+ // Create a loading indicator (optional)
+ const loadingCount = {current: 0, total: imageUrls.length};
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log('All images preloaded successfully');
+ }
+ };
+
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+
+ // Set src to start loading
+ img.src = url;
+
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ };
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = 'connected-dots-styles';
+
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement('style');
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute('width', `${this.config.totalWidth}`);
+ this.svg.setAttribute('height', `${this.config.height}`);
+ this.svg.style.overflow = 'visible';
+ this.scrollContainer.appendChild(this.svg);
+
+ // Configure grid group
+ this.gridGroup.classList.add('grid');
+ this.svg.appendChild(this.gridGroup);
+
+ // Configure curve path
+ this.curvePath.setAttribute('fill', 'none');
+ this.curvePath.setAttribute('stroke', 'white');
+ this.curvePath.setAttribute('stroke-width', '2');
+ this.curvePath.setAttribute('stroke-linecap', 'round');
+ this.curvePath.classList.add('curve-path');
+ this.svg.appendChild(this.curvePath);
+
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add('tooltips');
+ this.svg.appendChild(this.tooltipGroup);
+ }
+
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * (this.config.height / 2 * 0.8);
+
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+
+ private calculateBezierControlPoints(dots: DotConfig[], index: number): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+
+ return { x1, y1, x2, y2 };
+ }
+
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return '';
+
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(this.dots[0].value)}`;
+
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(this.dots, i);
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+
+ return path;
+ }
+
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+
+ if (!this.config.showGrid) return;
+
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+ line.setAttribute('x1', '0');
+ line.setAttribute('y1', this.getDotY(value).toString());
+ line.setAttribute('x2', this.config.totalWidth.toString());
+ line.setAttribute('y2', this.getDotY(value).toString());
+ line.setAttribute('stroke', 'rgba(219, 39, 119, 0.4)');
+ line.setAttribute('stroke-width', '1');
+ this.gridGroup.appendChild(line);
+
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ text.setAttribute('x', '10');
+ text.setAttribute('y', (this.getDotY(value) + 4).toString());
+ text.setAttribute('fill', 'rgba(219, 39, 119, 0.8)');
+ text.setAttribute('font-size', '12');
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+
+ // Vertical grid lines
+ const numVertLines = Math.ceil(this.config.totalWidth / this.config.xUnitSize);
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+ line.setAttribute('x1', x.toString());
+ line.setAttribute('y1', '0');
+ line.setAttribute('x2', x.toString());
+ line.setAttribute('y2', this.config.height.toString());
+ line.setAttribute('stroke', 'rgba(219, 39, 119, 0.4)');
+ line.setAttribute('stroke-width', '1');
+ this.gridGroup.appendChild(line);
+
+ if (xValue !== 0) {
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ text.setAttribute('x', x.toString());
+ text.setAttribute('y', (this.config.height / 2 + 20).toString());
+ text.setAttribute('fill', 'rgba(219, 39, 119, 0.8)');
+ text.setAttribute('font-size', '12');
+ text.setAttribute('text-anchor', 'middle');
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ tooltip.classList.add('dot-tooltip');
+ tooltip.setAttribute('data-dot-id', dot.id.toString());
+
+ // Calculate tooltip position
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipHeight = this.config.tooltipHeight;
+ const tooltipX = x - tooltipWidth / 2;
+
+ // Calculate tooltip Y position, ensuring it stays within the container
+ let tooltipY = y - tooltipHeight - 20; // Position above the dot with some spacing
+
+ // Ensure tooltip doesn't go above the container
+ tooltipY = Math.max(tooltipY, 10); // Keep at least 10px from the top
+
+ // Background rectangle
+ // Create a rectangle for the tooltip with a 9:16 aspect ratio
+ const bg = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+ bg.setAttribute('x', tooltipX.toString());
+ bg.setAttribute('y', tooltipY.toString());
+
+ // Calculate width and height based on ratio
+ const height = tooltipHeight;
+ const width = (9 / 16) * height;
+
+ // Set the width and height for a 9:16 aspect ratio
+ bg.setAttribute('width', width.toString());
+ bg.setAttribute('height', height.toString());
+
+ // Remove any background fill
+ bg.setAttribute('fill', 'none');
+
+ // Optional: Adjust corner rounding if needed
+ bg.setAttribute('rx', '5'); // You can set this to 0 for sharp corners
+ bg.setAttribute('ry', '5');
+
+ tooltip.appendChild(bg);
+
+
+
+ // Tooltip arrow (pointing to the dot)
+ const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ arrow.setAttribute('d', `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${tooltipY + tooltipHeight - 10} L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`);
+ arrow.setAttribute('fill', 'rgba(0, 0, 0, 0.8)');
+ tooltip.appendChild(arrow);
+
+ // Image (if provided)
+ if (dot.imageUrl) {
+ const imgContainer = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ imgContainer.setAttribute('x', (tooltipX + 10).toString());
+ imgContainer.setAttribute('y', (tooltipY + 10).toString());
+
+ // Set width and height to the same value for a square aspect ratio
+ const imageSize = Math.min((tooltipWidth - 20), (tooltipHeight / 2));
+ imgContainer.setAttribute('width', imageSize.toString());
+ imgContainer.setAttribute('height', imageSize.toString());
+
+ const img = document.createElement('img');
+ img.src = dot.imageUrl;
+ img.className = 'tooltip-img';
+ img.style.width = '100%';
+ img.style.height = '100%';
+ img.style.objectFit = 'cover'; // Ensure the image covers the space while maintaining aspect ratio
+
+ // Make the image circular and add a white border
+ img.style.borderRadius = '50%'; // Makes the image round
+ img.style.border = '2px solid white'; // Adds a 1px white border around the image
+
+ imgContainer.appendChild(img);
+ tooltip.appendChild(imgContainer);
+ }
+
+
+ // Title (if provided)
+ if (dot.title) {
+ const title = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ title.setAttribute('x', (tooltipX + 10).toString());
+ title.setAttribute('y', dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 26).toString()
+ : (tooltipY + 25).toString());
+ title.setAttribute('fill', 'white');
+ title.setAttribute('font-size', '14');
+ title.setAttribute('font-weight', 'bold');
+ title.textContent = dot.title;
+ tooltip.appendChild(title);
+ }
+
+ // Description (if provided)
+ if (dot.description) {
+ const descriptionFO = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ descriptionFO.setAttribute('x', (tooltipX + 10).toString());
+ descriptionFO.setAttribute('y', dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 32).toString()
+ : dot.title
+ ? (tooltipY + 35).toString()
+ : (tooltipY + 15).toString());
+ descriptionFO.setAttribute('width', (tooltipWidth - 20).toString());
+ descriptionFO.setAttribute('height', (tooltipHeight / 2 - 10).toString());
+
+ const descriptionDiv = document.createElement('div');
+ descriptionDiv.style.color = 'white';
+ descriptionDiv.style.fontSize = '12px';
+ descriptionDiv.style.overflow = 'hidden';
+ descriptionDiv.textContent = dot.description;
+
+ descriptionFO.appendChild(descriptionDiv);
+ tooltip.appendChild(descriptionFO);
+ }
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute('d', pathData);
+ }
+
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ return { leftmost, rightmost };
+ }
+
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+
+ const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
+ circle.setAttribute('cx', x.toString());
+ circle.setAttribute('cy', y.toString());
+ circle.setAttribute('r', this.config.dotRadius.toString());
+ circle.setAttribute('fill', 'white');
+ circle.setAttribute('data-dot-id', dot.id.toString());
+ circle.classList.add('dot');
+
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener('click', () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error('Dot has no link');
+ throw new Error('Dot has no link');
+ }
+ });
+ }
+
+ this.dotsGroup.appendChild(circle);
+ };
+ }
+
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute('width', `${this.config.totalWidth}`);
+
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map(dot => dot.x));
+ const maxX = Math.max(...this.dots.map(dot => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute('height', `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522092036.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522092036.ts
new file mode 100644
index 0000000..72817cb
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522092036.ts
@@ -0,0 +1,570 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+
+ private preloadedImages: Map = new Map();
+
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+
+ constructor(containerId: string, dots: DotConfig[], config?: Partial) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map(dot => dot.x));
+ const maxX = Math.max(...this.dots.map(dot => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config
+ };
+
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+
+ // Create SVG elements
+ this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+ this.gridGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ this.curvePath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ this.dotsGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ this.tooltipGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter(dot => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map(dot => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+
+ // Create a loading indicator (optional)
+ const loadingCount = {current: 0, total: imageUrls.length};
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log('All images preloaded successfully');
+ }
+ };
+
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+
+ // Set src to start loading
+ img.src = url;
+
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ };
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = 'connected-dots-styles';
+
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement('style');
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute('width', `${this.config.totalWidth}`);
+ this.svg.setAttribute('height', `${this.config.height}`);
+ this.svg.style.overflow = 'visible';
+ this.scrollContainer.appendChild(this.svg);
+
+ // Configure grid group
+ this.gridGroup.classList.add('grid');
+ this.svg.appendChild(this.gridGroup);
+
+ // Configure curve path
+ this.curvePath.setAttribute('fill', 'none');
+ this.curvePath.setAttribute('stroke', 'white');
+ this.curvePath.setAttribute('stroke-width', '2');
+ this.curvePath.setAttribute('stroke-linecap', 'round');
+ this.curvePath.classList.add('curve-path');
+ this.svg.appendChild(this.curvePath);
+
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add('tooltips');
+ this.svg.appendChild(this.tooltipGroup);
+ }
+
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * (this.config.height / 2 * 0.8);
+
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+
+ private calculateBezierControlPoints(dots: DotConfig[], index: number): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+
+ return { x1, y1, x2, y2 };
+ }
+
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return '';
+
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(this.dots[0].value)}`;
+
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(this.dots, i);
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+
+ return path;
+ }
+
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+
+ if (!this.config.showGrid) return;
+
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+ line.setAttribute('x1', '0');
+ line.setAttribute('y1', this.getDotY(value).toString());
+ line.setAttribute('x2', this.config.totalWidth.toString());
+ line.setAttribute('y2', this.getDotY(value).toString());
+ line.setAttribute('stroke', 'rgba(219, 39, 119, 0.4)');
+ line.setAttribute('stroke-width', '1');
+ this.gridGroup.appendChild(line);
+
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ text.setAttribute('x', '10');
+ text.setAttribute('y', (this.getDotY(value) + 4).toString());
+ text.setAttribute('fill', 'rgba(219, 39, 119, 0.8)');
+ text.setAttribute('font-size', '12');
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+
+ // Vertical grid lines
+ const numVertLines = Math.ceil(this.config.totalWidth / this.config.xUnitSize);
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+ line.setAttribute('x1', x.toString());
+ line.setAttribute('y1', '0');
+ line.setAttribute('x2', x.toString());
+ line.setAttribute('y2', this.config.height.toString());
+ line.setAttribute('stroke', 'rgba(219, 39, 119, 0.4)');
+ line.setAttribute('stroke-width', '1');
+ this.gridGroup.appendChild(line);
+
+ if (xValue !== 0) {
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ text.setAttribute('x', x.toString());
+ text.setAttribute('y', (this.config.height / 2 + 20).toString());
+ text.setAttribute('fill', 'rgba(219, 39, 119, 0.8)');
+ text.setAttribute('font-size', '12');
+ text.setAttribute('text-anchor', 'middle');
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ tooltip.classList.add('dot-tooltip');
+ tooltip.setAttribute('data-dot-id', dot.id.toString());
+
+ // Calculate tooltip position
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipHeight = this.config.tooltipHeight;
+ const tooltipX = x - tooltipWidth / 2;
+
+ // Calculate tooltip Y position, ensuring it stays within the container
+ let tooltipY = y - tooltipHeight - 20; // Position above the dot with some spacing
+
+ // Ensure tooltip doesn't go above the container
+ tooltipY = Math.max(tooltipY, 10); // Keep at least 10px from the top
+
+ // Background rectangle
+ // Create a rectangle for the tooltip with a 9:16 aspect ratio
+ const bg = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+ bg.setAttribute('x', tooltipX.toString());
+ bg.setAttribute('y', tooltipY.toString());
+
+ // Calculate width and height based on ratio
+ const height = tooltipHeight;
+ const width = (9 / 16) * height;
+
+ // Set the width and height for a 9:16 aspect ratio
+ bg.setAttribute('width', width.toString());
+ bg.setAttribute('height', height.toString());
+
+ // Remove any background fill
+ bg.setAttribute('fill', 'none');
+
+ // Optional: Adjust corner rounding if needed
+ bg.setAttribute('rx', '5'); // You can set this to 0 for sharp corners
+ bg.setAttribute('ry', '5');
+
+ tooltip.appendChild(bg);
+
+
+
+ // Tooltip arrow (pointing to the dot)
+ const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ arrow.setAttribute('d', `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${tooltipY + tooltipHeight - 10} L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`);
+ arrow.setAttribute('fill', 'rgba(0, 0, 0, 0.8)');
+ tooltip.appendChild(arrow);
+
+ // Image (if provided)
+ if (dot.imageUrl) {
+ const imgContainer = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ imgContainer.setAttribute('x', (tooltipX + 10).toString());
+ imgContainer.setAttribute('y', (tooltipY + 10).toString());
+
+ // Set width and height to the same value for a square aspect ratio
+ const imageSize = Math.min((tooltipWidth - 20), (tooltipHeight / 2));
+ imgContainer.setAttribute('width', imageSize.toString());
+ imgContainer.setAttribute('height', imageSize.toString());
+
+ const img = document.createElement('img');
+ img.src = dot.imageUrl;
+ img.className = 'tooltip-img';
+ img.style.width = '100%';
+ img.style.height = 'calc(100% - 4px)';
+ img.style.objectFit = 'cover'; // Ensure the image covers the space while maintaining aspect ratio
+
+ // Make the image circular and add a white border
+ img.style.borderRadius = '50%'; // Makes the image round
+ img.style.border = '2px solid white'; // Adds a 1px white border around the image
+
+ imgContainer.appendChild(img);
+ tooltip.appendChild(imgContainer);
+ }
+
+
+ // Title (if provided)
+ if (dot.title) {
+ const title = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ title.setAttribute('x', (tooltipX + 10).toString());
+ title.setAttribute('y', dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 26).toString()
+ : (tooltipY + 25).toString());
+ title.setAttribute('fill', 'white');
+ title.setAttribute('font-size', '14');
+ title.setAttribute('font-weight', 'bold');
+ title.textContent = dot.title;
+ tooltip.appendChild(title);
+ }
+
+ // Description (if provided)
+ if (dot.description) {
+ const descriptionFO = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ descriptionFO.setAttribute('x', (tooltipX + 10).toString());
+ descriptionFO.setAttribute('y', dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 32).toString()
+ : dot.title
+ ? (tooltipY + 35).toString()
+ : (tooltipY + 15).toString());
+ descriptionFO.setAttribute('width', (tooltipWidth - 20).toString());
+ descriptionFO.setAttribute('height', (tooltipHeight / 2 - 10).toString());
+
+ const descriptionDiv = document.createElement('div');
+ descriptionDiv.style.color = 'white';
+ descriptionDiv.style.fontSize = '12px';
+ descriptionDiv.style.overflow = 'hidden';
+ descriptionDiv.textContent = dot.description;
+
+ descriptionFO.appendChild(descriptionDiv);
+ tooltip.appendChild(descriptionFO);
+ }
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute('d', pathData);
+ }
+
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ return { leftmost, rightmost };
+ }
+
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+
+ const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
+ circle.setAttribute('cx', x.toString());
+ circle.setAttribute('cy', y.toString());
+ circle.setAttribute('r', this.config.dotRadius.toString());
+ circle.setAttribute('fill', 'white');
+ circle.setAttribute('data-dot-id', dot.id.toString());
+ circle.classList.add('dot');
+
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener('click', () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error('Dot has no link');
+ throw new Error('Dot has no link');
+ }
+ });
+ }
+
+ this.dotsGroup.appendChild(circle);
+ };
+ }
+
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute('width', `${this.config.totalWidth}`);
+
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map(dot => dot.x));
+ const maxX = Math.max(...this.dots.map(dot => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute('height', `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522092041.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522092041.ts
new file mode 100644
index 0000000..11e1464
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522092041.ts
@@ -0,0 +1,570 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+
+ private preloadedImages: Map = new Map();
+
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+
+ constructor(containerId: string, dots: DotConfig[], config?: Partial) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map(dot => dot.x));
+ const maxX = Math.max(...this.dots.map(dot => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config
+ };
+
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+
+ // Create SVG elements
+ this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+ this.gridGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ this.curvePath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ this.dotsGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ this.tooltipGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter(dot => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map(dot => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+
+ // Create a loading indicator (optional)
+ const loadingCount = {current: 0, total: imageUrls.length};
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log('All images preloaded successfully');
+ }
+ };
+
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+
+ // Set src to start loading
+ img.src = url;
+
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ };
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = 'connected-dots-styles';
+
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement('style');
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute('width', `${this.config.totalWidth}`);
+ this.svg.setAttribute('height', `${this.config.height}`);
+ this.svg.style.overflow = 'visible';
+ this.scrollContainer.appendChild(this.svg);
+
+ // Configure grid group
+ this.gridGroup.classList.add('grid');
+ this.svg.appendChild(this.gridGroup);
+
+ // Configure curve path
+ this.curvePath.setAttribute('fill', 'none');
+ this.curvePath.setAttribute('stroke', 'white');
+ this.curvePath.setAttribute('stroke-width', '2');
+ this.curvePath.setAttribute('stroke-linecap', 'round');
+ this.curvePath.classList.add('curve-path');
+ this.svg.appendChild(this.curvePath);
+
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add('tooltips');
+ this.svg.appendChild(this.tooltipGroup);
+ }
+
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * (this.config.height / 2 * 0.8);
+
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+
+ private calculateBezierControlPoints(dots: DotConfig[], index: number): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+
+ return { x1, y1, x2, y2 };
+ }
+
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return '';
+
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(this.dots[0].value)}`;
+
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(this.dots, i);
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+
+ return path;
+ }
+
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+
+ if (!this.config.showGrid) return;
+
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+ line.setAttribute('x1', '0');
+ line.setAttribute('y1', this.getDotY(value).toString());
+ line.setAttribute('x2', this.config.totalWidth.toString());
+ line.setAttribute('y2', this.getDotY(value).toString());
+ line.setAttribute('stroke', 'rgba(219, 39, 119, 0.4)');
+ line.setAttribute('stroke-width', '1');
+ this.gridGroup.appendChild(line);
+
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ text.setAttribute('x', '10');
+ text.setAttribute('y', (this.getDotY(value) + 4).toString());
+ text.setAttribute('fill', 'rgba(219, 39, 119, 0.8)');
+ text.setAttribute('font-size', '12');
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+
+ // Vertical grid lines
+ const numVertLines = Math.ceil(this.config.totalWidth / this.config.xUnitSize);
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+ line.setAttribute('x1', x.toString());
+ line.setAttribute('y1', '0');
+ line.setAttribute('x2', x.toString());
+ line.setAttribute('y2', this.config.height.toString());
+ line.setAttribute('stroke', 'rgba(219, 39, 119, 0.4)');
+ line.setAttribute('stroke-width', '1');
+ this.gridGroup.appendChild(line);
+
+ if (xValue !== 0) {
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ text.setAttribute('x', x.toString());
+ text.setAttribute('y', (this.config.height / 2 + 20).toString());
+ text.setAttribute('fill', 'rgba(219, 39, 119, 0.8)');
+ text.setAttribute('font-size', '12');
+ text.setAttribute('text-anchor', 'middle');
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS('http://www.w3.org/2000/svg', 'g');
+ tooltip.classList.add('dot-tooltip');
+ tooltip.setAttribute('data-dot-id', dot.id.toString());
+
+ // Calculate tooltip position
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipHeight = this.config.tooltipHeight;
+ const tooltipX = x - tooltipWidth / 2;
+
+ // Calculate tooltip Y position, ensuring it stays within the container
+ let tooltipY = y - tooltipHeight - 20; // Position above the dot with some spacing
+
+ // Ensure tooltip doesn't go above the container
+ tooltipY = Math.max(tooltipY, 10); // Keep at least 10px from the top
+
+ // Background rectangle
+ // Create a rectangle for the tooltip with a 9:16 aspect ratio
+ const bg = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+ bg.setAttribute('x', tooltipX.toString());
+ bg.setAttribute('y', tooltipY.toString());
+
+ // Calculate width and height based on ratio
+ const height = tooltipHeight;
+ const width = (9 / 16) * height;
+
+ // Set the width and height for a 9:16 aspect ratio
+ bg.setAttribute('width', width.toString());
+ bg.setAttribute('height', height.toString());
+
+ // Remove any background fill
+ bg.setAttribute('fill', 'none');
+
+ // Optional: Adjust corner rounding if needed
+ bg.setAttribute('rx', '5'); // You can set this to 0 for sharp corners
+ bg.setAttribute('ry', '5');
+
+ tooltip.appendChild(bg);
+
+
+
+ // Tooltip arrow (pointing to the dot)
+ const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ arrow.setAttribute('d', `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${tooltipY + tooltipHeight - 10} L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`);
+ arrow.setAttribute('fill', 'rgba(0, 0, 0, 0.8)');
+ tooltip.appendChild(arrow);
+
+ // Image (if provided)
+ if (dot.imageUrl) {
+ const imgContainer = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ imgContainer.setAttribute('x', (tooltipX + 10).toString());
+ imgContainer.setAttribute('y', (tooltipY + 10).toString());
+
+ // Set width and height to the same value for a square aspect ratio
+ const imageSize = Math.min((tooltipWidth - 20), (tooltipHeight / 2));
+ imgContainer.setAttribute('width', imageSize.toString());
+ imgContainer.setAttribute('height', imageSize.toString());
+
+ const img = document.createElement('img');
+ img.src = dot.imageUrl;
+ img.className = 'tooltip-img';
+ img.style.width = 'calc(100% - 4px)';
+ img.style.height = 'calc(100% - 4px)';
+ img.style.objectFit = 'cover'; // Ensure the image covers the space while maintaining aspect ratio
+
+ // Make the image circular and add a white border
+ img.style.borderRadius = '50%'; // Makes the image round
+ img.style.border = '2px solid white'; // Adds a 1px white border around the image
+
+ imgContainer.appendChild(img);
+ tooltip.appendChild(imgContainer);
+ }
+
+
+ // Title (if provided)
+ if (dot.title) {
+ const title = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+ title.setAttribute('x', (tooltipX + 10).toString());
+ title.setAttribute('y', dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 26).toString()
+ : (tooltipY + 25).toString());
+ title.setAttribute('fill', 'white');
+ title.setAttribute('font-size', '14');
+ title.setAttribute('font-weight', 'bold');
+ title.textContent = dot.title;
+ tooltip.appendChild(title);
+ }
+
+ // Description (if provided)
+ if (dot.description) {
+ const descriptionFO = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ descriptionFO.setAttribute('x', (tooltipX + 10).toString());
+ descriptionFO.setAttribute('y', dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 32).toString()
+ : dot.title
+ ? (tooltipY + 35).toString()
+ : (tooltipY + 15).toString());
+ descriptionFO.setAttribute('width', (tooltipWidth - 20).toString());
+ descriptionFO.setAttribute('height', (tooltipHeight / 2 - 10).toString());
+
+ const descriptionDiv = document.createElement('div');
+ descriptionDiv.style.color = 'white';
+ descriptionDiv.style.fontSize = '12px';
+ descriptionDiv.style.overflow = 'hidden';
+ descriptionDiv.textContent = dot.description;
+
+ descriptionFO.appendChild(descriptionDiv);
+ tooltip.appendChild(descriptionFO);
+ }
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute('d', pathData);
+ }
+
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ return { leftmost, rightmost };
+ }
+
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+
+ const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
+ circle.setAttribute('cx', x.toString());
+ circle.setAttribute('cy', y.toString());
+ circle.setAttribute('r', this.config.dotRadius.toString());
+ circle.setAttribute('fill', 'white');
+ circle.setAttribute('data-dot-id', dot.id.toString());
+ circle.classList.add('dot');
+
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener('click', () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error('Dot has no link');
+ throw new Error('Dot has no link');
+ }
+ });
+ }
+
+ this.dotsGroup.appendChild(circle);
+ };
+ }
+
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute('width', `${this.config.totalWidth}`);
+
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map(dot => dot.x));
+ const maxX = Math.max(...this.dots.map(dot => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute('height', `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522092303.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522092303.ts
new file mode 100644
index 0000000..17e85ce
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522092303.ts
@@ -0,0 +1,631 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+
+ private preloadedImages: Map = new Map();
+
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+
+ constructor(
+ containerId: string,
+ dots: DotConfig[],
+ config?: Partial
+ ) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config,
+ };
+
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+
+ // Create SVG elements
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ this.gridGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.curvePath = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ this.dotsGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.tooltipGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter((dot) => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map((dot) => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+
+ // Create a loading indicator (optional)
+ const loadingCount = { current: 0, total: imageUrls.length };
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log("All images preloaded successfully");
+ }
+ };
+
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+
+ // Set src to start loading
+ img.src = url;
+
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ }
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = "connected-dots-styles";
+
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement("style");
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.svg.style.overflow = "visible";
+ this.scrollContainer.appendChild(this.svg);
+
+ // Configure grid group
+ this.gridGroup.classList.add("grid");
+ this.svg.appendChild(this.gridGroup);
+
+ // Configure curve path
+ this.curvePath.setAttribute("fill", "none");
+ this.curvePath.setAttribute("stroke", "white");
+ this.curvePath.setAttribute("stroke-width", "2");
+ this.curvePath.setAttribute("stroke-linecap", "round");
+ this.curvePath.classList.add("curve-path");
+ this.svg.appendChild(this.curvePath);
+
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add("tooltips");
+ this.svg.appendChild(this.tooltipGroup);
+ }
+
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.8);
+
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+
+ private calculateBezierControlPoints(
+ dots: DotConfig[],
+ index: number
+ ): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+
+ return { x1, y1, x2, y2 };
+ }
+
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return "";
+
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(
+ this.dots[0].value
+ )}`;
+
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(
+ this.dots,
+ i
+ );
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+
+ return path;
+ }
+
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+
+ if (!this.config.showGrid) return;
+
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", "0");
+ line.setAttribute("y1", this.getDotY(value).toString());
+ line.setAttribute("x2", this.config.totalWidth.toString());
+ line.setAttribute("y2", this.getDotY(value).toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", "10");
+ text.setAttribute("y", (this.getDotY(value) + 4).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+
+ // Vertical grid lines
+ const numVertLines = Math.ceil(
+ this.config.totalWidth / this.config.xUnitSize
+ );
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", x.toString());
+ line.setAttribute("y1", "0");
+ line.setAttribute("x2", x.toString());
+ line.setAttribute("y2", this.config.height.toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+
+ if (xValue !== 0) {
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", x.toString());
+ text.setAttribute("y", (this.config.height / 2 + 20).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.setAttribute("text-anchor", "middle");
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ tooltip.classList.add("dot-tooltip");
+ tooltip.setAttribute("data-dot-id", dot.id.toString());
+
+ // Calculate tooltip position
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipHeight = this.config.tooltipHeight;
+ const tooltipX = x - tooltipWidth / 2;
+
+ // Calculate tooltip Y position, ensuring it stays within the container
+ let tooltipY = y - tooltipHeight - 20; // Position above the dot with some spacing
+
+ // Ensure tooltip doesn't go above the container
+ tooltipY = Math.max(tooltipY, 10); // Keep at least 10px from the top
+
+ // Background rectangle
+ // Create a rectangle for the tooltip with a 9:16 aspect ratio
+ const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ bg.setAttribute("x", tooltipX.toString());
+ bg.setAttribute("y", tooltipY.toString());
+
+ // Calculate width and height based on ratio
+ const height = tooltipHeight;
+ const width = (9 / 16) * height;
+
+ // Set the width and height for a 9:16 aspect ratio
+ bg.setAttribute("width", width.toString());
+ bg.setAttribute("height", height.toString());
+
+ // Remove any background fill
+ bg.setAttribute("fill", "none");
+
+ // Optional: Adjust corner rounding if needed
+ bg.setAttribute("rx", "5"); // You can set this to 0 for sharp corners
+ bg.setAttribute("ry", "5");
+
+ tooltip.appendChild(bg);
+
+ // Tooltip arrow (pointing to the dot)
+ const arrow = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ arrow.setAttribute(
+ "d",
+ `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${
+ tooltipY + tooltipHeight - 10
+ } L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`
+ );
+ arrow.setAttribute("fill", "rgba(0, 0, 0, 0.8)");
+ tooltip.appendChild(arrow);
+
+ // Image (if provided)
+ if (dot.imageUrl) {
+ const imgContainer = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "foreignObject"
+ );
+ imgContainer.setAttribute("x", (tooltipX + 10).toString());
+ imgContainer.setAttribute("y", (tooltipY + 10).toString());
+
+ // Set width and height to the same value for a square aspect ratio
+ const imageSize = Math.min(tooltipWidth - 20, tooltipHeight / 2);
+ imgContainer.setAttribute("width", imageSize.toString());
+ imgContainer.setAttribute("height", imageSize.toString());
+
+ const img = document.createElement("img");
+ img.src = dot.imageUrl;
+ img.className = "tooltip-img";
+ img.style.width = "calc(100% - 4px)";
+ img.style.height = "calc(100% - 4px)";
+ img.style.objectFit = "cover"; // Ensure the image covers the space while maintaining aspect ratio
+
+ // Make the image circular and add a white border
+ img.style.borderRadius = "50%"; // Makes the image round
+ img.style.border = "2px solid white"; // Adds a 1px white border around the image
+
+ imgContainer.appendChild(img);
+ tooltip.appendChild(imgContainer);
+ }
+
+ // Title (if provided)
+ if (dot.title) {
+ const title = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ title.setAttribute("x", (tooltipX + 10).toString());
+ title.setAttribute(
+ "y",
+ dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 26).toString()
+ : (tooltipY + 25).toString()
+ );
+ title.setAttribute("fill", "white");
+ title.setAttribute("font-size", "14");
+ title.setAttribute("font-weight", "bold");
+ title.textContent = dot.title;
+ tooltip.appendChild(title);
+ }
+
+ // Description (if provided)
+ if (dot.description) {
+ const descriptionFO = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "foreignObject"
+ );
+ descriptionFO.setAttribute("x", (tooltipX + 10).toString());
+ descriptionFO.setAttribute(
+ "y",
+ dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 32).toString()
+ : dot.title
+ ? (tooltipY + 35).toString()
+ : (tooltipY + 15).toString()
+ );
+ descriptionFO.setAttribute("width", (tooltipWidth - 20).toString());
+ descriptionFO.setAttribute("height", (tooltipHeight / 2 - 10).toString());
+
+ const descriptionDiv = document.createElement("div");
+ descriptionDiv.style.color = "white";
+ descriptionDiv.style.fontSize = "12px";
+ descriptionDiv.style.overflow = "hidden";
+ descriptionDiv.textContent = dot.description;
+
+ descriptionFO.appendChild(descriptionDiv);
+ tooltip.appendChild(descriptionFO);
+ }
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute("d", pathData);
+ }
+
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ return { leftmost, rightmost };
+ }
+
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+
+ const circle = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "circle"
+ );
+ circle.setAttribute("cx", x.toString());
+ circle.setAttribute("cy", y.toString());
+ circle.setAttribute("r", this.config.dotRadius.toString());
+ circle.setAttribute("fill", "white");
+ circle.setAttribute("data-dot-id", dot.id.toString());
+ circle.classList.add("dot");
+
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener("click", () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error("Dot has no link");
+ throw new Error("Dot has no link");
+ }
+ });
+ }
+
+ this.dotsGroup.appendChild(circle);
+ }
+ }
+
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522092543.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522092543.ts
new file mode 100644
index 0000000..e5b8d73
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522092543.ts
@@ -0,0 +1,632 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+
+ private preloadedImages: Map = new Map();
+
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+
+ constructor(
+ containerId: string,
+ dots: DotConfig[],
+ config?: Partial
+ ) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config,
+ };
+
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+
+ // Create SVG elements
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ this.gridGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.curvePath = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ this.dotsGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.tooltipGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter((dot) => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map((dot) => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+
+ // Create a loading indicator (optional)
+ const loadingCount = { current: 0, total: imageUrls.length };
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log("All images preloaded successfully");
+ }
+ };
+
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+
+ // Set src to start loading
+ img.src = url;
+
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ }
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = "connected-dots-styles";
+
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement("style");
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.svg.style.overflow = "visible";
+ this.scrollContainer.appendChild(this.svg);
+
+ // Configure grid group
+ this.gridGroup.classList.add("grid");
+ this.svg.appendChild(this.gridGroup);
+
+ // Configure curve path
+ this.curvePath.setAttribute("fill", "none");
+ this.curvePath.setAttribute("stroke", "white");
+ this.curvePath.setAttribute("stroke-width", "2");
+ this.curvePath.setAttribute("stroke-linecap", "round");
+ this.curvePath.classList.add("curve-path");
+ this.svg.appendChild(this.curvePath);
+
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add("tooltips");
+ this.svg.appendChild(this.tooltipGroup);
+ }
+
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.8);
+
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+
+ private calculateBezierControlPoints(
+ dots: DotConfig[],
+ index: number
+ ): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+
+ return { x1, y1, x2, y2 };
+ }
+
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return "";
+
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(
+ this.dots[0].value
+ )}`;
+
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(
+ this.dots,
+ i
+ );
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+
+ return path;
+ }
+
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+
+ if (!this.config.showGrid) return;
+
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", "0");
+ line.setAttribute("y1", this.getDotY(value).toString());
+ line.setAttribute("x2", this.config.totalWidth.toString());
+ line.setAttribute("y2", this.getDotY(value).toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", "10");
+ text.setAttribute("y", (this.getDotY(value) + 4).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+
+ // Vertical grid lines
+ const numVertLines = Math.ceil(
+ this.config.totalWidth / this.config.xUnitSize
+ );
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", x.toString());
+ line.setAttribute("y1", "0");
+ line.setAttribute("x2", x.toString());
+ line.setAttribute("y2", this.config.height.toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+
+ if (xValue !== 0) {
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", x.toString());
+ text.setAttribute("y", (this.config.height / 2 + 20).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.setAttribute("text-anchor", "middle");
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ tooltip.classList.add("dot-tooltip");
+ tooltip.setAttribute("data-dot-id", dot.id.toString());
+
+ // Calculate tooltip position
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipHeight = this.config.tooltipHeight;
+ const tooltipX = x - tooltipWidth / 2;
+
+ // Calculate tooltip Y position, ensuring it stays within the container
+ let tooltipY = y - tooltipHeight - 20; // Position above the dot with some spacing
+
+ // Ensure tooltip doesn't go above the container
+ tooltipY = Math.max(tooltipY, 10); // Keep at least 10px from the top
+
+ // Background rectangle
+ // Create a rectangle for the tooltip with a 9:16 aspect ratio
+ const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ bg.setAttribute("x", tooltipX.toString());
+ bg.setAttribute("y", tooltipY.toString());
+
+ // Calculate width and height based on ratio
+ const height = tooltipHeight;
+ const width = (9 / 16) * height;
+
+ // Set the width and height for a 9:16 aspect ratio
+ bg.setAttribute("width", width.toString());
+ bg.setAttribute("height", height.toString());
+
+ // Remove any background fill
+ bg.setAttribute("fill", "none");
+
+ // Optional: Adjust corner rounding if needed
+ bg.setAttribute("rx", "5"); // You can set this to 0 for sharp corners
+ bg.setAttribute("ry", "5");
+
+ tooltip.appendChild(bg);
+
+ // Tooltip arrow (pointing to the dot)
+ const arrow = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ arrow.setAttribute(
+ "d",
+ `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${
+ tooltipY + tooltipHeight - 10
+ } L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`
+ );
+ arrow.setAttribute("fill", "rgba(0, 0, 0, 0.8)");
+ tooltip.appendChild(arrow);
+
+ // Image (if provided)
+ if (dot.imageUrl) {
+ const imgContainer = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "foreignObject"
+ );
+ imgContainer.setAttribute("x", (tooltipX + 10).toString());
+ imgContainer.setAttribute("y", (tooltipY + 10).toString());
+
+ // Set width and height to the same value for a square aspect ratio
+ const imageSize = Math.min(tooltipWidth - 20, tooltipHeight / 2);
+ imgContainer.setAttribute("width", imageSize.toString());
+ imgContainer.setAttribute("height", imageSize.toString());
+
+ const img = document.createElement("img");
+ img.src = dot.imageUrl;
+ img.className = "tooltip-img";
+ img.style.width = "calc(100% - 4px)";
+ img.style.height = "calc(100% - 4px)";
+ img.style.objectFit = "cover"; // Ensure the image covers the space while maintaining aspect ratio
+
+ // Make the image circular and add a white border
+ img.style.borderRadius = "50%"; // Makes the image round
+ img.style.border = "2px solid white"; // Adds a 1px white border around the image
+
+ imgContainer.appendChild(img);
+ tooltip.appendChild(imgContainer);
+ }
+
+ // Title (if provided)
+ if (dot.title) {
+ const title = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ title.setAttribute("x", (tooltipX + 10).toString());
+ title.setAttribute(
+ "y",
+ dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 26).toString()
+ : (tooltipY + 25).toString()
+ );
+ title.setAttribute("class", "title");
+ title.setAttribute("fill", "white");
+ title.setAttribute("font-size", "14");
+ title.setAttribute("font-weight", "bold");
+ title.textContent = dot.title;
+ tooltip.appendChild(title);
+ }
+
+ // Description (if provided)
+ if (dot.description) {
+ const descriptionFO = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "foreignObject"
+ );
+ descriptionFO.setAttribute("x", (tooltipX + 10).toString());
+ descriptionFO.setAttribute(
+ "y",
+ dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 32).toString()
+ : dot.title
+ ? (tooltipY + 35).toString()
+ : (tooltipY + 15).toString()
+ );
+ descriptionFO.setAttribute("width", (tooltipWidth - 20).toString());
+ descriptionFO.setAttribute("height", (tooltipHeight / 2 - 10).toString());
+
+ const descriptionDiv = document.createElement("div");
+ descriptionDiv.style.color = "white";
+ descriptionDiv.style.fontSize = "12px";
+ descriptionDiv.style.overflow = "hidden";
+ descriptionDiv.textContent = dot.description;
+
+ descriptionFO.appendChild(descriptionDiv);
+ tooltip.appendChild(descriptionFO);
+ }
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute("d", pathData);
+ }
+
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ return { leftmost, rightmost };
+ }
+
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+
+ const circle = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "circle"
+ );
+ circle.setAttribute("cx", x.toString());
+ circle.setAttribute("cy", y.toString());
+ circle.setAttribute("r", this.config.dotRadius.toString());
+ circle.setAttribute("fill", "white");
+ circle.setAttribute("data-dot-id", dot.id.toString());
+ circle.classList.add("dot");
+
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener("click", () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error("Dot has no link");
+ throw new Error("Dot has no link");
+ }
+ });
+ }
+
+ this.dotsGroup.appendChild(circle);
+ }
+ }
+
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522092725.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522092725.ts
new file mode 100644
index 0000000..7030e16
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522092725.ts
@@ -0,0 +1,641 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+
+ private preloadedImages: Map = new Map();
+
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+
+ constructor(
+ containerId: string,
+ dots: DotConfig[],
+ config?: Partial
+ ) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config,
+ };
+
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+
+ // Create SVG elements
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ this.gridGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.curvePath = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ this.dotsGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.tooltipGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter((dot) => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map((dot) => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+
+ // Create a loading indicator (optional)
+ const loadingCount = { current: 0, total: imageUrls.length };
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log("All images preloaded successfully");
+ }
+ };
+
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+
+ // Set src to start loading
+ img.src = url;
+
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ }
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = "connected-dots-styles";
+
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement("style");
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.svg.style.overflow = "visible";
+ this.scrollContainer.appendChild(this.svg);
+
+ // Configure grid group
+ this.gridGroup.classList.add("grid");
+ this.svg.appendChild(this.gridGroup);
+
+ // Configure curve path
+ this.curvePath.setAttribute("fill", "none");
+ this.curvePath.setAttribute("stroke", "white");
+ this.curvePath.setAttribute("stroke-width", "2");
+ this.curvePath.setAttribute("stroke-linecap", "round");
+ this.curvePath.classList.add("curve-path");
+ this.svg.appendChild(this.curvePath);
+
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add("tooltips");
+ this.svg.appendChild(this.tooltipGroup);
+ }
+
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.8);
+
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+
+ private calculateBezierControlPoints(
+ dots: DotConfig[],
+ index: number
+ ): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+
+ return { x1, y1, x2, y2 };
+ }
+
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return "";
+
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(
+ this.dots[0].value
+ )}`;
+
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(
+ this.dots,
+ i
+ );
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+
+ return path;
+ }
+
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+
+ if (!this.config.showGrid) return;
+
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", "0");
+ line.setAttribute("y1", this.getDotY(value).toString());
+ line.setAttribute("x2", this.config.totalWidth.toString());
+ line.setAttribute("y2", this.getDotY(value).toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", "10");
+ text.setAttribute("y", (this.getDotY(value) + 4).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+
+ // Vertical grid lines
+ const numVertLines = Math.ceil(
+ this.config.totalWidth / this.config.xUnitSize
+ );
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", x.toString());
+ line.setAttribute("y1", "0");
+ line.setAttribute("x2", x.toString());
+ line.setAttribute("y2", this.config.height.toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+
+ if (xValue !== 0) {
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", x.toString());
+ text.setAttribute("y", (this.config.height / 2 + 20).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.setAttribute("text-anchor", "middle");
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ tooltip.classList.add("dot-tooltip");
+ tooltip.setAttribute("data-dot-id", dot.id.toString());
+
+ // Calculate tooltip position
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipHeight = this.config.tooltipHeight;
+ const tooltipX = x - tooltipWidth / 2;
+
+ // Calculate tooltip Y position, ensuring it stays within the container
+ let tooltipY = y - tooltipHeight - 20; // Position above the dot with some spacing
+
+ // Ensure tooltip doesn't go above the container
+ tooltipY = Math.max(tooltipY, 10); // Keep at least 10px from the top
+
+ // Background rectangle
+ // Create a rectangle for the tooltip with a 9:16 aspect ratio
+ const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ bg.setAttribute("x", tooltipX.toString());
+ bg.setAttribute("y", tooltipY.toString());
+
+ // Calculate width and height based on ratio
+ const height = tooltipHeight;
+ const width = (9 / 16) * height;
+
+ // Set the width and height for a 9:16 aspect ratio
+ bg.setAttribute("width", width.toString());
+ bg.setAttribute("height", height.toString());
+
+ // Remove any background fill
+ bg.setAttribute("fill", "none");
+
+ // Optional: Adjust corner rounding if needed
+ bg.setAttribute("rx", "5"); // You can set this to 0 for sharp corners
+ bg.setAttribute("ry", "5");
+
+ tooltip.appendChild(bg);
+
+
+ // Create a div with flexbox for centering content
+const div = document.createElement('div');
+div.style.display = 'flex';
+div.style.justifyContent = 'center'; // Center horizontally
+div.style.alignItems = 'center'; // Center vertically
+div.style.width = '100%';
+div.style.height = '100%';
+
+ // Tooltip arrow (pointing to the dot)
+ const arrow = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ arrow.setAttribute(
+ "d",
+ `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${
+ tooltipY + tooltipHeight - 10
+ } L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`
+ );
+ arrow.setAttribute("fill", "rgba(0, 0, 0, 0.8)");
+ tooltip.appendChild(arrow);
+
+ // Image (if provided)
+ if (dot.imageUrl) {
+ const imgContainer = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "foreignObject"
+ );
+ imgContainer.setAttribute("x", (tooltipX + 10).toString());
+ imgContainer.setAttribute("y", (tooltipY + 10).toString());
+
+ // Set width and height to the same value for a square aspect ratio
+ const imageSize = Math.min(tooltipWidth - 20, tooltipHeight / 2);
+ imgContainer.setAttribute("width", imageSize.toString());
+ imgContainer.setAttribute("height", imageSize.toString());
+
+ const img = document.createElement("img");
+ img.src = dot.imageUrl;
+ img.className = "tooltip-img";
+ img.style.width = "calc(100% - 4px)";
+ img.style.height = "calc(100% - 4px)";
+ img.style.objectFit = "cover"; // Ensure the image covers the space while maintaining aspect ratio
+
+ // Make the image circular and add a white border
+ img.style.borderRadius = "50%"; // Makes the image round
+ img.style.border = "2px solid white"; // Adds a 1px white border around the image
+
+ imgContainer.appendChild(img);
+ tooltip.appendChild(imgContainer);
+ }
+
+ // Title (if provided)
+ if (dot.title) {
+ const title = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ title.setAttribute("x", (tooltipX + 10).toString());
+ title.setAttribute(
+ "y",
+ dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 26).toString()
+ : (tooltipY + 25).toString()
+ );
+ title.setAttribute("class", "title");
+ title.setAttribute("fill", "white");
+ title.setAttribute("font-size", "14");
+ title.setAttribute("font-weight", "bold");
+ title.textContent = dot.title;
+ tooltip.appendChild(title);
+ }
+
+ // Description (if provided)
+ if (dot.description) {
+ const descriptionFO = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "foreignObject"
+ );
+ descriptionFO.setAttribute("x", (tooltipX + 10).toString());
+ descriptionFO.setAttribute(
+ "y",
+ dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 32).toString()
+ : dot.title
+ ? (tooltipY + 35).toString()
+ : (tooltipY + 15).toString()
+ );
+ descriptionFO.setAttribute("width", (tooltipWidth - 20).toString());
+ descriptionFO.setAttribute("height", (tooltipHeight / 2 - 10).toString());
+
+ const descriptionDiv = document.createElement("div");
+ descriptionDiv.style.color = "white";
+ descriptionDiv.style.fontSize = "12px";
+ descriptionDiv.style.overflow = "hidden";
+ descriptionDiv.textContent = dot.description;
+
+ descriptionFO.appendChild(descriptionDiv);
+ tooltip.appendChild(descriptionFO);
+ }
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute("d", pathData);
+ }
+
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ return { leftmost, rightmost };
+ }
+
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+
+ const circle = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "circle"
+ );
+ circle.setAttribute("cx", x.toString());
+ circle.setAttribute("cy", y.toString());
+ circle.setAttribute("r", this.config.dotRadius.toString());
+ circle.setAttribute("fill", "white");
+ circle.setAttribute("data-dot-id", dot.id.toString());
+ circle.classList.add("dot");
+
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener("click", () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error("Dot has no link");
+ throw new Error("Dot has no link");
+ }
+ });
+ }
+
+ this.dotsGroup.appendChild(circle);
+ }
+ }
+
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522092810.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522092810.ts
new file mode 100644
index 0000000..9fdf164
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522092810.ts
@@ -0,0 +1,647 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+
+ private preloadedImages: Map = new Map();
+
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+
+ constructor(
+ containerId: string,
+ dots: DotConfig[],
+ config?: Partial
+ ) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config,
+ };
+
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+
+ // Create SVG elements
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ this.gridGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.curvePath = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ this.dotsGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.tooltipGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter((dot) => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map((dot) => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+
+ // Create a loading indicator (optional)
+ const loadingCount = { current: 0, total: imageUrls.length };
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log("All images preloaded successfully");
+ }
+ };
+
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+
+ // Set src to start loading
+ img.src = url;
+
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ }
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = "connected-dots-styles";
+
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement("style");
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.svg.style.overflow = "visible";
+ this.scrollContainer.appendChild(this.svg);
+
+ // Configure grid group
+ this.gridGroup.classList.add("grid");
+ this.svg.appendChild(this.gridGroup);
+
+ // Configure curve path
+ this.curvePath.setAttribute("fill", "none");
+ this.curvePath.setAttribute("stroke", "white");
+ this.curvePath.setAttribute("stroke-width", "2");
+ this.curvePath.setAttribute("stroke-linecap", "round");
+ this.curvePath.classList.add("curve-path");
+ this.svg.appendChild(this.curvePath);
+
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add("tooltips");
+ this.svg.appendChild(this.tooltipGroup);
+ }
+
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.8);
+
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+
+ private calculateBezierControlPoints(
+ dots: DotConfig[],
+ index: number
+ ): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+
+ return { x1, y1, x2, y2 };
+ }
+
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return "";
+
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(
+ this.dots[0].value
+ )}`;
+
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(
+ this.dots,
+ i
+ );
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+
+ return path;
+ }
+
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+
+ if (!this.config.showGrid) return;
+
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", "0");
+ line.setAttribute("y1", this.getDotY(value).toString());
+ line.setAttribute("x2", this.config.totalWidth.toString());
+ line.setAttribute("y2", this.getDotY(value).toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", "10");
+ text.setAttribute("y", (this.getDotY(value) + 4).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+
+ // Vertical grid lines
+ const numVertLines = Math.ceil(
+ this.config.totalWidth / this.config.xUnitSize
+ );
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", x.toString());
+ line.setAttribute("y1", "0");
+ line.setAttribute("x2", x.toString());
+ line.setAttribute("y2", this.config.height.toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+
+ if (xValue !== 0) {
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", x.toString());
+ text.setAttribute("y", (this.config.height / 2 + 20).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.setAttribute("text-anchor", "middle");
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ tooltip.classList.add("dot-tooltip");
+ tooltip.setAttribute("data-dot-id", dot.id.toString());
+
+ // Calculate tooltip position
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipHeight = this.config.tooltipHeight;
+ const tooltipX = x - tooltipWidth / 2;
+
+ // Calculate tooltip Y position, ensuring it stays within the container
+ let tooltipY = y - tooltipHeight - 20; // Position above the dot with some spacing
+
+ // Ensure tooltip doesn't go above the container
+ tooltipY = Math.max(tooltipY, 10); // Keep at least 10px from the top
+
+ // Background rectangle
+ // Create a rectangle for the tooltip with a 9:16 aspect ratio
+ const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ bg.setAttribute("x", tooltipX.toString());
+ bg.setAttribute("y", tooltipY.toString());
+
+ // Calculate width and height based on ratio
+ const height = tooltipHeight;
+ const width = (9 / 16) * height;
+
+ // Set the width and height for a 9:16 aspect ratio
+ bg.setAttribute("width", width.toString());
+ bg.setAttribute("height", height.toString());
+
+ // Remove any background fill
+ bg.setAttribute("fill", "none");
+
+ // Optional: Adjust corner rounding if needed
+ bg.setAttribute("rx", "5"); // You can set this to 0 for sharp corners
+ bg.setAttribute("ry", "5");
+
+ tooltip.appendChild(bg);
+
+// Create a foreignObject for centering content
+const container = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+container.setAttribute('width', width.toString());
+container.setAttribute('height', height.toString());
+container.setAttribute('x', tooltipX.toString());
+container.setAttribute('y', tooltipY.toString());
+
+// Create a div with flexbox for centering content
+const div = document.createElement('div');
+div.style.display = 'flex';
+div.style.justifyContent = 'center'; // Center horizontally
+div.style.alignItems = 'center'; // Center vertically
+div.style.width = '100%';
+div.style.height = '100%';';
+
+ // Tooltip arrow (pointing to the dot)
+ const arrow = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ arrow.setAttribute(
+ "d",
+ `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${
+ tooltipY + tooltipHeight - 10
+ } L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`
+ );
+ arrow.setAttribute("fill", "rgba(0, 0, 0, 0.8)");
+ tooltip.appendChild(arrow);
+
+ // Image (if provided)
+ if (dot.imageUrl) {
+ const imgContainer = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "foreignObject"
+ );
+ imgContainer.setAttribute("x", (tooltipX + 10).toString());
+ imgContainer.setAttribute("y", (tooltipY + 10).toString());
+
+ // Set width and height to the same value for a square aspect ratio
+ const imageSize = Math.min(tooltipWidth - 20, tooltipHeight / 2);
+ imgContainer.setAttribute("width", imageSize.toString());
+ imgContainer.setAttribute("height", imageSize.toString());
+
+ const img = document.createElement("img");
+ img.src = dot.imageUrl;
+ img.className = "tooltip-img";
+ img.style.width = "calc(100% - 4px)";
+ img.style.height = "calc(100% - 4px)";
+ img.style.objectFit = "cover"; // Ensure the image covers the space while maintaining aspect ratio
+
+ // Make the image circular and add a white border
+ img.style.borderRadius = "50%"; // Makes the image round
+ img.style.border = "2px solid white"; // Adds a 1px white border around the image
+
+ imgContainer.appendChild(img);
+ tooltip.appendChild(imgContainer);
+ }
+
+ // Title (if provided)
+ if (dot.title) {
+ const title = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ title.setAttribute("x", (tooltipX + 10).toString());
+ title.setAttribute(
+ "y",
+ dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 26).toString()
+ : (tooltipY + 25).toString()
+ );
+ title.setAttribute("class", "title");
+ title.setAttribute("fill", "white");
+ title.setAttribute("font-size", "14");
+ title.setAttribute("font-weight", "bold");
+ title.textContent = dot.title;
+ tooltip.appendChild(title);
+ }
+
+ // Description (if provided)
+ if (dot.description) {
+ const descriptionFO = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "foreignObject"
+ );
+ descriptionFO.setAttribute("x", (tooltipX + 10).toString());
+ descriptionFO.setAttribute(
+ "y",
+ dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 32).toString()
+ : dot.title
+ ? (tooltipY + 35).toString()
+ : (tooltipY + 15).toString()
+ );
+ descriptionFO.setAttribute("width", (tooltipWidth - 20).toString());
+ descriptionFO.setAttribute("height", (tooltipHeight / 2 - 10).toString());
+
+ const descriptionDiv = document.createElement("div");
+ descriptionDiv.style.color = "white";
+ descriptionDiv.style.fontSize = "12px";
+ descriptionDiv.style.overflow = "hidden";
+ descriptionDiv.textContent = dot.description;
+
+ descriptionFO.appendChild(descriptionDiv);
+ tooltip.appendChild(descriptionFO);
+ }
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute("d", pathData);
+ }
+
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ return { leftmost, rightmost };
+ }
+
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+
+ const circle = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "circle"
+ );
+ circle.setAttribute("cx", x.toString());
+ circle.setAttribute("cy", y.toString());
+ circle.setAttribute("r", this.config.dotRadius.toString());
+ circle.setAttribute("fill", "white");
+ circle.setAttribute("data-dot-id", dot.id.toString());
+ circle.classList.add("dot");
+
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener("click", () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error("Dot has no link");
+ throw new Error("Dot has no link");
+ }
+ });
+ }
+
+ this.dotsGroup.appendChild(circle);
+ }
+ }
+
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522092817.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522092817.ts
new file mode 100644
index 0000000..6022b32
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522092817.ts
@@ -0,0 +1,648 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+
+ private preloadedImages: Map = new Map();
+
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+
+ constructor(
+ containerId: string,
+ dots: DotConfig[],
+ config?: Partial
+ ) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config,
+ };
+
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+
+ // Create SVG elements
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ this.gridGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.curvePath = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ this.dotsGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.tooltipGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter((dot) => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map((dot) => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+
+ // Create a loading indicator (optional)
+ const loadingCount = { current: 0, total: imageUrls.length };
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log("All images preloaded successfully");
+ }
+ };
+
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+
+ // Set src to start loading
+ img.src = url;
+
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ }
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = "connected-dots-styles";
+
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement("style");
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.svg.style.overflow = "visible";
+ this.scrollContainer.appendChild(this.svg);
+
+ // Configure grid group
+ this.gridGroup.classList.add("grid");
+ this.svg.appendChild(this.gridGroup);
+
+ // Configure curve path
+ this.curvePath.setAttribute("fill", "none");
+ this.curvePath.setAttribute("stroke", "white");
+ this.curvePath.setAttribute("stroke-width", "2");
+ this.curvePath.setAttribute("stroke-linecap", "round");
+ this.curvePath.classList.add("curve-path");
+ this.svg.appendChild(this.curvePath);
+
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add("tooltips");
+ this.svg.appendChild(this.tooltipGroup);
+ }
+
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.8);
+
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+
+ private calculateBezierControlPoints(
+ dots: DotConfig[],
+ index: number
+ ): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+
+ return { x1, y1, x2, y2 };
+ }
+
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return "";
+
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(
+ this.dots[0].value
+ )}`;
+
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(
+ this.dots,
+ i
+ );
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+
+ return path;
+ }
+
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+
+ if (!this.config.showGrid) return;
+
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", "0");
+ line.setAttribute("y1", this.getDotY(value).toString());
+ line.setAttribute("x2", this.config.totalWidth.toString());
+ line.setAttribute("y2", this.getDotY(value).toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", "10");
+ text.setAttribute("y", (this.getDotY(value) + 4).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+
+ // Vertical grid lines
+ const numVertLines = Math.ceil(
+ this.config.totalWidth / this.config.xUnitSize
+ );
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", x.toString());
+ line.setAttribute("y1", "0");
+ line.setAttribute("x2", x.toString());
+ line.setAttribute("y2", this.config.height.toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+
+ if (xValue !== 0) {
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", x.toString());
+ text.setAttribute("y", (this.config.height / 2 + 20).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.setAttribute("text-anchor", "middle");
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ tooltip.classList.add("dot-tooltip");
+ tooltip.setAttribute("data-dot-id", dot.id.toString());
+
+ // Calculate tooltip position
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipHeight = this.config.tooltipHeight;
+ const tooltipX = x - tooltipWidth / 2;
+
+ // Calculate tooltip Y position, ensuring it stays within the container
+ let tooltipY = y - tooltipHeight - 20; // Position above the dot with some spacing
+
+ // Ensure tooltip doesn't go above the container
+ tooltipY = Math.max(tooltipY, 10); // Keep at least 10px from the top
+
+ // Background rectangle
+ // Create a rectangle for the tooltip with a 9:16 aspect ratio
+ const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ bg.setAttribute("x", tooltipX.toString());
+ bg.setAttribute("y", tooltipY.toString());
+
+ // Calculate width and height based on ratio
+ const height = tooltipHeight;
+ const width = (9 / 16) * height;
+
+ // Set the width and height for a 9:16 aspect ratio
+ bg.setAttribute("width", width.toString());
+ bg.setAttribute("height", height.toString());
+
+ // Remove any background fill
+ bg.setAttribute("fill", "none");
+
+ // Optional: Adjust corner rounding if needed
+ bg.setAttribute("rx", "5"); // You can set this to 0 for sharp corners
+ bg.setAttribute("ry", "5");
+
+ tooltip.appendChild(bg);
+
+
+ // Create a foreignObject for centering content
+const container = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+container.setAttribute('width', width.toString());
+container.setAttribute('height', height.toString());
+container.setAttribute('x', tooltipX.toString());
+container.setAttribute('y', tooltipY.toString());
+
+// Create a div with flexbox for centering content
+const div = document.createElement('div');
+div.style.display = 'flex';
+div.style.justifyContent = 'center'; // Center horizontally
+div.style.alignItems = 'center'; // Center vertically
+div.style.width = '100%';
+div.style.height = '100%';
+
+ // Tooltip arrow (pointing to the dot)
+ const arrow = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ arrow.setAttribute(
+ "d",
+ `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${
+ tooltipY + tooltipHeight - 10
+ } L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`
+ );
+ arrow.setAttribute("fill", "rgba(0, 0, 0, 0.8)");
+ tooltip.appendChild(arrow);
+
+ // Image (if provided)
+ if (dot.imageUrl) {
+ const imgContainer = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "foreignObject"
+ );
+ imgContainer.setAttribute("x", (tooltipX + 10).toString());
+ imgContainer.setAttribute("y", (tooltipY + 10).toString());
+
+ // Set width and height to the same value for a square aspect ratio
+ const imageSize = Math.min(tooltipWidth - 20, tooltipHeight / 2);
+ imgContainer.setAttribute("width", imageSize.toString());
+ imgContainer.setAttribute("height", imageSize.toString());
+
+ const img = document.createElement("img");
+ img.src = dot.imageUrl;
+ img.className = "tooltip-img";
+ img.style.width = "calc(100% - 4px)";
+ img.style.height = "calc(100% - 4px)";
+ img.style.objectFit = "cover"; // Ensure the image covers the space while maintaining aspect ratio
+
+ // Make the image circular and add a white border
+ img.style.borderRadius = "50%"; // Makes the image round
+ img.style.border = "2px solid white"; // Adds a 1px white border around the image
+
+ imgContainer.appendChild(img);
+ tooltip.appendChild(imgContainer);
+ }
+
+ // Title (if provided)
+ if (dot.title) {
+ const title = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ title.setAttribute("x", (tooltipX + 10).toString());
+ title.setAttribute(
+ "y",
+ dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 26).toString()
+ : (tooltipY + 25).toString()
+ );
+ title.setAttribute("class", "title");
+ title.setAttribute("fill", "white");
+ title.setAttribute("font-size", "14");
+ title.setAttribute("font-weight", "bold");
+ title.textContent = dot.title;
+ tooltip.appendChild(title);
+ }
+
+ // Description (if provided)
+ if (dot.description) {
+ const descriptionFO = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "foreignObject"
+ );
+ descriptionFO.setAttribute("x", (tooltipX + 10).toString());
+ descriptionFO.setAttribute(
+ "y",
+ dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 32).toString()
+ : dot.title
+ ? (tooltipY + 35).toString()
+ : (tooltipY + 15).toString()
+ );
+ descriptionFO.setAttribute("width", (tooltipWidth - 20).toString());
+ descriptionFO.setAttribute("height", (tooltipHeight / 2 - 10).toString());
+
+ const descriptionDiv = document.createElement("div");
+ descriptionDiv.style.color = "white";
+ descriptionDiv.style.fontSize = "12px";
+ descriptionDiv.style.overflow = "hidden";
+ descriptionDiv.textContent = dot.description;
+
+ descriptionFO.appendChild(descriptionDiv);
+ tooltip.appendChild(descriptionFO);
+ }
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute("d", pathData);
+ }
+
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ return { leftmost, rightmost };
+ }
+
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+
+ const circle = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "circle"
+ );
+ circle.setAttribute("cx", x.toString());
+ circle.setAttribute("cy", y.toString());
+ circle.setAttribute("r", this.config.dotRadius.toString());
+ circle.setAttribute("fill", "white");
+ circle.setAttribute("data-dot-id", dot.id.toString());
+ circle.classList.add("dot");
+
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener("click", () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error("Dot has no link");
+ throw new Error("Dot has no link");
+ }
+ });
+ }
+
+ this.dotsGroup.appendChild(circle);
+ }
+ }
+
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522092936.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522092936.ts
new file mode 100644
index 0000000..6022b32
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522092936.ts
@@ -0,0 +1,648 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+
+ private preloadedImages: Map = new Map();
+
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+
+ constructor(
+ containerId: string,
+ dots: DotConfig[],
+ config?: Partial
+ ) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config,
+ };
+
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+
+ // Create SVG elements
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ this.gridGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.curvePath = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ this.dotsGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.tooltipGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter((dot) => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map((dot) => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+
+ // Create a loading indicator (optional)
+ const loadingCount = { current: 0, total: imageUrls.length };
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log("All images preloaded successfully");
+ }
+ };
+
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+
+ // Set src to start loading
+ img.src = url;
+
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ }
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = "connected-dots-styles";
+
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement("style");
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.svg.style.overflow = "visible";
+ this.scrollContainer.appendChild(this.svg);
+
+ // Configure grid group
+ this.gridGroup.classList.add("grid");
+ this.svg.appendChild(this.gridGroup);
+
+ // Configure curve path
+ this.curvePath.setAttribute("fill", "none");
+ this.curvePath.setAttribute("stroke", "white");
+ this.curvePath.setAttribute("stroke-width", "2");
+ this.curvePath.setAttribute("stroke-linecap", "round");
+ this.curvePath.classList.add("curve-path");
+ this.svg.appendChild(this.curvePath);
+
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add("tooltips");
+ this.svg.appendChild(this.tooltipGroup);
+ }
+
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.8);
+
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+
+ private calculateBezierControlPoints(
+ dots: DotConfig[],
+ index: number
+ ): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+
+ return { x1, y1, x2, y2 };
+ }
+
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return "";
+
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(
+ this.dots[0].value
+ )}`;
+
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(
+ this.dots,
+ i
+ );
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+
+ return path;
+ }
+
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+
+ if (!this.config.showGrid) return;
+
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", "0");
+ line.setAttribute("y1", this.getDotY(value).toString());
+ line.setAttribute("x2", this.config.totalWidth.toString());
+ line.setAttribute("y2", this.getDotY(value).toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", "10");
+ text.setAttribute("y", (this.getDotY(value) + 4).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+
+ // Vertical grid lines
+ const numVertLines = Math.ceil(
+ this.config.totalWidth / this.config.xUnitSize
+ );
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", x.toString());
+ line.setAttribute("y1", "0");
+ line.setAttribute("x2", x.toString());
+ line.setAttribute("y2", this.config.height.toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+
+ if (xValue !== 0) {
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", x.toString());
+ text.setAttribute("y", (this.config.height / 2 + 20).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.setAttribute("text-anchor", "middle");
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ tooltip.classList.add("dot-tooltip");
+ tooltip.setAttribute("data-dot-id", dot.id.toString());
+
+ // Calculate tooltip position
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipHeight = this.config.tooltipHeight;
+ const tooltipX = x - tooltipWidth / 2;
+
+ // Calculate tooltip Y position, ensuring it stays within the container
+ let tooltipY = y - tooltipHeight - 20; // Position above the dot with some spacing
+
+ // Ensure tooltip doesn't go above the container
+ tooltipY = Math.max(tooltipY, 10); // Keep at least 10px from the top
+
+ // Background rectangle
+ // Create a rectangle for the tooltip with a 9:16 aspect ratio
+ const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ bg.setAttribute("x", tooltipX.toString());
+ bg.setAttribute("y", tooltipY.toString());
+
+ // Calculate width and height based on ratio
+ const height = tooltipHeight;
+ const width = (9 / 16) * height;
+
+ // Set the width and height for a 9:16 aspect ratio
+ bg.setAttribute("width", width.toString());
+ bg.setAttribute("height", height.toString());
+
+ // Remove any background fill
+ bg.setAttribute("fill", "none");
+
+ // Optional: Adjust corner rounding if needed
+ bg.setAttribute("rx", "5"); // You can set this to 0 for sharp corners
+ bg.setAttribute("ry", "5");
+
+ tooltip.appendChild(bg);
+
+
+ // Create a foreignObject for centering content
+const container = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+container.setAttribute('width', width.toString());
+container.setAttribute('height', height.toString());
+container.setAttribute('x', tooltipX.toString());
+container.setAttribute('y', tooltipY.toString());
+
+// Create a div with flexbox for centering content
+const div = document.createElement('div');
+div.style.display = 'flex';
+div.style.justifyContent = 'center'; // Center horizontally
+div.style.alignItems = 'center'; // Center vertically
+div.style.width = '100%';
+div.style.height = '100%';
+
+ // Tooltip arrow (pointing to the dot)
+ const arrow = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ arrow.setAttribute(
+ "d",
+ `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${
+ tooltipY + tooltipHeight - 10
+ } L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`
+ );
+ arrow.setAttribute("fill", "rgba(0, 0, 0, 0.8)");
+ tooltip.appendChild(arrow);
+
+ // Image (if provided)
+ if (dot.imageUrl) {
+ const imgContainer = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "foreignObject"
+ );
+ imgContainer.setAttribute("x", (tooltipX + 10).toString());
+ imgContainer.setAttribute("y", (tooltipY + 10).toString());
+
+ // Set width and height to the same value for a square aspect ratio
+ const imageSize = Math.min(tooltipWidth - 20, tooltipHeight / 2);
+ imgContainer.setAttribute("width", imageSize.toString());
+ imgContainer.setAttribute("height", imageSize.toString());
+
+ const img = document.createElement("img");
+ img.src = dot.imageUrl;
+ img.className = "tooltip-img";
+ img.style.width = "calc(100% - 4px)";
+ img.style.height = "calc(100% - 4px)";
+ img.style.objectFit = "cover"; // Ensure the image covers the space while maintaining aspect ratio
+
+ // Make the image circular and add a white border
+ img.style.borderRadius = "50%"; // Makes the image round
+ img.style.border = "2px solid white"; // Adds a 1px white border around the image
+
+ imgContainer.appendChild(img);
+ tooltip.appendChild(imgContainer);
+ }
+
+ // Title (if provided)
+ if (dot.title) {
+ const title = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ title.setAttribute("x", (tooltipX + 10).toString());
+ title.setAttribute(
+ "y",
+ dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 26).toString()
+ : (tooltipY + 25).toString()
+ );
+ title.setAttribute("class", "title");
+ title.setAttribute("fill", "white");
+ title.setAttribute("font-size", "14");
+ title.setAttribute("font-weight", "bold");
+ title.textContent = dot.title;
+ tooltip.appendChild(title);
+ }
+
+ // Description (if provided)
+ if (dot.description) {
+ const descriptionFO = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "foreignObject"
+ );
+ descriptionFO.setAttribute("x", (tooltipX + 10).toString());
+ descriptionFO.setAttribute(
+ "y",
+ dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 32).toString()
+ : dot.title
+ ? (tooltipY + 35).toString()
+ : (tooltipY + 15).toString()
+ );
+ descriptionFO.setAttribute("width", (tooltipWidth - 20).toString());
+ descriptionFO.setAttribute("height", (tooltipHeight / 2 - 10).toString());
+
+ const descriptionDiv = document.createElement("div");
+ descriptionDiv.style.color = "white";
+ descriptionDiv.style.fontSize = "12px";
+ descriptionDiv.style.overflow = "hidden";
+ descriptionDiv.textContent = dot.description;
+
+ descriptionFO.appendChild(descriptionDiv);
+ tooltip.appendChild(descriptionFO);
+ }
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute("d", pathData);
+ }
+
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ return { leftmost, rightmost };
+ }
+
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+
+ const circle = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "circle"
+ );
+ circle.setAttribute("cx", x.toString());
+ circle.setAttribute("cy", y.toString());
+ circle.setAttribute("r", this.config.dotRadius.toString());
+ circle.setAttribute("fill", "white");
+ circle.setAttribute("data-dot-id", dot.id.toString());
+ circle.classList.add("dot");
+
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener("click", () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error("Dot has no link");
+ throw new Error("Dot has no link");
+ }
+ });
+ }
+
+ this.dotsGroup.appendChild(circle);
+ }
+ }
+
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522093038.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522093038.ts
new file mode 100644
index 0000000..9759151
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522093038.ts
@@ -0,0 +1,650 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+
+ private preloadedImages: Map = new Map();
+
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+
+ constructor(
+ containerId: string,
+ dots: DotConfig[],
+ config?: Partial
+ ) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config,
+ };
+
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+
+ // Create SVG elements
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ this.gridGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.curvePath = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ this.dotsGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.tooltipGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter((dot) => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map((dot) => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+
+ // Create a loading indicator (optional)
+ const loadingCount = { current: 0, total: imageUrls.length };
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log("All images preloaded successfully");
+ }
+ };
+
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+
+ // Set src to start loading
+ img.src = url;
+
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ }
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = "connected-dots-styles";
+
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement("style");
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.svg.style.overflow = "visible";
+ this.scrollContainer.appendChild(this.svg);
+
+ // Configure grid group
+ this.gridGroup.classList.add("grid");
+ this.svg.appendChild(this.gridGroup);
+
+ // Configure curve path
+ this.curvePath.setAttribute("fill", "none");
+ this.curvePath.setAttribute("stroke", "white");
+ this.curvePath.setAttribute("stroke-width", "2");
+ this.curvePath.setAttribute("stroke-linecap", "round");
+ this.curvePath.classList.add("curve-path");
+ this.svg.appendChild(this.curvePath);
+
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add("tooltips");
+ this.svg.appendChild(this.tooltipGroup);
+ }
+
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.8);
+
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+
+ private calculateBezierControlPoints(
+ dots: DotConfig[],
+ index: number
+ ): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+
+ return { x1, y1, x2, y2 };
+ }
+
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return "";
+
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(
+ this.dots[0].value
+ )}`;
+
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(
+ this.dots,
+ i
+ );
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+
+ return path;
+ }
+
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+
+ if (!this.config.showGrid) return;
+
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", "0");
+ line.setAttribute("y1", this.getDotY(value).toString());
+ line.setAttribute("x2", this.config.totalWidth.toString());
+ line.setAttribute("y2", this.getDotY(value).toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", "10");
+ text.setAttribute("y", (this.getDotY(value) + 4).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+
+ // Vertical grid lines
+ const numVertLines = Math.ceil(
+ this.config.totalWidth / this.config.xUnitSize
+ );
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", x.toString());
+ line.setAttribute("y1", "0");
+ line.setAttribute("x2", x.toString());
+ line.setAttribute("y2", this.config.height.toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+
+ if (xValue !== 0) {
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", x.toString());
+ text.setAttribute("y", (this.config.height / 2 + 20).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.setAttribute("text-anchor", "middle");
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ tooltip.classList.add("dot-tooltip");
+ tooltip.setAttribute("data-dot-id", dot.id.toString());
+
+ // Calculate tooltip position
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipHeight = this.config.tooltipHeight;
+ const tooltipX = x - tooltipWidth / 2;
+
+ // Calculate tooltip Y position, ensuring it stays within the container
+ let tooltipY = y - tooltipHeight - 20; // Position above the dot with some spacing
+
+ // Ensure tooltip doesn't go above the container
+ tooltipY = Math.max(tooltipY, 10); // Keep at least 10px from the top
+
+ // Background rectangle
+ // Create a rectangle for the tooltip with a 9:16 aspect ratio
+ const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ bg.setAttribute("x", tooltipX.toString());
+ bg.setAttribute("y", tooltipY.toString());
+
+ // Calculate width and height based on ratio
+ const height = tooltipHeight;
+ const width = (9 / 16) * height;
+
+ // Set the width and height for a 9:16 aspect ratio
+ bg.setAttribute("width", width.toString());
+ bg.setAttribute("height", height.toString());
+
+ // Remove any background fill
+ bg.setAttribute("fill", "none");
+
+ // Optional: Adjust corner rounding if needed
+ bg.setAttribute("rx", "5"); // You can set this to 0 for sharp corners
+ bg.setAttribute("ry", "5");
+
+ tooltip.appendChild(bg);
+
+ // Create a foreignObject for centering content
+ const container = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "foreignObject"
+ );
+ container.setAttribute("width", width.toString());
+ container.setAttribute("height", height.toString());
+ container.setAttribute("x", tooltipX.toString());
+ container.setAttribute("y", tooltipY.toString());
+
+ // Create a div with flexbox for centering content
+ const div = document.createElement("div");
+ div.style.display = "flex";
+ div.style.justifyContent = "center"; // Center horizontally
+ div.style.alignItems = "center"; // Center vertically
+ div.style.width = "100%";
+ div.style.height = "100%";
+
+ // Tooltip arrow (pointing to the dot)
+ const arrow = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ arrow.setAttribute(
+ "d",
+ `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${
+ tooltipY + tooltipHeight - 10
+ } L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`
+ );
+ arrow.setAttribute("fill", "rgba(0, 0, 0, 0.8)");
+ tooltip.appendChild(arrow);
+
+ // Image (if provided)
+ if (dot.imageUrl) {
+ const imgContainer = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "foreignObject"
+ );
+ imgContainer.setAttribute("x", (tooltipX + 10).toString());
+ imgContainer.setAttribute("y", (tooltipY + 10).toString());
+
+ // Set width and height to the same value for a square aspect ratio
+ const imageSize = Math.min(tooltipWidth - 20, tooltipHeight / 2);
+ imgContainer.setAttribute("width", imageSize.toString());
+ imgContainer.setAttribute("height", imageSize.toString());
+
+ const img = document.createElement("img");
+ img.src = dot.imageUrl;
+ img.className = "tooltip-img";
+ img.style.width = "calc(100% - 4px)";
+ img.style.height = "calc(100% - 4px)";
+ img.style.objectFit = "cover"; // Ensure the image covers the space while maintaining aspect ratio
+
+ // Make the image circular and add a white border
+ img.style.borderRadius = "50%"; // Makes the image round
+ img.style.border = "2px solid white"; // Adds a 1px white border around the image
+
+ imgContainer.appendChild(img);
+ tooltip.appendChild(imgContainer);
+ }
+
+ // Title (if provided)
+ if (dot.title) {
+ const title = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ title.setAttribute("x", (tooltipX + 10).toString());
+ title.setAttribute(
+ "y",
+ dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 26).toString()
+ : (tooltipY + 25).toString()
+ );
+ title.setAttribute("class", "title");
+ title.setAttribute("fill", "white");
+ title.setAttribute("font-size", "14");
+ title.setAttribute("font-weight", "bold");
+ title.textContent = dot.title;
+ tooltip.appendChild(title);
+ }
+
+ // Description (if provided)
+ if (dot.description) {
+ const descriptionFO = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "foreignObject"
+ );
+ descriptionFO.setAttribute("x", (tooltipX + 10).toString());
+ descriptionFO.setAttribute(
+ "y",
+ dot.imageUrl
+ ? (tooltipY + tooltipHeight / 2 + 32).toString()
+ : dot.title
+ ? (tooltipY + 35).toString()
+ : (tooltipY + 15).toString()
+ );
+ descriptionFO.setAttribute("width", (tooltipWidth - 20).toString());
+ descriptionFO.setAttribute("height", (tooltipHeight / 2 - 10).toString());
+
+ const descriptionDiv = document.createElement("div");
+ descriptionDiv.style.color = "white";
+ descriptionDiv.style.fontSize = "12px";
+ descriptionDiv.style.overflow = "hidden";
+ descriptionDiv.textContent = dot.description;
+
+ descriptionFO.appendChild(descriptionDiv);
+ tooltip.appendChild(descriptionFO);
+ }
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute("d", pathData);
+ }
+
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ return { leftmost, rightmost };
+ }
+
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+
+ const circle = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "circle"
+ );
+ circle.setAttribute("cx", x.toString());
+ circle.setAttribute("cy", y.toString());
+ circle.setAttribute("r", this.config.dotRadius.toString());
+ circle.setAttribute("fill", "white");
+ circle.setAttribute("data-dot-id", dot.id.toString());
+ circle.classList.add("dot");
+
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener("click", () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error("Dot has no link");
+ throw new Error("Dot has no link");
+ }
+ });
+ }
+
+ this.dotsGroup.appendChild(circle);
+ }
+ }
+
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522093141.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522093141.ts
new file mode 100644
index 0000000..45ddb43
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522093141.ts
@@ -0,0 +1,583 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+
+ private preloadedImages: Map = new Map();
+
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+
+ constructor(
+ containerId: string,
+ dots: DotConfig[],
+ config?: Partial
+ ) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config,
+ };
+
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+
+ // Create SVG elements
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ this.gridGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.curvePath = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ this.dotsGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.tooltipGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter((dot) => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map((dot) => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+
+ // Create a loading indicator (optional)
+ const loadingCount = { current: 0, total: imageUrls.length };
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log("All images preloaded successfully");
+ }
+ };
+
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+
+ // Set src to start loading
+ img.src = url;
+
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ }
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = "connected-dots-styles";
+
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement("style");
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.svg.style.overflow = "visible";
+ this.scrollContainer.appendChild(this.svg);
+
+ // Configure grid group
+ this.gridGroup.classList.add("grid");
+ this.svg.appendChild(this.gridGroup);
+
+ // Configure curve path
+ this.curvePath.setAttribute("fill", "none");
+ this.curvePath.setAttribute("stroke", "white");
+ this.curvePath.setAttribute("stroke-width", "2");
+ this.curvePath.setAttribute("stroke-linecap", "round");
+ this.curvePath.classList.add("curve-path");
+ this.svg.appendChild(this.curvePath);
+
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add("tooltips");
+ this.svg.appendChild(this.tooltipGroup);
+ }
+
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.8);
+
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+
+ private calculateBezierControlPoints(
+ dots: DotConfig[],
+ index: number
+ ): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+
+ return { x1, y1, x2, y2 };
+ }
+
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return "";
+
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(
+ this.dots[0].value
+ )}`;
+
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(
+ this.dots,
+ i
+ );
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+
+ return path;
+ }
+
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+
+ if (!this.config.showGrid) return;
+
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", "0");
+ line.setAttribute("y1", this.getDotY(value).toString());
+ line.setAttribute("x2", this.config.totalWidth.toString());
+ line.setAttribute("y2", this.getDotY(value).toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", "10");
+ text.setAttribute("y", (this.getDotY(value) + 4).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+
+ // Vertical grid lines
+ const numVertLines = Math.ceil(
+ this.config.totalWidth / this.config.xUnitSize
+ );
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", x.toString());
+ line.setAttribute("y1", "0");
+ line.setAttribute("x2", x.toString());
+ line.setAttribute("y2", this.config.height.toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+
+ if (xValue !== 0) {
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", x.toString());
+ text.setAttribute("y", (this.config.height / 2 + 20).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.setAttribute("text-anchor", "middle");
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ tooltip.classList.add("dot-tooltip");
+ tooltip.setAttribute("data-dot-id", dot.id.toString());
+
+ // Calculate tooltip dimensions and position
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipHeight = this.config.tooltipHeight;
+ const tooltipX = x - tooltipWidth / 2;
+ let tooltipY = y - tooltipHeight - 20; // Positioned above the dot
+ tooltipY = Math.max(tooltipY, 10); // Ensure it doesn't go above the view
+
+ // Create background rectangle
+ const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ bg.setAttribute("x", tooltipX.toString());
+ bg.setAttribute("y", tooltipY.toString());
+ bg.setAttribute("width", tooltipWidth.toString());
+ bg.setAttribute("height", tooltipHeight.toString());
+ bg.setAttribute("fill", "rgba(0, 0, 0, 0.8)");
+ bg.setAttribute("rx", "5"); // Rounded corners
+ tooltip.appendChild(bg);
+
+ // Create foreignObject for the content
+ const contentContainer = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ contentContainer.setAttribute('x', tooltipX.toString());
+ contentContainer.setAttribute('y', tooltipY.toString());
+ contentContainer.setAttribute('width', tooltipWidth.toString());
+ contentContainer.setAttribute('height', tooltipHeight.toString());
+
+ // Create a div with flexbox for centering content
+ const div = document.createElement('div');
+ div.style.display = 'flex';
+ div.style.flexDirection = 'column';
+ div.style.justifyContent = 'center'; // Center vertically
+ div.style.alignItems = 'center'; // Center horizontally
+ div.style.width = '100%';
+ div.style.height = '100%';
+ div.style.color = 'white'; // Set text color to white
+
+ // Add image if available
+ if (dot.imageUrl) {
+ const img = document.createElement('img');
+ img.src = dot.imageUrl;
+ img.style.width = '50px';
+ img.style.height = '50px';
+ img.style.borderRadius = '50%'; // Circular image
+ img.style.border = '2px solid white';
+ div.appendChild(img);
+ }
+
+ // Add title if available
+ if (dot.title) {
+ const title = document.createElement('div');
+ title.style.fontSize = '14px';
+ title.style.fontWeight = 'bold';
+ title.textContent = dot.title;
+ div.appendChild(title);
+ }
+
+ // Add description if available
+ if (dot.description) {
+ const desc = document.createElement('div');
+ desc.style.fontSize = '12px';
+ desc.textContent = dot.description;
+ div.appendChild(desc);
+ }
+
+ contentContainer.appendChild(div);
+ tooltip.appendChild(contentContainer);
+
+ // Add arrow path if needed
+ const arrow = document.createElementNS("http://www.w3.org/2000/svg", "path");
+ arrow.setAttribute(
+ "d",
+ `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${
+ tooltipY + tooltipHeight - 10
+ } L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`
+ );
+ arrow.setAttribute("fill", "rgba(0, 0, 0, 0.8)");
+ tooltip.appendChild(arrow);
+
+ return tooltip;
+ }
+
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute("d", pathData);
+ }
+
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ return { leftmost, rightmost };
+ }
+
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+
+ const circle = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "circle"
+ );
+ circle.setAttribute("cx", x.toString());
+ circle.setAttribute("cy", y.toString());
+ circle.setAttribute("r", this.config.dotRadius.toString());
+ circle.setAttribute("fill", "white");
+ circle.setAttribute("data-dot-id", dot.id.toString());
+ circle.classList.add("dot");
+
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener("click", () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error("Dot has no link");
+ throw new Error("Dot has no link");
+ }
+ });
+ }
+
+ this.dotsGroup.appendChild(circle);
+ }
+ }
+
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522093202.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522093202.ts
new file mode 100644
index 0000000..45ddb43
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522093202.ts
@@ -0,0 +1,583 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+
+ private preloadedImages: Map = new Map();
+
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+
+ constructor(
+ containerId: string,
+ dots: DotConfig[],
+ config?: Partial
+ ) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config,
+ };
+
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+
+ // Create SVG elements
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ this.gridGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.curvePath = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ this.dotsGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.tooltipGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter((dot) => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map((dot) => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+
+ // Create a loading indicator (optional)
+ const loadingCount = { current: 0, total: imageUrls.length };
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log("All images preloaded successfully");
+ }
+ };
+
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+
+ // Set src to start loading
+ img.src = url;
+
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ }
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = "connected-dots-styles";
+
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement("style");
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.svg.style.overflow = "visible";
+ this.scrollContainer.appendChild(this.svg);
+
+ // Configure grid group
+ this.gridGroup.classList.add("grid");
+ this.svg.appendChild(this.gridGroup);
+
+ // Configure curve path
+ this.curvePath.setAttribute("fill", "none");
+ this.curvePath.setAttribute("stroke", "white");
+ this.curvePath.setAttribute("stroke-width", "2");
+ this.curvePath.setAttribute("stroke-linecap", "round");
+ this.curvePath.classList.add("curve-path");
+ this.svg.appendChild(this.curvePath);
+
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add("tooltips");
+ this.svg.appendChild(this.tooltipGroup);
+ }
+
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.8);
+
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+
+ private calculateBezierControlPoints(
+ dots: DotConfig[],
+ index: number
+ ): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+
+ return { x1, y1, x2, y2 };
+ }
+
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return "";
+
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(
+ this.dots[0].value
+ )}`;
+
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(
+ this.dots,
+ i
+ );
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+
+ return path;
+ }
+
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+
+ if (!this.config.showGrid) return;
+
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", "0");
+ line.setAttribute("y1", this.getDotY(value).toString());
+ line.setAttribute("x2", this.config.totalWidth.toString());
+ line.setAttribute("y2", this.getDotY(value).toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", "10");
+ text.setAttribute("y", (this.getDotY(value) + 4).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+
+ // Vertical grid lines
+ const numVertLines = Math.ceil(
+ this.config.totalWidth / this.config.xUnitSize
+ );
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", x.toString());
+ line.setAttribute("y1", "0");
+ line.setAttribute("x2", x.toString());
+ line.setAttribute("y2", this.config.height.toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+
+ if (xValue !== 0) {
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", x.toString());
+ text.setAttribute("y", (this.config.height / 2 + 20).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.setAttribute("text-anchor", "middle");
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ tooltip.classList.add("dot-tooltip");
+ tooltip.setAttribute("data-dot-id", dot.id.toString());
+
+ // Calculate tooltip dimensions and position
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipHeight = this.config.tooltipHeight;
+ const tooltipX = x - tooltipWidth / 2;
+ let tooltipY = y - tooltipHeight - 20; // Positioned above the dot
+ tooltipY = Math.max(tooltipY, 10); // Ensure it doesn't go above the view
+
+ // Create background rectangle
+ const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ bg.setAttribute("x", tooltipX.toString());
+ bg.setAttribute("y", tooltipY.toString());
+ bg.setAttribute("width", tooltipWidth.toString());
+ bg.setAttribute("height", tooltipHeight.toString());
+ bg.setAttribute("fill", "rgba(0, 0, 0, 0.8)");
+ bg.setAttribute("rx", "5"); // Rounded corners
+ tooltip.appendChild(bg);
+
+ // Create foreignObject for the content
+ const contentContainer = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ contentContainer.setAttribute('x', tooltipX.toString());
+ contentContainer.setAttribute('y', tooltipY.toString());
+ contentContainer.setAttribute('width', tooltipWidth.toString());
+ contentContainer.setAttribute('height', tooltipHeight.toString());
+
+ // Create a div with flexbox for centering content
+ const div = document.createElement('div');
+ div.style.display = 'flex';
+ div.style.flexDirection = 'column';
+ div.style.justifyContent = 'center'; // Center vertically
+ div.style.alignItems = 'center'; // Center horizontally
+ div.style.width = '100%';
+ div.style.height = '100%';
+ div.style.color = 'white'; // Set text color to white
+
+ // Add image if available
+ if (dot.imageUrl) {
+ const img = document.createElement('img');
+ img.src = dot.imageUrl;
+ img.style.width = '50px';
+ img.style.height = '50px';
+ img.style.borderRadius = '50%'; // Circular image
+ img.style.border = '2px solid white';
+ div.appendChild(img);
+ }
+
+ // Add title if available
+ if (dot.title) {
+ const title = document.createElement('div');
+ title.style.fontSize = '14px';
+ title.style.fontWeight = 'bold';
+ title.textContent = dot.title;
+ div.appendChild(title);
+ }
+
+ // Add description if available
+ if (dot.description) {
+ const desc = document.createElement('div');
+ desc.style.fontSize = '12px';
+ desc.textContent = dot.description;
+ div.appendChild(desc);
+ }
+
+ contentContainer.appendChild(div);
+ tooltip.appendChild(contentContainer);
+
+ // Add arrow path if needed
+ const arrow = document.createElementNS("http://www.w3.org/2000/svg", "path");
+ arrow.setAttribute(
+ "d",
+ `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${
+ tooltipY + tooltipHeight - 10
+ } L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`
+ );
+ arrow.setAttribute("fill", "rgba(0, 0, 0, 0.8)");
+ tooltip.appendChild(arrow);
+
+ return tooltip;
+ }
+
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute("d", pathData);
+ }
+
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+
+ return { leftmost, rightmost };
+ }
+
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+
+ const circle = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "circle"
+ );
+ circle.setAttribute("cx", x.toString());
+ circle.setAttribute("cy", y.toString());
+ circle.setAttribute("r", this.config.dotRadius.toString());
+ circle.setAttribute("fill", "white");
+ circle.setAttribute("data-dot-id", dot.id.toString());
+ circle.classList.add("dot");
+
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener("click", () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error("Dot has no link");
+ throw new Error("Dot has no link");
+ }
+ });
+ }
+
+ this.dotsGroup.appendChild(circle);
+ }
+ }
+
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522093316.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522093316.ts
new file mode 100644
index 0000000..06180d3
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522093316.ts
@@ -0,0 +1,500 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+ private preloadedImages: Map = new Map();
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+ constructor(
+ containerId: string,
+ dots: DotConfig[],
+ config?: Partial
+ ) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config,
+ };
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+ // Create SVG elements
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ this.gridGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.curvePath = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ this.dotsGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.tooltipGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter((dot) => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map((dot) => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+ // Create a loading indicator (optional)
+ const loadingCount = { current: 0, total: imageUrls.length };
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log("All images preloaded successfully");
+ }
+ };
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+ // Set src to start loading
+ img.src = url;
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ }
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = "connected-dots-styles";
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement("style");
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.svg.style.overflow = "visible";
+ this.scrollContainer.appendChild(this.svg);
+ // Configure grid group
+ this.gridGroup.classList.add("grid");
+ this.svg.appendChild(this.gridGroup);
+ // Configure curve path
+ this.curvePath.setAttribute("fill", "none");
+ this.curvePath.setAttribute("stroke", "white");
+ this.curvePath.setAttribute("stroke-width", "2");
+ this.curvePath.setAttribute("stroke-linecap", "round");
+ this.curvePath.classList.add("curve-path");
+ this.svg.appendChild(this.curvePath);
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add("tooltips");
+ this.svg.appendChild(this.tooltipGroup);
+ }
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.8);
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+ private calculateBezierControlPoints(
+ dots: DotConfig[],
+ index: number
+ ): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+ return { x1, y1, x2, y2 };
+ }
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return "";
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(
+ this.dots[0].value
+ )}`;
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(
+ this.dots,
+ i
+ );
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+ return path;
+ }
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+ if (!this.config.showGrid) return;
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", "0");
+ line.setAttribute("y1", this.getDotY(value).toString());
+ line.setAttribute("x2", this.config.totalWidth.toString());
+ line.setAttribute("y2", this.getDotY(value).toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", "10");
+ text.setAttribute("y", (this.getDotY(value) + 4).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+ // Vertical grid lines
+ const numVertLines = Math.ceil(
+ this.config.totalWidth / this.config.xUnitSize
+ );
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", x.toString());
+ line.setAttribute("y1", "0");
+ line.setAttribute("x2", x.toString());
+ line.setAttribute("y2", this.config.height.toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ if (xValue !== 0) {
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", x.toString());
+ text.setAttribute("y", (this.config.height / 2 + 20).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.setAttribute("text-anchor", "middle");
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ tooltip.classList.add("dot-tooltip");
+ tooltip.setAttribute("data-dot-id", dot.id.toString());
+ // Calculate tooltip dimensions and position
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipHeight = this.config.tooltipHeight;
+ const tooltipX = x - tooltipWidth / 2;
+ let tooltipY = y - tooltipHeight - 20; // Positioned above the dot
+ tooltipY = Math.max(tooltipY, 10); // Ensure it doesn't go above the view
+ // Create background rectangle
+ const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ bg.setAttribute("x", tooltipX.toString());
+ bg.setAttribute("y", tooltipY.toString());
+ bg.setAttribute("width", tooltipWidth.toString());
+ bg.setAttribute("height", tooltipHeight.toString());
+ bg.setAttribute("fill", "rgba(0, 0, 0, 0.8)");
+ bg.setAttribute("rx", "5"); // Rounded corners
+ tooltip.appendChild(bg);
+ // Create foreignObject for the content
+ const contentContainer = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "foreignObject"
+ );
+ contentContainer.setAttribute("x", tooltipX.toString());
+ contentContainer.setAttribute("y", tooltipY.toString());
+ contentContainer.setAttribute("width", tooltipWidth.toString());
+ contentContainer.setAttribute("height", tooltipHeight.toString());
+ // Create a div with flexbox for centering content
+ const div = document.createElement("div");
+ div.style.display = "flex";
+ div.style.flexDirection = "column";
+ div.style.justifyContent = "center"; // Center vertically
+ div.style.alignItems = "center"; // Center horizontally
+ div.style.width = "100%";
+ div.style.height = "100%";
+ div.style.color = "white"; // Set text color to white
+ // Add image if available
+ if (dot.imageUrl) {
+ const img = document.createElement("img");
+ img.src = dot.imageUrl;
+ img.style.width = "50px";
+ img.style.height = "50px";
+ img.style.borderRadius = "50%"; // Circular image
+ img.style.border = "2px solid white";
+ div.appendChild(img);
+ }
+ // Add title if available
+ if (dot.title) {
+ const title = document.createElement("div");
+ title.style.fontSize = "14px";
+ title.style.fontWeight = "bold";
+ title.textContent = dot.title;
+ div.appendChild(title);
+ }
+ // Add description if available
+ if (dot.description) {
+ const desc = document.createElement("div");
+ desc.style.fontSize = "12px";
+ desc.textContent = dot.description;
+ div.appendChild(desc);
+ }
+ contentContainer.appendChild(div);
+ tooltip.appendChild(contentContainer);
+ // Add arrow path if needed
+ const arrow = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ arrow.setAttribute(
+ "d",
+ `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${
+ tooltipY + tooltipHeight - 10
+ } L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`
+ );
+ arrow.setAttribute("fill", "rgba(0, 0, 0, 0.8)");
+ tooltip.appendChild(arrow);
+ return tooltip;
+ }
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute("d", pathData);
+ }
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ return { leftmost, rightmost };
+ }
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+ const circle = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "circle"
+ );
+ circle.setAttribute("cx", x.toString());
+ circle.setAttribute("cy", y.toString());
+ circle.setAttribute("r", this.config.dotRadius.toString());
+ circle.setAttribute("fill", "white");
+ circle.setAttribute("data-dot-id", dot.id.toString());
+ circle.classList.add("dot");
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener("click", () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error("Dot has no link");
+ throw new Error("Dot has no link");
+ }
+ });
+ }
+ this.dotsGroup.appendChild(circle);
+ }
+ }
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522093434.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522093434.ts
new file mode 100644
index 0000000..b1ea08f
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522093434.ts
@@ -0,0 +1,507 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+ private preloadedImages: Map = new Map();
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+ constructor(
+ containerId: string,
+ dots: DotConfig[],
+ config?: Partial
+ ) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config,
+ };
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+ // Create SVG elements
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ this.gridGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.curvePath = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ this.dotsGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.tooltipGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter((dot) => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map((dot) => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+ // Create a loading indicator (optional)
+ const loadingCount = { current: 0, total: imageUrls.length };
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log("All images preloaded successfully");
+ }
+ };
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+ // Set src to start loading
+ img.src = url;
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ }
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = "connected-dots-styles";
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement("style");
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.svg.style.overflow = "visible";
+ this.scrollContainer.appendChild(this.svg);
+ // Configure grid group
+ this.gridGroup.classList.add("grid");
+ this.svg.appendChild(this.gridGroup);
+ // Configure curve path
+ this.curvePath.setAttribute("fill", "none");
+ this.curvePath.setAttribute("stroke", "white");
+ this.curvePath.setAttribute("stroke-width", "2");
+ this.curvePath.setAttribute("stroke-linecap", "round");
+ this.curvePath.classList.add("curve-path");
+ this.svg.appendChild(this.curvePath);
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add("tooltips");
+ this.svg.appendChild(this.tooltipGroup);
+ }
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.8);
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+ private calculateBezierControlPoints(
+ dots: DotConfig[],
+ index: number
+ ): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+ return { x1, y1, x2, y2 };
+ }
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return "";
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(
+ this.dots[0].value
+ )}`;
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(
+ this.dots,
+ i
+ );
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+ return path;
+ }
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+ if (!this.config.showGrid) return;
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", "0");
+ line.setAttribute("y1", this.getDotY(value).toString());
+ line.setAttribute("x2", this.config.totalWidth.toString());
+ line.setAttribute("y2", this.getDotY(value).toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", "10");
+ text.setAttribute("y", (this.getDotY(value) + 4).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+ // Vertical grid lines
+ const numVertLines = Math.ceil(
+ this.config.totalWidth / this.config.xUnitSize
+ );
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", x.toString());
+ line.setAttribute("y1", "0");
+ line.setAttribute("x2", x.toString());
+ line.setAttribute("y2", this.config.height.toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ if (xValue !== 0) {
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", x.toString());
+ text.setAttribute("y", (this.config.height / 2 + 20).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.setAttribute("text-anchor", "middle");
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ tooltip.classList.add("dot-tooltip");
+ tooltip.setAttribute("data-dot-id", dot.id.toString());
+ // Calculate tooltip dimensions and position
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipHeight = this.config.tooltipHeight;
+ const tooltipX = x - tooltipWidth / 2;
+ let tooltipY = y - tooltipHeight - 20; // Positioned above the dot
+ tooltipY = Math.max(tooltipY, 10); // Ensure it doesn't go above the view
+ // Create background rectangle
+ const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ bg.setAttribute("x", tooltipX.toString());
+ bg.setAttribute("y", tooltipY.toString());
+
+ // Calculate width and height based on ratio
+ const height = tooltipHeight;
+ const width = (9 / 16) * height;
+
+ // Set the width and height for a 9:16 aspect ratio
+ bg.setAttribute("width", width.toString());
+ bg.setAttribute("height", height.toString());
+
+ bg.setAttribute("fill", "rgba(0, 0, 0, 0.8)");
+ bg.setAttribute("rx", "5"); // Rounded corners
+ tooltip.appendChild(bg);
+ // Create foreignObject for the content
+ const contentContainer = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "foreignObject"
+ );
+ contentContainer.setAttribute("x", tooltipX.toString());
+ contentContainer.setAttribute("y", tooltipY.toString());
+ contentContainer.setAttribute("width", tooltipWidth.toString());
+ contentContainer.setAttribute("height", tooltipHeight.toString());
+ // Create a div with flexbox for centering content
+ const div = document.createElement("div");
+ div.style.display = "flex";
+ div.style.flexDirection = "column";
+ div.style.justifyContent = "center"; // Center vertically
+ div.style.alignItems = "center"; // Center horizontally
+ div.style.width = "100%";
+ div.style.height = "100%";
+ div.style.color = "white"; // Set text color to white
+ // Add image if available
+ if (dot.imageUrl) {
+ const img = document.createElement("img");
+ img.src = dot.imageUrl;
+ img.style.width = "50px";
+ img.style.height = "50px";
+ img.style.borderRadius = "50%"; // Circular image
+ img.style.border = "2px solid white";
+ div.appendChild(img);
+ }
+ // Add title if available
+ if (dot.title) {
+ const title = document.createElement("div");
+ title.style.fontSize = "14px";
+ title.style.fontWeight = "bold";
+ title.textContent = dot.title;
+ div.appendChild(title);
+ }
+ // Add description if available
+ if (dot.description) {
+ const desc = document.createElement("div");
+ desc.style.fontSize = "12px";
+ desc.textContent = dot.description;
+ div.appendChild(desc);
+ }
+ contentContainer.appendChild(div);
+ tooltip.appendChild(contentContainer);
+ // Add arrow path if needed
+ const arrow = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ arrow.setAttribute(
+ "d",
+ `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${
+ tooltipY + tooltipHeight - 10
+ } L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`
+ );
+ arrow.setAttribute("fill", "rgba(0, 0, 0, 0.8)");
+ tooltip.appendChild(arrow);
+ return tooltip;
+ }
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute("d", pathData);
+ }
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ return { leftmost, rightmost };
+ }
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+ const circle = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "circle"
+ );
+ circle.setAttribute("cx", x.toString());
+ circle.setAttribute("cy", y.toString());
+ circle.setAttribute("r", this.config.dotRadius.toString());
+ circle.setAttribute("fill", "white");
+ circle.setAttribute("data-dot-id", dot.id.toString());
+ circle.classList.add("dot");
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener("click", () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error("Dot has no link");
+ throw new Error("Dot has no link");
+ }
+ });
+ }
+ this.dotsGroup.appendChild(circle);
+ }
+ }
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522093501.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522093501.ts
new file mode 100644
index 0000000..f31146f
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522093501.ts
@@ -0,0 +1,504 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+ private preloadedImages: Map = new Map();
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+ constructor(
+ containerId: string,
+ dots: DotConfig[],
+ config?: Partial
+ ) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config,
+ };
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+ // Create SVG elements
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ this.gridGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.curvePath = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ this.dotsGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.tooltipGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter((dot) => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map((dot) => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+ // Create a loading indicator (optional)
+ const loadingCount = { current: 0, total: imageUrls.length };
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log("All images preloaded successfully");
+ }
+ };
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+ // Set src to start loading
+ img.src = url;
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ }
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = "connected-dots-styles";
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement("style");
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.svg.style.overflow = "visible";
+ this.scrollContainer.appendChild(this.svg);
+ // Configure grid group
+ this.gridGroup.classList.add("grid");
+ this.svg.appendChild(this.gridGroup);
+ // Configure curve path
+ this.curvePath.setAttribute("fill", "none");
+ this.curvePath.setAttribute("stroke", "white");
+ this.curvePath.setAttribute("stroke-width", "2");
+ this.curvePath.setAttribute("stroke-linecap", "round");
+ this.curvePath.classList.add("curve-path");
+ this.svg.appendChild(this.curvePath);
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add("tooltips");
+ this.svg.appendChild(this.tooltipGroup);
+ }
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.8);
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+ private calculateBezierControlPoints(
+ dots: DotConfig[],
+ index: number
+ ): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+ return { x1, y1, x2, y2 };
+ }
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return "";
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(
+ this.dots[0].value
+ )}`;
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(
+ this.dots,
+ i
+ );
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+ return path;
+ }
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+ if (!this.config.showGrid) return;
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", "0");
+ line.setAttribute("y1", this.getDotY(value).toString());
+ line.setAttribute("x2", this.config.totalWidth.toString());
+ line.setAttribute("y2", this.getDotY(value).toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", "10");
+ text.setAttribute("y", (this.getDotY(value) + 4).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+ // Vertical grid lines
+ const numVertLines = Math.ceil(
+ this.config.totalWidth / this.config.xUnitSize
+ );
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", x.toString());
+ line.setAttribute("y1", "0");
+ line.setAttribute("x2", x.toString());
+ line.setAttribute("y2", this.config.height.toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ if (xValue !== 0) {
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", x.toString());
+ text.setAttribute("y", (this.config.height / 2 + 20).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.setAttribute("text-anchor", "middle");
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ tooltip.classList.add("dot-tooltip");
+ tooltip.setAttribute("data-dot-id", dot.id.toString());
+ // Calculate tooltip dimensions and position
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipHeight = this.config.tooltipHeight;
+ const tooltipX = x - tooltipWidth / 2;
+ let tooltipY = y - tooltipHeight - 20; // Positioned above the dot
+ tooltipY = Math.max(tooltipY, 10); // Ensure it doesn't go above the view
+ // Create background rectangle
+ const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ bg.setAttribute("x", tooltipX.toString());
+ bg.setAttribute("y", tooltipY.toString());
+
+ // Calculate width and height based on ratio
+ const height = tooltipHeight;
+ const width = (9 / 16) * height;
+
+ // Set the width and height for a 9:16 aspect ratio
+ bg.setAttribute("width", width.toString());
+ bg.setAttribute("height", height.toString());
+
+ bg.setAttribute("fill", "rgba(0, 0, 0, 0.8)");
+ bg.setAttribute("rx", "5"); // Rounded corners
+ tooltip.appendChild(bg);
+ // Create a foreignObject for centering content
+const container = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+container.setAttribute('width', width.toString());
+container.setAttribute('height', height.toString());
+container.setAttribute('x', tooltipX.toString());
+container.setAttribute('y', tooltipY.toString());
+ // Create a div with flexbox for centering content
+ const div = document.createElement("div");
+ div.style.display = "flex";
+ div.style.flexDirection = "column";
+ div.style.justifyContent = "center"; // Center vertically
+ div.style.alignItems = "center"; // Center horizontally
+ div.style.width = "100%";
+ div.style.height = "100%";
+ div.style.color = "white"; // Set text color to white
+ // Add image if available
+ if (dot.imageUrl) {
+ const img = document.createElement("img");
+ img.src = dot.imageUrl;
+ img.style.width = "50px";
+ img.style.height = "50px";
+ img.style.borderRadius = "50%"; // Circular image
+ img.style.border = "2px solid white";
+ div.appendChild(img);
+ }
+ // Add title if available
+ if (dot.title) {
+ const title = document.createElement("div");
+ title.style.fontSize = "14px";
+ title.style.fontWeight = "bold";
+ title.textContent = dot.title;
+ div.appendChild(title);
+ }
+ // Add description if available
+ if (dot.description) {
+ const desc = document.createElement("div");
+ desc.style.fontSize = "12px";
+ desc.textContent = dot.description;
+ div.appendChild(desc);
+ }
+ contentContainer.appendChild(div);
+ tooltip.appendChild(contentContainer);
+ // Add arrow path if needed
+ const arrow = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ arrow.setAttribute(
+ "d",
+ `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${
+ tooltipY + tooltipHeight - 10
+ } L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`
+ );
+ arrow.setAttribute("fill", "rgba(0, 0, 0, 0.8)");
+ tooltip.appendChild(arrow);
+ return tooltip;
+ }
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute("d", pathData);
+ }
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ return { leftmost, rightmost };
+ }
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+ const circle = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "circle"
+ );
+ circle.setAttribute("cx", x.toString());
+ circle.setAttribute("cy", y.toString());
+ circle.setAttribute("r", this.config.dotRadius.toString());
+ circle.setAttribute("fill", "white");
+ circle.setAttribute("data-dot-id", dot.id.toString());
+ circle.classList.add("dot");
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener("click", () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error("Dot has no link");
+ throw new Error("Dot has no link");
+ }
+ });
+ }
+ this.dotsGroup.appendChild(circle);
+ }
+ }
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522093509.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522093509.ts
new file mode 100644
index 0000000..753c922
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522093509.ts
@@ -0,0 +1,509 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+ private preloadedImages: Map = new Map();
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+ constructor(
+ containerId: string,
+ dots: DotConfig[],
+ config?: Partial
+ ) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config,
+ };
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+ // Create SVG elements
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ this.gridGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.curvePath = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ this.dotsGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.tooltipGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter((dot) => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map((dot) => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+ // Create a loading indicator (optional)
+ const loadingCount = { current: 0, total: imageUrls.length };
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log("All images preloaded successfully");
+ }
+ };
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+ // Set src to start loading
+ img.src = url;
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ }
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = "connected-dots-styles";
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement("style");
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.svg.style.overflow = "visible";
+ this.scrollContainer.appendChild(this.svg);
+ // Configure grid group
+ this.gridGroup.classList.add("grid");
+ this.svg.appendChild(this.gridGroup);
+ // Configure curve path
+ this.curvePath.setAttribute("fill", "none");
+ this.curvePath.setAttribute("stroke", "white");
+ this.curvePath.setAttribute("stroke-width", "2");
+ this.curvePath.setAttribute("stroke-linecap", "round");
+ this.curvePath.classList.add("curve-path");
+ this.svg.appendChild(this.curvePath);
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add("tooltips");
+ this.svg.appendChild(this.tooltipGroup);
+ }
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.8);
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+ private calculateBezierControlPoints(
+ dots: DotConfig[],
+ index: number
+ ): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+ return { x1, y1, x2, y2 };
+ }
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return "";
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(
+ this.dots[0].value
+ )}`;
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(
+ this.dots,
+ i
+ );
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+ return path;
+ }
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+ if (!this.config.showGrid) return;
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", "0");
+ line.setAttribute("y1", this.getDotY(value).toString());
+ line.setAttribute("x2", this.config.totalWidth.toString());
+ line.setAttribute("y2", this.getDotY(value).toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", "10");
+ text.setAttribute("y", (this.getDotY(value) + 4).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+ // Vertical grid lines
+ const numVertLines = Math.ceil(
+ this.config.totalWidth / this.config.xUnitSize
+ );
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", x.toString());
+ line.setAttribute("y1", "0");
+ line.setAttribute("x2", x.toString());
+ line.setAttribute("y2", this.config.height.toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ if (xValue !== 0) {
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", x.toString());
+ text.setAttribute("y", (this.config.height / 2 + 20).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.setAttribute("text-anchor", "middle");
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ tooltip.classList.add("dot-tooltip");
+ tooltip.setAttribute("data-dot-id", dot.id.toString());
+ // Calculate tooltip dimensions and position
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipHeight = this.config.tooltipHeight;
+ const tooltipX = x - tooltipWidth / 2;
+ let tooltipY = y - tooltipHeight - 20; // Positioned above the dot
+ tooltipY = Math.max(tooltipY, 10); // Ensure it doesn't go above the view
+ // Create background rectangle
+ const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ bg.setAttribute("x", tooltipX.toString());
+ bg.setAttribute("y", tooltipY.toString());
+
+ // Calculate width and height based on ratio
+ const height = tooltipHeight;
+ const width = (9 / 16) * height;
+
+ // Set the width and height for a 9:16 aspect ratio
+ bg.setAttribute("width", width.toString());
+ bg.setAttribute("height", height.toString());
+
+ bg.setAttribute("fill", "rgba(0, 0, 0, 0.8)");
+ bg.setAttribute("rx", "5"); // Rounded corners
+ tooltip.appendChild(bg);
+
+ // Create foreignObject for the content
+ const contentContainer = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "foreignObject"
+ );
+ contentContainer.setAttribute("x", tooltipX.toString());
+ contentContainer.setAttribute("y", tooltipY.toString());
+ contentContainer.setAttribute("width", tooltipWidth.toString());
+ contentContainer.setAttribute("height", tooltipHeight.toString());
+
+ // Create a div with flexbox for centering content
+ const div = document.createElement("div");
+ div.style.display = "flex";
+ div.style.flexDirection = "column";
+ div.style.justifyContent = "center"; // Center vertically
+ div.style.alignItems = "center"; // Center horizontally
+ div.style.width = "100%";
+ div.style.height = "100%";
+ div.style.color = "white"; // Set text color to white
+ // Add image if available
+ if (dot.imageUrl) {
+ const img = document.createElement("img");
+ img.src = dot.imageUrl;
+ img.style.width = "50px";
+ img.style.height = "50px";
+ img.style.borderRadius = "50%"; // Circular image
+ img.style.border = "2px solid white";
+ div.appendChild(img);
+ }
+ // Add title if available
+ if (dot.title) {
+ const title = document.createElement("div");
+ title.style.fontSize = "14px";
+ title.style.fontWeight = "bold";
+ title.textContent = dot.title;
+ div.appendChild(title);
+ }
+ // Add description if available
+ if (dot.description) {
+ const desc = document.createElement("div");
+ desc.style.fontSize = "12px";
+ desc.textContent = dot.description;
+ div.appendChild(desc);
+ }
+ contentContainer.appendChild(div);
+ tooltip.appendChild(contentContainer);
+ // Add arrow path if needed
+ const arrow = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ arrow.setAttribute(
+ "d",
+ `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${
+ tooltipY + tooltipHeight - 10
+ } L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`
+ );
+ arrow.setAttribute("fill", "rgba(0, 0, 0, 0.8)");
+ tooltip.appendChild(arrow);
+ return tooltip;
+ }
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute("d", pathData);
+ }
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ return { leftmost, rightmost };
+ }
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+ const circle = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "circle"
+ );
+ circle.setAttribute("cx", x.toString());
+ circle.setAttribute("cy", y.toString());
+ circle.setAttribute("r", this.config.dotRadius.toString());
+ circle.setAttribute("fill", "white");
+ circle.setAttribute("data-dot-id", dot.id.toString());
+ circle.classList.add("dot");
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener("click", () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error("Dot has no link");
+ throw new Error("Dot has no link");
+ }
+ });
+ }
+ this.dotsGroup.appendChild(circle);
+ }
+ }
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522093827.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522093827.ts
new file mode 100644
index 0000000..bf2b1db
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522093827.ts
@@ -0,0 +1,498 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+ private preloadedImages: Map = new Map();
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+ constructor(
+ containerId: string,
+ dots: DotConfig[],
+ config?: Partial
+ ) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config,
+ };
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+ // Create SVG elements
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ this.gridGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.curvePath = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ this.dotsGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.tooltipGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter((dot) => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map((dot) => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+ // Create a loading indicator (optional)
+ const loadingCount = { current: 0, total: imageUrls.length };
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log("All images preloaded successfully");
+ }
+ };
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+ // Set src to start loading
+ img.src = url;
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ }
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = "connected-dots-styles";
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement("style");
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.svg.style.overflow = "visible";
+ this.scrollContainer.appendChild(this.svg);
+ // Configure grid group
+ this.gridGroup.classList.add("grid");
+ this.svg.appendChild(this.gridGroup);
+ // Configure curve path
+ this.curvePath.setAttribute("fill", "none");
+ this.curvePath.setAttribute("stroke", "white");
+ this.curvePath.setAttribute("stroke-width", "2");
+ this.curvePath.setAttribute("stroke-linecap", "round");
+ this.curvePath.classList.add("curve-path");
+ this.svg.appendChild(this.curvePath);
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add("tooltips");
+ this.svg.appendChild(this.tooltipGroup);
+ }
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.8);
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+ private calculateBezierControlPoints(
+ dots: DotConfig[],
+ index: number
+ ): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+ return { x1, y1, x2, y2 };
+ }
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return "";
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(
+ this.dots[0].value
+ )}`;
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(
+ this.dots,
+ i
+ );
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+ return path;
+ }
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+ if (!this.config.showGrid) return;
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", "0");
+ line.setAttribute("y1", this.getDotY(value).toString());
+ line.setAttribute("x2", this.config.totalWidth.toString());
+ line.setAttribute("y2", this.getDotY(value).toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", "10");
+ text.setAttribute("y", (this.getDotY(value) + 4).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+ // Vertical grid lines
+ const numVertLines = Math.ceil(
+ this.config.totalWidth / this.config.xUnitSize
+ );
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", x.toString());
+ line.setAttribute("y1", "0");
+ line.setAttribute("x2", x.toString());
+ line.setAttribute("y2", this.config.height.toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ if (xValue !== 0) {
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", x.toString());
+ text.setAttribute("y", (this.config.height / 2 + 20).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.setAttribute("text-anchor", "middle");
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ tooltip.classList.add("dot-tooltip");
+ tooltip.setAttribute("data-dot-id", dot.id.toString());
+
+ // Calculate tooltip dimensions and position
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipHeight = this.config.tooltipHeight;
+ const tooltipX = x - tooltipWidth / 2;
+ let tooltipY = y - tooltipHeight - 20; // Positioned above the dot
+ tooltipY = Math.max(tooltipY, 10); // Ensure it doesn't go above the view
+
+ // Create background rectangle
+ const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ bg.setAttribute("x", tooltipX.toString());
+ bg.setAttribute("y", tooltipY.toString());
+ bg.setAttribute("width", tooltipWidth.toString());
+ bg.setAttribute("height", tooltipHeight.toString());
+ bg.setAttribute("rx", "5"); // Rounded corners
+ bg.classList.add("tooltip-background");
+ tooltip.appendChild(bg);
+
+ // Create foreignObject for the content
+ const contentContainer = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ contentContainer.setAttribute('x', tooltipX.toString());
+ contentContainer.setAttribute('y', tooltipY.toString());
+ contentContainer.setAttribute('width', tooltipWidth.toString());
+ contentContainer.setAttribute('height', tooltipHeight.toString());
+
+ // Create a div to contain the content
+ const div = document.createElement('div');
+ div.classList.add("tooltip-content");
+
+ // Add image if available
+ if (dot.imageUrl) {
+ const img = document.createElement('img');
+ img.src = dot.imageUrl;
+ img.classList.add("tooltip-image");
+ div.appendChild(img);
+ }
+
+ // Add title if available
+ if (dot.title) {
+ const title = document.createElement('div');
+ title.textContent = dot.title;
+ title.classList.add("tooltip-title");
+ div.appendChild(title);
+ }
+
+ // Add description if available
+ if (dot.description) {
+ const desc = document.createElement('div');
+ desc.textContent = dot.description;
+ desc.classList.add("tooltip-description");
+ div.appendChild(desc);
+ }
+
+ contentContainer.appendChild(div);
+ tooltip.appendChild(contentContainer);
+
+ // Add arrow path if needed
+ const arrow = document.createElementNS("http://www.w3.org/2000/svg", "path");
+ arrow.setAttribute(
+ "d",
+ `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${
+ tooltipY + tooltipHeight - 10
+ } L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`
+ );
+ arrow.classList.add("tooltip-arrow");
+ tooltip.appendChild(arrow);
+
+ return tooltip;
+}
+
+
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute("d", pathData);
+ }
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ return { leftmost, rightmost };
+ }
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+ const circle = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "circle"
+ );
+ circle.setAttribute("cx", x.toString());
+ circle.setAttribute("cy", y.toString());
+ circle.setAttribute("r", this.config.dotRadius.toString());
+ circle.setAttribute("fill", "white");
+ circle.setAttribute("data-dot-id", dot.id.toString());
+ circle.classList.add("dot");
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener("click", () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error("Dot has no link");
+ throw new Error("Dot has no link");
+ }
+ });
+ }
+ this.dotsGroup.appendChild(circle);
+ }
+ }
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522094341.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522094341.ts
new file mode 100644
index 0000000..32ca3df
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522094341.ts
@@ -0,0 +1,502 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+ private preloadedImages: Map = new Map();
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+ constructor(
+ containerId: string,
+ dots: DotConfig[],
+ config?: Partial
+ ) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config,
+ };
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+ // Create SVG elements
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ this.gridGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.curvePath = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ this.dotsGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.tooltipGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter((dot) => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map((dot) => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+ // Create a loading indicator (optional)
+ const loadingCount = { current: 0, total: imageUrls.length };
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log("All images preloaded successfully");
+ }
+ };
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+ // Set src to start loading
+ img.src = url;
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ }
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = "connected-dots-styles";
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement("style");
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.svg.style.overflow = "visible";
+ this.scrollContainer.appendChild(this.svg);
+ // Configure grid group
+ this.gridGroup.classList.add("grid");
+ this.svg.appendChild(this.gridGroup);
+ // Configure curve path
+ this.curvePath.setAttribute("fill", "none");
+ this.curvePath.setAttribute("stroke", "white");
+ this.curvePath.setAttribute("stroke-width", "2");
+ this.curvePath.setAttribute("stroke-linecap", "round");
+ this.curvePath.classList.add("curve-path");
+ this.svg.appendChild(this.curvePath);
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add("tooltips");
+ this.svg.appendChild(this.tooltipGroup);
+ }
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.8);
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+ private calculateBezierControlPoints(
+ dots: DotConfig[],
+ index: number
+ ): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+ return { x1, y1, x2, y2 };
+ }
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return "";
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(
+ this.dots[0].value
+ )}`;
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(
+ this.dots,
+ i
+ );
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+ return path;
+ }
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+ if (!this.config.showGrid) return;
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", "0");
+ line.setAttribute("y1", this.getDotY(value).toString());
+ line.setAttribute("x2", this.config.totalWidth.toString());
+ line.setAttribute("y2", this.getDotY(value).toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", "10");
+ text.setAttribute("y", (this.getDotY(value) + 4).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+ // Vertical grid lines
+ const numVertLines = Math.ceil(
+ this.config.totalWidth / this.config.xUnitSize
+ );
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", x.toString());
+ line.setAttribute("y1", "0");
+ line.setAttribute("x2", x.toString());
+ line.setAttribute("y2", this.config.height.toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ if (xValue !== 0) {
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", x.toString());
+ text.setAttribute("y", (this.config.height / 2 + 20).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.setAttribute("text-anchor", "middle");
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ tooltip.classList.add("dot-tooltip");
+ tooltip.setAttribute("data-dot-id", dot.id.toString());
+
+// const tooltipWidth = 200; // or any other desired width
+// const tooltipHeight = (16 / 9) * tooltipWidth;
+
+ // Calculate tooltip dimensions and position
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipHeight = this.config.tooltipHeight;
+ const tooltipX = x - tooltipWidth / 2;
+ let tooltipY = y - tooltipHeight - 20; // Positioned above the dot
+ tooltipY = Math.max(tooltipY, 10); // Ensure it doesn't go above the view
+
+ // Create background rectangle
+ const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ bg.setAttribute("x", tooltipX.toString());
+ bg.setAttribute("y", tooltipY.toString());
+ bg.setAttribute("width", tooltipWidth.toString());
+ bg.setAttribute("height", tooltipHeight.toString());
+ bg.setAttribute("rx", "5"); // Rounded corners
+ bg.classList.add("tooltip-background");
+ tooltip.appendChild(bg);
+
+ // Create foreignObject for the content
+
+ const contentContainer = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ contentContainer.setAttribute('x', tooltipX.toString());
+ contentContainer.setAttribute('y', tooltipY.toString());
+ contentContainer.setAttribute('width', tooltipWidth.toString());
+ contentContainer.setAttribute('height', tooltipHeight.toString());
+
+ // Create a div to contain the content
+ const div = document.createElement('div');
+ div.classList.add("tooltip-content");
+
+ // Add image if available
+ if (dot.imageUrl) {
+ const img = document.createElement('img');
+ img.src = dot.imageUrl;
+ img.classList.add("tooltip-image");
+ div.appendChild(img);
+ }
+
+ // Add title if available
+ if (dot.title) {
+ const title = document.createElement('div');
+ title.textContent = dot.title;
+ title.classList.add("tooltip-title");
+ div.appendChild(title);
+ }
+
+ // Add description if available
+ if (dot.description) {
+ const desc = document.createElement('div');
+ desc.textContent = dot.description;
+ desc.classList.add("tooltip-description");
+ div.appendChild(desc);
+ }
+
+ contentContainer.appendChild(div);
+ tooltip.appendChild(contentContainer);
+
+ // Add arrow path if needed
+ const arrow = document.createElementNS("http://www.w3.org/2000/svg", "path");
+ arrow.setAttribute(
+ "d",
+ `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${
+ tooltipY + tooltipHeight - 10
+ } L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`
+ );
+ arrow.classList.add("tooltip-arrow");
+ tooltip.appendChild(arrow);
+
+ return tooltip;
+}
+
+
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute("d", pathData);
+ }
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ return { leftmost, rightmost };
+ }
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+ const circle = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "circle"
+ );
+ circle.setAttribute("cx", x.toString());
+ circle.setAttribute("cy", y.toString());
+ circle.setAttribute("r", this.config.dotRadius.toString());
+ circle.setAttribute("fill", "white");
+ circle.setAttribute("data-dot-id", dot.id.toString());
+ circle.classList.add("dot");
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener("click", () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error("Dot has no link");
+ throw new Error("Dot has no link");
+ }
+ });
+ }
+ this.dotsGroup.appendChild(circle);
+ }
+ }
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522094352.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522094352.ts
new file mode 100644
index 0000000..2b23558
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522094352.ts
@@ -0,0 +1,502 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+ private preloadedImages: Map = new Map();
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+ constructor(
+ containerId: string,
+ dots: DotConfig[],
+ config?: Partial
+ ) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config,
+ };
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+ // Create SVG elements
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ this.gridGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.curvePath = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ this.dotsGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.tooltipGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter((dot) => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map((dot) => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+ // Create a loading indicator (optional)
+ const loadingCount = { current: 0, total: imageUrls.length };
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log("All images preloaded successfully");
+ }
+ };
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+ // Set src to start loading
+ img.src = url;
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ }
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = "connected-dots-styles";
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement("style");
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.svg.style.overflow = "visible";
+ this.scrollContainer.appendChild(this.svg);
+ // Configure grid group
+ this.gridGroup.classList.add("grid");
+ this.svg.appendChild(this.gridGroup);
+ // Configure curve path
+ this.curvePath.setAttribute("fill", "none");
+ this.curvePath.setAttribute("stroke", "white");
+ this.curvePath.setAttribute("stroke-width", "2");
+ this.curvePath.setAttribute("stroke-linecap", "round");
+ this.curvePath.classList.add("curve-path");
+ this.svg.appendChild(this.curvePath);
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add("tooltips");
+ this.svg.appendChild(this.tooltipGroup);
+ }
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.8);
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+ private calculateBezierControlPoints(
+ dots: DotConfig[],
+ index: number
+ ): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+ return { x1, y1, x2, y2 };
+ }
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return "";
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(
+ this.dots[0].value
+ )}`;
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(
+ this.dots,
+ i
+ );
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+ return path;
+ }
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+ if (!this.config.showGrid) return;
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", "0");
+ line.setAttribute("y1", this.getDotY(value).toString());
+ line.setAttribute("x2", this.config.totalWidth.toString());
+ line.setAttribute("y2", this.getDotY(value).toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", "10");
+ text.setAttribute("y", (this.getDotY(value) + 4).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+ // Vertical grid lines
+ const numVertLines = Math.ceil(
+ this.config.totalWidth / this.config.xUnitSize
+ );
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", x.toString());
+ line.setAttribute("y1", "0");
+ line.setAttribute("x2", x.toString());
+ line.setAttribute("y2", this.config.height.toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ if (xValue !== 0) {
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", x.toString());
+ text.setAttribute("y", (this.config.height / 2 + 20).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.setAttribute("text-anchor", "middle");
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ tooltip.classList.add("dot-tooltip");
+ tooltip.setAttribute("data-dot-id", dot.id.toString());
+
+// const tooltipWidth = 200; // or any other desired width
+const tooltipHeight = (16 / 9) * tooltipWidth;
+
+ // Calculate tooltip dimensions and position
+ const tooltipWidth = this.config.tooltipWidth;
+// const tooltipHeight = this.config.tooltipHeight;
+ const tooltipX = x - tooltipWidth / 2;
+ let tooltipY = y - tooltipHeight - 20; // Positioned above the dot
+ tooltipY = Math.max(tooltipY, 10); // Ensure it doesn't go above the view
+
+ // Create background rectangle
+ const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ bg.setAttribute("x", tooltipX.toString());
+ bg.setAttribute("y", tooltipY.toString());
+ bg.setAttribute("width", tooltipWidth.toString());
+ bg.setAttribute("height", tooltipHeight.toString());
+ bg.setAttribute("rx", "5"); // Rounded corners
+ bg.classList.add("tooltip-background");
+ tooltip.appendChild(bg);
+
+ // Create foreignObject for the content
+
+ const contentContainer = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ contentContainer.setAttribute('x', tooltipX.toString());
+ contentContainer.setAttribute('y', tooltipY.toString());
+ contentContainer.setAttribute('width', tooltipWidth.toString());
+ contentContainer.setAttribute('height', tooltipHeight.toString());
+
+ // Create a div to contain the content
+ const div = document.createElement('div');
+ div.classList.add("tooltip-content");
+
+ // Add image if available
+ if (dot.imageUrl) {
+ const img = document.createElement('img');
+ img.src = dot.imageUrl;
+ img.classList.add("tooltip-image");
+ div.appendChild(img);
+ }
+
+ // Add title if available
+ if (dot.title) {
+ const title = document.createElement('div');
+ title.textContent = dot.title;
+ title.classList.add("tooltip-title");
+ div.appendChild(title);
+ }
+
+ // Add description if available
+ if (dot.description) {
+ const desc = document.createElement('div');
+ desc.textContent = dot.description;
+ desc.classList.add("tooltip-description");
+ div.appendChild(desc);
+ }
+
+ contentContainer.appendChild(div);
+ tooltip.appendChild(contentContainer);
+
+ // Add arrow path if needed
+ const arrow = document.createElementNS("http://www.w3.org/2000/svg", "path");
+ arrow.setAttribute(
+ "d",
+ `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${
+ tooltipY + tooltipHeight - 10
+ } L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`
+ );
+ arrow.classList.add("tooltip-arrow");
+ tooltip.appendChild(arrow);
+
+ return tooltip;
+}
+
+
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute("d", pathData);
+ }
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ return { leftmost, rightmost };
+ }
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+ const circle = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "circle"
+ );
+ circle.setAttribute("cx", x.toString());
+ circle.setAttribute("cy", y.toString());
+ circle.setAttribute("r", this.config.dotRadius.toString());
+ circle.setAttribute("fill", "white");
+ circle.setAttribute("data-dot-id", dot.id.toString());
+ circle.classList.add("dot");
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener("click", () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error("Dot has no link");
+ throw new Error("Dot has no link");
+ }
+ });
+ }
+ this.dotsGroup.appendChild(circle);
+ }
+ }
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522094405.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522094405.ts
new file mode 100644
index 0000000..4cd7fcf
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522094405.ts
@@ -0,0 +1,502 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+ private preloadedImages: Map = new Map();
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+ constructor(
+ containerId: string,
+ dots: DotConfig[],
+ config?: Partial
+ ) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config,
+ };
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+ // Create SVG elements
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ this.gridGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.curvePath = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ this.dotsGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.tooltipGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter((dot) => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map((dot) => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+ // Create a loading indicator (optional)
+ const loadingCount = { current: 0, total: imageUrls.length };
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log("All images preloaded successfully");
+ }
+ };
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+ // Set src to start loading
+ img.src = url;
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ }
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = "connected-dots-styles";
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement("style");
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.svg.style.overflow = "visible";
+ this.scrollContainer.appendChild(this.svg);
+ // Configure grid group
+ this.gridGroup.classList.add("grid");
+ this.svg.appendChild(this.gridGroup);
+ // Configure curve path
+ this.curvePath.setAttribute("fill", "none");
+ this.curvePath.setAttribute("stroke", "white");
+ this.curvePath.setAttribute("stroke-width", "2");
+ this.curvePath.setAttribute("stroke-linecap", "round");
+ this.curvePath.classList.add("curve-path");
+ this.svg.appendChild(this.curvePath);
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add("tooltips");
+ this.svg.appendChild(this.tooltipGroup);
+ }
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.8);
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+ private calculateBezierControlPoints(
+ dots: DotConfig[],
+ index: number
+ ): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+ return { x1, y1, x2, y2 };
+ }
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return "";
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(
+ this.dots[0].value
+ )}`;
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(
+ this.dots,
+ i
+ );
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+ return path;
+ }
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+ if (!this.config.showGrid) return;
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", "0");
+ line.setAttribute("y1", this.getDotY(value).toString());
+ line.setAttribute("x2", this.config.totalWidth.toString());
+ line.setAttribute("y2", this.getDotY(value).toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", "10");
+ text.setAttribute("y", (this.getDotY(value) + 4).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+ // Vertical grid lines
+ const numVertLines = Math.ceil(
+ this.config.totalWidth / this.config.xUnitSize
+ );
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", x.toString());
+ line.setAttribute("y1", "0");
+ line.setAttribute("x2", x.toString());
+ line.setAttribute("y2", this.config.height.toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ if (xValue !== 0) {
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", x.toString());
+ text.setAttribute("y", (this.config.height / 2 + 20).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.setAttribute("text-anchor", "middle");
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ tooltip.classList.add("dot-tooltip");
+ tooltip.setAttribute("data-dot-id", dot.id.toString());
+
+// const tooltipWidth = 200; // or any other desired width
+const tooltipHeight = (16 / 9) * tooltipWidth;
+
+ // Calculate tooltip dimensions and position
+// const tooltipWidth = this.config.tooltipWidth;
+ const tooltipHeight = this.config.tooltipHeight;
+ const tooltipX = x - tooltipWidth / 2;
+ let tooltipY = y - tooltipHeight - 20; // Positioned above the dot
+ tooltipY = Math.max(tooltipY, 10); // Ensure it doesn't go above the view
+
+ // Create background rectangle
+ const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ bg.setAttribute("x", tooltipX.toString());
+ bg.setAttribute("y", tooltipY.toString());
+ bg.setAttribute("width", tooltipWidth.toString());
+ bg.setAttribute("height", tooltipHeight.toString());
+ bg.setAttribute("rx", "5"); // Rounded corners
+ bg.classList.add("tooltip-background");
+ tooltip.appendChild(bg);
+
+ // Create foreignObject for the content
+
+ const contentContainer = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ contentContainer.setAttribute('x', tooltipX.toString());
+ contentContainer.setAttribute('y', tooltipY.toString());
+ contentContainer.setAttribute('width', tooltipWidth.toString());
+ contentContainer.setAttribute('height', tooltipHeight.toString());
+
+ // Create a div to contain the content
+ const div = document.createElement('div');
+ div.classList.add("tooltip-content");
+
+ // Add image if available
+ if (dot.imageUrl) {
+ const img = document.createElement('img');
+ img.src = dot.imageUrl;
+ img.classList.add("tooltip-image");
+ div.appendChild(img);
+ }
+
+ // Add title if available
+ if (dot.title) {
+ const title = document.createElement('div');
+ title.textContent = dot.title;
+ title.classList.add("tooltip-title");
+ div.appendChild(title);
+ }
+
+ // Add description if available
+ if (dot.description) {
+ const desc = document.createElement('div');
+ desc.textContent = dot.description;
+ desc.classList.add("tooltip-description");
+ div.appendChild(desc);
+ }
+
+ contentContainer.appendChild(div);
+ tooltip.appendChild(contentContainer);
+
+ // Add arrow path if needed
+ const arrow = document.createElementNS("http://www.w3.org/2000/svg", "path");
+ arrow.setAttribute(
+ "d",
+ `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${
+ tooltipY + tooltipHeight - 10
+ } L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`
+ );
+ arrow.classList.add("tooltip-arrow");
+ tooltip.appendChild(arrow);
+
+ return tooltip;
+}
+
+
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute("d", pathData);
+ }
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ return { leftmost, rightmost };
+ }
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+ const circle = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "circle"
+ );
+ circle.setAttribute("cx", x.toString());
+ circle.setAttribute("cy", y.toString());
+ circle.setAttribute("r", this.config.dotRadius.toString());
+ circle.setAttribute("fill", "white");
+ circle.setAttribute("data-dot-id", dot.id.toString());
+ circle.classList.add("dot");
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener("click", () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error("Dot has no link");
+ throw new Error("Dot has no link");
+ }
+ });
+ }
+ this.dotsGroup.appendChild(circle);
+ }
+ }
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522094427.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522094427.ts
new file mode 100644
index 0000000..32ca3df
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522094427.ts
@@ -0,0 +1,502 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+ private preloadedImages: Map = new Map();
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+ constructor(
+ containerId: string,
+ dots: DotConfig[],
+ config?: Partial
+ ) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config,
+ };
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+ // Create SVG elements
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ this.gridGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.curvePath = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ this.dotsGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.tooltipGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter((dot) => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map((dot) => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+ // Create a loading indicator (optional)
+ const loadingCount = { current: 0, total: imageUrls.length };
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log("All images preloaded successfully");
+ }
+ };
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+ // Set src to start loading
+ img.src = url;
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ }
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = "connected-dots-styles";
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement("style");
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.svg.style.overflow = "visible";
+ this.scrollContainer.appendChild(this.svg);
+ // Configure grid group
+ this.gridGroup.classList.add("grid");
+ this.svg.appendChild(this.gridGroup);
+ // Configure curve path
+ this.curvePath.setAttribute("fill", "none");
+ this.curvePath.setAttribute("stroke", "white");
+ this.curvePath.setAttribute("stroke-width", "2");
+ this.curvePath.setAttribute("stroke-linecap", "round");
+ this.curvePath.classList.add("curve-path");
+ this.svg.appendChild(this.curvePath);
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add("tooltips");
+ this.svg.appendChild(this.tooltipGroup);
+ }
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.8);
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+ private calculateBezierControlPoints(
+ dots: DotConfig[],
+ index: number
+ ): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+ return { x1, y1, x2, y2 };
+ }
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return "";
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(
+ this.dots[0].value
+ )}`;
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(
+ this.dots,
+ i
+ );
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+ return path;
+ }
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+ if (!this.config.showGrid) return;
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", "0");
+ line.setAttribute("y1", this.getDotY(value).toString());
+ line.setAttribute("x2", this.config.totalWidth.toString());
+ line.setAttribute("y2", this.getDotY(value).toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", "10");
+ text.setAttribute("y", (this.getDotY(value) + 4).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+ // Vertical grid lines
+ const numVertLines = Math.ceil(
+ this.config.totalWidth / this.config.xUnitSize
+ );
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", x.toString());
+ line.setAttribute("y1", "0");
+ line.setAttribute("x2", x.toString());
+ line.setAttribute("y2", this.config.height.toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ if (xValue !== 0) {
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", x.toString());
+ text.setAttribute("y", (this.config.height / 2 + 20).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.setAttribute("text-anchor", "middle");
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ tooltip.classList.add("dot-tooltip");
+ tooltip.setAttribute("data-dot-id", dot.id.toString());
+
+// const tooltipWidth = 200; // or any other desired width
+// const tooltipHeight = (16 / 9) * tooltipWidth;
+
+ // Calculate tooltip dimensions and position
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipHeight = this.config.tooltipHeight;
+ const tooltipX = x - tooltipWidth / 2;
+ let tooltipY = y - tooltipHeight - 20; // Positioned above the dot
+ tooltipY = Math.max(tooltipY, 10); // Ensure it doesn't go above the view
+
+ // Create background rectangle
+ const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ bg.setAttribute("x", tooltipX.toString());
+ bg.setAttribute("y", tooltipY.toString());
+ bg.setAttribute("width", tooltipWidth.toString());
+ bg.setAttribute("height", tooltipHeight.toString());
+ bg.setAttribute("rx", "5"); // Rounded corners
+ bg.classList.add("tooltip-background");
+ tooltip.appendChild(bg);
+
+ // Create foreignObject for the content
+
+ const contentContainer = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ contentContainer.setAttribute('x', tooltipX.toString());
+ contentContainer.setAttribute('y', tooltipY.toString());
+ contentContainer.setAttribute('width', tooltipWidth.toString());
+ contentContainer.setAttribute('height', tooltipHeight.toString());
+
+ // Create a div to contain the content
+ const div = document.createElement('div');
+ div.classList.add("tooltip-content");
+
+ // Add image if available
+ if (dot.imageUrl) {
+ const img = document.createElement('img');
+ img.src = dot.imageUrl;
+ img.classList.add("tooltip-image");
+ div.appendChild(img);
+ }
+
+ // Add title if available
+ if (dot.title) {
+ const title = document.createElement('div');
+ title.textContent = dot.title;
+ title.classList.add("tooltip-title");
+ div.appendChild(title);
+ }
+
+ // Add description if available
+ if (dot.description) {
+ const desc = document.createElement('div');
+ desc.textContent = dot.description;
+ desc.classList.add("tooltip-description");
+ div.appendChild(desc);
+ }
+
+ contentContainer.appendChild(div);
+ tooltip.appendChild(contentContainer);
+
+ // Add arrow path if needed
+ const arrow = document.createElementNS("http://www.w3.org/2000/svg", "path");
+ arrow.setAttribute(
+ "d",
+ `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${
+ tooltipY + tooltipHeight - 10
+ } L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`
+ );
+ arrow.classList.add("tooltip-arrow");
+ tooltip.appendChild(arrow);
+
+ return tooltip;
+}
+
+
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute("d", pathData);
+ }
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ return { leftmost, rightmost };
+ }
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+ const circle = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "circle"
+ );
+ circle.setAttribute("cx", x.toString());
+ circle.setAttribute("cy", y.toString());
+ circle.setAttribute("r", this.config.dotRadius.toString());
+ circle.setAttribute("fill", "white");
+ circle.setAttribute("data-dot-id", dot.id.toString());
+ circle.classList.add("dot");
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener("click", () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error("Dot has no link");
+ throw new Error("Dot has no link");
+ }
+ });
+ }
+ this.dotsGroup.appendChild(circle);
+ }
+ }
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522094438.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522094438.ts
new file mode 100644
index 0000000..3c582aa
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522094438.ts
@@ -0,0 +1,499 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+ private preloadedImages: Map = new Map();
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+ constructor(
+ containerId: string,
+ dots: DotConfig[],
+ config?: Partial
+ ) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config,
+ };
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+ // Create SVG elements
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ this.gridGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.curvePath = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ this.dotsGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.tooltipGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter((dot) => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map((dot) => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+ // Create a loading indicator (optional)
+ const loadingCount = { current: 0, total: imageUrls.length };
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log("All images preloaded successfully");
+ }
+ };
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+ // Set src to start loading
+ img.src = url;
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ }
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = "connected-dots-styles";
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement("style");
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.svg.style.overflow = "visible";
+ this.scrollContainer.appendChild(this.svg);
+ // Configure grid group
+ this.gridGroup.classList.add("grid");
+ this.svg.appendChild(this.gridGroup);
+ // Configure curve path
+ this.curvePath.setAttribute("fill", "none");
+ this.curvePath.setAttribute("stroke", "white");
+ this.curvePath.setAttribute("stroke-width", "2");
+ this.curvePath.setAttribute("stroke-linecap", "round");
+ this.curvePath.classList.add("curve-path");
+ this.svg.appendChild(this.curvePath);
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add("tooltips");
+ this.svg.appendChild(this.tooltipGroup);
+ }
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.8);
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+ private calculateBezierControlPoints(
+ dots: DotConfig[],
+ index: number
+ ): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+ return { x1, y1, x2, y2 };
+ }
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return "";
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(
+ this.dots[0].value
+ )}`;
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(
+ this.dots,
+ i
+ );
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+ return path;
+ }
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+ if (!this.config.showGrid) return;
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", "0");
+ line.setAttribute("y1", this.getDotY(value).toString());
+ line.setAttribute("x2", this.config.totalWidth.toString());
+ line.setAttribute("y2", this.getDotY(value).toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", "10");
+ text.setAttribute("y", (this.getDotY(value) + 4).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+ // Vertical grid lines
+ const numVertLines = Math.ceil(
+ this.config.totalWidth / this.config.xUnitSize
+ );
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", x.toString());
+ line.setAttribute("y1", "0");
+ line.setAttribute("x2", x.toString());
+ line.setAttribute("y2", this.config.height.toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ if (xValue !== 0) {
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", x.toString());
+ text.setAttribute("y", (this.config.height / 2 + 20).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.setAttribute("text-anchor", "middle");
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ tooltip.classList.add("dot-tooltip");
+ tooltip.setAttribute("data-dot-id", dot.id.toString());
+
+ // Calculate tooltip dimensions and position
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipHeight = this.config.tooltipHeight;
+ const tooltipX = x - tooltipWidth / 4;
+ let tooltipY = y - tooltipHeight - 20; // Positioned above the dot
+ tooltipY = Math.max(tooltipY, 10); // Ensure it doesn't go above the view
+
+ // Create background rectangle
+ const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ bg.setAttribute("x", tooltipX.toString());
+ bg.setAttribute("y", tooltipY.toString());
+ bg.setAttribute("width", tooltipWidth.toString());
+ bg.setAttribute("height", tooltipHeight.toString());
+ bg.setAttribute("rx", "5"); // Rounded corners
+ bg.classList.add("tooltip-background");
+ tooltip.appendChild(bg);
+
+ // Create foreignObject for the content
+
+ const contentContainer = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ contentContainer.setAttribute('x', tooltipX.toString());
+ contentContainer.setAttribute('y', tooltipY.toString());
+ contentContainer.setAttribute('width', tooltipWidth.toString());
+ contentContainer.setAttribute('height', tooltipHeight.toString());
+
+ // Create a div to contain the content
+ const div = document.createElement('div');
+ div.classList.add("tooltip-content");
+
+ // Add image if available
+ if (dot.imageUrl) {
+ const img = document.createElement('img');
+ img.src = dot.imageUrl;
+ img.classList.add("tooltip-image");
+ div.appendChild(img);
+ }
+
+ // Add title if available
+ if (dot.title) {
+ const title = document.createElement('div');
+ title.textContent = dot.title;
+ title.classList.add("tooltip-title");
+ div.appendChild(title);
+ }
+
+ // Add description if available
+ if (dot.description) {
+ const desc = document.createElement('div');
+ desc.textContent = dot.description;
+ desc.classList.add("tooltip-description");
+ div.appendChild(desc);
+ }
+
+ contentContainer.appendChild(div);
+ tooltip.appendChild(contentContainer);
+
+ // Add arrow path if needed
+ const arrow = document.createElementNS("http://www.w3.org/2000/svg", "path");
+ arrow.setAttribute(
+ "d",
+ `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${
+ tooltipY + tooltipHeight - 10
+ } L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`
+ );
+ arrow.classList.add("tooltip-arrow");
+ tooltip.appendChild(arrow);
+
+ return tooltip;
+}
+
+
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute("d", pathData);
+ }
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ return { leftmost, rightmost };
+ }
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+ const circle = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "circle"
+ );
+ circle.setAttribute("cx", x.toString());
+ circle.setAttribute("cy", y.toString());
+ circle.setAttribute("r", this.config.dotRadius.toString());
+ circle.setAttribute("fill", "white");
+ circle.setAttribute("data-dot-id", dot.id.toString());
+ circle.classList.add("dot");
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener("click", () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error("Dot has no link");
+ throw new Error("Dot has no link");
+ }
+ });
+ }
+ this.dotsGroup.appendChild(circle);
+ }
+ }
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522094441.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522094441.ts
new file mode 100644
index 0000000..c5265a2
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522094441.ts
@@ -0,0 +1,499 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+ private preloadedImages: Map = new Map();
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+ constructor(
+ containerId: string,
+ dots: DotConfig[],
+ config?: Partial
+ ) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config,
+ };
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+ // Create SVG elements
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ this.gridGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.curvePath = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ this.dotsGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.tooltipGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter((dot) => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map((dot) => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+ // Create a loading indicator (optional)
+ const loadingCount = { current: 0, total: imageUrls.length };
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log("All images preloaded successfully");
+ }
+ };
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+ // Set src to start loading
+ img.src = url;
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ }
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = "connected-dots-styles";
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement("style");
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.svg.style.overflow = "visible";
+ this.scrollContainer.appendChild(this.svg);
+ // Configure grid group
+ this.gridGroup.classList.add("grid");
+ this.svg.appendChild(this.gridGroup);
+ // Configure curve path
+ this.curvePath.setAttribute("fill", "none");
+ this.curvePath.setAttribute("stroke", "white");
+ this.curvePath.setAttribute("stroke-width", "2");
+ this.curvePath.setAttribute("stroke-linecap", "round");
+ this.curvePath.classList.add("curve-path");
+ this.svg.appendChild(this.curvePath);
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add("tooltips");
+ this.svg.appendChild(this.tooltipGroup);
+ }
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.8);
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+ private calculateBezierControlPoints(
+ dots: DotConfig[],
+ index: number
+ ): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+ return { x1, y1, x2, y2 };
+ }
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return "";
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(
+ this.dots[0].value
+ )}`;
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(
+ this.dots,
+ i
+ );
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+ return path;
+ }
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+ if (!this.config.showGrid) return;
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", "0");
+ line.setAttribute("y1", this.getDotY(value).toString());
+ line.setAttribute("x2", this.config.totalWidth.toString());
+ line.setAttribute("y2", this.getDotY(value).toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", "10");
+ text.setAttribute("y", (this.getDotY(value) + 4).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+ // Vertical grid lines
+ const numVertLines = Math.ceil(
+ this.config.totalWidth / this.config.xUnitSize
+ );
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", x.toString());
+ line.setAttribute("y1", "0");
+ line.setAttribute("x2", x.toString());
+ line.setAttribute("y2", this.config.height.toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ if (xValue !== 0) {
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", x.toString());
+ text.setAttribute("y", (this.config.height / 2 + 20).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.setAttribute("text-anchor", "middle");
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ tooltip.classList.add("dot-tooltip");
+ tooltip.setAttribute("data-dot-id", dot.id.toString());
+
+ // Calculate tooltip dimensions and position
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipHeight = this.config.tooltipHeight;
+ const tooltipX = x - tooltipWidth / 2;
+ let tooltipY = y - tooltipHeight - 20; // Positioned above the dot
+ tooltipY = Math.max(tooltipY, 10); // Ensure it doesn't go above the view
+
+ // Create background rectangle
+ const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ bg.setAttribute("x", tooltipX.toString());
+ bg.setAttribute("y", tooltipY.toString());
+ bg.setAttribute("width", tooltipWidth.toString());
+ bg.setAttribute("height", tooltipHeight.toString());
+ bg.setAttribute("rx", "5"); // Rounded corners
+ bg.classList.add("tooltip-background");
+ tooltip.appendChild(bg);
+
+ // Create foreignObject for the content
+
+ const contentContainer = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
+ contentContainer.setAttribute('x', tooltipX.toString());
+ contentContainer.setAttribute('y', tooltipY.toString());
+ contentContainer.setAttribute('width', tooltipWidth.toString());
+ contentContainer.setAttribute('height', tooltipHeight.toString());
+
+ // Create a div to contain the content
+ const div = document.createElement('div');
+ div.classList.add("tooltip-content");
+
+ // Add image if available
+ if (dot.imageUrl) {
+ const img = document.createElement('img');
+ img.src = dot.imageUrl;
+ img.classList.add("tooltip-image");
+ div.appendChild(img);
+ }
+
+ // Add title if available
+ if (dot.title) {
+ const title = document.createElement('div');
+ title.textContent = dot.title;
+ title.classList.add("tooltip-title");
+ div.appendChild(title);
+ }
+
+ // Add description if available
+ if (dot.description) {
+ const desc = document.createElement('div');
+ desc.textContent = dot.description;
+ desc.classList.add("tooltip-description");
+ div.appendChild(desc);
+ }
+
+ contentContainer.appendChild(div);
+ tooltip.appendChild(contentContainer);
+
+ // Add arrow path if needed
+ const arrow = document.createElementNS("http://www.w3.org/2000/svg", "path");
+ arrow.setAttribute(
+ "d",
+ `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${
+ tooltipY + tooltipHeight - 10
+ } L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`
+ );
+ arrow.classList.add("tooltip-arrow");
+ tooltip.appendChild(arrow);
+
+ return tooltip;
+}
+
+
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute("d", pathData);
+ }
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ return { leftmost, rightmost };
+ }
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+ const circle = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "circle"
+ );
+ circle.setAttribute("cx", x.toString());
+ circle.setAttribute("cy", y.toString());
+ circle.setAttribute("r", this.config.dotRadius.toString());
+ circle.setAttribute("fill", "white");
+ circle.setAttribute("data-dot-id", dot.id.toString());
+ circle.classList.add("dot");
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener("click", () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error("Dot has no link");
+ throw new Error("Dot has no link");
+ }
+ });
+ }
+ this.dotsGroup.appendChild(circle);
+ }
+ }
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522094711.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522094711.ts
new file mode 100644
index 0000000..30e1c6f
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522094711.ts
@@ -0,0 +1,503 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+ private preloadedImages: Map = new Map();
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+ constructor(
+ containerId: string,
+ dots: DotConfig[],
+ config?: Partial
+ ) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 80,
+ tooltipHeight: 128,
+ ...config,
+ };
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+ // Create SVG elements
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ this.gridGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.curvePath = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ this.dotsGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.tooltipGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter((dot) => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map((dot) => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+ // Create a loading indicator (optional)
+ const loadingCount = { current: 0, total: imageUrls.length };
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log("All images preloaded successfully");
+ }
+ };
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+ // Set src to start loading
+ img.src = url;
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ }
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = "connected-dots-styles";
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement("style");
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.svg.style.overflow = "visible";
+ this.scrollContainer.appendChild(this.svg);
+ // Configure grid group
+ this.gridGroup.classList.add("grid");
+ this.svg.appendChild(this.gridGroup);
+ // Configure curve path
+ this.curvePath.setAttribute("fill", "none");
+ this.curvePath.setAttribute("stroke", "white");
+ this.curvePath.setAttribute("stroke-width", "2");
+ this.curvePath.setAttribute("stroke-linecap", "round");
+ this.curvePath.classList.add("curve-path");
+ this.svg.appendChild(this.curvePath);
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add("tooltips");
+ this.svg.appendChild(this.tooltipGroup);
+ }
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.8);
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+ private calculateBezierControlPoints(
+ dots: DotConfig[],
+ index: number
+ ): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+ return { x1, y1, x2, y2 };
+ }
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return "";
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(
+ this.dots[0].value
+ )}`;
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(
+ this.dots,
+ i
+ );
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+ return path;
+ }
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+ if (!this.config.showGrid) return;
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", "0");
+ line.setAttribute("y1", this.getDotY(value).toString());
+ line.setAttribute("x2", this.config.totalWidth.toString());
+ line.setAttribute("y2", this.getDotY(value).toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", "10");
+ text.setAttribute("y", (this.getDotY(value) + 4).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+ // Vertical grid lines
+ const numVertLines = Math.ceil(
+ this.config.totalWidth / this.config.xUnitSize
+ );
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", x.toString());
+ line.setAttribute("y1", "0");
+ line.setAttribute("x2", x.toString());
+ line.setAttribute("y2", this.config.height.toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ if (xValue !== 0) {
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", x.toString());
+ text.setAttribute("y", (this.config.height / 2 + 20).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.setAttribute("text-anchor", "middle");
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ tooltip.classList.add("dot-tooltip");
+ tooltip.setAttribute("data-dot-id", dot.id.toString());
+
+ // Calculate tooltip dimensions and position
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipHeight = this.config.tooltipHeight;
+ const tooltipX = x - tooltipWidth / 2;
+ let tooltipY = y - tooltipHeight - 20; // Positioned above the dot
+ tooltipY = Math.max(tooltipY, 10); // Ensure it doesn't go above the view
+
+ // Create background rectangle
+ const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ bg.setAttribute("x", tooltipX.toString());
+ bg.setAttribute("y", tooltipY.toString());
+ bg.setAttribute("width", tooltipWidth.toString());
+ bg.setAttribute("height", tooltipHeight.toString());
+ bg.setAttribute("rx", "5"); // Rounded corners
+ bg.classList.add("tooltip-background");
+ tooltip.appendChild(bg);
+
+ // Create foreignObject for the content
+
+ const contentContainer = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "foreignObject"
+ );
+ contentContainer.setAttribute("x", tooltipX.toString());
+ contentContainer.setAttribute("y", tooltipY.toString());
+ contentContainer.setAttribute("width", tooltipWidth.toString());
+ contentContainer.setAttribute("height", tooltipHeight.toString());
+
+ // Create a div to contain the content
+ const div = document.createElement("div");
+ div.classList.add("tooltip-content");
+
+ // Add image if available
+ if (dot.imageUrl) {
+ const img = document.createElement("img");
+ img.src = dot.imageUrl;
+ img.classList.add("tooltip-image");
+ div.appendChild(img);
+ }
+
+ // Add title if available
+ if (dot.title) {
+ const title = document.createElement("div");
+ title.textContent = dot.title;
+ title.classList.add("tooltip-title");
+ div.appendChild(title);
+ }
+
+ // Add description if available
+ if (dot.description) {
+ const desc = document.createElement("div");
+ desc.textContent = dot.description;
+ desc.classList.add("tooltip-description");
+ div.appendChild(desc);
+ }
+
+ contentContainer.appendChild(div);
+ tooltip.appendChild(contentContainer);
+
+ // Add arrow path if needed
+ const arrow = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ arrow.setAttribute(
+ "d",
+ `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${
+ tooltipY + tooltipHeight - 10
+ } L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`
+ );
+ arrow.classList.add("tooltip-arrow");
+ tooltip.appendChild(arrow);
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute("d", pathData);
+ }
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ return { leftmost, rightmost };
+ }
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+ const circle = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "circle"
+ );
+ circle.setAttribute("cx", x.toString());
+ circle.setAttribute("cy", y.toString());
+ circle.setAttribute("r", this.config.dotRadius.toString());
+ circle.setAttribute("fill", "white");
+ circle.setAttribute("data-dot-id", dot.id.toString());
+ circle.classList.add("dot");
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener("click", () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error("Dot has no link");
+ throw new Error("Dot has no link");
+ }
+ });
+ }
+ this.dotsGroup.appendChild(circle);
+ }
+ }
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522094716.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522094716.ts
new file mode 100644
index 0000000..e47ca10
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522094716.ts
@@ -0,0 +1,503 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+ private preloadedImages: Map = new Map();
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+ constructor(
+ containerId: string,
+ dots: DotConfig[],
+ config?: Partial
+ ) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 256,
+ tooltipHeight: 128,
+ ...config,
+ };
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+ // Create SVG elements
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ this.gridGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.curvePath = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ this.dotsGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.tooltipGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter((dot) => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map((dot) => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+ // Create a loading indicator (optional)
+ const loadingCount = { current: 0, total: imageUrls.length };
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log("All images preloaded successfully");
+ }
+ };
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+ // Set src to start loading
+ img.src = url;
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ }
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = "connected-dots-styles";
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement("style");
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.svg.style.overflow = "visible";
+ this.scrollContainer.appendChild(this.svg);
+ // Configure grid group
+ this.gridGroup.classList.add("grid");
+ this.svg.appendChild(this.gridGroup);
+ // Configure curve path
+ this.curvePath.setAttribute("fill", "none");
+ this.curvePath.setAttribute("stroke", "white");
+ this.curvePath.setAttribute("stroke-width", "2");
+ this.curvePath.setAttribute("stroke-linecap", "round");
+ this.curvePath.classList.add("curve-path");
+ this.svg.appendChild(this.curvePath);
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add("tooltips");
+ this.svg.appendChild(this.tooltipGroup);
+ }
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.8);
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+ private calculateBezierControlPoints(
+ dots: DotConfig[],
+ index: number
+ ): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+ return { x1, y1, x2, y2 };
+ }
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return "";
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(
+ this.dots[0].value
+ )}`;
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(
+ this.dots,
+ i
+ );
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+ return path;
+ }
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+ if (!this.config.showGrid) return;
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", "0");
+ line.setAttribute("y1", this.getDotY(value).toString());
+ line.setAttribute("x2", this.config.totalWidth.toString());
+ line.setAttribute("y2", this.getDotY(value).toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", "10");
+ text.setAttribute("y", (this.getDotY(value) + 4).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+ // Vertical grid lines
+ const numVertLines = Math.ceil(
+ this.config.totalWidth / this.config.xUnitSize
+ );
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", x.toString());
+ line.setAttribute("y1", "0");
+ line.setAttribute("x2", x.toString());
+ line.setAttribute("y2", this.config.height.toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ if (xValue !== 0) {
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", x.toString());
+ text.setAttribute("y", (this.config.height / 2 + 20).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.setAttribute("text-anchor", "middle");
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ tooltip.classList.add("dot-tooltip");
+ tooltip.setAttribute("data-dot-id", dot.id.toString());
+
+ // Calculate tooltip dimensions and position
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipHeight = this.config.tooltipHeight;
+ const tooltipX = x - tooltipWidth / 2;
+ let tooltipY = y - tooltipHeight - 20; // Positioned above the dot
+ tooltipY = Math.max(tooltipY, 10); // Ensure it doesn't go above the view
+
+ // Create background rectangle
+ const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ bg.setAttribute("x", tooltipX.toString());
+ bg.setAttribute("y", tooltipY.toString());
+ bg.setAttribute("width", tooltipWidth.toString());
+ bg.setAttribute("height", tooltipHeight.toString());
+ bg.setAttribute("rx", "5"); // Rounded corners
+ bg.classList.add("tooltip-background");
+ tooltip.appendChild(bg);
+
+ // Create foreignObject for the content
+
+ const contentContainer = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "foreignObject"
+ );
+ contentContainer.setAttribute("x", tooltipX.toString());
+ contentContainer.setAttribute("y", tooltipY.toString());
+ contentContainer.setAttribute("width", tooltipWidth.toString());
+ contentContainer.setAttribute("height", tooltipHeight.toString());
+
+ // Create a div to contain the content
+ const div = document.createElement("div");
+ div.classList.add("tooltip-content");
+
+ // Add image if available
+ if (dot.imageUrl) {
+ const img = document.createElement("img");
+ img.src = dot.imageUrl;
+ img.classList.add("tooltip-image");
+ div.appendChild(img);
+ }
+
+ // Add title if available
+ if (dot.title) {
+ const title = document.createElement("div");
+ title.textContent = dot.title;
+ title.classList.add("tooltip-title");
+ div.appendChild(title);
+ }
+
+ // Add description if available
+ if (dot.description) {
+ const desc = document.createElement("div");
+ desc.textContent = dot.description;
+ desc.classList.add("tooltip-description");
+ div.appendChild(desc);
+ }
+
+ contentContainer.appendChild(div);
+ tooltip.appendChild(contentContainer);
+
+ // Add arrow path if needed
+ const arrow = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ arrow.setAttribute(
+ "d",
+ `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${
+ tooltipY + tooltipHeight - 10
+ } L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`
+ );
+ arrow.classList.add("tooltip-arrow");
+ tooltip.appendChild(arrow);
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute("d", pathData);
+ }
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ return { leftmost, rightmost };
+ }
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+ const circle = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "circle"
+ );
+ circle.setAttribute("cx", x.toString());
+ circle.setAttribute("cy", y.toString());
+ circle.setAttribute("r", this.config.dotRadius.toString());
+ circle.setAttribute("fill", "white");
+ circle.setAttribute("data-dot-id", dot.id.toString());
+ circle.classList.add("dot");
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener("click", () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error("Dot has no link");
+ throw new Error("Dot has no link");
+ }
+ });
+ }
+ this.dotsGroup.appendChild(circle);
+ }
+ }
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522094721.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522094721.ts
new file mode 100644
index 0000000..2af5013
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522094721.ts
@@ -0,0 +1,503 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+ private preloadedImages: Map = new Map();
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+ constructor(
+ containerId: string,
+ dots: DotConfig[],
+ config?: Partial
+ ) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config,
+ };
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+ // Create SVG elements
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ this.gridGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.curvePath = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ this.dotsGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.tooltipGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter((dot) => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map((dot) => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+ // Create a loading indicator (optional)
+ const loadingCount = { current: 0, total: imageUrls.length };
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log("All images preloaded successfully");
+ }
+ };
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+ // Set src to start loading
+ img.src = url;
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ }
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = "connected-dots-styles";
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement("style");
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.svg.style.overflow = "visible";
+ this.scrollContainer.appendChild(this.svg);
+ // Configure grid group
+ this.gridGroup.classList.add("grid");
+ this.svg.appendChild(this.gridGroup);
+ // Configure curve path
+ this.curvePath.setAttribute("fill", "none");
+ this.curvePath.setAttribute("stroke", "white");
+ this.curvePath.setAttribute("stroke-width", "2");
+ this.curvePath.setAttribute("stroke-linecap", "round");
+ this.curvePath.classList.add("curve-path");
+ this.svg.appendChild(this.curvePath);
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add("tooltips");
+ this.svg.appendChild(this.tooltipGroup);
+ }
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.8);
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+ private calculateBezierControlPoints(
+ dots: DotConfig[],
+ index: number
+ ): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+ return { x1, y1, x2, y2 };
+ }
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return "";
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(
+ this.dots[0].value
+ )}`;
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(
+ this.dots,
+ i
+ );
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+ return path;
+ }
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+ if (!this.config.showGrid) return;
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", "0");
+ line.setAttribute("y1", this.getDotY(value).toString());
+ line.setAttribute("x2", this.config.totalWidth.toString());
+ line.setAttribute("y2", this.getDotY(value).toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", "10");
+ text.setAttribute("y", (this.getDotY(value) + 4).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+ // Vertical grid lines
+ const numVertLines = Math.ceil(
+ this.config.totalWidth / this.config.xUnitSize
+ );
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", x.toString());
+ line.setAttribute("y1", "0");
+ line.setAttribute("x2", x.toString());
+ line.setAttribute("y2", this.config.height.toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ if (xValue !== 0) {
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", x.toString());
+ text.setAttribute("y", (this.config.height / 2 + 20).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.setAttribute("text-anchor", "middle");
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ tooltip.classList.add("dot-tooltip");
+ tooltip.setAttribute("data-dot-id", dot.id.toString());
+
+ // Calculate tooltip dimensions and position
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipHeight = this.config.tooltipHeight;
+ const tooltipX = x - tooltipWidth / 2;
+ let tooltipY = y - tooltipHeight - 20; // Positioned above the dot
+ tooltipY = Math.max(tooltipY, 10); // Ensure it doesn't go above the view
+
+ // Create background rectangle
+ const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ bg.setAttribute("x", tooltipX.toString());
+ bg.setAttribute("y", tooltipY.toString());
+ bg.setAttribute("width", tooltipWidth.toString());
+ bg.setAttribute("height", tooltipHeight.toString());
+ bg.setAttribute("rx", "5"); // Rounded corners
+ bg.classList.add("tooltip-background");
+ tooltip.appendChild(bg);
+
+ // Create foreignObject for the content
+
+ const contentContainer = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "foreignObject"
+ );
+ contentContainer.setAttribute("x", tooltipX.toString());
+ contentContainer.setAttribute("y", tooltipY.toString());
+ contentContainer.setAttribute("width", tooltipWidth.toString());
+ contentContainer.setAttribute("height", tooltipHeight.toString());
+
+ // Create a div to contain the content
+ const div = document.createElement("div");
+ div.classList.add("tooltip-content");
+
+ // Add image if available
+ if (dot.imageUrl) {
+ const img = document.createElement("img");
+ img.src = dot.imageUrl;
+ img.classList.add("tooltip-image");
+ div.appendChild(img);
+ }
+
+ // Add title if available
+ if (dot.title) {
+ const title = document.createElement("div");
+ title.textContent = dot.title;
+ title.classList.add("tooltip-title");
+ div.appendChild(title);
+ }
+
+ // Add description if available
+ if (dot.description) {
+ const desc = document.createElement("div");
+ desc.textContent = dot.description;
+ desc.classList.add("tooltip-description");
+ div.appendChild(desc);
+ }
+
+ contentContainer.appendChild(div);
+ tooltip.appendChild(contentContainer);
+
+ // Add arrow path if needed
+ const arrow = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ arrow.setAttribute(
+ "d",
+ `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${
+ tooltipY + tooltipHeight - 10
+ } L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`
+ );
+ arrow.classList.add("tooltip-arrow");
+ tooltip.appendChild(arrow);
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute("d", pathData);
+ }
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ return { leftmost, rightmost };
+ }
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+ const circle = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "circle"
+ );
+ circle.setAttribute("cx", x.toString());
+ circle.setAttribute("cy", y.toString());
+ circle.setAttribute("r", this.config.dotRadius.toString());
+ circle.setAttribute("fill", "white");
+ circle.setAttribute("data-dot-id", dot.id.toString());
+ circle.classList.add("dot");
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener("click", () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error("Dot has no link");
+ throw new Error("Dot has no link");
+ }
+ });
+ }
+ this.dotsGroup.appendChild(circle);
+ }
+ }
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522094758.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522094758.ts
new file mode 100644
index 0000000..8617fec
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522094758.ts
@@ -0,0 +1,504 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+ private preloadedImages: Map = new Map();
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+ constructor(
+ containerId: string,
+ dots: DotConfig[],
+ config?: Partial
+ ) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config,
+ };
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+ // Create SVG elements
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ this.gridGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.curvePath = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ this.dotsGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.tooltipGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter((dot) => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map((dot) => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+ // Create a loading indicator (optional)
+ const loadingCount = { current: 0, total: imageUrls.length };
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log("All images preloaded successfully");
+ }
+ };
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+ // Set src to start loading
+ img.src = url;
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ }
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = "connected-dots-styles";
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement("style");
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.svg.style.overflow = "visible";
+ this.scrollContainer.appendChild(this.svg);
+ // Configure grid group
+ this.gridGroup.classList.add("grid");
+ this.svg.appendChild(this.gridGroup);
+ // Configure curve path
+ this.curvePath.setAttribute("fill", "none");
+ this.curvePath.setAttribute("stroke", "white");
+ this.curvePath.setAttribute("stroke-width", "2");
+ this.curvePath.setAttribute("stroke-linecap", "round");
+ this.curvePath.classList.add("curve-path");
+ this.svg.appendChild(this.curvePath);
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add("tooltips");
+ this.svg.appendChild(this.tooltipGroup);
+ }
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.8);
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+ private calculateBezierControlPoints(
+ dots: DotConfig[],
+ index: number
+ ): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+ return { x1, y1, x2, y2 };
+ }
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return "";
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(
+ this.dots[0].value
+ )}`;
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(
+ this.dots,
+ i
+ );
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+ return path;
+ }
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+ if (!this.config.showGrid) return;
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", "0");
+ line.setAttribute("y1", this.getDotY(value).toString());
+ line.setAttribute("x2", this.config.totalWidth.toString());
+ line.setAttribute("y2", this.getDotY(value).toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", "10");
+ text.setAttribute("y", (this.getDotY(value) + 4).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+ // Vertical grid lines
+ const numVertLines = Math.ceil(
+ this.config.totalWidth / this.config.xUnitSize
+ );
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", x.toString());
+ line.setAttribute("y1", "0");
+ line.setAttribute("x2", x.toString());
+ line.setAttribute("y2", this.config.height.toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ if (xValue !== 0) {
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", x.toString());
+ text.setAttribute("y", (this.config.height / 2 + 20).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.setAttribute("text-anchor", "middle");
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ tooltip.classList.add("dot-tooltip");
+ tooltip.setAttribute("data-dot-id", dot.id.toString());
+
+ // Calculate tooltip dimensions and position
+ const tooltipWidth = this.config.tooltipWidth;
+ // const tooltipHeight = this.config.tooltipHeight;
+ const tooltipHeight = (16 / 9) * tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ let tooltipY = y - tooltipHeight - 20; // Positioned above the dot
+ tooltipY = Math.max(tooltipY, 10); // Ensure it doesn't go above the view
+
+ // Create background rectangle
+ const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ bg.setAttribute("x", tooltipX.toString());
+ bg.setAttribute("y", tooltipY.toString());
+ bg.setAttribute("width", tooltipWidth.toString());
+ bg.setAttribute("height", tooltipHeight.toString());
+ bg.setAttribute("rx", "5"); // Rounded corners
+ bg.classList.add("tooltip-background");
+ tooltip.appendChild(bg);
+
+ // Create foreignObject for the content
+
+ const contentContainer = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "foreignObject"
+ );
+ contentContainer.setAttribute("x", tooltipX.toString());
+ contentContainer.setAttribute("y", tooltipY.toString());
+ contentContainer.setAttribute("width", tooltipWidth.toString());
+ contentContainer.setAttribute("height", tooltipHeight.toString());
+
+ // Create a div to contain the content
+ const div = document.createElement("div");
+ div.classList.add("tooltip-content");
+
+ // Add image if available
+ if (dot.imageUrl) {
+ const img = document.createElement("img");
+ img.src = dot.imageUrl;
+ img.classList.add("tooltip-image");
+ div.appendChild(img);
+ }
+
+ // Add title if available
+ if (dot.title) {
+ const title = document.createElement("div");
+ title.textContent = dot.title;
+ title.classList.add("tooltip-title");
+ div.appendChild(title);
+ }
+
+ // Add description if available
+ if (dot.description) {
+ const desc = document.createElement("div");
+ desc.textContent = dot.description;
+ desc.classList.add("tooltip-description");
+ div.appendChild(desc);
+ }
+
+ contentContainer.appendChild(div);
+ tooltip.appendChild(contentContainer);
+
+ // Add arrow path if needed
+ const arrow = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ arrow.setAttribute(
+ "d",
+ `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${
+ tooltipY + tooltipHeight - 10
+ } L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`
+ );
+ arrow.classList.add("tooltip-arrow");
+ tooltip.appendChild(arrow);
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute("d", pathData);
+ }
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ return { leftmost, rightmost };
+ }
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+ const circle = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "circle"
+ );
+ circle.setAttribute("cx", x.toString());
+ circle.setAttribute("cy", y.toString());
+ circle.setAttribute("r", this.config.dotRadius.toString());
+ circle.setAttribute("fill", "white");
+ circle.setAttribute("data-dot-id", dot.id.toString());
+ circle.classList.add("dot");
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener("click", () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error("Dot has no link");
+ throw new Error("Dot has no link");
+ }
+ });
+ }
+ this.dotsGroup.appendChild(circle);
+ }
+ }
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522094923.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522094923.ts
new file mode 100644
index 0000000..32f7715
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522094923.ts
@@ -0,0 +1,504 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+ private preloadedImages: Map = new Map();
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+ constructor(
+ containerId: string,
+ dots: DotConfig[],
+ config?: Partial
+ ) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 2000,
+ tooltipHeight: 128,
+ ...config,
+ };
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+ // Create SVG elements
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ this.gridGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.curvePath = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ this.dotsGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.tooltipGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter((dot) => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map((dot) => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+ // Create a loading indicator (optional)
+ const loadingCount = { current: 0, total: imageUrls.length };
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log("All images preloaded successfully");
+ }
+ };
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+ // Set src to start loading
+ img.src = url;
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ }
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = "connected-dots-styles";
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement("style");
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.svg.style.overflow = "visible";
+ this.scrollContainer.appendChild(this.svg);
+ // Configure grid group
+ this.gridGroup.classList.add("grid");
+ this.svg.appendChild(this.gridGroup);
+ // Configure curve path
+ this.curvePath.setAttribute("fill", "none");
+ this.curvePath.setAttribute("stroke", "white");
+ this.curvePath.setAttribute("stroke-width", "2");
+ this.curvePath.setAttribute("stroke-linecap", "round");
+ this.curvePath.classList.add("curve-path");
+ this.svg.appendChild(this.curvePath);
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add("tooltips");
+ this.svg.appendChild(this.tooltipGroup);
+ }
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.8);
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+ private calculateBezierControlPoints(
+ dots: DotConfig[],
+ index: number
+ ): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+ return { x1, y1, x2, y2 };
+ }
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return "";
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(
+ this.dots[0].value
+ )}`;
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(
+ this.dots,
+ i
+ );
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+ return path;
+ }
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+ if (!this.config.showGrid) return;
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", "0");
+ line.setAttribute("y1", this.getDotY(value).toString());
+ line.setAttribute("x2", this.config.totalWidth.toString());
+ line.setAttribute("y2", this.getDotY(value).toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", "10");
+ text.setAttribute("y", (this.getDotY(value) + 4).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+ // Vertical grid lines
+ const numVertLines = Math.ceil(
+ this.config.totalWidth / this.config.xUnitSize
+ );
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", x.toString());
+ line.setAttribute("y1", "0");
+ line.setAttribute("x2", x.toString());
+ line.setAttribute("y2", this.config.height.toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ if (xValue !== 0) {
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", x.toString());
+ text.setAttribute("y", (this.config.height / 2 + 20).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.setAttribute("text-anchor", "middle");
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ tooltip.classList.add("dot-tooltip");
+ tooltip.setAttribute("data-dot-id", dot.id.toString());
+
+ // Calculate tooltip dimensions and position
+ const tooltipWidth = this.config.tooltipWidth;
+ // const tooltipHeight = this.config.tooltipHeight;
+ const tooltipHeight = (16 / 9) * tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ let tooltipY = y - tooltipHeight - 20; // Positioned above the dot
+ tooltipY = Math.max(tooltipY, 10); // Ensure it doesn't go above the view
+
+ // Create background rectangle
+ const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ bg.setAttribute("x", tooltipX.toString());
+ bg.setAttribute("y", tooltipY.toString());
+ bg.setAttribute("width", tooltipWidth.toString());
+ bg.setAttribute("height", tooltipHeight.toString());
+ bg.setAttribute("rx", "5"); // Rounded corners
+ bg.classList.add("tooltip-background");
+ tooltip.appendChild(bg);
+
+ // Create foreignObject for the content
+
+ const contentContainer = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "foreignObject"
+ );
+ contentContainer.setAttribute("x", tooltipX.toString());
+ contentContainer.setAttribute("y", tooltipY.toString());
+ contentContainer.setAttribute("width", tooltipWidth.toString());
+ contentContainer.setAttribute("height", tooltipHeight.toString());
+
+ // Create a div to contain the content
+ const div = document.createElement("div");
+ div.classList.add("tooltip-content");
+
+ // Add image if available
+ if (dot.imageUrl) {
+ const img = document.createElement("img");
+ img.src = dot.imageUrl;
+ img.classList.add("tooltip-image");
+ div.appendChild(img);
+ }
+
+ // Add title if available
+ if (dot.title) {
+ const title = document.createElement("div");
+ title.textContent = dot.title;
+ title.classList.add("tooltip-title");
+ div.appendChild(title);
+ }
+
+ // Add description if available
+ if (dot.description) {
+ const desc = document.createElement("div");
+ desc.textContent = dot.description;
+ desc.classList.add("tooltip-description");
+ div.appendChild(desc);
+ }
+
+ contentContainer.appendChild(div);
+ tooltip.appendChild(contentContainer);
+
+ // Add arrow path if needed
+ const arrow = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ arrow.setAttribute(
+ "d",
+ `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${
+ tooltipY + tooltipHeight - 10
+ } L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`
+ );
+ arrow.classList.add("tooltip-arrow");
+ tooltip.appendChild(arrow);
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute("d", pathData);
+ }
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ return { leftmost, rightmost };
+ }
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+ const circle = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "circle"
+ );
+ circle.setAttribute("cx", x.toString());
+ circle.setAttribute("cy", y.toString());
+ circle.setAttribute("r", this.config.dotRadius.toString());
+ circle.setAttribute("fill", "white");
+ circle.setAttribute("data-dot-id", dot.id.toString());
+ circle.classList.add("dot");
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener("click", () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error("Dot has no link");
+ throw new Error("Dot has no link");
+ }
+ });
+ }
+ this.dotsGroup.appendChild(circle);
+ }
+ }
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522094927.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522094927.ts
new file mode 100644
index 0000000..8617fec
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522094927.ts
@@ -0,0 +1,504 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+ private preloadedImages: Map = new Map();
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+ constructor(
+ containerId: string,
+ dots: DotConfig[],
+ config?: Partial
+ ) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config,
+ };
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+ // Create SVG elements
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ this.gridGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.curvePath = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ this.dotsGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.tooltipGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter((dot) => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map((dot) => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+ // Create a loading indicator (optional)
+ const loadingCount = { current: 0, total: imageUrls.length };
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log("All images preloaded successfully");
+ }
+ };
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+ // Set src to start loading
+ img.src = url;
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ }
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = "connected-dots-styles";
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement("style");
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.svg.style.overflow = "visible";
+ this.scrollContainer.appendChild(this.svg);
+ // Configure grid group
+ this.gridGroup.classList.add("grid");
+ this.svg.appendChild(this.gridGroup);
+ // Configure curve path
+ this.curvePath.setAttribute("fill", "none");
+ this.curvePath.setAttribute("stroke", "white");
+ this.curvePath.setAttribute("stroke-width", "2");
+ this.curvePath.setAttribute("stroke-linecap", "round");
+ this.curvePath.classList.add("curve-path");
+ this.svg.appendChild(this.curvePath);
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add("tooltips");
+ this.svg.appendChild(this.tooltipGroup);
+ }
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.8);
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+ private calculateBezierControlPoints(
+ dots: DotConfig[],
+ index: number
+ ): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+ return { x1, y1, x2, y2 };
+ }
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return "";
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(
+ this.dots[0].value
+ )}`;
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(
+ this.dots,
+ i
+ );
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+ return path;
+ }
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+ if (!this.config.showGrid) return;
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", "0");
+ line.setAttribute("y1", this.getDotY(value).toString());
+ line.setAttribute("x2", this.config.totalWidth.toString());
+ line.setAttribute("y2", this.getDotY(value).toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", "10");
+ text.setAttribute("y", (this.getDotY(value) + 4).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+ // Vertical grid lines
+ const numVertLines = Math.ceil(
+ this.config.totalWidth / this.config.xUnitSize
+ );
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", x.toString());
+ line.setAttribute("y1", "0");
+ line.setAttribute("x2", x.toString());
+ line.setAttribute("y2", this.config.height.toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ if (xValue !== 0) {
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", x.toString());
+ text.setAttribute("y", (this.config.height / 2 + 20).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.setAttribute("text-anchor", "middle");
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ tooltip.classList.add("dot-tooltip");
+ tooltip.setAttribute("data-dot-id", dot.id.toString());
+
+ // Calculate tooltip dimensions and position
+ const tooltipWidth = this.config.tooltipWidth;
+ // const tooltipHeight = this.config.tooltipHeight;
+ const tooltipHeight = (16 / 9) * tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ let tooltipY = y - tooltipHeight - 20; // Positioned above the dot
+ tooltipY = Math.max(tooltipY, 10); // Ensure it doesn't go above the view
+
+ // Create background rectangle
+ const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ bg.setAttribute("x", tooltipX.toString());
+ bg.setAttribute("y", tooltipY.toString());
+ bg.setAttribute("width", tooltipWidth.toString());
+ bg.setAttribute("height", tooltipHeight.toString());
+ bg.setAttribute("rx", "5"); // Rounded corners
+ bg.classList.add("tooltip-background");
+ tooltip.appendChild(bg);
+
+ // Create foreignObject for the content
+
+ const contentContainer = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "foreignObject"
+ );
+ contentContainer.setAttribute("x", tooltipX.toString());
+ contentContainer.setAttribute("y", tooltipY.toString());
+ contentContainer.setAttribute("width", tooltipWidth.toString());
+ contentContainer.setAttribute("height", tooltipHeight.toString());
+
+ // Create a div to contain the content
+ const div = document.createElement("div");
+ div.classList.add("tooltip-content");
+
+ // Add image if available
+ if (dot.imageUrl) {
+ const img = document.createElement("img");
+ img.src = dot.imageUrl;
+ img.classList.add("tooltip-image");
+ div.appendChild(img);
+ }
+
+ // Add title if available
+ if (dot.title) {
+ const title = document.createElement("div");
+ title.textContent = dot.title;
+ title.classList.add("tooltip-title");
+ div.appendChild(title);
+ }
+
+ // Add description if available
+ if (dot.description) {
+ const desc = document.createElement("div");
+ desc.textContent = dot.description;
+ desc.classList.add("tooltip-description");
+ div.appendChild(desc);
+ }
+
+ contentContainer.appendChild(div);
+ tooltip.appendChild(contentContainer);
+
+ // Add arrow path if needed
+ const arrow = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ arrow.setAttribute(
+ "d",
+ `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${
+ tooltipY + tooltipHeight - 10
+ } L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`
+ );
+ arrow.classList.add("tooltip-arrow");
+ tooltip.appendChild(arrow);
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute("d", pathData);
+ }
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ return { leftmost, rightmost };
+ }
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+ const circle = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "circle"
+ );
+ circle.setAttribute("cx", x.toString());
+ circle.setAttribute("cy", y.toString());
+ circle.setAttribute("r", this.config.dotRadius.toString());
+ circle.setAttribute("fill", "white");
+ circle.setAttribute("data-dot-id", dot.id.toString());
+ circle.classList.add("dot");
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener("click", () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error("Dot has no link");
+ throw new Error("Dot has no link");
+ }
+ });
+ }
+ this.dotsGroup.appendChild(circle);
+ }
+ }
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522094944.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522094944.ts
new file mode 100644
index 0000000..b59444b
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522094944.ts
@@ -0,0 +1,505 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+ private preloadedImages: Map = new Map();
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+ constructor(
+ containerId: string,
+ dots: DotConfig[],
+ config?: Partial
+ ) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config,
+ };
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+ // Create SVG elements
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ this.gridGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.curvePath = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ this.dotsGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.tooltipGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter((dot) => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map((dot) => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+ // Create a loading indicator (optional)
+ const loadingCount = { current: 0, total: imageUrls.length };
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log("All images preloaded successfully");
+ }
+ };
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+ // Set src to start loading
+ img.src = url;
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ }
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = "connected-dots-styles";
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement("style");
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.svg.style.overflow = "visible";
+ this.scrollContainer.appendChild(this.svg);
+ // Configure grid group
+ this.gridGroup.classList.add("grid");
+ this.svg.appendChild(this.gridGroup);
+ // Configure curve path
+ this.curvePath.setAttribute("fill", "none");
+ this.curvePath.setAttribute("stroke", "white");
+ this.curvePath.setAttribute("stroke-width", "2");
+ this.curvePath.setAttribute("stroke-linecap", "round");
+ this.curvePath.classList.add("curve-path");
+ this.svg.appendChild(this.curvePath);
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add("tooltips");
+ this.svg.appendChild(this.tooltipGroup);
+ }
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.8);
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+ private calculateBezierControlPoints(
+ dots: DotConfig[],
+ index: number
+ ): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+ return { x1, y1, x2, y2 };
+ }
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return "";
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(
+ this.dots[0].value
+ )}`;
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(
+ this.dots,
+ i
+ );
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+ return path;
+ }
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+ if (!this.config.showGrid) return;
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", "0");
+ line.setAttribute("y1", this.getDotY(value).toString());
+ line.setAttribute("x2", this.config.totalWidth.toString());
+ line.setAttribute("y2", this.getDotY(value).toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", "10");
+ text.setAttribute("y", (this.getDotY(value) + 4).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+ // Vertical grid lines
+ const numVertLines = Math.ceil(
+ this.config.totalWidth / this.config.xUnitSize
+ );
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", x.toString());
+ line.setAttribute("y1", "0");
+ line.setAttribute("x2", x.toString());
+ line.setAttribute("y2", this.config.height.toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ if (xValue !== 0) {
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", x.toString());
+ text.setAttribute("y", (this.config.height / 2 + 20).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.setAttribute("text-anchor", "middle");
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ tooltip.classList.add("dot-tooltip");
+ tooltip.setAttribute("data-dot-id", dot.id.toString());
+
+ // Calculate tooltip dimensions and position
+ // const tooltipWidth = this.config.tooltipWidth;
+ const tooltipWidth = 128; // Base width for your tooltip
+ // const tooltipHeight = this.config.tooltipHeight;
+ const tooltipHeight = (16 / 9) * tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ let tooltipY = y - tooltipHeight - 20; // Positioned above the dot
+ tooltipY = Math.max(tooltipY, 10); // Ensure it doesn't go above the view
+
+ // Create background rectangle
+ const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ bg.setAttribute("x", tooltipX.toString());
+ bg.setAttribute("y", tooltipY.toString());
+ bg.setAttribute("width", tooltipWidth.toString());
+ bg.setAttribute("height", tooltipHeight.toString());
+ bg.setAttribute("rx", "5"); // Rounded corners
+ bg.classList.add("tooltip-background");
+ tooltip.appendChild(bg);
+
+ // Create foreignObject for the content
+
+ const contentContainer = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "foreignObject"
+ );
+ contentContainer.setAttribute("x", tooltipX.toString());
+ contentContainer.setAttribute("y", tooltipY.toString());
+ contentContainer.setAttribute("width", tooltipWidth.toString());
+ contentContainer.setAttribute("height", tooltipHeight.toString());
+
+ // Create a div to contain the content
+ const div = document.createElement("div");
+ div.classList.add("tooltip-content");
+
+ // Add image if available
+ if (dot.imageUrl) {
+ const img = document.createElement("img");
+ img.src = dot.imageUrl;
+ img.classList.add("tooltip-image");
+ div.appendChild(img);
+ }
+
+ // Add title if available
+ if (dot.title) {
+ const title = document.createElement("div");
+ title.textContent = dot.title;
+ title.classList.add("tooltip-title");
+ div.appendChild(title);
+ }
+
+ // Add description if available
+ if (dot.description) {
+ const desc = document.createElement("div");
+ desc.textContent = dot.description;
+ desc.classList.add("tooltip-description");
+ div.appendChild(desc);
+ }
+
+ contentContainer.appendChild(div);
+ tooltip.appendChild(contentContainer);
+
+ // Add arrow path if needed
+ const arrow = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ arrow.setAttribute(
+ "d",
+ `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${
+ tooltipY + tooltipHeight - 10
+ } L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`
+ );
+ arrow.classList.add("tooltip-arrow");
+ tooltip.appendChild(arrow);
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute("d", pathData);
+ }
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ return { leftmost, rightmost };
+ }
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+ const circle = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "circle"
+ );
+ circle.setAttribute("cx", x.toString());
+ circle.setAttribute("cy", y.toString());
+ circle.setAttribute("r", this.config.dotRadius.toString());
+ circle.setAttribute("fill", "white");
+ circle.setAttribute("data-dot-id", dot.id.toString());
+ circle.classList.add("dot");
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener("click", () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error("Dot has no link");
+ throw new Error("Dot has no link");
+ }
+ });
+ }
+ this.dotsGroup.appendChild(circle);
+ }
+ }
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522095334.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522095334.ts
new file mode 100644
index 0000000..39b6cfa
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522095334.ts
@@ -0,0 +1,503 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+ private preloadedImages: Map = new Map();
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+ constructor(
+ containerId: string,
+ dots: DotConfig[],
+ config?: Partial
+ ) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config,
+ };
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+ // Create SVG elements
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ this.gridGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.curvePath = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ this.dotsGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.tooltipGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter((dot) => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map((dot) => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+ // Create a loading indicator (optional)
+ const loadingCount = { current: 0, total: imageUrls.length };
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log("All images preloaded successfully");
+ }
+ };
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+ // Set src to start loading
+ img.src = url;
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ }
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = "connected-dots-styles";
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement("style");
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.svg.style.overflow = "visible";
+ this.scrollContainer.appendChild(this.svg);
+ // Configure grid group
+ this.gridGroup.classList.add("grid");
+ this.svg.appendChild(this.gridGroup);
+ // Configure curve path
+ this.curvePath.setAttribute("fill", "none");
+ this.curvePath.setAttribute("stroke", "white");
+ this.curvePath.setAttribute("stroke-width", "2");
+ this.curvePath.setAttribute("stroke-linecap", "round");
+ this.curvePath.classList.add("curve-path");
+ this.svg.appendChild(this.curvePath);
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add("tooltips");
+ this.svg.appendChild(this.tooltipGroup);
+ }
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.8);
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+ private calculateBezierControlPoints(
+ dots: DotConfig[],
+ index: number
+ ): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+ return { x1, y1, x2, y2 };
+ }
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return "";
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(
+ this.dots[0].value
+ )}`;
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(
+ this.dots,
+ i
+ );
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+ return path;
+ }
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+ if (!this.config.showGrid) return;
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", "0");
+ line.setAttribute("y1", this.getDotY(value).toString());
+ line.setAttribute("x2", this.config.totalWidth.toString());
+ line.setAttribute("y2", this.getDotY(value).toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", "10");
+ text.setAttribute("y", (this.getDotY(value) + 4).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+ // Vertical grid lines
+ const numVertLines = Math.ceil(
+ this.config.totalWidth / this.config.xUnitSize
+ );
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", x.toString());
+ line.setAttribute("y1", "0");
+ line.setAttribute("x2", x.toString());
+ line.setAttribute("y2", this.config.height.toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ if (xValue !== 0) {
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", x.toString());
+ text.setAttribute("y", (this.config.height / 2 + 20).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.setAttribute("text-anchor", "middle");
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ tooltip.classList.add("dot-tooltip");
+ tooltip.setAttribute("data-dot-id", dot.id.toString());
+
+ // Calculate tooltip dimensions and position
+ const tooltipWidth = 128; // Base width for your tooltip
+ const tooltipHeight = (16 / 9) * tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ let tooltipY = y - tooltipHeight - 20; // Positioned above the dot
+ tooltipY = Math.max(tooltipY, 10); // Ensure it doesn't go above the view
+
+ // Create background rectangle
+ const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ bg.setAttribute("x", tooltipX.toString());
+ bg.setAttribute("y", tooltipY.toString());
+ bg.setAttribute("width", tooltipWidth.toString());
+ bg.setAttribute("height", tooltipHeight.toString());
+ bg.setAttribute("rx", "5"); // Rounded corners
+ bg.classList.add("tooltip-background");
+ tooltip.appendChild(bg);
+
+ // Create foreignObject for the content
+
+ const contentContainer = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "foreignObject"
+ );
+ contentContainer.setAttribute("x", tooltipX.toString());
+ contentContainer.setAttribute("y", tooltipY.toString());
+ contentContainer.setAttribute("width", tooltipWidth.toString());
+ contentContainer.setAttribute("height", tooltipHeight.toString());
+
+ // Create a div to contain the content
+ const div = document.createElement("div");
+ div.classList.add("tooltip-content");
+
+ // Add image if available
+ if (dot.imageUrl) {
+ const img = document.createElement("img");
+ img.src = dot.imageUrl;
+ img.classList.add("tooltip-image");
+ div.appendChild(img);
+ }
+
+ // Add title if available
+ if (dot.title) {
+ const title = document.createElement("div");
+ title.textContent = dot.title;
+ title.classList.add("tooltip-title");
+ div.appendChild(title);
+ }
+
+ // Add description if available
+ if (dot.description) {
+ const desc = document.createElement("div");
+ desc.textContent = dot.description;
+ desc.classList.add("tooltip-description");
+ div.appendChild(desc);
+ }
+
+ contentContainer.appendChild(div);
+ tooltip.appendChild(contentContainer);
+
+ // Add arrow path if needed
+ const arrow = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ arrow.setAttribute(
+ "d",
+ `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${
+ tooltipY + tooltipHeight - 10
+ } L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`
+ );
+ arrow.classList.add("tooltip-arrow");
+ tooltip.appendChild(arrow);
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute("d", pathData);
+ }
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ return { leftmost, rightmost };
+ }
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+ const circle = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "circle"
+ );
+ circle.setAttribute("cx", x.toString());
+ circle.setAttribute("cy", y.toString());
+ circle.setAttribute("r", this.config.dotRadius.toString());
+ circle.setAttribute("fill", "white");
+ circle.setAttribute("data-dot-id", dot.id.toString());
+ circle.classList.add("dot");
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener("click", () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error("Dot has no link");
+ throw new Error("Dot has no link");
+ }
+ });
+ }
+ this.dotsGroup.appendChild(circle);
+ }
+ }
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522095400.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522095400.ts
new file mode 100644
index 0000000..909803b
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522095400.ts
@@ -0,0 +1,505 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+ private preloadedImages: Map = new Map();
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+ constructor(
+ containerId: string,
+ dots: DotConfig[],
+ config?: Partial
+ ) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config,
+ };
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+ // Create SVG elements
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ this.gridGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.curvePath = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ this.dotsGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.tooltipGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter((dot) => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map((dot) => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+ // Create a loading indicator (optional)
+ const loadingCount = { current: 0, total: imageUrls.length };
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log("All images preloaded successfully");
+ }
+ };
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+ // Set src to start loading
+ img.src = url;
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ }
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = "connected-dots-styles";
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement("style");
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.svg.style.overflow = "visible";
+ this.scrollContainer.appendChild(this.svg);
+ // Configure grid group
+ this.gridGroup.classList.add("grid");
+ this.svg.appendChild(this.gridGroup);
+ // Configure curve path
+ this.curvePath.setAttribute("fill", "none");
+ this.curvePath.setAttribute("stroke", "white");
+ this.curvePath.setAttribute("stroke-width", "2");
+ this.curvePath.setAttribute("stroke-linecap", "round");
+ this.curvePath.classList.add("curve-path");
+ this.svg.appendChild(this.curvePath);
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add("tooltips");
+ this.svg.appendChild(this.tooltipGroup);
+ }
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.8);
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+ private calculateBezierControlPoints(
+ dots: DotConfig[],
+ index: number
+ ): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+ return { x1, y1, x2, y2 };
+ }
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return "";
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(
+ this.dots[0].value
+ )}`;
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(
+ this.dots,
+ i
+ );
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+ return path;
+ }
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+ if (!this.config.showGrid) return;
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", "0");
+ line.setAttribute("y1", this.getDotY(value).toString());
+ line.setAttribute("x2", this.config.totalWidth.toString());
+ line.setAttribute("y2", this.getDotY(value).toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", "10");
+ text.setAttribute("y", (this.getDotY(value) + 4).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+ // Vertical grid lines
+ const numVertLines = Math.ceil(
+ this.config.totalWidth / this.config.xUnitSize
+ );
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", x.toString());
+ line.setAttribute("y1", "0");
+ line.setAttribute("x2", x.toString());
+ line.setAttribute("y2", this.config.height.toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ if (xValue !== 0) {
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", x.toString());
+ text.setAttribute("y", (this.config.height / 2 + 20).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.setAttribute("text-anchor", "middle");
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ tooltip.classList.add("dot-tooltip");
+ tooltip.setAttribute("data-dot-id", dot.id.toString());
+
+ // Calculate tooltip dimensions and position
+ const tooltipWidth = 128; // Base width for your tooltip
+ const tooltipHeight = (16 / 9) * tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ let tooltipY = y - tooltipHeight - 20; // Positioned above the dot
+ tooltipY = Math.max(tooltipY, 10); // Ensure it doesn't go above the view
+
+ // Create background rectangle
+ const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ bg.setAttribute("x", tooltipX.toString());
+ bg.setAttribute("y", tooltipY.toString());
+ bg.setAttribute("width", tooltipWidth.toString());
+ bg.setAttribute("height", tooltipHeight.toString());
+ bg.setAttribute("rx", "5"); // Rounded corners
+ bg.classList.add("tooltip-background");
+ tooltip.appendChild(bg);
+
+ // Create foreignObject for the content
+
+ const contentContainer = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "foreignObject"
+ );
+ contentContainer.setAttribute("x", tooltipX.toString());
+ contentContainer.setAttribute("y", tooltipY.toString());
+ contentContainer.setAttribute("width", tooltipWidth.toString());
+ contentContainer.setAttribute("height", tooltipHeight.toString());
+
+ // Create a div to contain the content
+ const div = document.createElement("div");
+ div.classList.add("tooltip-content");
+
+
+ // Add description if available
+ if (dot.description) {
+ const desc = document.createElement("div");
+ desc.textContent = dot.description;
+ desc.classList.add("tooltip-description");
+ div.appendChild(desc);
+ }
+
+
+ // Add image if available
+ if (dot.imageUrl) {
+ const img = document.createElement("img");
+ img.src = dot.imageUrl;
+ img.classList.add("tooltip-image");
+ div.appendChild(img);
+ }
+
+ // Add title if available
+ if (dot.title) {
+ const title = document.createElement("div");
+ title.textContent = dot.title;
+ title.classList.add("tooltip-title");
+ div.appendChild(title);
+ }
+
+ contentContainer.appendChild(div);
+ tooltip.appendChild(contentContainer);
+
+ // Add arrow path if needed
+ const arrow = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ arrow.setAttribute(
+ "d",
+ `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${
+ tooltipY + tooltipHeight - 10
+ } L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`
+ );
+ arrow.classList.add("tooltip-arrow");
+ tooltip.appendChild(arrow);
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute("d", pathData);
+ }
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ return { leftmost, rightmost };
+ }
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+ const circle = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "circle"
+ );
+ circle.setAttribute("cx", x.toString());
+ circle.setAttribute("cy", y.toString());
+ circle.setAttribute("r", this.config.dotRadius.toString());
+ circle.setAttribute("fill", "white");
+ circle.setAttribute("data-dot-id", dot.id.toString());
+ circle.classList.add("dot");
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener("click", () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error("Dot has no link");
+ throw new Error("Dot has no link");
+ }
+ });
+ }
+ this.dotsGroup.appendChild(circle);
+ }
+ }
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522095543.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522095543.ts
new file mode 100644
index 0000000..17a9f48
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522095543.ts
@@ -0,0 +1,505 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+ private preloadedImages: Map = new Map();
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+ constructor(
+ containerId: string,
+ dots: DotConfig[],
+ config?: Partial
+ ) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config,
+ };
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+ // Create SVG elements
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ this.gridGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.curvePath = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ this.dotsGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.tooltipGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter((dot) => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map((dot) => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+ // Create a loading indicator (optional)
+ const loadingCount = { current: 0, total: imageUrls.length };
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log("All images preloaded successfully");
+ }
+ };
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+ // Set src to start loading
+ img.src = url;
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ }
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = "connected-dots-styles";
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement("style");
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.svg.style.overflow = "visible";
+ this.scrollContainer.appendChild(this.svg);
+ // Configure grid group
+ this.gridGroup.classList.add("grid");
+ this.svg.appendChild(this.gridGroup);
+ // Configure curve path
+ this.curvePath.setAttribute("fill", "none");
+ this.curvePath.setAttribute("stroke", "white");
+ this.curvePath.setAttribute("stroke-width", "2");
+ this.curvePath.setAttribute("stroke-linecap", "round");
+ this.curvePath.classList.add("curve-path");
+ this.svg.appendChild(this.curvePath);
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add("tooltips");
+ this.svg.appendChild(this.tooltipGroup);
+ }
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.8);
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+ private calculateBezierControlPoints(
+ dots: DotConfig[],
+ index: number
+ ): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+ return { x1, y1, x2, y2 };
+ }
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return "";
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(
+ this.dots[0].value
+ )}`;
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(
+ this.dots,
+ i
+ );
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+ return path;
+ }
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+ if (!this.config.showGrid) return;
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", "0");
+ line.setAttribute("y1", this.getDotY(value).toString());
+ line.setAttribute("x2", this.config.totalWidth.toString());
+ line.setAttribute("y2", this.getDotY(value).toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", "10");
+ text.setAttribute("y", (this.getDotY(value) + 4).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+ // Vertical grid lines
+ const numVertLines = Math.ceil(
+ this.config.totalWidth / this.config.xUnitSize
+ );
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", x.toString());
+ line.setAttribute("y1", "0");
+ line.setAttribute("x2", x.toString());
+ line.setAttribute("y2", this.config.height.toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ if (xValue !== 0) {
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", x.toString());
+ text.setAttribute("y", (this.config.height / 2 + 20).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.setAttribute("text-anchor", "middle");
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ tooltip.classList.add("dot-tooltip");
+ tooltip.setAttribute("data-dot-id", dot.id.toString());
+
+ // Calculate tooltip dimensions and position
+ const tooltipWidth = 128; // Base width for your tooltip
+ const tooltipHeight = (3 / 2) * tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ let tooltipY = y - tooltipHeight - 20; // Positioned above the dot
+ tooltipY = Math.max(tooltipY, 10); // Ensure it doesn't go above the view
+
+ // Create background rectangle
+ const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ bg.setAttribute("x", tooltipX.toString());
+ bg.setAttribute("y", tooltipY.toString());
+ bg.setAttribute("width", tooltipWidth.toString());
+ bg.setAttribute("height", tooltipHeight.toString());
+ bg.setAttribute("rx", "5"); // Rounded corners
+ bg.classList.add("tooltip-background");
+ tooltip.appendChild(bg);
+
+ // Create foreignObject for the content
+
+ const contentContainer = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "foreignObject"
+ );
+ contentContainer.setAttribute("x", tooltipX.toString());
+ contentContainer.setAttribute("y", tooltipY.toString());
+ contentContainer.setAttribute("width", tooltipWidth.toString());
+ contentContainer.setAttribute("height", tooltipHeight.toString());
+
+ // Create a div to contain the content
+ const div = document.createElement("div");
+ div.classList.add("tooltip-content");
+
+
+ // Add description if available
+ if (dot.description) {
+ const desc = document.createElement("div");
+ desc.textContent = dot.description;
+ desc.classList.add("tooltip-description");
+ div.appendChild(desc);
+ }
+
+
+ // Add image if available
+ if (dot.imageUrl) {
+ const img = document.createElement("img");
+ img.src = dot.imageUrl;
+ img.classList.add("tooltip-image");
+ div.appendChild(img);
+ }
+
+ // Add title if available
+ if (dot.title) {
+ const title = document.createElement("div");
+ title.textContent = dot.title;
+ title.classList.add("tooltip-title");
+ div.appendChild(title);
+ }
+
+ contentContainer.appendChild(div);
+ tooltip.appendChild(contentContainer);
+
+ // Add arrow path if needed
+ const arrow = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ arrow.setAttribute(
+ "d",
+ `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${
+ tooltipY + tooltipHeight - 10
+ } L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`
+ );
+ arrow.classList.add("tooltip-arrow");
+ tooltip.appendChild(arrow);
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute("d", pathData);
+ }
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ return { leftmost, rightmost };
+ }
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+ const circle = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "circle"
+ );
+ circle.setAttribute("cx", x.toString());
+ circle.setAttribute("cy", y.toString());
+ circle.setAttribute("r", this.config.dotRadius.toString());
+ circle.setAttribute("fill", "white");
+ circle.setAttribute("data-dot-id", dot.id.toString());
+ circle.classList.add("dot");
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener("click", () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error("Dot has no link");
+ throw new Error("Dot has no link");
+ }
+ });
+ }
+ this.dotsGroup.appendChild(circle);
+ }
+ }
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522101227.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522101227.ts
new file mode 100644
index 0000000..c6917bc
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522101227.ts
@@ -0,0 +1,516 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+ private preloadedImages: Map = new Map();
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+ constructor(
+ containerId: string,
+ dots: DotConfig[],
+ config?: Partial
+ ) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config,
+ };
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+ // Create SVG elements
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ this.gridGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.curvePath = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ this.dotsGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.tooltipGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter((dot) => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map((dot) => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+ // Create a loading indicator (optional)
+ const loadingCount = { current: 0, total: imageUrls.length };
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log("All images preloaded successfully");
+ }
+ };
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+ // Set src to start loading
+ img.src = url;
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ }
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = "connected-dots-styles";
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement("style");
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.svg.style.overflow = "visible";
+ this.scrollContainer.appendChild(this.svg);
+ // Configure grid group
+ this.gridGroup.classList.add("grid");
+ this.svg.appendChild(this.gridGroup);
+ // Configure curve path
+ this.curvePath.setAttribute("fill", "none");
+ this.curvePath.setAttribute("stroke", "white");
+ this.curvePath.setAttribute("stroke-width", "2");
+ this.curvePath.setAttribute("stroke-linecap", "round");
+ this.curvePath.classList.add("curve-path");
+ this.svg.appendChild(this.curvePath);
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add("tooltips");
+ this.svg.appendChild(this.tooltipGroup);
+ }
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.8);
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+ private calculateBezierControlPoints(
+ dots: DotConfig[],
+ index: number
+ ): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+ return { x1, y1, x2, y2 };
+ }
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return "";
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(
+ this.dots[0].value
+ )}`;
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(
+ this.dots,
+ i
+ );
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+ return path;
+ }
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+ if (!this.config.showGrid) return;
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", "0");
+ line.setAttribute("y1", this.getDotY(value).toString());
+ line.setAttribute("x2", this.config.totalWidth.toString());
+ line.setAttribute("y2", this.getDotY(value).toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", "10");
+ text.setAttribute("y", (this.getDotY(value) + 4).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+ // Vertical grid lines
+ const numVertLines = Math.ceil(
+ this.config.totalWidth / this.config.xUnitSize
+ );
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", x.toString());
+ line.setAttribute("y1", "0");
+ line.setAttribute("x2", x.toString());
+ line.setAttribute("y2", this.config.height.toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ if (xValue !== 0) {
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", x.toString());
+ text.setAttribute("y", (this.config.height / 2 + 20).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.setAttribute("text-anchor", "middle");
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ tooltip.classList.add("dot-tooltip");
+ tooltip.setAttribute("data-dot-id", dot.id.toString());
+
+ // Calculate tooltip dimensions and position
+ const tooltipWidth = 128; // Base width for your tooltip
+ const tooltipHeight = (3 / 2) * tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ let tooltipY = y - tooltipHeight - 20; // Positioned above the dot
+ tooltipY = Math.max(tooltipY, 10); // Ensure it doesn't go above the view
+
+ // Create background rectangle
+ const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ bg.setAttribute("x", tooltipX.toString());
+ bg.setAttribute("y", tooltipY.toString());
+ bg.setAttribute("width", tooltipWidth.toString());
+ bg.setAttribute("height", tooltipHeight.toString());
+ bg.setAttribute("rx", "5"); // Rounded corners
+ bg.classList.add("tooltip-background");
+ tooltip.appendChild(bg);
+
+ // Create foreignObject for the content
+
+ const contentContainer = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "foreignObject"
+ );
+ contentContainer.setAttribute("x", tooltipX.toString());
+ contentContainer.setAttribute("y", tooltipY.toString());
+ contentContainer.setAttribute("width", tooltipWidth.toString());
+ contentContainer.setAttribute("height", tooltipHeight.toString());
+
+ // Create a div to contain the content
+ const div = document.createElement("div");
+ div.classList.add("tooltip-content");
+
+
+ // Add description if available
+ if (dot.description) {
+ const desc = document.createElement("div");
+ desc.textContent = dot.description;
+ desc.classList.add("tooltip-description");
+ div.appendChild(desc);
+ }
+
+
+// Add image if available
+if (dot.imageUrl) {
+ // Create a container div
+ const imageContainer = document.createElement("div");
+ imageContainer.classList.add("image_container"); // Add image_container class
+
+ // Create the image element
+ const img = document.createElement("img");
+ img.src = dot.imageUrl;
+ img.classList.add("tooltip-image");
+
+ // Append image to the container
+ imageContainer.appendChild(img);
+
+ // Append the image container to the main div
+ div.appendChild(imageContainer);
+}
+
+
+ // Add title if available
+ if (dot.title) {
+ const title = document.createElement("div");
+ title.textContent = dot.title;
+ title.classList.add("tooltip-title");
+ div.appendChild(title);
+ }
+
+ contentContainer.appendChild(div);
+ tooltip.appendChild(contentContainer);
+
+ // Add arrow path if needed
+ const arrow = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ arrow.setAttribute(
+ "d",
+ `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${
+ tooltipY + tooltipHeight - 10
+ } L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`
+ );
+ arrow.classList.add("tooltip-arrow");
+ tooltip.appendChild(arrow);
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute("d", pathData);
+ }
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ return { leftmost, rightmost };
+ }
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+ const circle = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "circle"
+ );
+ circle.setAttribute("cx", x.toString());
+ circle.setAttribute("cy", y.toString());
+ circle.setAttribute("r", this.config.dotRadius.toString());
+ circle.setAttribute("fill", "white");
+ circle.setAttribute("data-dot-id", dot.id.toString());
+ circle.classList.add("dot");
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener("click", () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error("Dot has no link");
+ throw new Error("Dot has no link");
+ }
+ });
+ }
+ this.dotsGroup.appendChild(circle);
+ }
+ }
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522102503.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522102503.ts
new file mode 100644
index 0000000..b7cb7fa
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522102503.ts
@@ -0,0 +1,513 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+ private preloadedImages: Map = new Map();
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+ constructor(
+ containerId: string,
+ dots: DotConfig[],
+ config?: Partial
+ ) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config,
+ };
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+ // Create SVG elements
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ this.gridGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.curvePath = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ this.dotsGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.tooltipGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter((dot) => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map((dot) => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+ // Create a loading indicator (optional)
+ const loadingCount = { current: 0, total: imageUrls.length };
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log("All images preloaded successfully");
+ }
+ };
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+ // Set src to start loading
+ img.src = url;
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ }
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = "connected-dots-styles";
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement("style");
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.svg.style.overflow = "visible";
+ this.scrollContainer.appendChild(this.svg);
+ // Configure grid group
+ this.gridGroup.classList.add("grid");
+ this.svg.appendChild(this.gridGroup);
+ // Configure curve path
+ this.curvePath.setAttribute("fill", "none");
+ this.curvePath.setAttribute("stroke", "white");
+ this.curvePath.setAttribute("stroke-width", "2");
+ this.curvePath.setAttribute("stroke-linecap", "round");
+ this.curvePath.classList.add("curve-path");
+ this.svg.appendChild(this.curvePath);
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add("tooltips");
+ this.svg.appendChild(this.tooltipGroup);
+ }
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.8);
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+ private calculateBezierControlPoints(
+ dots: DotConfig[],
+ index: number
+ ): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+ return { x1, y1, x2, y2 };
+ }
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return "";
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(
+ this.dots[0].value
+ )}`;
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(
+ this.dots,
+ i
+ );
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+ return path;
+ }
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+ if (!this.config.showGrid) return;
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", "0");
+ line.setAttribute("y1", this.getDotY(value).toString());
+ line.setAttribute("x2", this.config.totalWidth.toString());
+ line.setAttribute("y2", this.getDotY(value).toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", "10");
+ text.setAttribute("y", (this.getDotY(value) + 4).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+ // Vertical grid lines
+ const numVertLines = Math.ceil(
+ this.config.totalWidth / this.config.xUnitSize
+ );
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", x.toString());
+ line.setAttribute("y1", "0");
+ line.setAttribute("x2", x.toString());
+ line.setAttribute("y2", this.config.height.toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ if (xValue !== 0) {
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", x.toString());
+ text.setAttribute("y", (this.config.height / 2 + 20).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.setAttribute("text-anchor", "middle");
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ tooltip.classList.add("dot-tooltip");
+ tooltip.setAttribute("data-dot-id", dot.id.toString());
+
+ // Calculate tooltip dimensions and position
+ const tooltipWidth = 128; // Base width for your tooltip
+ const tooltipHeight = (2 / 1) * tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ let tooltipY = y - tooltipHeight - 20; // Positioned above the dot
+ tooltipY = Math.max(tooltipY, 10); // Ensure it doesn't go above the view
+
+ // Create background rectangle
+ const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ bg.setAttribute("x", tooltipX.toString());
+ bg.setAttribute("y", tooltipY.toString());
+ bg.setAttribute("width", tooltipWidth.toString());
+ bg.setAttribute("height", tooltipHeight.toString());
+ bg.setAttribute("rx", "5"); // Rounded corners
+ bg.classList.add("tooltip-background");
+ tooltip.appendChild(bg);
+
+ // Create foreignObject for the content
+
+ const contentContainer = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "foreignObject"
+ );
+ contentContainer.setAttribute("x", tooltipX.toString());
+ contentContainer.setAttribute("y", tooltipY.toString());
+ contentContainer.setAttribute("width", tooltipWidth.toString());
+ contentContainer.setAttribute("height", tooltipHeight.toString());
+
+ // Create a div to contain the content
+ const div = document.createElement("div");
+ div.classList.add("tooltip-content");
+
+ // Add description if available
+ if (dot.description) {
+ const desc = document.createElement("div");
+ desc.textContent = dot.description;
+ desc.classList.add("tooltip-description");
+ div.appendChild(desc);
+ }
+
+ // Add image if available
+ if (dot.imageUrl) {
+ // Create a container div
+ const imageContainer = document.createElement("div");
+ imageContainer.classList.add("image_container"); // Add image_container class
+
+ // Create the image element
+ const img = document.createElement("img");
+ img.src = dot.imageUrl;
+ img.classList.add("tooltip-image");
+
+ // Append image to the container
+ imageContainer.appendChild(img);
+
+ // Append the image container to the main div
+ div.appendChild(imageContainer);
+ }
+
+ // Add title if available
+ if (dot.title) {
+ const title = document.createElement("div");
+ title.textContent = dot.title;
+ title.classList.add("tooltip-title");
+ div.appendChild(title);
+ }
+
+ contentContainer.appendChild(div);
+ tooltip.appendChild(contentContainer);
+
+ // Add arrow path if needed
+ const arrow = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ arrow.setAttribute(
+ "d",
+ `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${
+ tooltipY + tooltipHeight - 10
+ } L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`
+ );
+ arrow.classList.add("tooltip-arrow");
+ tooltip.appendChild(arrow);
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute("d", pathData);
+ }
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ return { leftmost, rightmost };
+ }
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+ const circle = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "circle"
+ );
+ circle.setAttribute("cx", x.toString());
+ circle.setAttribute("cy", y.toString());
+ circle.setAttribute("r", this.config.dotRadius.toString());
+ circle.setAttribute("fill", "white");
+ circle.setAttribute("data-dot-id", dot.id.toString());
+ circle.classList.add("dot");
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener("click", () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error("Dot has no link");
+ throw new Error("Dot has no link");
+ }
+ });
+ }
+ this.dotsGroup.appendChild(circle);
+ }
+ }
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522102521.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522102521.ts
new file mode 100644
index 0000000..4da8300
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522102521.ts
@@ -0,0 +1,513 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+ private preloadedImages: Map = new Map();
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+ constructor(
+ containerId: string,
+ dots: DotConfig[],
+ config?: Partial
+ ) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config,
+ };
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+ // Create SVG elements
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ this.gridGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.curvePath = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ this.dotsGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.tooltipGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter((dot) => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map((dot) => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+ // Create a loading indicator (optional)
+ const loadingCount = { current: 0, total: imageUrls.length };
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log("All images preloaded successfully");
+ }
+ };
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+ // Set src to start loading
+ img.src = url;
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ }
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = "connected-dots-styles";
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement("style");
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.svg.style.overflow = "visible";
+ this.scrollContainer.appendChild(this.svg);
+ // Configure grid group
+ this.gridGroup.classList.add("grid");
+ this.svg.appendChild(this.gridGroup);
+ // Configure curve path
+ this.curvePath.setAttribute("fill", "none");
+ this.curvePath.setAttribute("stroke", "white");
+ this.curvePath.setAttribute("stroke-width", "2");
+ this.curvePath.setAttribute("stroke-linecap", "round");
+ this.curvePath.classList.add("curve-path");
+ this.svg.appendChild(this.curvePath);
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add("tooltips");
+ this.svg.appendChild(this.tooltipGroup);
+ }
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.8);
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+ private calculateBezierControlPoints(
+ dots: DotConfig[],
+ index: number
+ ): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+ return { x1, y1, x2, y2 };
+ }
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return "";
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(
+ this.dots[0].value
+ )}`;
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(
+ this.dots,
+ i
+ );
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+ return path;
+ }
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+ if (!this.config.showGrid) return;
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", "0");
+ line.setAttribute("y1", this.getDotY(value).toString());
+ line.setAttribute("x2", this.config.totalWidth.toString());
+ line.setAttribute("y2", this.getDotY(value).toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", "10");
+ text.setAttribute("y", (this.getDotY(value) + 4).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+ // Vertical grid lines
+ const numVertLines = Math.ceil(
+ this.config.totalWidth / this.config.xUnitSize
+ );
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", x.toString());
+ line.setAttribute("y1", "0");
+ line.setAttribute("x2", x.toString());
+ line.setAttribute("y2", this.config.height.toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ if (xValue !== 0) {
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", x.toString());
+ text.setAttribute("y", (this.config.height / 2 + 20).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.setAttribute("text-anchor", "middle");
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ tooltip.classList.add("dot-tooltip");
+ tooltip.setAttribute("data-dot-id", dot.id.toString());
+
+ // Calculate tooltip dimensions and position
+ const tooltipWidth = 128; // Base width for your tooltip
+ const tooltipHeight = (4 / 3) * tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ let tooltipY = y - tooltipHeight - 20; // Positioned above the dot
+ tooltipY = Math.max(tooltipY, 10); // Ensure it doesn't go above the view
+
+ // Create background rectangle
+ const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ bg.setAttribute("x", tooltipX.toString());
+ bg.setAttribute("y", tooltipY.toString());
+ bg.setAttribute("width", tooltipWidth.toString());
+ bg.setAttribute("height", tooltipHeight.toString());
+ bg.setAttribute("rx", "5"); // Rounded corners
+ bg.classList.add("tooltip-background");
+ tooltip.appendChild(bg);
+
+ // Create foreignObject for the content
+
+ const contentContainer = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "foreignObject"
+ );
+ contentContainer.setAttribute("x", tooltipX.toString());
+ contentContainer.setAttribute("y", tooltipY.toString());
+ contentContainer.setAttribute("width", tooltipWidth.toString());
+ contentContainer.setAttribute("height", tooltipHeight.toString());
+
+ // Create a div to contain the content
+ const div = document.createElement("div");
+ div.classList.add("tooltip-content");
+
+ // Add description if available
+ if (dot.description) {
+ const desc = document.createElement("div");
+ desc.textContent = dot.description;
+ desc.classList.add("tooltip-description");
+ div.appendChild(desc);
+ }
+
+ // Add image if available
+ if (dot.imageUrl) {
+ // Create a container div
+ const imageContainer = document.createElement("div");
+ imageContainer.classList.add("image_container"); // Add image_container class
+
+ // Create the image element
+ const img = document.createElement("img");
+ img.src = dot.imageUrl;
+ img.classList.add("tooltip-image");
+
+ // Append image to the container
+ imageContainer.appendChild(img);
+
+ // Append the image container to the main div
+ div.appendChild(imageContainer);
+ }
+
+ // Add title if available
+ if (dot.title) {
+ const title = document.createElement("div");
+ title.textContent = dot.title;
+ title.classList.add("tooltip-title");
+ div.appendChild(title);
+ }
+
+ contentContainer.appendChild(div);
+ tooltip.appendChild(contentContainer);
+
+ // Add arrow path if needed
+ const arrow = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ arrow.setAttribute(
+ "d",
+ `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${
+ tooltipY + tooltipHeight - 10
+ } L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`
+ );
+ arrow.classList.add("tooltip-arrow");
+ tooltip.appendChild(arrow);
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute("d", pathData);
+ }
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ return { leftmost, rightmost };
+ }
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+ const circle = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "circle"
+ );
+ circle.setAttribute("cx", x.toString());
+ circle.setAttribute("cy", y.toString());
+ circle.setAttribute("r", this.config.dotRadius.toString());
+ circle.setAttribute("fill", "white");
+ circle.setAttribute("data-dot-id", dot.id.toString());
+ circle.classList.add("dot");
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener("click", () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error("Dot has no link");
+ throw new Error("Dot has no link");
+ }
+ });
+ }
+ this.dotsGroup.appendChild(circle);
+ }
+ }
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522102742.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522102742.ts
new file mode 100644
index 0000000..aa55f93
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522102742.ts
@@ -0,0 +1,515 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+ private preloadedImages: Map = new Map();
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+ constructor(
+ containerId: string,
+ dots: DotConfig[],
+ config?: Partial
+ ) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config,
+ };
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+ // Create SVG elements
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ this.gridGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.curvePath = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ this.dotsGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.tooltipGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter((dot) => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map((dot) => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+ // Create a loading indicator (optional)
+ const loadingCount = { current: 0, total: imageUrls.length };
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log("All images preloaded successfully");
+ }
+ };
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+ // Set src to start loading
+ img.src = url;
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ }
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = "connected-dots-styles";
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement("style");
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.svg.style.overflow = "visible";
+ this.scrollContainer.appendChild(this.svg);
+ // Configure grid group
+ this.gridGroup.classList.add("grid");
+ this.svg.appendChild(this.gridGroup);
+ // Configure curve path
+ this.curvePath.setAttribute("fill", "none");
+ this.curvePath.setAttribute("stroke", "white");
+ this.curvePath.setAttribute("stroke-width", "2");
+ this.curvePath.setAttribute("stroke-linecap", "round");
+ this.curvePath.classList.add("curve-path");
+ this.svg.appendChild(this.curvePath);
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add("tooltips");
+ this.svg.appendChild(this.tooltipGroup);
+ }
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.8);
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+ private calculateBezierControlPoints(
+ dots: DotConfig[],
+ index: number
+ ): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+ return { x1, y1, x2, y2 };
+ }
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return "";
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(
+ this.dots[0].value
+ )}`;
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(
+ this.dots,
+ i
+ );
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+ return path;
+ }
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+ if (!this.config.showGrid) return;
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", "0");
+ line.setAttribute("y1", this.getDotY(value).toString());
+ line.setAttribute("x2", this.config.totalWidth.toString());
+ line.setAttribute("y2", this.getDotY(value).toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", "10");
+ text.setAttribute("y", (this.getDotY(value) + 4).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+ // Vertical grid lines
+ const numVertLines = Math.ceil(
+ this.config.totalWidth / this.config.xUnitSize
+ );
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", x.toString());
+ line.setAttribute("y1", "0");
+ line.setAttribute("x2", x.toString());
+ line.setAttribute("y2", this.config.height.toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ if (xValue !== 0) {
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", x.toString());
+ text.setAttribute("y", (this.config.height / 2 + 20).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.setAttribute("text-anchor", "middle");
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ tooltip.classList.add("dot-tooltip");
+ tooltip.setAttribute("data-dot-id", dot.id.toString());
+
+ // Calculate tooltip dimensions and position
+ const tooltipWidth = 128; // Base width for your tooltip
+ const tooltipHeight = (4 / 3) * tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ let tooltipY = y - tooltipHeight - 20; // Positioned above the dot
+ tooltipY = Math.max(tooltipY, 10); // Ensure it doesn't go above the view
+
+ // Create background rectangle
+ const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ bg.setAttribute("x", tooltipX.toString());
+ bg.setAttribute("y", tooltipY.toString());
+ bg.setAttribute("width", tooltipWidth.toString());
+ bg.setAttribute("height", tooltipHeight.toString());
+ bg.setAttribute("rx", "5"); // Rounded corners
+ bg.classList.add("tooltip-background");
+ tooltip.appendChild(bg);
+
+ // Create foreignObject for the content
+
+ const contentContainer = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "foreignObject"
+ );
+ contentContainer.setAttribute("x", tooltipX.toString());
+ contentContainer.setAttribute("y", tooltipY.toString());
+ contentContainer.setAttribute("width", tooltipWidth.toString());
+ contentContainer.setAttribute("height", tooltipHeight.toString());
+
+ // Create a div to contain the content
+ const div = document.createElement("div");
+ div.classList.add("tooltip-content");
+
+ // Add description if available
+ if (dot.description) {
+ const desc = document.createElement("div");
+ desc.textContent = dot.description;
+ desc.classList.add("tooltip-description");
+ div.appendChild(desc);
+ }
+
+
+ // Add title if available
+ if (dot.title) {
+ const title = document.createElement("div");
+ title.textContent = dot.title;
+ title.classList.add("tooltip-title");
+ div.appendChild(title);
+ }
+
+ // Add image if available
+ if (dot.imageUrl) {
+ // Create a container div
+ const imageContainer = document.createElement("div");
+ imageContainer.classList.add("image_container"); // Add image_container class
+
+ // Create the image element
+ const img = document.createElement("img");
+ img.src = dot.imageUrl;
+ img.classList.add("tooltip-image");
+
+ // Append image to the container
+ imageContainer.appendChild(img);
+
+ // Append the image container to the main div
+ div.appendChild(imageContainer);
+ }
+
+
+ contentContainer.appendChild(div);
+ tooltip.appendChild(contentContainer);
+
+ // Add arrow path if needed
+ const arrow = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ arrow.setAttribute(
+ "d",
+ `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${
+ tooltipY + tooltipHeight - 10
+ } L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`
+ );
+ arrow.classList.add("tooltip-arrow");
+ tooltip.appendChild(arrow);
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute("d", pathData);
+ }
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ return { leftmost, rightmost };
+ }
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+ const circle = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "circle"
+ );
+ circle.setAttribute("cx", x.toString());
+ circle.setAttribute("cy", y.toString());
+ circle.setAttribute("r", this.config.dotRadius.toString());
+ circle.setAttribute("fill", "white");
+ circle.setAttribute("data-dot-id", dot.id.toString());
+ circle.classList.add("dot");
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener("click", () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error("Dot has no link");
+ throw new Error("Dot has no link");
+ }
+ });
+ }
+ this.dotsGroup.appendChild(circle);
+ }
+ }
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522103253.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522103253.ts
new file mode 100644
index 0000000..50be4e1
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522103253.ts
@@ -0,0 +1,513 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+ private preloadedImages: Map = new Map();
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+ constructor(
+ containerId: string,
+ dots: DotConfig[],
+ config?: Partial
+ ) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config,
+ };
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+ // Create SVG elements
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ this.gridGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.curvePath = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ this.dotsGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.tooltipGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter((dot) => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map((dot) => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+ // Create a loading indicator (optional)
+ const loadingCount = { current: 0, total: imageUrls.length };
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log("All images preloaded successfully");
+ }
+ };
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+ // Set src to start loading
+ img.src = url;
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ }
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = "connected-dots-styles";
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement("style");
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.svg.style.overflow = "visible";
+ this.scrollContainer.appendChild(this.svg);
+ // Configure grid group
+ this.gridGroup.classList.add("grid");
+ this.svg.appendChild(this.gridGroup);
+ // Configure curve path
+ this.curvePath.setAttribute("fill", "none");
+ this.curvePath.setAttribute("stroke", "white");
+ this.curvePath.setAttribute("stroke-width", "2");
+ this.curvePath.setAttribute("stroke-linecap", "round");
+ this.curvePath.classList.add("curve-path");
+ this.svg.appendChild(this.curvePath);
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add("tooltips");
+ this.svg.appendChild(this.tooltipGroup);
+ }
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.8);
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+ private calculateBezierControlPoints(
+ dots: DotConfig[],
+ index: number
+ ): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+ return { x1, y1, x2, y2 };
+ }
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return "";
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(
+ this.dots[0].value
+ )}`;
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(
+ this.dots,
+ i
+ );
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+ return path;
+ }
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+ if (!this.config.showGrid) return;
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", "0");
+ line.setAttribute("y1", this.getDotY(value).toString());
+ line.setAttribute("x2", this.config.totalWidth.toString());
+ line.setAttribute("y2", this.getDotY(value).toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", "10");
+ text.setAttribute("y", (this.getDotY(value) + 4).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+ // Vertical grid lines
+ const numVertLines = Math.ceil(
+ this.config.totalWidth / this.config.xUnitSize
+ );
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", x.toString());
+ line.setAttribute("y1", "0");
+ line.setAttribute("x2", x.toString());
+ line.setAttribute("y2", this.config.height.toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ if (xValue !== 0) {
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", x.toString());
+ text.setAttribute("y", (this.config.height / 2 + 20).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.setAttribute("text-anchor", "middle");
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ tooltip.classList.add("dot-tooltip");
+ tooltip.setAttribute("data-dot-id", dot.id.toString());
+
+ // Calculate tooltip dimensions and position
+ const tooltipWidth = 128; // Base width for your tooltip
+ const tooltipHeight = (4 / 3) * tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ let tooltipY = y - tooltipHeight - 20; // Positioned above the dot
+ tooltipY = Math.max(tooltipY, 10); // Ensure it doesn't go above the view
+
+ // Create background rectangle
+ const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ bg.setAttribute("x", tooltipX.toString());
+ bg.setAttribute("y", tooltipY.toString());
+ bg.setAttribute("width", tooltipWidth.toString());
+ bg.setAttribute("height", tooltipHeight.toString());
+ bg.setAttribute("rx", "5"); // Rounded corners
+ bg.classList.add("tooltip-background");
+ tooltip.appendChild(bg);
+
+ // Create foreignObject for the content
+
+ const contentContainer = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "foreignObject"
+ );
+ contentContainer.setAttribute("x", tooltipX.toString());
+ contentContainer.setAttribute("y", tooltipY.toString());
+ contentContainer.setAttribute("width", tooltipWidth.toString());
+ contentContainer.setAttribute("height", tooltipHeight.toString());
+
+ // Create a div to contain the content
+ const div = document.createElement("div");
+ div.classList.add("tooltip-content");
+
+ // Add title if available
+ if (dot.title) {
+ const title = document.createElement("div");
+ title.textContent = dot.title;
+ title.classList.add("tooltip-title");
+ div.appendChild(title);
+ }
+
+ // Add description if available
+ if (dot.description) {
+ const desc = document.createElement("div");
+ desc.textContent = dot.description;
+ desc.classList.add("tooltip-description");
+ div.appendChild(desc);
+ }
+
+ // Add image if available
+ if (dot.imageUrl) {
+ // Create a container div
+ const imageContainer = document.createElement("div");
+ imageContainer.classList.add("image_container"); // Add image_container class
+
+ // Create the image element
+ const img = document.createElement("img");
+ img.src = dot.imageUrl;
+ img.classList.add("tooltip-image");
+
+ // Append image to the container
+ imageContainer.appendChild(img);
+
+ // Append the image container to the main div
+ div.appendChild(imageContainer);
+ }
+
+ contentContainer.appendChild(div);
+ tooltip.appendChild(contentContainer);
+
+ // Add arrow path if needed
+ const arrow = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ arrow.setAttribute(
+ "d",
+ `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${
+ tooltipY + tooltipHeight - 10
+ } L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`
+ );
+ arrow.classList.add("tooltip-arrow");
+ tooltip.appendChild(arrow);
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute("d", pathData);
+ }
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ return { leftmost, rightmost };
+ }
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+ const circle = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "circle"
+ );
+ circle.setAttribute("cx", x.toString());
+ circle.setAttribute("cy", y.toString());
+ circle.setAttribute("r", this.config.dotRadius.toString());
+ circle.setAttribute("fill", "white");
+ circle.setAttribute("data-dot-id", dot.id.toString());
+ circle.classList.add("dot");
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener("click", () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error("Dot has no link");
+ throw new Error("Dot has no link");
+ }
+ });
+ }
+ this.dotsGroup.appendChild(circle);
+ }
+ }
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522104846.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522104846.ts
new file mode 100644
index 0000000..49a13c9
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522104846.ts
@@ -0,0 +1,514 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+ private preloadedImages: Map = new Map();
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+ constructor(
+ containerId: string,
+ dots: DotConfig[],
+ config?: Partial
+ ) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config,
+ };
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+ // Create SVG elements
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ this.gridGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.curvePath = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ this.dotsGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.tooltipGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter((dot) => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map((dot) => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+ // Create a loading indicator (optional)
+ const loadingCount = { current: 0, total: imageUrls.length };
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log("All images preloaded successfully");
+ }
+ };
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+ // Set src to start loading
+ img.src = url;
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ }
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = "connected-dots-styles";
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement("style");
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.svg.style.overflow = "visible";
+ this.scrollContainer.appendChild(this.svg);
+ // Configure grid group
+ this.gridGroup.classList.add("grid");
+ this.svg.appendChild(this.gridGroup);
+ // Configure curve path
+ this.curvePath.setAttribute("fill", "none");
+ this.curvePath.setAttribute("stroke", "white");
+ this.curvePath.setAttribute("stroke-width", "2");
+ this.curvePath.setAttribute("stroke-linecap", "round");
+ this.curvePath.classList.add("curve-path");
+ this.svg.appendChild(this.curvePath);
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add("tooltips");
+ this.svg.appendChild(this.tooltipGroup);
+ }
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.8);
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+ private calculateBezierControlPoints(
+ dots: DotConfig[],
+ index: number
+ ): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+ return { x1, y1, x2, y2 };
+ }
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return "";
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(
+ this.dots[0].value
+ )}`;
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(
+ this.dots,
+ i
+ );
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+ return path;
+ }
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+ if (!this.config.showGrid) return;
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", "0");
+ line.setAttribute("y1", this.getDotY(value).toString());
+ line.setAttribute("x2", this.config.totalWidth.toString());
+ line.setAttribute("y2", this.getDotY(value).toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", "10");
+ text.setAttribute("y", (this.getDotY(value) + 4).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+ // Vertical grid lines
+ const numVertLines = Math.ceil(
+ this.config.totalWidth / this.config.xUnitSize
+ );
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", x.toString());
+ line.setAttribute("y1", "0");
+ line.setAttribute("x2", x.toString());
+ line.setAttribute("y2", this.config.height.toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ if (xValue !== 0) {
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", x.toString());
+ text.setAttribute("y", (this.config.height / 2 + 20).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.setAttribute("text-anchor", "middle");
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ tooltip.classList.add("dot-tooltip");
+ tooltip.setAttribute("data-dot-id", dot.id.toString());
+
+ // Calculate tooltip dimensions and position
+ const tooltipWidth = 128; // Base width for your tooltip
+ const tooltipHeight = (4 / 3) * tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ let tooltipY = y - tooltipHeight - 20; // Positioned above the dot
+ tooltipY = Math.max(tooltipY, 10); // Ensure it doesn't go above the view
+
+ // Create background rectangle
+ const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ bg.setAttribute("x", tooltipX.toString());
+ bg.setAttribute("y", tooltipY.toString());
+ bg.setAttribute("width", tooltipWidth.toString());
+ bg.setAttribute("height", tooltipHeight.toString());
+ bg.setAttribute("rx", "5"); // Rounded corners
+ bg.classList.add("tooltip-background");
+ tooltip.appendChild(bg);
+
+ // Create foreignObject for the content
+
+ const contentContainer = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "foreignObject"
+ );
+ contentContainer.setAttribute("x", tooltipX.toString());
+ contentContainer.setAttribute("y", tooltipY.toString());
+ contentContainer.setAttribute("width", tooltipWidth.toString());
+ contentContainer.setAttribute("height", tooltipHeight.toString());
+
+ // Create a div to contain the content
+ const div = document.createElement("div");
+ div.classList.add("tooltip-content");
+
+ // Add title if available
+ if (dot.title) {
+ const title = document.createElement("div");
+ title.textContent = dot.title;
+ title.classList.add("tooltip-title");
+ div.appendChild(title);
+ }
+
+ // Add description if available
+ if (dot.description) {
+ const desc = document.createElement("div");
+ desc.textContent = dot.description;
+ desc.classList.add("tooltip-description");
+ div.appendChild(desc);
+ }
+
+ // Add image if available
+ if (dot.imageUrl) {
+ // Create a container div
+ const imageContainer = document.createElement("div");
+ imageContainer.classList.add("image_container"); // Add image_container class
+
+ // Create the image element
+ const img = document.createElement("img");
+ img.src = dot.imageUrl;
+ img.classList.add("tooltip-image");
+
+ // Append image to the container
+ imageContainer.appendChild(img);
+
+ // Append the image container to the main div
+ div.appendChild(imageContainer);
+ }
+
+ contentContainer.appendChild(div);
+ tooltip.appendChild(contentContainer);
+
+ // Add arrow path if needed
+ const arrow = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ arrow.setAttribute(
+ "d",
+ `M ${x} ${tooltipY + tooltipHeight} L ${x} ${
+ tooltipY + tooltipHeight - 10
+ }`
+ );
+ arrow.classList.add("tooltip-arrow");
+ tooltip.appendChild(arrow);
+
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute("d", pathData);
+ }
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ return { leftmost, rightmost };
+ }
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+ const circle = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "circle"
+ );
+ circle.setAttribute("cx", x.toString());
+ circle.setAttribute("cy", y.toString());
+ circle.setAttribute("r", this.config.dotRadius.toString());
+ circle.setAttribute("fill", "white");
+ circle.setAttribute("data-dot-id", dot.id.toString());
+ circle.classList.add("dot");
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener("click", () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error("Dot has no link");
+ throw new Error("Dot has no link");
+ }
+ });
+ }
+ this.dotsGroup.appendChild(circle);
+ }
+ }
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522104938.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522104938.ts
new file mode 100644
index 0000000..50be4e1
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522104938.ts
@@ -0,0 +1,513 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+ private preloadedImages: Map = new Map();
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+ constructor(
+ containerId: string,
+ dots: DotConfig[],
+ config?: Partial
+ ) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config,
+ };
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+ // Create SVG elements
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ this.gridGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.curvePath = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ this.dotsGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.tooltipGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter((dot) => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map((dot) => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+ // Create a loading indicator (optional)
+ const loadingCount = { current: 0, total: imageUrls.length };
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log("All images preloaded successfully");
+ }
+ };
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+ // Set src to start loading
+ img.src = url;
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ }
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = "connected-dots-styles";
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement("style");
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.svg.style.overflow = "visible";
+ this.scrollContainer.appendChild(this.svg);
+ // Configure grid group
+ this.gridGroup.classList.add("grid");
+ this.svg.appendChild(this.gridGroup);
+ // Configure curve path
+ this.curvePath.setAttribute("fill", "none");
+ this.curvePath.setAttribute("stroke", "white");
+ this.curvePath.setAttribute("stroke-width", "2");
+ this.curvePath.setAttribute("stroke-linecap", "round");
+ this.curvePath.classList.add("curve-path");
+ this.svg.appendChild(this.curvePath);
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add("tooltips");
+ this.svg.appendChild(this.tooltipGroup);
+ }
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.8);
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+ private calculateBezierControlPoints(
+ dots: DotConfig[],
+ index: number
+ ): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+ return { x1, y1, x2, y2 };
+ }
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return "";
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(
+ this.dots[0].value
+ )}`;
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(
+ this.dots,
+ i
+ );
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+ return path;
+ }
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+ if (!this.config.showGrid) return;
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", "0");
+ line.setAttribute("y1", this.getDotY(value).toString());
+ line.setAttribute("x2", this.config.totalWidth.toString());
+ line.setAttribute("y2", this.getDotY(value).toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", "10");
+ text.setAttribute("y", (this.getDotY(value) + 4).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+ // Vertical grid lines
+ const numVertLines = Math.ceil(
+ this.config.totalWidth / this.config.xUnitSize
+ );
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", x.toString());
+ line.setAttribute("y1", "0");
+ line.setAttribute("x2", x.toString());
+ line.setAttribute("y2", this.config.height.toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ if (xValue !== 0) {
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", x.toString());
+ text.setAttribute("y", (this.config.height / 2 + 20).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.setAttribute("text-anchor", "middle");
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ tooltip.classList.add("dot-tooltip");
+ tooltip.setAttribute("data-dot-id", dot.id.toString());
+
+ // Calculate tooltip dimensions and position
+ const tooltipWidth = 128; // Base width for your tooltip
+ const tooltipHeight = (4 / 3) * tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ let tooltipY = y - tooltipHeight - 20; // Positioned above the dot
+ tooltipY = Math.max(tooltipY, 10); // Ensure it doesn't go above the view
+
+ // Create background rectangle
+ const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ bg.setAttribute("x", tooltipX.toString());
+ bg.setAttribute("y", tooltipY.toString());
+ bg.setAttribute("width", tooltipWidth.toString());
+ bg.setAttribute("height", tooltipHeight.toString());
+ bg.setAttribute("rx", "5"); // Rounded corners
+ bg.classList.add("tooltip-background");
+ tooltip.appendChild(bg);
+
+ // Create foreignObject for the content
+
+ const contentContainer = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "foreignObject"
+ );
+ contentContainer.setAttribute("x", tooltipX.toString());
+ contentContainer.setAttribute("y", tooltipY.toString());
+ contentContainer.setAttribute("width", tooltipWidth.toString());
+ contentContainer.setAttribute("height", tooltipHeight.toString());
+
+ // Create a div to contain the content
+ const div = document.createElement("div");
+ div.classList.add("tooltip-content");
+
+ // Add title if available
+ if (dot.title) {
+ const title = document.createElement("div");
+ title.textContent = dot.title;
+ title.classList.add("tooltip-title");
+ div.appendChild(title);
+ }
+
+ // Add description if available
+ if (dot.description) {
+ const desc = document.createElement("div");
+ desc.textContent = dot.description;
+ desc.classList.add("tooltip-description");
+ div.appendChild(desc);
+ }
+
+ // Add image if available
+ if (dot.imageUrl) {
+ // Create a container div
+ const imageContainer = document.createElement("div");
+ imageContainer.classList.add("image_container"); // Add image_container class
+
+ // Create the image element
+ const img = document.createElement("img");
+ img.src = dot.imageUrl;
+ img.classList.add("tooltip-image");
+
+ // Append image to the container
+ imageContainer.appendChild(img);
+
+ // Append the image container to the main div
+ div.appendChild(imageContainer);
+ }
+
+ contentContainer.appendChild(div);
+ tooltip.appendChild(contentContainer);
+
+ // Add arrow path if needed
+ const arrow = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ arrow.setAttribute(
+ "d",
+ `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${
+ tooltipY + tooltipHeight - 10
+ } L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`
+ );
+ arrow.classList.add("tooltip-arrow");
+ tooltip.appendChild(arrow);
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute("d", pathData);
+ }
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ return { leftmost, rightmost };
+ }
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+ const circle = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "circle"
+ );
+ circle.setAttribute("cx", x.toString());
+ circle.setAttribute("cy", y.toString());
+ circle.setAttribute("r", this.config.dotRadius.toString());
+ circle.setAttribute("fill", "white");
+ circle.setAttribute("data-dot-id", dot.id.toString());
+ circle.classList.add("dot");
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener("click", () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error("Dot has no link");
+ throw new Error("Dot has no link");
+ }
+ });
+ }
+ this.dotsGroup.appendChild(circle);
+ }
+ }
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522105020.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522105020.ts
new file mode 100644
index 0000000..1d00c27
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522105020.ts
@@ -0,0 +1,519 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+ private preloadedImages: Map = new Map();
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+ constructor(
+ containerId: string,
+ dots: DotConfig[],
+ config?: Partial
+ ) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config,
+ };
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+ // Create SVG elements
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ this.gridGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.curvePath = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ this.dotsGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.tooltipGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter((dot) => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map((dot) => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+ // Create a loading indicator (optional)
+ const loadingCount = { current: 0, total: imageUrls.length };
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log("All images preloaded successfully");
+ }
+ };
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+ // Set src to start loading
+ img.src = url;
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ }
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = "connected-dots-styles";
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement("style");
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.svg.style.overflow = "visible";
+ this.scrollContainer.appendChild(this.svg);
+ // Configure grid group
+ this.gridGroup.classList.add("grid");
+ this.svg.appendChild(this.gridGroup);
+ // Configure curve path
+ this.curvePath.setAttribute("fill", "none");
+ this.curvePath.setAttribute("stroke", "white");
+ this.curvePath.setAttribute("stroke-width", "2");
+ this.curvePath.setAttribute("stroke-linecap", "round");
+ this.curvePath.classList.add("curve-path");
+ this.svg.appendChild(this.curvePath);
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add("tooltips");
+ this.svg.appendChild(this.tooltipGroup);
+ }
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.8);
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+ private calculateBezierControlPoints(
+ dots: DotConfig[],
+ index: number
+ ): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+ return { x1, y1, x2, y2 };
+ }
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return "";
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(
+ this.dots[0].value
+ )}`;
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(
+ this.dots,
+ i
+ );
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+ return path;
+ }
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+ if (!this.config.showGrid) return;
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", "0");
+ line.setAttribute("y1", this.getDotY(value).toString());
+ line.setAttribute("x2", this.config.totalWidth.toString());
+ line.setAttribute("y2", this.getDotY(value).toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", "10");
+ text.setAttribute("y", (this.getDotY(value) + 4).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+ // Vertical grid lines
+ const numVertLines = Math.ceil(
+ this.config.totalWidth / this.config.xUnitSize
+ );
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", x.toString());
+ line.setAttribute("y1", "0");
+ line.setAttribute("x2", x.toString());
+ line.setAttribute("y2", this.config.height.toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ if (xValue !== 0) {
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", x.toString());
+ text.setAttribute("y", (this.config.height / 2 + 20).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.setAttribute("text-anchor", "middle");
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ tooltip.classList.add("dot-tooltip");
+ tooltip.setAttribute("data-dot-id", dot.id.toString());
+
+ // Calculate tooltip dimensions and position
+ const tooltipWidth = 128; // Base width for your tooltip
+ const tooltipHeight = (4 / 3) * tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ let tooltipY = y - tooltipHeight - 20; // Positioned above the dot
+ tooltipY = Math.max(tooltipY, 10); // Ensure it doesn't go above the view
+
+ // Create background rectangle
+ const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ bg.setAttribute("x", tooltipX.toString());
+ bg.setAttribute("y", tooltipY.toString());
+ bg.setAttribute("width", tooltipWidth.toString());
+ bg.setAttribute("height", tooltipHeight.toString());
+ bg.setAttribute("rx", "5"); // Rounded corners
+ bg.classList.add("tooltip-background");
+ tooltip.appendChild(bg);
+
+ // Create foreignObject for the content
+
+ const contentContainer = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "foreignObject"
+ );
+ contentContainer.setAttribute("x", tooltipX.toString());
+ contentContainer.setAttribute("y", tooltipY.toString());
+ contentContainer.setAttribute("width", tooltipWidth.toString());
+ contentContainer.setAttribute("height", tooltipHeight.toString());
+
+ // Create a div to contain the content
+ const div = document.createElement("div");
+ div.classList.add("tooltip-content");
+
+ // Add title if available
+ if (dot.title) {
+ const title = document.createElement("div");
+ title.textContent = dot.title;
+ title.classList.add("tooltip-title");
+ div.appendChild(title);
+ }
+
+ // Add description if available
+ if (dot.description) {
+ const desc = document.createElement("div");
+ desc.textContent = dot.description;
+ desc.classList.add("tooltip-description");
+ div.appendChild(desc);
+ }
+
+ // Add image if available
+ if (dot.imageUrl) {
+ // Create a container div
+ const imageContainer = document.createElement("div");
+ imageContainer.classList.add("image_container"); // Add image_container class
+
+ // Create the image element
+ const img = document.createElement("img");
+ img.src = dot.imageUrl;
+ img.classList.add("tooltip-image");
+
+ // Append image to the container
+ imageContainer.appendChild(img);
+
+ // Append the image container to the main div
+ div.appendChild(imageContainer);
+ }
+
+ contentContainer.appendChild(div);
+ tooltip.appendChild(contentContainer);
+
+ // Add arrow path if needed
+ const arrow = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ arrow.setAttribute(
+ "d",
+ `M ${x} ${tooltipY + tooltipHeight} L ${x} ${
+ tooltipY + tooltipHeight - 10
+ }`
+);
+ // arrow.setAttribute(
+ // "d",
+ // `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${
+ // tooltipY + tooltipHeight - 10
+ // } L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`
+ // );
+ arrow.classList.add("tooltip-arrow");
+ tooltip.appendChild(arrow);
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute("d", pathData);
+ }
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ return { leftmost, rightmost };
+ }
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+ const circle = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "circle"
+ );
+ circle.setAttribute("cx", x.toString());
+ circle.setAttribute("cy", y.toString());
+ circle.setAttribute("r", this.config.dotRadius.toString());
+ circle.setAttribute("fill", "white");
+ circle.setAttribute("data-dot-id", dot.id.toString());
+ circle.classList.add("dot");
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener("click", () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error("Dot has no link");
+ throw new Error("Dot has no link");
+ }
+ });
+ }
+ this.dotsGroup.appendChild(circle);
+ }
+ }
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522105023.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522105023.ts
new file mode 100644
index 0000000..7e730c0
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522105023.ts
@@ -0,0 +1,519 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+ private preloadedImages: Map = new Map();
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+ constructor(
+ containerId: string,
+ dots: DotConfig[],
+ config?: Partial
+ ) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config,
+ };
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+ // Create SVG elements
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ this.gridGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.curvePath = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ this.dotsGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.tooltipGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter((dot) => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map((dot) => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+ // Create a loading indicator (optional)
+ const loadingCount = { current: 0, total: imageUrls.length };
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log("All images preloaded successfully");
+ }
+ };
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+ // Set src to start loading
+ img.src = url;
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ }
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = "connected-dots-styles";
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement("style");
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.svg.style.overflow = "visible";
+ this.scrollContainer.appendChild(this.svg);
+ // Configure grid group
+ this.gridGroup.classList.add("grid");
+ this.svg.appendChild(this.gridGroup);
+ // Configure curve path
+ this.curvePath.setAttribute("fill", "none");
+ this.curvePath.setAttribute("stroke", "white");
+ this.curvePath.setAttribute("stroke-width", "2");
+ this.curvePath.setAttribute("stroke-linecap", "round");
+ this.curvePath.classList.add("curve-path");
+ this.svg.appendChild(this.curvePath);
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add("tooltips");
+ this.svg.appendChild(this.tooltipGroup);
+ }
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.8);
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+ private calculateBezierControlPoints(
+ dots: DotConfig[],
+ index: number
+ ): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+ return { x1, y1, x2, y2 };
+ }
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return "";
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(
+ this.dots[0].value
+ )}`;
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(
+ this.dots,
+ i
+ );
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+ return path;
+ }
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+ if (!this.config.showGrid) return;
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", "0");
+ line.setAttribute("y1", this.getDotY(value).toString());
+ line.setAttribute("x2", this.config.totalWidth.toString());
+ line.setAttribute("y2", this.getDotY(value).toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", "10");
+ text.setAttribute("y", (this.getDotY(value) + 4).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+ // Vertical grid lines
+ const numVertLines = Math.ceil(
+ this.config.totalWidth / this.config.xUnitSize
+ );
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", x.toString());
+ line.setAttribute("y1", "0");
+ line.setAttribute("x2", x.toString());
+ line.setAttribute("y2", this.config.height.toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ if (xValue !== 0) {
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", x.toString());
+ text.setAttribute("y", (this.config.height / 2 + 20).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.setAttribute("text-anchor", "middle");
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ tooltip.classList.add("dot-tooltip");
+ tooltip.setAttribute("data-dot-id", dot.id.toString());
+
+ // Calculate tooltip dimensions and position
+ const tooltipWidth = 128; // Base width for your tooltip
+ const tooltipHeight = (4 / 3) * tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ let tooltipY = y - tooltipHeight - 20; // Positioned above the dot
+ tooltipY = Math.max(tooltipY, 10); // Ensure it doesn't go above the view
+
+ // Create background rectangle
+ const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ bg.setAttribute("x", tooltipX.toString());
+ bg.setAttribute("y", tooltipY.toString());
+ bg.setAttribute("width", tooltipWidth.toString());
+ bg.setAttribute("height", tooltipHeight.toString());
+ bg.setAttribute("rx", "5"); // Rounded corners
+ bg.classList.add("tooltip-background");
+ tooltip.appendChild(bg);
+
+ // Create foreignObject for the content
+
+ const contentContainer = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "foreignObject"
+ );
+ contentContainer.setAttribute("x", tooltipX.toString());
+ contentContainer.setAttribute("y", tooltipY.toString());
+ contentContainer.setAttribute("width", tooltipWidth.toString());
+ contentContainer.setAttribute("height", tooltipHeight.toString());
+
+ // Create a div to contain the content
+ const div = document.createElement("div");
+ div.classList.add("tooltip-content");
+
+ // Add title if available
+ if (dot.title) {
+ const title = document.createElement("div");
+ title.textContent = dot.title;
+ title.classList.add("tooltip-title");
+ div.appendChild(title);
+ }
+
+ // Add description if available
+ if (dot.description) {
+ const desc = document.createElement("div");
+ desc.textContent = dot.description;
+ desc.classList.add("tooltip-description");
+ div.appendChild(desc);
+ }
+
+ // Add image if available
+ if (dot.imageUrl) {
+ // Create a container div
+ const imageContainer = document.createElement("div");
+ imageContainer.classList.add("image_container"); // Add image_container class
+
+ // Create the image element
+ const img = document.createElement("img");
+ img.src = dot.imageUrl;
+ img.classList.add("tooltip-image");
+
+ // Append image to the container
+ imageContainer.appendChild(img);
+
+ // Append the image container to the main div
+ div.appendChild(imageContainer);
+ }
+
+ contentContainer.appendChild(div);
+ tooltip.appendChild(contentContainer);
+
+ // Add arrow path if needed
+ const arrow = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ arrow.setAttribute(
+ "d",
+ `M ${x} ${tooltipY + tooltipHeight} L ${x} ${
+ tooltipY + tooltipHeight - 100
+ }`
+);
+ // arrow.setAttribute(
+ // "d",
+ // `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${
+ // tooltipY + tooltipHeight - 10
+ // } L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`
+ // );
+ arrow.classList.add("tooltip-arrow");
+ tooltip.appendChild(arrow);
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute("d", pathData);
+ }
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ return { leftmost, rightmost };
+ }
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+ const circle = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "circle"
+ );
+ circle.setAttribute("cx", x.toString());
+ circle.setAttribute("cy", y.toString());
+ circle.setAttribute("r", this.config.dotRadius.toString());
+ circle.setAttribute("fill", "white");
+ circle.setAttribute("data-dot-id", dot.id.toString());
+ circle.classList.add("dot");
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener("click", () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error("Dot has no link");
+ throw new Error("Dot has no link");
+ }
+ });
+ }
+ this.dotsGroup.appendChild(circle);
+ }
+ }
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522105055.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522105055.ts
new file mode 100644
index 0000000..afceca6
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522105055.ts
@@ -0,0 +1,522 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+ private preloadedImages: Map = new Map();
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+ constructor(
+ containerId: string,
+ dots: DotConfig[],
+ config?: Partial
+ ) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config,
+ };
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+ // Create SVG elements
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ this.gridGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.curvePath = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ this.dotsGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.tooltipGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter((dot) => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map((dot) => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+ // Create a loading indicator (optional)
+ const loadingCount = { current: 0, total: imageUrls.length };
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log("All images preloaded successfully");
+ }
+ };
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+ // Set src to start loading
+ img.src = url;
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ }
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = "connected-dots-styles";
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement("style");
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.svg.style.overflow = "visible";
+ this.scrollContainer.appendChild(this.svg);
+ // Configure grid group
+ this.gridGroup.classList.add("grid");
+ this.svg.appendChild(this.gridGroup);
+ // Configure curve path
+ this.curvePath.setAttribute("fill", "none");
+ this.curvePath.setAttribute("stroke", "white");
+ this.curvePath.setAttribute("stroke-width", "2");
+ this.curvePath.setAttribute("stroke-linecap", "round");
+ this.curvePath.classList.add("curve-path");
+ this.svg.appendChild(this.curvePath);
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add("tooltips");
+ this.svg.appendChild(this.tooltipGroup);
+ }
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.8);
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+ private calculateBezierControlPoints(
+ dots: DotConfig[],
+ index: number
+ ): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+ return { x1, y1, x2, y2 };
+ }
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return "";
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(
+ this.dots[0].value
+ )}`;
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(
+ this.dots,
+ i
+ );
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+ return path;
+ }
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+ if (!this.config.showGrid) return;
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", "0");
+ line.setAttribute("y1", this.getDotY(value).toString());
+ line.setAttribute("x2", this.config.totalWidth.toString());
+ line.setAttribute("y2", this.getDotY(value).toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", "10");
+ text.setAttribute("y", (this.getDotY(value) + 4).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+ // Vertical grid lines
+ const numVertLines = Math.ceil(
+ this.config.totalWidth / this.config.xUnitSize
+ );
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", x.toString());
+ line.setAttribute("y1", "0");
+ line.setAttribute("x2", x.toString());
+ line.setAttribute("y2", this.config.height.toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ if (xValue !== 0) {
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", x.toString());
+ text.setAttribute("y", (this.config.height / 2 + 20).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.setAttribute("text-anchor", "middle");
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ tooltip.classList.add("dot-tooltip");
+ tooltip.setAttribute("data-dot-id", dot.id.toString());
+
+ // Calculate tooltip dimensions and position
+ const tooltipWidth = 128; // Base width for your tooltip
+ const tooltipHeight = (4 / 3) * tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ let tooltipY = y - tooltipHeight - 20; // Positioned above the dot
+ tooltipY = Math.max(tooltipY, 10); // Ensure it doesn't go above the view
+
+ // Create background rectangle
+ const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ bg.setAttribute("x", tooltipX.toString());
+ bg.setAttribute("y", tooltipY.toString());
+ bg.setAttribute("width", tooltipWidth.toString());
+ bg.setAttribute("height", tooltipHeight.toString());
+ bg.setAttribute("rx", "5"); // Rounded corners
+ bg.classList.add("tooltip-background");
+ tooltip.appendChild(bg);
+
+ // Create foreignObject for the content
+
+ const contentContainer = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "foreignObject"
+ );
+ contentContainer.setAttribute("x", tooltipX.toString());
+ contentContainer.setAttribute("y", tooltipY.toString());
+ contentContainer.setAttribute("width", tooltipWidth.toString());
+ contentContainer.setAttribute("height", tooltipHeight.toString());
+
+ // Create a div to contain the content
+ const div = document.createElement("div");
+ div.classList.add("tooltip-content");
+
+ // Add title if available
+ if (dot.title) {
+ const title = document.createElement("div");
+ title.textContent = dot.title;
+ title.classList.add("tooltip-title");
+ div.appendChild(title);
+ }
+
+ // Add description if available
+ if (dot.description) {
+ const desc = document.createElement("div");
+ desc.textContent = dot.description;
+ desc.classList.add("tooltip-description");
+ div.appendChild(desc);
+ }
+
+ // Add image if available
+ if (dot.imageUrl) {
+ // Create a container div
+ const imageContainer = document.createElement("div");
+ imageContainer.classList.add("image_container"); // Add image_container class
+
+ // Create the image element
+ const img = document.createElement("img");
+ img.src = dot.imageUrl;
+ img.classList.add("tooltip-image");
+
+ // Append image to the container
+ imageContainer.appendChild(img);
+
+ // Append the image container to the main div
+ div.appendChild(imageContainer);
+ }
+
+ contentContainer.appendChild(div);
+ tooltip.appendChild(contentContainer);
+
+ // Add arrow path if needed
+ const arrow = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ arrow.setAttribute(
+ "d",
+ `M ${x} ${tooltipY + tooltipHeight} L ${x} ${
+ tooltipY + tooltipHeight - 100
+ }`
+);
+ // arrow.setAttribute(
+ // "d",
+ // `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${
+ // tooltipY + tooltipHeight - 10
+ // } L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`
+ // );
+
+ arrow.setAttribute("stroke", "black"); // Set the color of the line
+arrow.setAttribute("stroke-width", "1"); // Set the line width to 1px
+ arrow.classList.add("tooltip-arrow");
+ tooltip.appendChild(arrow);
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute("d", pathData);
+ }
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ return { leftmost, rightmost };
+ }
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+ const circle = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "circle"
+ );
+ circle.setAttribute("cx", x.toString());
+ circle.setAttribute("cy", y.toString());
+ circle.setAttribute("r", this.config.dotRadius.toString());
+ circle.setAttribute("fill", "white");
+ circle.setAttribute("data-dot-id", dot.id.toString());
+ circle.classList.add("dot");
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener("click", () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error("Dot has no link");
+ throw new Error("Dot has no link");
+ }
+ });
+ }
+ this.dotsGroup.appendChild(circle);
+ }
+ }
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522105115.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522105115.ts
new file mode 100644
index 0000000..86a13eb
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522105115.ts
@@ -0,0 +1,522 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+ private preloadedImages: Map = new Map();
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+ constructor(
+ containerId: string,
+ dots: DotConfig[],
+ config?: Partial
+ ) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config,
+ };
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+ // Create SVG elements
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ this.gridGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.curvePath = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ this.dotsGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.tooltipGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter((dot) => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map((dot) => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+ // Create a loading indicator (optional)
+ const loadingCount = { current: 0, total: imageUrls.length };
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log("All images preloaded successfully");
+ }
+ };
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+ // Set src to start loading
+ img.src = url;
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ }
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = "connected-dots-styles";
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement("style");
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.svg.style.overflow = "visible";
+ this.scrollContainer.appendChild(this.svg);
+ // Configure grid group
+ this.gridGroup.classList.add("grid");
+ this.svg.appendChild(this.gridGroup);
+ // Configure curve path
+ this.curvePath.setAttribute("fill", "none");
+ this.curvePath.setAttribute("stroke", "white");
+ this.curvePath.setAttribute("stroke-width", "2");
+ this.curvePath.setAttribute("stroke-linecap", "round");
+ this.curvePath.classList.add("curve-path");
+ this.svg.appendChild(this.curvePath);
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add("tooltips");
+ this.svg.appendChild(this.tooltipGroup);
+ }
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.8);
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+ private calculateBezierControlPoints(
+ dots: DotConfig[],
+ index: number
+ ): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+ return { x1, y1, x2, y2 };
+ }
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return "";
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(
+ this.dots[0].value
+ )}`;
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(
+ this.dots,
+ i
+ );
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+ return path;
+ }
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+ if (!this.config.showGrid) return;
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", "0");
+ line.setAttribute("y1", this.getDotY(value).toString());
+ line.setAttribute("x2", this.config.totalWidth.toString());
+ line.setAttribute("y2", this.getDotY(value).toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", "10");
+ text.setAttribute("y", (this.getDotY(value) + 4).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+ // Vertical grid lines
+ const numVertLines = Math.ceil(
+ this.config.totalWidth / this.config.xUnitSize
+ );
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", x.toString());
+ line.setAttribute("y1", "0");
+ line.setAttribute("x2", x.toString());
+ line.setAttribute("y2", this.config.height.toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ if (xValue !== 0) {
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", x.toString());
+ text.setAttribute("y", (this.config.height / 2 + 20).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.setAttribute("text-anchor", "middle");
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ tooltip.classList.add("dot-tooltip");
+ tooltip.setAttribute("data-dot-id", dot.id.toString());
+
+ // Calculate tooltip dimensions and position
+ const tooltipWidth = 128; // Base width for your tooltip
+ const tooltipHeight = (4 / 3) * tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ let tooltipY = y - tooltipHeight - 20; // Positioned above the dot
+ tooltipY = Math.max(tooltipY, 10); // Ensure it doesn't go above the view
+
+ // Create background rectangle
+ const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ bg.setAttribute("x", tooltipX.toString());
+ bg.setAttribute("y", tooltipY.toString());
+ bg.setAttribute("width", tooltipWidth.toString());
+ bg.setAttribute("height", tooltipHeight.toString());
+ bg.setAttribute("rx", "5"); // Rounded corners
+ bg.classList.add("tooltip-background");
+ tooltip.appendChild(bg);
+
+ // Create foreignObject for the content
+
+ const contentContainer = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "foreignObject"
+ );
+ contentContainer.setAttribute("x", tooltipX.toString());
+ contentContainer.setAttribute("y", tooltipY.toString());
+ contentContainer.setAttribute("width", tooltipWidth.toString());
+ contentContainer.setAttribute("height", tooltipHeight.toString());
+
+ // Create a div to contain the content
+ const div = document.createElement("div");
+ div.classList.add("tooltip-content");
+
+ // Add title if available
+ if (dot.title) {
+ const title = document.createElement("div");
+ title.textContent = dot.title;
+ title.classList.add("tooltip-title");
+ div.appendChild(title);
+ }
+
+ // Add description if available
+ if (dot.description) {
+ const desc = document.createElement("div");
+ desc.textContent = dot.description;
+ desc.classList.add("tooltip-description");
+ div.appendChild(desc);
+ }
+
+ // Add image if available
+ if (dot.imageUrl) {
+ // Create a container div
+ const imageContainer = document.createElement("div");
+ imageContainer.classList.add("image_container"); // Add image_container class
+
+ // Create the image element
+ const img = document.createElement("img");
+ img.src = dot.imageUrl;
+ img.classList.add("tooltip-image");
+
+ // Append image to the container
+ imageContainer.appendChild(img);
+
+ // Append the image container to the main div
+ div.appendChild(imageContainer);
+ }
+
+ contentContainer.appendChild(div);
+ tooltip.appendChild(contentContainer);
+
+ // Add arrow path if needed
+ const arrow = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ arrow.setAttribute(
+ "d",
+ `M ${x} ${tooltipY + tooltipHeight} L ${x} ${
+ tooltipY + tooltipHeight - 10
+ }`
+ );
+ // arrow.setAttribute(
+ // "d",
+ // `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${
+ // tooltipY + tooltipHeight - 10
+ // } L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`
+ // );
+
+ arrow.setAttribute("stroke", "black"); // Set the color of the line
+ arrow.setAttribute("stroke-width", "1"); // Set the line width to 1px
+ arrow.classList.add("tooltip-arrow");
+ tooltip.appendChild(arrow);
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute("d", pathData);
+ }
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ return { leftmost, rightmost };
+ }
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+ const circle = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "circle"
+ );
+ circle.setAttribute("cx", x.toString());
+ circle.setAttribute("cy", y.toString());
+ circle.setAttribute("r", this.config.dotRadius.toString());
+ circle.setAttribute("fill", "white");
+ circle.setAttribute("data-dot-id", dot.id.toString());
+ circle.classList.add("dot");
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener("click", () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error("Dot has no link");
+ throw new Error("Dot has no link");
+ }
+ });
+ }
+ this.dotsGroup.appendChild(circle);
+ }
+ }
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522105129.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522105129.ts
new file mode 100644
index 0000000..39a5153
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522105129.ts
@@ -0,0 +1,522 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+ private preloadedImages: Map = new Map();
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+ constructor(
+ containerId: string,
+ dots: DotConfig[],
+ config?: Partial
+ ) {
+ // Use the provided dots or empty array
+ this.dots = dots || [];
+ // Calculate the total width based on dots data
+ const xUnitSize = config?.xUnitSize || 120;
+ let calculatedWidth = 0;
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ calculatedWidth = (maxX - minX + 6) * xUnitSize;
+ } else {
+ calculatedWidth = 6 * xUnitSize; // Default width if no dots
+ }
+ // Default configuration
+ this.config = {
+ totalWidth: calculatedWidth,
+ height: window.innerHeight,
+ dotRadius: 6,
+ xUnitSize: xUnitSize,
+ tension: 0.5,
+ showGrid: false,
+ tooltipWidth: 128,
+ tooltipHeight: 128,
+ ...config,
+ };
+ // Initialize DOM elements
+ this.scrollContainer = document.getElementById(containerId) as HTMLElement;
+ // Create SVG elements
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ this.gridGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.curvePath = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ this.dotsGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ this.tooltipGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ // Initialize the visualization
+ this.addStyles();
+ this.initializeSVG();
+ this.setupEventListeners();
+ this.preloadImages();
+ this.render();
+ }
+ private preloadImages(): void {
+ // Extract all unique image URLs from dots
+ const imageUrls: string[] = this.dots
+ .filter((dot) => dot.imageUrl)
+ // biome-ignore lint/style/noNonNullAssertion:
+ .map((dot) => dot.imageUrl!)
+ .filter((url, index, self) => self.indexOf(url) === index); // Remove duplicates
+ // Create a loading indicator (optional)
+ const loadingCount = { current: 0, total: imageUrls.length };
+ if (imageUrls.length > 0) {
+ console.log(`Preloading ${imageUrls.length} images...`);
+ }
+ // Preload each image
+ for (const url of imageUrls) {
+ const img = new Image();
+ // Optional loading events
+ img.onload = () => {
+ loadingCount.current++;
+ if (loadingCount.current === loadingCount.total) {
+ console.log("All images preloaded successfully");
+ }
+ };
+ img.onerror = () => {
+ loadingCount.current++;
+ console.error(`Failed to preload image: ${url}`);
+ };
+ // Set src to start loading
+ img.src = url;
+ // Store in map for potential later use
+ this.preloadedImages.set(url, img);
+ }
+ }
+ private addStyles(): void {
+ // Add necessary styles for tooltips and interactions
+ const styleId = "connected-dots-styles";
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement("style");
+ style.id = styleId;
+ style.textContent = `
+ .dot {
+ transition: r 0.2s ease, fill 0.2s ease;
+ cursor: pointer;
+ }
+ .dot:hover {
+ fill: rgba(255, 255, 255, 0.9);
+ filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
+ }
+ .dot-tooltip {
+ pointer-events: none;
+ opacity: 1; /* Always visible */
+ }
+ .tooltip-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 4px;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+ }
+ private initializeSVG(): void {
+ // Configure SVG
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.svg.style.overflow = "visible";
+ this.scrollContainer.appendChild(this.svg);
+ // Configure grid group
+ this.gridGroup.classList.add("grid");
+ this.svg.appendChild(this.gridGroup);
+ // Configure curve path
+ this.curvePath.setAttribute("fill", "none");
+ this.curvePath.setAttribute("stroke", "white");
+ this.curvePath.setAttribute("stroke-width", "2");
+ this.curvePath.setAttribute("stroke-linecap", "round");
+ this.curvePath.classList.add("curve-path");
+ this.svg.appendChild(this.curvePath);
+ // Configure dots group
+ this.svg.appendChild(this.dotsGroup);
+ // Configure tooltip group (always on top)
+ this.tooltipGroup.classList.add("tooltips");
+ this.svg.appendChild(this.tooltipGroup);
+ }
+ private setupEventListeners(): void {
+ // Event listeners removed as the controls were removed
+ }
+ private getDotX(x: number): number {
+ return (x + 3) * this.config.xUnitSize;
+ }
+ private getDotY(value: number): number {
+ const centerY = this.config.height / 2;
+ // Calculate raw Y position
+ const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.8);
+ // Calculate minimum Y position to ensure tooltip fits
+ const minY = this.config.tooltipHeight + 30; // tooltip height + some padding
+ // Ensure Y is never less than minimum (never too high on screen)
+ return Math.max(rawY, minY);
+ }
+ private calculateBezierControlPoints(
+ dots: DotConfig[],
+ index: number
+ ): ControlPoints {
+ const tension = this.config.tension * 260; // Scale tension for Bezier curve
+ // Get current point and its neighbors
+ const curr = dots[index];
+ const next = dots[index + 1];
+ // Calculate control points for a smooth bezier curve
+ const x1 = this.getDotX(curr.x) + tension;
+ const y1 = this.getDotY(curr.value);
+ const x2 = this.getDotX(next.x) - tension;
+ const y2 = this.getDotY(next.value);
+ return { x1, y1, x2, y2 };
+ }
+ private generateBezierPath(): string {
+ if (this.dots.length < 2) return "";
+ let path = `M ${this.getDotX(this.dots[0].x)} ${this.getDotY(
+ this.dots[0].value
+ )}`;
+ for (let i = 0; i < this.dots.length - 1; i++) {
+ const { x1, y1, x2, y2 } = this.calculateBezierControlPoints(
+ this.dots,
+ i
+ );
+ const nextX = this.getDotX(this.dots[i + 1].x);
+ const nextY = this.getDotY(this.dots[i + 1].value);
+ path += ` C ${x1} ${y1}, ${x2} ${y2}, ${nextX} ${nextY}`;
+ }
+ return path;
+ }
+ private drawGrid(): void {
+ // Clear previous grid
+ while (this.gridGroup.firstChild) {
+ this.gridGroup.removeChild(this.gridGroup.firstChild);
+ }
+ if (!this.config.showGrid) return;
+ // Horizontal grid lines
+ for (const value of [-3, -2, -1, 0, 1, 2, 3]) {
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", "0");
+ line.setAttribute("y1", this.getDotY(value).toString());
+ line.setAttribute("x2", this.config.totalWidth.toString());
+ line.setAttribute("y2", this.getDotY(value).toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", "10");
+ text.setAttribute("y", (this.getDotY(value) + 4).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.textContent = value.toString();
+ this.gridGroup.appendChild(text);
+ }
+ // Vertical grid lines
+ const numVertLines = Math.ceil(
+ this.config.totalWidth / this.config.xUnitSize
+ );
+ for (let i = 0; i < numVertLines; i++) {
+ const x = i * this.config.xUnitSize;
+ const xValue = i - 3; // Starting from -3
+ const line = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "line"
+ );
+ line.setAttribute("x1", x.toString());
+ line.setAttribute("y1", "0");
+ line.setAttribute("x2", x.toString());
+ line.setAttribute("y2", this.config.height.toString());
+ line.setAttribute("stroke", "rgba(219, 39, 119, 0.4)");
+ line.setAttribute("stroke-width", "1");
+ this.gridGroup.appendChild(line);
+ if (xValue !== 0) {
+ const text = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "text"
+ );
+ text.setAttribute("x", x.toString());
+ text.setAttribute("y", (this.config.height / 2 + 20).toString());
+ text.setAttribute("fill", "rgba(219, 39, 119, 0.8)");
+ text.setAttribute("font-size", "12");
+ text.setAttribute("text-anchor", "middle");
+ text.textContent = xValue.toString();
+ this.gridGroup.appendChild(text);
+ }
+ }
+ }
+
+ private createTooltip(dot: DotConfig, x: number, y: number): SVGElement {
+ const tooltip = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ tooltip.classList.add("dot-tooltip");
+ tooltip.setAttribute("data-dot-id", dot.id.toString());
+
+ // Calculate tooltip dimensions and position
+ const tooltipWidth = 128; // Base width for your tooltip
+ const tooltipHeight = (4 / 3) * tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ let tooltipY = y - tooltipHeight - 20; // Positioned above the dot
+ tooltipY = Math.max(tooltipY, 10); // Ensure it doesn't go above the view
+
+ // Create background rectangle
+ const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ bg.setAttribute("x", tooltipX.toString());
+ bg.setAttribute("y", tooltipY.toString());
+ bg.setAttribute("width", tooltipWidth.toString());
+ bg.setAttribute("height", tooltipHeight.toString());
+ bg.setAttribute("rx", "5"); // Rounded corners
+ bg.classList.add("tooltip-background");
+ tooltip.appendChild(bg);
+
+ // Create foreignObject for the content
+
+ const contentContainer = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "foreignObject"
+ );
+ contentContainer.setAttribute("x", tooltipX.toString());
+ contentContainer.setAttribute("y", tooltipY.toString());
+ contentContainer.setAttribute("width", tooltipWidth.toString());
+ contentContainer.setAttribute("height", tooltipHeight.toString());
+
+ // Create a div to contain the content
+ const div = document.createElement("div");
+ div.classList.add("tooltip-content");
+
+ // Add title if available
+ if (dot.title) {
+ const title = document.createElement("div");
+ title.textContent = dot.title;
+ title.classList.add("tooltip-title");
+ div.appendChild(title);
+ }
+
+ // Add description if available
+ if (dot.description) {
+ const desc = document.createElement("div");
+ desc.textContent = dot.description;
+ desc.classList.add("tooltip-description");
+ div.appendChild(desc);
+ }
+
+ // Add image if available
+ if (dot.imageUrl) {
+ // Create a container div
+ const imageContainer = document.createElement("div");
+ imageContainer.classList.add("image_container"); // Add image_container class
+
+ // Create the image element
+ const img = document.createElement("img");
+ img.src = dot.imageUrl;
+ img.classList.add("tooltip-image");
+
+ // Append image to the container
+ imageContainer.appendChild(img);
+
+ // Append the image container to the main div
+ div.appendChild(imageContainer);
+ }
+
+ contentContainer.appendChild(div);
+ tooltip.appendChild(contentContainer);
+
+ // Add arrow path if needed
+ const arrow = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "path"
+ );
+ arrow.setAttribute(
+ "d",
+ `M ${x} ${tooltipY + tooltipHeight} L ${x} ${
+ tooltipY + tooltipHeight - 10
+ }`
+ );
+ // arrow.setAttribute(
+ // "d",
+ // `M ${x} ${tooltipY + tooltipHeight} L ${x - 10} ${
+ // tooltipY + tooltipHeight - 10
+ // } L ${x + 10} ${tooltipY + tooltipHeight - 10} Z`
+ // );
+
+ arrow.setAttribute("stroke", "white"); // Set the color of the line
+ arrow.setAttribute("stroke-width", "1"); // Set the line width to 1px
+ arrow.classList.add("tooltip-arrow");
+ tooltip.appendChild(arrow);
+
+ return tooltip;
+ }
+
+ private showTooltip(dot: DotConfig, x: number, y: number): void {
+ // Create tooltip
+ const tooltip = this.createTooltip(dot, x, y);
+ this.tooltipGroup.appendChild(tooltip);
+ this.activeTooltip = tooltip;
+ }
+ private hideTooltip(): void {
+ // This method is kept for compatibility but doesn't hide tooltips anymore
+ }
+ private drawCurve(): void {
+ const pathData = this.generateBezierPath();
+ this.curvePath.setAttribute("d", pathData);
+ }
+ private calculateTooltipEdges(): TooltipEdges {
+ let leftmost = 0;
+ let rightmost = 0;
+ let firstTooltipFound = false;
+ // If no dots with tooltips, return default values
+ if (this.dots.length === 0) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ // Calculate the leftmost and rightmost edges of all tooltips
+ for (const dot of this.dots) {
+ // Skip dots without tooltip content
+ if (!dot.imageUrl && !dot.title && !dot.description) {
+ continue;
+ }
+ const x = this.getDotX(dot.x);
+ const tooltipWidth = this.config.tooltipWidth;
+ const tooltipX = x - tooltipWidth / 2;
+ if (!firstTooltipFound) {
+ leftmost = tooltipX;
+ rightmost = tooltipX + tooltipWidth;
+ firstTooltipFound = true;
+ } else {
+ // Update leftmost and rightmost values
+ leftmost = Math.min(leftmost, tooltipX);
+ rightmost = Math.max(rightmost, tooltipX + tooltipWidth);
+ }
+ }
+ // If no dots with tooltips were found, use default values
+ if (!firstTooltipFound) {
+ return { leftmost: 0, rightmost: this.config.totalWidth };
+ }
+ return { leftmost, rightmost };
+ }
+ private drawDots(): void {
+ // Clear previous dots
+ while (this.dotsGroup.firstChild) {
+ this.dotsGroup.removeChild(this.dotsGroup.firstChild);
+ }
+ // Clear previous tooltips
+ while (this.tooltipGroup.firstChild) {
+ this.tooltipGroup.removeChild(this.tooltipGroup.firstChild);
+ }
+ for (const dot of this.dots) {
+ const x = this.getDotX(dot.x);
+ const y = this.getDotY(dot.value);
+ const circle = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "circle"
+ );
+ circle.setAttribute("cx", x.toString());
+ circle.setAttribute("cy", y.toString());
+ circle.setAttribute("r", this.config.dotRadius.toString());
+ circle.setAttribute("fill", "white");
+ circle.setAttribute("data-dot-id", dot.id.toString());
+ circle.classList.add("dot");
+ // Always show tooltip if it has content
+ if (dot.imageUrl || dot.title || dot.description) {
+ this.showTooltip(dot, x, y);
+ }
+ // Click event for navigation
+ if (dot.link) {
+ circle.addEventListener("click", () => {
+ if (dot.link) {
+ window.location.href = dot.link;
+ } else {
+ console.error("Dot has no link");
+ throw new Error("Dot has no link");
+ }
+ });
+ }
+ this.dotsGroup.appendChild(circle);
+ }
+ }
+ public render(): void {
+ this.drawGrid();
+ this.drawCurve();
+ this.drawDots();
+ // Calculate tooltip edges and set SVG width
+ const { leftmost, rightmost } = this.calculateTooltipEdges();
+ // Set the SVG width based on the rightmost tooltip edge
+ if (rightmost > 0) {
+ // Add some padding
+ const padding = 40;
+ this.config.totalWidth = rightmost + padding;
+ this.svg.setAttribute("width", `${this.config.totalWidth}`);
+ // Update grid width
+ this.drawGrid();
+ }
+ }
+ // Public API methods for external use
+ public updateDots(newDots: DotConfig[]): void {
+ this.dots = newDots;
+ // Initial width calculation based on dot positions (for grid)
+ if (this.dots.length > 0) {
+ // Find the minimum and maximum x values
+ const minX = Math.min(...this.dots.map((dot) => dot.x));
+ const maxX = Math.max(...this.dots.map((dot) => dot.x));
+ // Calculate width based on the range of x values
+ // Add padding on both sides (3 units on each side)
+ this.config.totalWidth = (maxX - minX + 6) * this.config.xUnitSize;
+ }
+ // Render will calculate the tooltip edges and update the SVG width
+ this.render();
+ }
+ public updateConfig(newConfig: Partial): void {
+ this.config = { ...this.config, ...newConfig };
+ this.render();
+ }
+ public resize(): void {
+ this.config.height = window.innerHeight;
+ this.svg.setAttribute("height", `${this.config.height}`);
+ this.render();
+ }
+}
diff --git a/dot-line-system/.history/src/ConnectedDotsVisualization_20250522105224.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522105224.ts
new file mode 100644
index 0000000..39a5153
--- /dev/null
+++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522105224.ts
@@ -0,0 +1,522 @@
+// Define interfaces
+export interface DotConfig {
+ id: number;
+ value: number;
+ x: number;
+ link?: string; // URL to navigate to when dot is clicked
+ imageUrl?: string; // Image to display in tooltip
+ title?: string; // Optional title for the tooltip
+ description?: string; // Optional description for the tooltip
+}
+export interface Config {
+ totalWidth: number;
+ height: number;
+ dotRadius: number;
+ xUnitSize: number;
+ tension: number;
+ showGrid: boolean;
+ tooltipWidth: number;
+ tooltipHeight: number;
+}
+interface ControlPoints {
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+}
+interface TooltipEdges {
+ leftmost: number;
+ rightmost: number;
+}
+export class ConnectedDotsVisualization {
+ private config: Config;
+ private dots: DotConfig[];
+ private preloadedImages: Map = new Map();
+ // DOM Elements
+ private scrollContainer: HTMLElement;
+ private svg: SVGElement;
+ private gridGroup: SVGGElement;
+ private curvePath: SVGPathElement;
+ private dotsGroup: SVGGElement;
+ private tooltipGroup: SVGGElement;
+ // Active tooltip
+ private activeTooltip: SVGElement | null = null;
+ constructor(
+ containerId: string,
+ dots: DotConfig[],
+ config?: Partial